From b2beeaa13656cb0704d02a134d42940c3ed87bb6 Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Mon, 11 May 2026 03:51:24 +0000 Subject: [PATCH] feat: deliver agent foundation day 1 --- document/development/agent plan/00_README.md | 20 + .../14_financial_document_canonical_model.md | 222 +++- .../00_execution_index.md | 155 +++ .../weekly_execution_details/README.md | 45 + .../day_1_foundation_models.md | 158 +++ .../day_2_rule_center_integration.md | 222 ++++ .../day_3_semantic_ontology_mvp.md | 238 +++++ .../day_4_orchestrator_runtime.md | 185 ++++ .../day_5_user_agent_mvp.md | 185 ++++ .../day_6_hermes_mvp.md | 193 ++++ .../day_7_hardening_demo_acceptance.md | 225 ++++ .../development/agent week plan/00_README.md | 128 +-- .../agent week plan/MASTER_TODO.md | 143 +-- .../day_1_foundation_models.md | 348 +------ .../day_2_rule_center_integration.md | 239 +---- .../day_3_semantic_ontology_mvp.md | 253 +---- .../day_4_orchestrator_runtime.md | 202 +--- .../agent week plan/day_5_user_agent_mvp.md | 203 +--- .../agent week plan/day_6_hermes_mvp.md | 210 +--- .../day_7_hardening_demo_acceptance.md | 242 +---- .../agent_week_plan_html/day-1.html | 137 +++ .../agent_week_plan_html/day-2.html | 132 +++ .../agent_week_plan_html/day-3.html | 132 +++ .../agent_week_plan_html/day-4.html | 133 +++ .../agent_week_plan_html/day-5.html | 133 +++ .../agent_week_plan_html/day-6.html | 133 +++ .../agent_week_plan_html/day-7.html | 132 +++ .../agent_week_plan_html/index.html | 181 ++++ .../agent_week_plan_html/styles.css | 426 ++++++++ .../src/app/api/v1/endpoints/agent_assets.py | 163 +++ server/src/app/api/v1/endpoints/agent_runs.py | 34 + server/src/app/api/v1/endpoints/audit_logs.py | 29 + server/src/app/api/v1/router.py | 36 +- server/src/app/core/agent_enums.py | 70 ++ server/src/app/db/base.py | 66 +- server/src/app/main.py | 24 +- server/src/app/models/__init__.py | 62 +- server/src/app/models/agent_asset.py | 79 ++ server/src/app/models/agent_run.py | 86 ++ server/src/app/models/audit_log.py | 25 + server/src/app/models/financial_record.py | 118 +++ server/src/app/repositories/agent_asset.py | 110 ++ server/src/app/repositories/agent_run.py | 57 + server/src/app/repositories/audit_log.py | 35 + server/src/app/schemas/agent_asset.py | 115 +++ server/src/app/schemas/agent_run.py | 61 ++ server/src/app/schemas/audit_log.py | 20 + server/src/app/services/agent_assets.py | 407 ++++++++ server/src/app/services/agent_foundation.py | 977 ++++++++++++++++++ server/src/app/services/agent_runs.py | 168 +++ server/src/app/services/audit.py | 70 ++ .../x_financial_server.egg-info/SOURCES.txt | 26 +- server/tests/test_agent_asset_service.py | 186 ++++ .../tests/test_agent_foundation_endpoints.py | 92 ++ 54 files changed, 6747 insertions(+), 1724 deletions(-) create mode 100644 document/development/agent plan/weekly_execution_details/00_execution_index.md create mode 100644 document/development/agent plan/weekly_execution_details/README.md create mode 100644 document/development/agent plan/weekly_execution_details/day_1_foundation_models.md create mode 100644 document/development/agent plan/weekly_execution_details/day_2_rule_center_integration.md create mode 100644 document/development/agent plan/weekly_execution_details/day_3_semantic_ontology_mvp.md create mode 100644 document/development/agent plan/weekly_execution_details/day_4_orchestrator_runtime.md create mode 100644 document/development/agent plan/weekly_execution_details/day_5_user_agent_mvp.md create mode 100644 document/development/agent plan/weekly_execution_details/day_6_hermes_mvp.md create mode 100644 document/development/agent plan/weekly_execution_details/day_7_hardening_demo_acceptance.md create mode 100644 document/development/agent_week_plan_html/day-1.html create mode 100644 document/development/agent_week_plan_html/day-2.html create mode 100644 document/development/agent_week_plan_html/day-3.html create mode 100644 document/development/agent_week_plan_html/day-4.html create mode 100644 document/development/agent_week_plan_html/day-5.html create mode 100644 document/development/agent_week_plan_html/day-6.html create mode 100644 document/development/agent_week_plan_html/day-7.html create mode 100644 document/development/agent_week_plan_html/index.html create mode 100644 document/development/agent_week_plan_html/styles.css create mode 100644 server/src/app/api/v1/endpoints/agent_assets.py create mode 100644 server/src/app/api/v1/endpoints/agent_runs.py create mode 100644 server/src/app/api/v1/endpoints/audit_logs.py create mode 100644 server/src/app/core/agent_enums.py create mode 100644 server/src/app/models/agent_asset.py create mode 100644 server/src/app/models/agent_run.py create mode 100644 server/src/app/models/audit_log.py create mode 100644 server/src/app/models/financial_record.py create mode 100644 server/src/app/repositories/agent_asset.py create mode 100644 server/src/app/repositories/agent_run.py create mode 100644 server/src/app/repositories/audit_log.py create mode 100644 server/src/app/schemas/agent_asset.py create mode 100644 server/src/app/schemas/agent_run.py create mode 100644 server/src/app/schemas/audit_log.py create mode 100644 server/src/app/services/agent_assets.py create mode 100644 server/src/app/services/agent_foundation.py create mode 100644 server/src/app/services/agent_runs.py create mode 100644 server/src/app/services/audit.py create mode 100644 server/tests/test_agent_asset_service.py create mode 100644 server/tests/test_agent_foundation_endpoints.py diff --git a/document/development/agent plan/00_README.md b/document/development/agent plan/00_README.md index 558715c..accc846 100644 --- a/document/development/agent plan/00_README.md +++ b/document/development/agent plan/00_README.md @@ -11,6 +11,25 @@ - 建设 Agent Orchestrator,统一负责路由、权限、工具调用、审计和失败处理。 - 让规则中心、MCP、知识库、数据库查询和任务系统使用同一套语义协议。 +## 与一周计划的关系 + +`document/development/agent week plan` 是一周开发路线图,只描述每天要完成的大方向和交付结果。 + +本目录是具体架构和执行细则,包含: + +- 架构设计。 +- 数据协议。 +- Agent 职责。 +- Orchestrator 流程。 +- OCR、知识库、规则生命周期。 +- 一周开发中每天对应的详细 TODO。 + +执行时按这个顺序阅读: + +1. 先看 `document/development/agent week plan/MASTER_TODO.md`,确认今天做什么。 +2. 再看本目录的架构文档,理解为什么这样做。 +3. 最后进入 [weekly_execution_details](./weekly_execution_details/README.md),按具体 TODO 开发。 + 推荐阅读顺序: 1. [01_overall_architecture.md](./01_overall_architecture.md) @@ -28,6 +47,7 @@ 13. [13_rule_formation_lifecycle.md](./13_rule_formation_lifecycle.md) 14. [14_financial_document_canonical_model.md](./14_financial_document_canonical_model.md) 15. [15_feedback_learning_loop.md](./15_feedback_learning_loop.md) +16. [weekly_execution_details/README.md](./weekly_execution_details/README.md) 开发原则: diff --git a/document/development/agent plan/14_financial_document_canonical_model.md b/document/development/agent plan/14_financial_document_canonical_model.md index 7d6fc3c..446bdb7 100644 --- a/document/development/agent plan/14_financial_document_canonical_model.md +++ b/document/development/agent plan/14_financial_document_canonical_model.md @@ -11,6 +11,13 @@ OCR、MCP、用户填写、业务数据库可能都描述同一张发票,但 - Hermes 难以批量统计。 - MCP 返回结果难以合并。 +这里要区分两层: + +- 标准模型:定义 Agent、规则、MCP、OCR、数据库之间统一交换的数据结构。 +- 业务数据库表:定义 MVP 阶段真正落库存储、查询和统计所依赖的最小业务表。 + +如果只有标准模型,没有最小业务表,User Agent 和 Hermes 仍然无法完成报销、应收、应付的查询、解释、巡检和统计。 + ## 2. 标准对象 第一版建议定义这些对象: @@ -20,6 +27,8 @@ Invoice Receipt ReimbursementRequest PaymentRequest +AccountsReceivableRecord +AccountsPayableRecord BankTransaction Contract Customer @@ -57,11 +66,17 @@ CostCenter "request_id": "", "request_no": "", "employee_id": "", + "employee_name": "", "department_id": "", + "department_name": "", + "project_code": "", "expense_type": "", + "reason": "", + "location": "", "amount": 0, "currency": "CNY", "status": "", + "occurred_at": "", "submitted_at": "", "approval_stage": "", "invoices": [], @@ -70,7 +85,55 @@ CostCenter } ``` -## 5. BankTransaction 标准模型 +说明: + +- `reason`、`location`、`occurred_at` 是报销查询、规则解释、风险识别的最小必要字段。 +- 如果一张报销单包含多条费用明细,应在数据库层拆到明细表,但对外仍可聚合为一个 `ReimbursementRequest`。 + +## 5. AccountsReceivableRecord 标准模型 + +```json +{ + "ar_id": "", + "document_no": "", + "customer_id": "", + "customer_name": "", + "contract_no": "", + "invoice_no": "", + "amount_receivable": 0, + "amount_received": 0, + "amount_outstanding": 0, + "currency": "CNY", + "due_date": "", + "posting_date": "", + "status": "", + "aging_days": 0, + "risk_flags": [] +} +``` + +## 6. AccountsPayableRecord 标准模型 + +```json +{ + "ap_id": "", + "document_no": "", + "vendor_id": "", + "vendor_name": "", + "invoice_no": "", + "amount_payable": 0, + "amount_paid": 0, + "amount_outstanding": 0, + "currency": "CNY", + "due_date": "", + "posting_date": "", + "status": "", + "aging_days": 0, + "risk_flags": [] +} +``` + +## 7. BankTransaction 标准模型 ```json { @@ -87,7 +150,133 @@ CostCenter } ``` -## 6. 字段来源优先级 +## 8. MVP 最小业务表设计建议 + +标准模型不等于数据库表,但 MVP 至少要有以下业务表,才能支撑 Day 5 和 Day 6。 + +### 8.1 报销主表 `expense_claims` + +建议字段: + +```text +id +claim_no +employee_id +employee_name +department_id +department_name +project_code +expense_type +reason +location +amount +currency +invoice_count +occurred_at +submitted_at +status +approval_stage +risk_flags_json +created_at +updated_at +``` + +适用场景: + +- 查询员工报销金额、状态、进度。 +- 解释报销为什么被拦截。 +- 识别超标、重复、异常等风险。 + +### 8.2 报销明细表 `expense_claim_items` + +建议字段: + +```text +id +claim_id +item_date +item_type +item_reason +item_location +item_amount +invoice_id +created_at +updated_at +``` + +适用场景: + +- 一单多明细。 +- 重复报销匹配。 +- 与发票 OCR 结果逐条比对。 + +### 8.3 应收主表 `accounts_receivable` + +建议字段: + +```text +id +receivable_no +customer_id +customer_name +contract_no +invoice_no +amount_receivable +amount_received +amount_outstanding +currency +posting_date +due_date +aging_days +status +risk_flags_json +created_at +updated_at +``` + +适用场景: + +- 客户应收查询。 +- 账龄分析。 +- 逾期风险巡检。 + +### 8.4 应付主表 `accounts_payable` + +建议字段: + +```text +id +payable_no +vendor_id +vendor_name +invoice_no +amount_payable +amount_paid +amount_outstanding +currency +posting_date +due_date +aging_days +status +risk_flags_json +created_at +updated_at +``` + +适用场景: + +- 供应商待付款查询。 +- 付款状态查询。 +- 逾期应付和异常付款巡检。 + +### 8.5 MVP 设计边界 + +- 不要求一次建完整 ERP 总账、分录、核销、凭证体系。 +- 第一周只要求支撑查询、解释、统计、风险识别的最小字段。 +- 如果现有业务系统已有对应表或 API,优先复用,不重复造表。 +- 如果当前环境没有真实业务数据源,可先建立 Mock 表,但字段命名应尽量贴近最终标准模型。 + +## 9. 字段来源优先级 建议优先级: @@ -101,7 +290,7 @@ CostCenter LLM 推断字段必须标记来源和置信度。 -## 7. 与语义本体关系 +## 10. 与语义本体关系 语义本体识别的是用户意图和对象。 @@ -112,15 +301,28 @@ ontology.entities[].type = invoice -> 映射到 Invoice 标准模型 ``` -## 8. 开发阶段建议 +补充映射建议: + +```text +ontology.scenario = expense + -> 查询 expense_claims / expense_claim_items + +ontology.scenario = accounts_receivable + -> 查询 accounts_receivable + +ontology.scenario = accounts_payable + -> 查询 accounts_payable +``` + +## 11. 开发阶段建议 ```text Step 1: 定义 Invoice 标准模型 Step 2: 定义 ReimbursementRequest 标准模型 -Step 3: OCR 输出映射到 Invoice -Step 4: MCP 输出映射到 Invoice -Step 5: 规则中心基于标准模型执行 -Step 6: 扩展 AR/AP 标准模型 -Step 7: 建立字段血缘和置信度 +Step 3: 定义 AccountsReceivableRecord / AccountsPayableRecord 标准模型 +Step 4: 设计 MVP 最小业务表 expense_claims / expense_claim_items / accounts_receivable / accounts_payable +Step 5: OCR 输出映射到 Invoice +Step 6: MCP 输出映射到 Invoice 或 AR/AP 标准模型 +Step 7: 规则中心基于标准模型执行 +Step 8: 建立字段血缘和置信度 ``` - diff --git a/document/development/agent plan/weekly_execution_details/00_execution_index.md b/document/development/agent plan/weekly_execution_details/00_execution_index.md new file mode 100644 index 0000000..93dd71e --- /dev/null +++ b/document/development/agent plan/weekly_execution_details/00_execution_index.md @@ -0,0 +1,155 @@ +# Weekly Execution Details 总执行清单 + +本文件是 `agent plan` 下的执行索引,承接 `agent week plan` 的 7 天路线图。 + +这里不重新解释为什么这样排期,只负责把每天的“大开发点”映射到可执行 TODO 文档。 + +## 执行方式 + +- [ ] 先读 `document/development/agent week plan/MASTER_TODO.md`,确认当天主题。 +- [ ] 再读当天 daily 文档,确认交付物和验收门槛。 +- [ ] 最后进入本目录对应的详细 TODO 文档。 +- [ ] 每完成一个最小 TODO,就改成 `[x] ~~...~~`。 +- [ ] 每天结束时回到 daily 文档,确认当天是否达到验收门槛。 + +## Day 1:基础模型与工程骨架 + +路线图: + +- `document/development/agent week plan/day_1_foundation_models.md` + +执行细则: + +- [day_1_foundation_models.md](./day_1_foundation_models.md) + +核心完成物: + +- [x] ~~统一资产模型。~~ +- [x] ~~版本模型。~~ +- [x] ~~审核模型。~~ +- [x] ~~AgentRun。~~ +- [x] ~~ToolCall。~~ +- [x] ~~SemanticParseLog。~~ +- [x] ~~AuditLog。~~ +- [x] ~~最小财务业务数据来源。~~ + +## Day 2:任务规则中心联调 + +路线图: + +- `document/development/agent week plan/day_2_rule_center_integration.md` + +执行细则: + +- [day_2_rule_center_integration.md](./day_2_rule_center_integration.md) + +核心完成物: + +- [ ] 规则、技能、MCP、任务列表。 +- [ ] 资产详情。 +- [ ] 规则 Markdown 编辑。 +- [ ] 最近 5 个版本。 +- [ ] 版本切换弹窗。 +- [ ] 审核者信息。 +- [ ] 未审核不能上线。 + +## Day 3:语义本体 MVP + +路线图: + +- `document/development/agent week plan/day_3_semantic_ontology_mvp.md` + +执行细则: + +- [day_3_semantic_ontology_mvp.md](./day_3_semantic_ontology_mvp.md) + +核心完成物: + +- [ ] 8 字段语义结构。 +- [ ] 语义解析 API。 +- [ ] 解析日志。 +- [ ] 权限级别判断。 +- [ ] 最小评测集。 + +## Day 4:Orchestrator 运行时 + +路线图: + +- `document/development/agent week plan/day_4_orchestrator_runtime.md` + +执行细则: + +- [day_4_orchestrator_runtime.md](./day_4_orchestrator_runtime.md) + +核心完成物: + +- [ ] Orchestrator 入口。 +- [ ] Agent 路由。 +- [ ] 权限拦截。 +- [ ] 工具调用封装。 +- [ ] Trace 查询。 +- [ ] 降级返回。 + +## Day 5:User Agent MVP + +路线图: + +- `document/development/agent week plan/day_5_user_agent_mvp.md` + +执行细则: + +- [day_5_user_agent_mvp.md](./day_5_user_agent_mvp.md) + +核心完成物: + +- [ ] 用户自然语言入口。 +- [ ] 报销查询解释。 +- [ ] 应收查询解释。 +- [ ] 应付查询解释。 +- [ ] 规则引用解释。 +- [ ] 草稿生成。 + +## Day 6:Hermes MVP + +路线图: + +- `document/development/agent week plan/day_6_hermes_mvp.md` + +执行细则: + +- [day_6_hermes_mvp.md](./day_6_hermes_mvp.md) + +核心完成物: + +- [ ] 任务触发入口。 +- [ ] 风险巡检。 +- [ ] 每日统计。 +- [ ] OCR Mock 接入。 +- [ ] 知识候选生成。 +- [ ] 规则草稿生成。 + +## Day 7:加固、演示和验收 + +路线图: + +- `document/development/agent week plan/day_7_hardening_demo_acceptance.md` + +执行细则: + +- [day_7_hardening_demo_acceptance.md](./day_7_hardening_demo_acceptance.md) + +核心完成物: + +- [ ] 核心链路回归。 +- [ ] 权限边界复查。 +- [ ] 审计和 Trace 补齐。 +- [ ] 测试记录。 +- [ ] 演示脚本。 +- [ ] 下一阶段交接。 + +## 最终完成标准 + +- [ ] 周计划每一天都有清晰大开发点。 +- [ ] 每个大开发点都能跳转到具体执行细则。 +- [ ] 执行细则覆盖模型、接口、服务、前端、测试、验收。 +- [ ] Codex 可以从任意一天开始,根据 TODO 独立推进开发。 diff --git a/document/development/agent plan/weekly_execution_details/README.md b/document/development/agent plan/weekly_execution_details/README.md new file mode 100644 index 0000000..b03f11e --- /dev/null +++ b/document/development/agent plan/weekly_execution_details/README.md @@ -0,0 +1,45 @@ +# Weekly Execution Details 执行细则 + +本目录承接 `document/development/agent week plan` 的 7 天路线图。 + +分工方式: + +- `agent week plan`:说明每天的大方向、交付物、验收门槛。 +- `agent plan/weekly_execution_details`:说明每天具体怎么做,拆到模型、字段、接口、服务、前端、测试和验收证据。 + +执行时先看周计划,再进入本目录对应日期的详细 TODO。 + +## 对应关系 + +| 周计划 Day | 开发主题 | 执行细则 | +| --- | --- | --- | +| Day 1 | 基础模型与工程骨架 | [day_1_foundation_models.md](./day_1_foundation_models.md) | +| Day 2 | 任务规则中心联调 | [day_2_rule_center_integration.md](./day_2_rule_center_integration.md) | +| Day 3 | 语义本体 MVP | [day_3_semantic_ontology_mvp.md](./day_3_semantic_ontology_mvp.md) | +| Day 4 | Orchestrator 运行时 | [day_4_orchestrator_runtime.md](./day_4_orchestrator_runtime.md) | +| Day 5 | User Agent MVP | [day_5_user_agent_mvp.md](./day_5_user_agent_mvp.md) | +| Day 6 | Hermes MVP | [day_6_hermes_mvp.md](./day_6_hermes_mvp.md) | +| Day 7 | 加固、演示和验收 | [day_7_hardening_demo_acceptance.md](./day_7_hardening_demo_acceptance.md) | + +## 完成标记规则 + +未完成: + +```md +- [ ] 建立 AgentAsset 数据模型 +``` + +完成后: + +```md +- [x] ~~建立 AgentAsset 数据模型~~ +``` + +执行要求: + +- [ ] 每次只处理一个最小 TODO。 +- [ ] 完成后先自测,再改成 `[x]`。 +- [ ] 改成 `[x]` 时,同时用 `~~` 画线。 +- [ ] 不能因为代码写完就标完成,必须满足该 TODO 的验收证据。 +- [ ] 遇到阻塞时,在当天文档的“阻塞记录”下新增一条说明。 +- [ ] 每天收尾时更新当天文档的“日终交接”。 diff --git a/document/development/agent plan/weekly_execution_details/day_1_foundation_models.md b/document/development/agent plan/weekly_execution_details/day_1_foundation_models.md new file mode 100644 index 0000000..8906071 --- /dev/null +++ b/document/development/agent plan/weekly_execution_details/day_1_foundation_models.md @@ -0,0 +1,158 @@ +# Day 1:基础模型与工程骨架 TODO + +本文件是周计划 Day 1 的具体执行细则。路线图见 `document/development/agent week plan/day_1_foundation_models.md`。 + +状态:Day 1 已于 `2026-05-11` 完成,以下 TODO 已按完成态回填。 + +## 完成摘要 + +- [x] ~~完成 Agent 资产、版本、审核、运行日志、工具调用日志、语义解析日志、审计日志基础模型。~~ +- [x] ~~完成报销、应收、应付最小业务数据源,后续 User Agent 和 Hermes 有明确查询来源。~~ +- [x] ~~完成基础 API、服务层、种子数据和测试,Day 2 可直接进入前后端联调。~~ + +## 0. 开始前检查 + +- [x] ~~确认后端目录为 `/app/server`,模型、路由、启动入口和测试目录已定位。~~ +- [x] ~~确认本次改动以增量方式落到现有 FastAPI + SQLAlchemy 工程,不回退无关文件。~~ + +验收证据: + +- [x] ~~模型注册位于 `server/src/app/db/base.py`,路由注册位于 `server/src/app/api/v1/router.py`,启动入口位于 `server/src/app/main.py`,测试位于 `server/tests`。~~ + +## 1. 统一命名和边界 + +- [x] ~~统一枚举:`rule | skill | mcp | task`、`draft | review | active | disabled`、`pending | approved | rejected`、`orchestrator | user_agent | hermes`。~~ +- [x] ~~统一运行来源、权限级别、内容类型、运行状态和工具类型命名,避免出现第二套并行语义。~~ + +验收证据: + +- [x] ~~`server/src/app/core/agent_enums.py` 已成为模型、Schema 和服务层的统一枚举入口。~~ + +## 2. 设计最小财务业务数据模型 + +- [x] ~~建立 `expense_claims`、`expense_claim_items`、`accounts_receivable`、`accounts_payable`。~~ +- [x] ~~字段覆盖时间、地点、理由、金额、员工、部门、状态,以及应收 / 应付的金额、到期日、账龄、风险标记。~~ + +验收证据: + +- [x] ~~`server/src/app/models/financial_record.py` 与 `document/development/agent plan/14_financial_document_canonical_model.md` 形成直接映射。~~ + +## 3. 建立 AgentAsset 模型 + +- [x] ~~建立 `AgentAsset`,包含 `asset_type`、`code`、`name`、`description`、`domain`、`scenario_json`、`owner`、`reviewer`、`status`、`current_version`、`config_json` 等核心字段。~~ +- [x] ~~对 `code`、`asset_type`、`status`、`domain` 建立唯一约束或索引。~~ + +验收证据: + +- [x] ~~资产列表可按 `rule`、`skill`、`mcp`、`task` 四类过滤返回。~~ + +## 4. 建立 AgentAssetVersion 模型 + +- [x] ~~建立 `AgentAssetVersion`,规则版本保存 Markdown,其余资产版本保存 JSON 快照。~~ +- [x] ~~对 `asset_id + version` 建立唯一约束,并支持按资产读取最近版本列表。~~ + +验收证据: + +- [x] ~~规则详情接口可返回 `current_version_content` 和 `recent_versions`。~~ + +## 5. 建立 AgentAssetReview 模型 + +- [x] ~~建立 `AgentAssetReview`,保存版本、审核人、审核状态、审核备注和审核时间。~~ +- [x] ~~服务层实现规则版本未 `approved` 时禁止上线。~~ + +验收证据: + +- [x] ~~`POST /api/v1/agent-assets/{asset_id}/activate` 对待审规则返回 400 拦截。~~ + +## 6. 建立 AgentRun 模型 + +- [x] ~~建立 `AgentRun`,包含 `run_id`、`agent`、`source`、`ontology_json`、`route_json`、`permission_level`、`status`、`result_summary`、`error_message` 等字段。~~ +- [x] ~~所有运行记录统一生成 `run_id`,并允许失败态保存错误信息。~~ + +验收证据: + +- [x] ~~`AgentRunService.create_run()` 会自动生成 `run_` 前缀标识,并可回读失败摘要。~~ + +## 7. 建立 AgentToolCall 模型 + +- [x] ~~建立 `AgentToolCall`,可记录工具类型、工具名、请求 / 响应 JSON、耗时和错误信息。~~ +- [x] ~~同一个 `run_id` 下支持多次工具调用追踪。~~ + +验收证据: + +- [x] ~~种子运行数据已覆盖数据库查询、MCP 调用和权限规则引擎调用。~~ + +## 8. 建立 SemanticParseLog 模型 + +- [x] ~~建立 `SemanticParseLog`,覆盖场景、意图、实体、时间范围、指标、约束、风险、权限和置信度。~~ +- [x] ~~支持按 `run_id` 回放 Day 3 语义结果。~~ + +验收证据: + +- [x] ~~`GET /api/v1/agent-runs/{run_id}` 已能携带 `semantic_parse` 返回。~~ + +## 9. 建立 AuditLog 模型 + +- [x] ~~建立 `AuditLog` 和统一 `AuditLogService`。~~ +- [x] ~~资产创建、版本保存、审核、上线等写操作都会留下审计记录。~~ + +验收证据: + +- [x] ~~`GET /api/v1/audit-logs` 可返回种子审计日志,服务层新建资产也会落审计。~~ + +## 10. 建立 Schema / DTO + +- [x] ~~建立 `AgentAssetCreate / Update / Read / ListItem`、`AgentAssetVersionRead`、`AgentAssetReviewRead`、`RuleMarkdownUpdate`、`AgentRunRead`、`AgentToolCallRead`、`SemanticParseRead`。~~ +- [x] ~~所有 JSON 字段以结构化对象返回,不回传字符串化 JSON。~~ + +验收证据: + +- [x] ~~列表 DTO 不返回大块 Markdown,详情 DTO 返回当前版本正文和最近版本。~~ + +## 11. 建立 API 骨架 + +- [x] ~~建立 `GET/POST/PATCH /api/v1/agent-assets`、`GET /api/v1/agent-assets/{asset_id}`、`GET/POST /api/v1/agent-assets/{asset_id}/versions`、`POST /api/v1/agent-assets/{asset_id}/reviews`、`POST /api/v1/agent-assets/{asset_id}/activate`。~~ +- [x] ~~建立 `GET /api/v1/agent-runs`、`GET /api/v1/agent-runs/{run_id}`、`GET /api/v1/audit-logs`。~~ + +验收证据: + +- [x] ~~所有接口已挂到 `server/src/app/api/v1/router.py`,并通过 `create_app()` 自动暴露。~~ + +## 12. 建立种子数据 + +- [x] ~~种子资产补齐到 3 条规则、2 条技能、2 条 MCP、3 条任务。~~ +- [x] ~~三条规则都具备至少 2 个版本,并覆盖 `approved / pending / rejected` 三种审核样本。~~ +- [x] ~~旧开发数据库启动时会自动增量补齐新增资产和版本,不要求手动清库。~~ + +验收证据: + +- [x] ~~Smoke:`GET /api/v1/agent-assets` 返回 10 条资产,`GET /api/v1/agent-runs` 返回 3 条运行日志,`GET /api/v1/audit-logs` 返回 4 条审计日志。~~ + +## 13. 最小测试 + +- [x] ~~新增 Day 1 服务层与接口层测试,覆盖种子完整性、版本历史、未审核不能上线、运行日志生成和审计日志写入。~~ +- [x] ~~Ruff 校验通过,Day 1 新增文件保持可检查状态。~~ + +验收证据: + +- [x] ~~`/app/server/.venv/bin/pytest -q /app/server/tests/test_agent_asset_service.py /app/server/tests/test_agent_foundation_endpoints.py` -> `11 passed`。~~ +- [x] ~~`/app/server/.venv/bin/pytest -q tests` 已通过全量后端测试。~~ + +## 14. Day 1 验收 + +- [x] ~~数据库能创建所有新增表或等价结构。~~ +- [x] ~~API 服务能启动,OpenAPI 能看到新增接口。~~ +- [x] ~~资产列表接口返回规则、技能、MCP、任务;规则详情带 Markdown 当前版本和最近版本列表。~~ +- [x] ~~未审核规则不能上线;AgentRun 和 AuditLog 均可保存记录。~~ +- [x] ~~所有 Day 1 TODO 已改为完成态。~~ + +## 阻塞记录 + +- [x] ~~暂无阻塞。~~ + +## 日终交接 + +- [x] ~~已完成模型:资产、版本、审核、运行日志、工具调用、语义解析、审计、报销、应收、应付。~~ +- [x] ~~已完成 API:`/api/v1/agent-assets`、`/api/v1/agent-runs`、`/api/v1/audit-logs`。~~ +- [x] ~~Day 2 前端联调应优先使用 `GET /api/v1/agent-assets`、`GET /api/v1/agent-assets/{asset_id}`、`GET /api/v1/agent-assets/{asset_id}/versions?limit=5`、`POST /api/v1/agent-assets/{asset_id}/reviews`、`POST /api/v1/agent-assets/{asset_id}/activate`。~~ +- [x] ~~后续 Day 4 及以后运行时方向按用户要求转向 `LangChain + LangGraph`,Hermes 继续作为内部数字员工入口;Day 1 保留为数据与治理底座。~~ diff --git a/document/development/agent plan/weekly_execution_details/day_2_rule_center_integration.md b/document/development/agent plan/weekly_execution_details/day_2_rule_center_integration.md new file mode 100644 index 0000000..855689d --- /dev/null +++ b/document/development/agent plan/weekly_execution_details/day_2_rule_center_integration.md @@ -0,0 +1,222 @@ +# Day 2:任务规则中心联调 TODO + +本文件是周计划 Day 2 的具体执行细则。路线图见 `document/development/agent week plan/day_2_rule_center_integration.md`。 + +目标:把任务规则中心从静态 UI 改成可对接后端的生产形态,覆盖规则、技能、MCP、任务四类资产。重点是规则 Markdown 编辑、版本切换、审核者信息、上线约束。 + +参考文档: + +- `document/development/agent plan/07_capability_registry.md` +- `document/development/agent plan/13_rule_formation_lifecycle.md` +- `document/development/agent plan/06_data_contracts_and_governance.md` + +## 0. 开始前检查 + +- [ ] 确认 Day 1 API 已可访问。 +- [ ] 确认前端任务规则中心文件位置。 +- [ ] 确认现有路由名称和导航名称。 +- [ ] 确认现有 UI 风格,不重新做大改版。 +- [ ] 确认当前页面已有页签:规则、技能、MCP、任务。 +- [ ] 确认详情页隐藏顶部 title bar 的逻辑仍然有效。 +- [ ] 确认返回列表栏高度没有被重新拉高。 + +## 1. API Client + +- [ ] 新增或扩展资产列表请求函数。 +- [ ] 新增资产详情请求函数。 +- [ ] 新增版本列表请求函数。 +- [ ] 新增规则 Markdown 保存请求函数。 +- [ ] 新增审核请求函数。 +- [ ] 新增上线请求函数。 +- [ ] 新增运行日志请求函数。 +- [ ] 给所有请求增加加载态。 +- [ ] 给所有请求增加错误态。 +- [ ] 给所有写请求增加成功提示。 + +验收证据: + +- [ ] 前端不再只依赖本地硬编码资产数据。 +- [ ] 后端不可用时页面有明确错误提示。 + +## 2. 列表页数据接入 + +- [ ] 规则页签请求 `asset_type=rule`。 +- [ ] 技能页签请求 `asset_type=skill`。 +- [ ] MCP 页签请求 `asset_type=mcp`。 +- [ ] 任务页签请求 `asset_type=task`。 +- [ ] 搜索框传递关键词或本地过滤。 +- [ ] 类型下拉和搜索框可以同时生效。 +- [ ] 状态筛选可以过滤 `draft | review | active | disabled`。 +- [ ] 列表卡片展示名称。 +- [ ] 列表卡片展示摘要。 +- [ ] 列表卡片展示状态。 +- [ ] 列表卡片展示负责人。 +- [ ] 列表卡片展示最近更新时间。 +- [ ] 空数据时展示空态。 +- [ ] 加载中时展示骨架或加载状态。 + +验收证据: + +- [ ] 四个页签都能切换。 +- [ ] 四个页签都有数据或空态。 +- [ ] 搜索和筛选不会互相覆盖。 + +## 3. 规则详情页主信息 + +- [ ] 打开规则资产时请求详情 API。 +- [ ] Hero title 展示规则名称。 +- [ ] Hero title 下方展示审核者。 +- [ ] Hero title 下方展示审核状态。 +- [ ] Hero title 下方展示上线条件。 +- [ ] Hero title 高度保持紧凑。 +- [ ] 详情页不显示外层顶部 title bar。 +- [ ] 返回列表栏高度保持原有紧凑高度。 + +验收证据: + +- [ ] 用户能一眼看到该规则是否已审核。 +- [ ] 用户不会看到两层 title。 + +## 4. Markdown 编辑器 + +- [ ] 从当前版本读取 Markdown 内容。 +- [ ] Markdown 编辑框高度和右侧版本卡片底部对齐。 +- [ ] Markdown 编辑框支持长内容滚动。 +- [ ] Markdown 编辑框保存时调用 API。 +- [ ] 保存后创建新版本或更新草稿版本,按后端约定执行。 +- [ ] 保存成功后刷新版本列表。 +- [ ] 保存失败时保留用户输入。 +- [ ] 编辑器禁用态覆盖 `active` 且无编辑权限的情况。 +- [ ] 编辑器底部展示最后保存时间。 + +验收证据: + +- [ ] 编辑 Markdown 后刷新页面内容仍存在。 +- [ ] 保存失败不会丢内容。 +- [ ] 左右卡片底部视觉对齐。 + +## 5. 版本卡片 + +- [ ] 右侧只保留版本信息卡片。 +- [ ] 版本卡片宽度足够展示版本号、日期、状态。 +- [ ] 展示最近 5 个版本。 +- [ ] 当前版本有明显但不突兀的标识。 +- [ ] 当前版本标识居中显示。 +- [ ] 选中状态只变色,不改变内容对齐。 +- [ ] 日期列和其他版本日期对齐。 +- [ ] 点击非当前版本时弹出确认弹窗。 +- [ ] 弹窗展示目标版本号。 +- [ ] 弹窗展示切换风险提示。 +- [ ] 确认后切换当前展示内容。 +- [ ] 取消后不改变当前版本。 + +验收证据: + +- [ ] 版本切换不会造成列表文字位移。 +- [ ] 当前版本背景能完全覆盖内容区域。 +- [ ] 版本卡片不贴右侧边界。 + +## 6. 审核与上线 + +- [ ] 详情中展示审核者姓名。 +- [ ] 详情中展示审核时间。 +- [ ] 详情中展示审核意见。 +- [ ] 未审核规则显示不能上线原因。 +- [ ] 点击上线时调用后端上线接口。 +- [ ] 后端拒绝时展示拒绝原因。 +- [ ] 审核通过后上线按钮可用。 +- [ ] 审核动作写入审计日志。 +- [ ] 上线动作写入审计日志。 + +验收证据: + +- [ ] pending 规则无法上线。 +- [ ] approved 规则可以上线。 +- [ ] rejected 规则无法上线。 + +## 7. 技能详情 + +- [ ] 技能页签列表展示能力名称。 +- [ ] 技能详情展示能力说明。 +- [ ] 技能详情展示输入参数。 +- [ ] 技能详情展示输出参数。 +- [ ] 技能详情展示依赖能力。 +- [ ] 技能详情展示适用场景。 +- [ ] 技能详情展示负责人。 +- [ ] 技能详情展示版本。 +- [ ] 技能详情不使用规则 Markdown 编辑器。 + +验收证据: + +- [ ] 技能和规则详情不会混用 UI。 + +## 8. MCP 详情 + +- [ ] MCP 页签列表展示外部服务名称。 +- [ ] MCP 详情展示服务类型。 +- [ ] MCP 详情展示调用地址或能力名。 +- [ ] MCP 详情展示鉴权方式。 +- [ ] MCP 详情展示超时配置。 +- [ ] MCP 详情展示降级策略。 +- [ ] MCP 详情展示最近调用状态。 +- [ ] MCP 详情展示负责人。 + +验收证据: + +- [ ] MCP 被定义为外部服务,而不是技能规则。 + +## 9. 任务详情 + +- [ ] 任务页签展示定时任务名称。 +- [ ] 任务详情展示 cron 或调度周期。 +- [ ] 任务详情展示执行 Agent,默认 Hermes。 +- [ ] 任务详情展示任务目标。 +- [ ] 任务详情展示风险等级。 +- [ ] 任务详情展示最近执行时间。 +- [ ] 任务详情展示最近执行结果。 +- [ ] 任务详情展示启停状态。 + +验收证据: + +- [ ] 定时任务用户可见名称为“任务”。 +- [ ] 技术字段可保留 `schedule`,但 UI 不显示“定时任务”。 + +## 10. 前端质量 + +- [ ] 页面在 1366 宽度下无横向滚动。 +- [ ] 页面在 1920 宽度下右侧卡片不过宽。 +- [ ] 页面在窄屏下详情区域可滚动。 +- [ ] 所有按钮有禁用态。 +- [ ] 所有弹窗有取消按钮。 +- [ ] 所有表单错误有提示。 +- [ ] 所有日期格式统一。 +- [ ] 状态颜色和现有系统一致。 + +验收证据: + +- [ ] `npm run build` 通过。 +- [ ] 任务规则中心手动走查通过。 + +## 11. Day 2 验收 + +- [ ] 规则、技能、MCP、任务四个页签可用。 +- [ ] 搜索框和筛选下拉可用。 +- [ ] 规则详情展示 Markdown。 +- [ ] 规则 Markdown 可保存。 +- [ ] 右侧只保留版本信息。 +- [ ] 版本可切换且有弹窗确认。 +- [ ] 审核者信息在标题下方。 +- [ ] 未审核规则不能上线。 +- [ ] 前端构建通过。 +- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 + +## 阻塞记录 + +- [ ] 暂无。 + +## 日终交接 + +- [ ] 写明已接入的 API。 +- [ ] 写明仍然使用 Mock 的字段。 +- [ ] 写明 UI 未完成项。 +- [ ] 写明 Day 3 语义本体需要复用的资产数据。 diff --git a/document/development/agent plan/weekly_execution_details/day_3_semantic_ontology_mvp.md b/document/development/agent plan/weekly_execution_details/day_3_semantic_ontology_mvp.md new file mode 100644 index 0000000..f083b13 --- /dev/null +++ b/document/development/agent plan/weekly_execution_details/day_3_semantic_ontology_mvp.md @@ -0,0 +1,238 @@ +# Day 3:语义本体 MVP TODO + +本文件是周计划 Day 3 的具体执行细则。路线图见 `document/development/agent week plan/day_3_semantic_ontology_mvp.md`。 + +目标:建立用户问题的语义解析层,输出稳定的 8 个核心字段,让 User Agent、Hermes 和 Orchestrator 都能使用同一套语义结构。 + +参考文档: + +- `document/development/agent plan/02_semantic_ontology.md` +- `document/development/agent plan/14_financial_document_canonical_model.md` +- `document/development/agent plan/06_data_contracts_and_governance.md` + +## 0. 开始前检查 + +- [ ] 确认 Day 1 的 `SemanticParseLog` 可用。 +- [ ] 确认 Day 1 的 `AgentRun` 可用。 +- [ ] 确认 Day 2 的资产 API 可用。 +- [ ] 找到后端服务层目录。 +- [ ] 找到现有 LLM 调用或 Mock 调用方式。 +- [ ] 确认当前是否允许真实调用 LLM。 +- [ ] 如果不能调用真实 LLM,准备规则解析加 Mock 解析。 + +## 1. 定义 8 个核心字段 + +- [ ] 定义字段 `scenario`,表示业务场景。 +- [ ] 定义字段 `intent`,表示用户意图。 +- [ ] 定义字段 `entities`,表示业务对象。 +- [ ] 定义字段 `time_range`,表示时间范围。 +- [ ] 定义字段 `metrics`,表示指标或金额口径。 +- [ ] 定义字段 `constraints`,表示过滤条件。 +- [ ] 定义字段 `risk_flags`,表示风险信号。 +- [ ] 定义字段 `permission`,表示动作权限。 +- [ ] 为每个字段写清楚类型。 +- [ ] 为每个字段写清楚是否必填。 +- [ ] 为每个字段写清楚默认值。 +- [ ] 为每个字段写清楚示例。 + +验收证据: + +- [ ] 8 个字段在 Schema、服务层、日志中名字一致。 + +## 2. 设计字段枚举 + +- [ ] `scenario` 支持 `expense`。 +- [ ] `scenario` 支持 `accounts_receivable`。 +- [ ] `scenario` 支持 `accounts_payable`。 +- [ ] `scenario` 支持 `knowledge`。 +- [ ] `scenario` 支持 `unknown`。 +- [ ] `intent` 支持 `query`。 +- [ ] `intent` 支持 `explain`。 +- [ ] `intent` 支持 `compare`。 +- [ ] `intent` 支持 `risk_check`。 +- [ ] `intent` 支持 `draft`。 +- [ ] `intent` 支持 `operate`。 +- [ ] `permission.level` 支持 `read`。 +- [ ] `permission.level` 支持 `draft_write`。 +- [ ] `permission.level` 支持 `approval_required`。 +- [ ] `permission.level` 支持 `forbidden`。 + +验收证据: + +- [ ] 未识别的问题不会抛异常,返回 `unknown`。 + +## 3. 建立 Schema + +- [ ] 定义 `OntologyParseRequest`。 +- [ ] `OntologyParseRequest` 包含 `query`。 +- [ ] `OntologyParseRequest` 包含 `user_id`。 +- [ ] `OntologyParseRequest` 包含 `context_json`。 +- [ ] 定义 `OntologyParseResult`。 +- [ ] `OntologyParseResult` 包含 8 个核心字段。 +- [ ] `OntologyParseResult` 包含 `confidence`。 +- [ ] `OntologyParseResult` 包含 `clarification_required`。 +- [ ] `OntologyParseResult` 包含 `clarification_question`。 +- [ ] `OntologyParseResult` 包含 `run_id`。 +- [ ] 定义字段级错误结构。 + +验收证据: + +- [ ] OpenAPI 中可以看到语义解析请求和响应。 + +## 4. 实现解析服务 + +- [ ] 新增 `SemanticOntologyService` 或同等服务。 +- [ ] 实现 `parse(query, user_context)` 主函数。 +- [ ] 先做关键词规则解析。 +- [ ] 报销关键词映射到 `expense`。 +- [ ] 应收、回款、客户欠款映射到 `accounts_receivable`。 +- [ ] 应付、供应商、付款映射到 `accounts_payable`。 +- [ ] 风险、异常、重复、超标映射到 `risk_check`。 +- [ ] 为什么、依据、规则映射到 `explain`。 +- [ ] 统计、汇总、多少映射到 `query`。 +- [ ] 生成、创建、发起映射到 `draft` 或 `operate`。 +- [ ] 无法识别时返回低置信度和澄清问题。 + +验收证据: + +- [ ] “查一下本周报销超标风险”能识别为 expense + risk_check。 +- [ ] “客户 A 这个月还有多少应收”能识别为 accounts_receivable + query。 +- [ ] “供应商 B 明天要付多少钱”能识别为 accounts_payable + query。 + +## 5. 解析业务对象 + +- [ ] 从问题中提取员工姓名。 +- [ ] 从问题中提取部门。 +- [ ] 从问题中提取客户。 +- [ ] 从问题中提取供应商。 +- [ ] 从问题中提取项目。 +- [ ] 从问题中提取单据号。 +- [ ] 从问题中提取金额。 +- [ ] 从问题中提取费用类型。 +- [ ] 无法提取时返回空数组,不返回 null。 + +验收证据: + +- [ ] “张三 4 月差旅报销”能提取员工、月份、费用类型。 + +## 6. 解析时间范围 + +- [ ] 支持今天。 +- [ ] 支持昨天。 +- [ ] 支持本周。 +- [ ] 支持上周。 +- [ ] 支持本月。 +- [ ] 支持上月。 +- [ ] 支持本季度。 +- [ ] 支持今年。 +- [ ] 支持明确日期。 +- [ ] 支持日期区间。 +- [ ] 解析结果包含 `start_date` 和 `end_date`。 +- [ ] 日期使用 ISO 格式。 + +验收证据: + +- [ ] “本周”能解析为当前周起止日期。 +- [ ] “2026 年 4 月”能解析为 `2026-04-01` 到 `2026-04-30`。 + +## 7. 解析指标与约束 + +- [ ] 识别金额指标。 +- [ ] 识别数量指标。 +- [ ] 识别超标指标。 +- [ ] 识别逾期指标。 +- [ ] 识别重复报销指标。 +- [ ] 识别部门过滤条件。 +- [ ] 识别状态过滤条件。 +- [ ] 识别金额阈值过滤条件。 +- [ ] 识别排序要求。 +- [ ] 识别 Top N 要求。 + +验收证据: + +- [ ] “列出金额最高的 10 笔报销”能识别排序和 Top 10。 + +## 8. 解析风险与权限 + +- [ ] 重复报销映射到 `duplicate_expense`。 +- [ ] 发票异常映射到 `invoice_anomaly`。 +- [ ] 金额超标映射到 `amount_over_limit`。 +- [ ] 逾期应收映射到 `ar_overdue`。 +- [ ] 逾期应付映射到 `ap_overdue`。 +- [ ] 查询类问题权限为 `read`。 +- [ ] 生成草稿权限为 `draft_write`。 +- [ ] 审批、上线、付款类动作权限为 `approval_required`。 +- [ ] 越权动作权限为 `forbidden`。 + +验收证据: + +- [ ] “帮我直接付款”不能被标为可直接执行。 + +## 9. API 接口 + +- [ ] 新增 `POST /api/ontology/parse`。 +- [ ] 请求参数包含用户问题。 +- [ ] 请求参数包含用户上下文。 +- [ ] 响应包含 8 个字段。 +- [ ] 响应包含 `run_id`。 +- [ ] 响应包含置信度。 +- [ ] 响应包含澄清问题。 +- [ ] 每次调用写入 `SemanticParseLog`。 +- [ ] 每次调用写入 `AgentRun` 或关联已有 `AgentRun`。 + +验收证据: + +- [ ] 连续调用 5 次都能在日志中查到。 + +## 10. 前端调试入口 + +- [ ] 在合适页面增加语义解析调试入口。 +- [ ] 输入框支持自然语言问题。 +- [ ] 点击解析后调用 API。 +- [ ] 展示 8 个字段。 +- [ ] 展示 JSON 原始结果。 +- [ ] 展示置信度。 +- [ ] 展示澄清问题。 +- [ ] 展示 `run_id`。 +- [ ] 错误时展示错误信息。 + +验收证据: + +- [ ] 产品和开发可以直接在页面验证解析结果。 + +## 11. 评测集 + +- [ ] 创建至少 5 条报销问题。 +- [ ] 创建至少 5 条应收问题。 +- [ ] 创建至少 5 条应付问题。 +- [ ] 创建至少 3 条知识库问题。 +- [ ] 创建至少 3 条越权操作问题。 +- [ ] 为每条问题写期望 `scenario`。 +- [ ] 为每条问题写期望 `intent`。 +- [ ] 为每条问题写期望权限级别。 +- [ ] 编写评测脚本或测试。 + +验收证据: + +- [ ] 核心场景识别准确率达到当天设定阈值,例如 80%。 + +## 12. Day 3 验收 + +- [ ] 语义解析 API 可用。 +- [ ] 8 个核心字段完整返回。 +- [ ] 解析日志可查询。 +- [ ] 低置信度问题有澄清问题。 +- [ ] 越权动作不会被标为可执行。 +- [ ] 前端调试入口可用。 +- [ ] 评测集可运行。 +- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 + +## 阻塞记录 + +- [ ] 暂无。 + +## 日终交接 + +- [ ] 写明已支持的关键词。 +- [ ] 写明识别不准的样例。 +- [ ] 写明 Day 4 Orchestrator 可以直接复用的响应结构。 diff --git a/document/development/agent plan/weekly_execution_details/day_4_orchestrator_runtime.md b/document/development/agent plan/weekly_execution_details/day_4_orchestrator_runtime.md new file mode 100644 index 0000000..63783fb --- /dev/null +++ b/document/development/agent plan/weekly_execution_details/day_4_orchestrator_runtime.md @@ -0,0 +1,185 @@ +# Day 4:Orchestrator 运行时 TODO + +本文件是周计划 Day 4 的具体执行细则。路线图见 `document/development/agent week plan/day_4_orchestrator_runtime.md`。 + +目标:建立统一调度层,让用户请求和系统任务都先进入 Orchestrator,再根据语义本体、权限、能力注册路由到 User Agent、Hermes、MCP 或规则引擎。 + +参考文档: + +- `document/development/agent plan/04_orchestrator_and_runtime_flow.md` +- `document/development/agent plan/07_capability_registry.md` +- `document/development/agent plan/08_permission_confirmation.md` +- `document/development/agent plan/09_observability_and_trace.md` + +## 0. 开始前检查 + +- [ ] 确认 Day 3 `POST /api/ontology/parse` 可用。 +- [ ] 确认 `AgentRun` 可创建。 +- [ ] 确认 `AgentToolCall` 可创建。 +- [ ] 确认资产列表能查询技能、MCP、任务。 +- [ ] 确认权限级别枚举已稳定。 +- [ ] 找到后端服务层适合放 Orchestrator 的位置。 + +## 1. Orchestrator 输入输出 + +- [ ] 定义 `OrchestratorRequest`。 +- [ ] 请求包含 `source`。 +- [ ] 请求包含 `user_id`。 +- [ ] 请求包含 `message`。 +- [ ] 请求包含 `task_id`。 +- [ ] 请求包含 `context_json`。 +- [ ] 定义 `OrchestratorResponse`。 +- [ ] 响应包含 `run_id`。 +- [ ] 响应包含 `selected_agent`。 +- [ ] 响应包含 `route_reason`。 +- [ ] 响应包含 `permission_level`。 +- [ ] 响应包含 `status`。 +- [ ] 响应包含 `result`。 +- [ ] 响应包含 `requires_confirmation`。 +- [ ] 响应包含 `trace_summary`。 + +验收证据: + +- [ ] Orchestrator 响应能直接被前端展示。 + +## 2. 建立 Orchestrator 服务 + +- [ ] 新增 `OrchestratorService`。 +- [ ] 实现 `run(request)` 主入口。 +- [ ] 主入口第一步创建 `AgentRun`。 +- [ ] 主入口第二步调用语义解析。 +- [ ] 主入口第三步执行权限判断。 +- [ ] 主入口第四步选择 Agent。 +- [ ] 主入口第五步调用目标 Agent 或返回阻断结果。 +- [ ] 主入口第六步更新 `AgentRun` 状态。 +- [ ] 所有异常都写入 `AgentRun.error_message`。 + +验收证据: + +- [ ] 正常请求状态为 `succeeded`。 +- [ ] 被权限拦截请求状态为 `blocked`。 +- [ ] 异常请求状态为 `failed`。 + +## 3. 路由规则 + +- [ ] `source=user_message` 默认路由到 User Agent。 +- [ ] `source=schedule` 默认路由到 Hermes。 +- [ ] `intent=risk_check` 且来源为 schedule 时路由到 Hermes。 +- [ ] `intent=query` 且来源为 user_message 时路由到 User Agent。 +- [ ] `intent=explain` 路由到 User Agent。 +- [ ] `intent=draft` 路由到 User Agent,但只允许生成草稿。 +- [ ] `permission.level=approval_required` 时设置 `requires_confirmation=true`。 +- [ ] `permission.level=forbidden` 时不调用下游 Agent。 +- [ ] 无法识别时返回澄清问题。 + +验收证据: + +- [ ] 同一句风险检查,在用户入口和任务入口有不同路由结果。 + +## 4. 权限判断 + +- [ ] 新增权限判断服务或函数。 +- [ ] 查询类请求返回 `read`。 +- [ ] 草稿类请求返回 `draft_write`。 +- [ ] 审批、上线、付款类请求返回 `approval_required`。 +- [ ] 用户无权限时返回 `forbidden`。 +- [ ] 高风险动作不允许自动执行。 +- [ ] 需要确认的动作返回确认提示。 +- [ ] 权限判断结果写入 `AgentRun.permission_level`。 + +验收证据: + +- [ ] “直接上线规则”不会被自动执行。 +- [ ] “直接付款”不会被自动执行。 + +## 5. 能力注册查询 + +- [ ] 从 `AgentAsset` 查询 active 技能。 +- [ ] 从 `AgentAsset` 查询 active MCP。 +- [ ] 从 `AgentAsset` 查询 active 任务。 +- [ ] 过滤 disabled 能力。 +- [ ] 过滤未审核 active 条件不满足的规则。 +- [ ] 为每次能力选择记录 `route_json`。 +- [ ] 找不到能力时返回降级说明。 + +验收证据: + +- [ ] 禁用 MCP 不会被 Orchestrator 调用。 + +## 6. 工具调用封装 + +- [ ] 定义统一工具调用接口。 +- [ ] 工具请求前写入 `AgentToolCall` running 或准备记录。 +- [ ] 工具成功后写入响应和耗时。 +- [ ] 工具失败后写入错误。 +- [ ] 外部 MCP 调用失败时返回降级结果。 +- [ ] 数据库查询失败时返回明确错误。 +- [ ] LLM 调用失败时返回可读提示。 + +验收证据: + +- [ ] 每次 Orchestrator 运行至少可以看到 0 到多条工具调用记录。 + +## 7. API 接口 + +- [ ] 新增 `POST /api/orchestrator/run`。 +- [ ] 请求支持用户消息。 +- [ ] 请求支持任务触发。 +- [ ] 响应返回 `run_id`。 +- [ ] 响应返回路由结果。 +- [ ] 响应返回权限结果。 +- [ ] 新增 `GET /api/orchestrator/runs/{run_id}/trace` 或复用 AgentRun 详情接口。 +- [ ] Trace 接口返回语义解析、路由、工具调用、最终结果。 + +验收证据: + +- [ ] 前端或 curl 可以完整看到一次运行链路。 + +## 8. 前端最小 Trace 查看 + +- [ ] 在合适位置展示最近运行记录。 +- [ ] 点击运行记录能查看 `run_id`。 +- [ ] 展示 selected_agent。 +- [ ] 展示 route_reason。 +- [ ] 展示 permission_level。 +- [ ] 展示工具调用列表。 +- [ ] 展示错误信息。 +- [ ] 展示耗时。 + +验收证据: + +- [ ] 开发调试时不需要直接查数据库才能理解路由结果。 + +## 9. 测试 + +- [ ] 测试用户查询路由到 User Agent。 +- [ ] 测试定时任务路由到 Hermes。 +- [ ] 测试 forbidden 不调用下游 Agent。 +- [ ] 测试 approval_required 返回确认。 +- [ ] 测试工具失败写入 ToolCall。 +- [ ] 测试 Orchestrator 异常写入 AgentRun。 + +验收证据: + +- [ ] Orchestrator 核心测试通过。 + +## 10. Day 4 验收 + +- [ ] Orchestrator API 可用。 +- [ ] 用户请求能路由到 User Agent 占位实现。 +- [ ] 定时任务能路由到 Hermes 占位实现。 +- [ ] 权限阻断有效。 +- [ ] 运行 Trace 可查询。 +- [ ] 工具调用日志可查询。 +- [ ] 降级结果可读。 +- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 + +## 阻塞记录 + +- [ ] 暂无。 + +## 日终交接 + +- [ ] 写明路由规则现状。 +- [ ] 写明权限判断现状。 +- [ ] 写明 Day 5 User Agent 需要实现的接口契约。 diff --git a/document/development/agent plan/weekly_execution_details/day_5_user_agent_mvp.md b/document/development/agent plan/weekly_execution_details/day_5_user_agent_mvp.md new file mode 100644 index 0000000..819db73 --- /dev/null +++ b/document/development/agent plan/weekly_execution_details/day_5_user_agent_mvp.md @@ -0,0 +1,185 @@ +# Day 5:User Agent MVP TODO + +本文件是周计划 Day 5 的具体执行细则。路线图见 `document/development/agent week plan/day_5_user_agent_mvp.md`。 + +目标:实现面向用户的自建 Agent。它负责用户提问、流程辅助、规则解释、查询结果解释和草稿生成,不做自动审批、自动付款、自动上线等高风险动作。 + +参考文档: + +- `document/development/agent plan/03_agent_responsibilities.md` +- `document/development/agent plan/04_orchestrator_and_runtime_flow.md` +- `document/development/agent plan/12_llm_wiki_knowledge_architecture.md` +- `document/development/agent plan/13_rule_formation_lifecycle.md` + +## 0. 开始前检查 + +- [ ] 确认 Orchestrator 能把用户请求路由到 User Agent。 +- [ ] 确认语义本体 8 字段可用。 +- [ ] 确认规则资产可查询。 +- [ ] 确认 AgentRun 和 ToolCall 可记录。 +- [ ] 确认是否有现成对话 UI。 +- [ ] 确认财务业务数据是否真实可查。 +- [ ] 如果业务数据不可查,准备最小 Mock 数据服务。 + +## 1. User Agent 输入输出 + +- [ ] 定义 `UserAgentRequest`。 +- [ ] 请求包含 `run_id`。 +- [ ] 请求包含 `user_id`。 +- [ ] 请求包含 `message`。 +- [ ] 请求包含 `ontology`。 +- [ ] 请求包含 `context_json`。 +- [ ] 定义 `UserAgentResponse`。 +- [ ] 响应包含 `answer`。 +- [ ] 响应包含 `citations`。 +- [ ] 响应包含 `suggested_actions`。 +- [ ] 响应包含 `draft_payload`。 +- [ ] 响应包含 `risk_flags`。 +- [ ] 响应包含 `requires_confirmation`。 + +验收证据: + +- [ ] User Agent 响应结构能被 Orchestrator 直接包装返回。 + +## 2. 查询处理 + +- [ ] 实现报销查询处理器。 +- [ ] 实现应收查询处理器。 +- [ ] 实现应付查询处理器。 +- [ ] 查询前检查权限级别。 +- [ ] 查询时记录 ToolCall。 +- [ ] 查询失败时返回可读错误。 +- [ ] 查询为空时返回空态解释。 +- [ ] 查询结果限制返回条数,避免一次返回过大。 + +验收证据: + +- [ ] “查本周报销金额”有可读回答。 +- [ ] “客户 A 本月应收多少”有可读回答。 +- [ ] “供应商 B 待付款多少”有可读回答。 + +## 3. 规则解释 + +- [ ] 根据语义场景查询相关规则资产。 +- [ ] 只引用 active 规则。 +- [ ] 读取规则当前版本 Markdown。 +- [ ] 从 Markdown 中提取规则摘要。 +- [ ] 回答中说明使用了哪些规则。 +- [ ] 回答中包含规则版本号。 +- [ ] 回答中包含规则更新时间。 +- [ ] 没有相关规则时说明缺失。 + +验收证据: + +- [ ] “为什么这笔报销有风险”能引用规则。 + +## 4. 风险解释 + +- [ ] 识别重复报销风险。 +- [ ] 识别金额超标风险。 +- [ ] 识别发票异常风险。 +- [ ] 识别逾期应收风险。 +- [ ] 识别逾期应付风险。 +- [ ] 风险回答包含风险类型。 +- [ ] 风险回答包含触发原因。 +- [ ] 风险回答包含建议处理动作。 +- [ ] 高风险建议不能变成自动执行。 + +验收证据: + +- [ ] 风险解释结果不是单纯“有风险”,而是有依据。 + +## 5. 草稿生成 + +- [ ] 支持生成报销处理意见草稿。 +- [ ] 支持生成应收催收建议草稿。 +- [ ] 支持生成应付付款建议草稿。 +- [ ] 草稿中标明“待人工确认”。 +- [ ] 草稿不直接提交业务系统。 +- [ ] 草稿生成写入审计日志。 +- [ ] 草稿生成写入 AgentRun 结果。 + +验收证据: + +- [ ] “帮我生成处理意见”只返回草稿,不执行审批。 + +## 6. 知识库读取骨架 + +- [ ] 建立知识条目查询接口或服务。 +- [ ] 支持按关键词查询知识条目。 +- [ ] 支持按业务场景查询知识条目。 +- [ ] User Agent 回答可以引用知识条目。 +- [ ] 引用中包含知识标题。 +- [ ] 引用中包含更新时间。 +- [ ] 知识库不可用时返回降级说明。 + +验收证据: + +- [ ] 知识库失败不会导致整个回答失败。 + +## 7. 对话或操作入口 + +- [ ] 前端增加用户问题输入框。 +- [ ] 输入框支持回车或按钮提交。 +- [ ] 提交时调用 Orchestrator,而不是绕过 Orchestrator。 +- [ ] 展示 Agent 回答。 +- [ ] 展示引用规则或知识。 +- [ ] 展示建议动作。 +- [ ] 展示需要人工确认的提示。 +- [ ] 展示 `run_id`。 +- [ ] 展示加载态。 +- [ ] 展示错误态。 + +验收证据: + +- [ ] 用户可在页面完成一次问答闭环。 + +## 8. 安全边界 + +- [ ] User Agent 不直接修改规则状态。 +- [ ] User Agent 不直接上线规则。 +- [ ] User Agent 不直接审批报销。 +- [ ] User Agent 不直接付款。 +- [ ] User Agent 不直接删除知识。 +- [ ] 所有高风险动作只返回建议或草稿。 +- [ ] 所有草稿动作标记 `requires_confirmation=true`。 + +验收证据: + +- [ ] 提示词要求“直接付款”时仍被阻断。 + +## 9. 测试 + +- [ ] 测试报销查询。 +- [ ] 测试应收查询。 +- [ ] 测试应付查询。 +- [ ] 测试规则解释。 +- [ ] 测试风险解释。 +- [ ] 测试草稿生成。 +- [ ] 测试越权动作阻断。 +- [ ] 测试知识库降级。 + +验收证据: + +- [ ] User Agent 核心测试通过。 + +## 10. Day 5 验收 + +- [ ] User Agent 服务可被 Orchestrator 调用。 +- [ ] 用户入口可提交自然语言问题。 +- [ ] 至少 3 个财务场景有回答。 +- [ ] 回答能引用规则或知识。 +- [ ] 高风险动作不会自动执行。 +- [ ] AgentRun Trace 能看到 User Agent 步骤。 +- [ ] 前端构建通过。 +- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 + +## 阻塞记录 + +- [ ] 暂无。 + +## 日终交接 + +- [ ] 写明已支持的问题类型。 +- [ ] 写明仍使用 Mock 的数据。 +- [ ] 写明 Day 6 Hermes 可以复用的规则、风险、知识接口。 diff --git a/document/development/agent plan/weekly_execution_details/day_6_hermes_mvp.md b/document/development/agent plan/weekly_execution_details/day_6_hermes_mvp.md new file mode 100644 index 0000000..f599e29 --- /dev/null +++ b/document/development/agent plan/weekly_execution_details/day_6_hermes_mvp.md @@ -0,0 +1,193 @@ +# Day 6:Hermes MVP TODO + +本文件是周计划 Day 6 的具体执行细则。路线图见 `document/development/agent week plan/day_6_hermes_mvp.md`。 + +目标:实现 Hermes 数字员工的最小闭环。Hermes 不面向用户即时对话,而是负责定时巡检、统计、风险预警、知识维护和规则草稿形成。 + +参考文档: + +- `document/development/agent plan/03_agent_responsibilities.md` +- `document/development/agent plan/11_ocr_invoice_architecture.md` +- `document/development/agent plan/12_llm_wiki_knowledge_architecture.md` +- `document/development/agent plan/15_feedback_learning_loop.md` + +## 0. 开始前检查 + +- [ ] 确认任务资产 `asset_type=task` 可查询。 +- [ ] 确认 Orchestrator 能处理 `source=schedule`。 +- [ ] 确认 Hermes 占位服务可被调用。 +- [ ] 确认 AgentRun 和 ToolCall 可记录。 +- [ ] 确认是否已有后台任务框架。 +- [ ] 如果没有后台任务框架,先用手动触发 API 模拟定时执行。 + +## 1. Hermes 输入输出 + +- [ ] 定义 `HermesTaskRequest`。 +- [ ] 请求包含 `run_id`。 +- [ ] 请求包含 `task_asset_id`。 +- [ ] 请求包含 `task_type`。 +- [ ] 请求包含 `schedule_time`。 +- [ ] 请求包含 `context_json`。 +- [ ] 定义 `HermesTaskResult`。 +- [ ] 响应包含 `summary`。 +- [ ] 响应包含 `risk_items`。 +- [ ] 响应包含 `statistics`。 +- [ ] 响应包含 `knowledge_updates`。 +- [ ] 响应包含 `draft_rules`。 +- [ ] 响应包含 `next_actions`。 + +验收证据: + +- [ ] Hermes 响应能被任务详情或运行日志展示。 + +## 2. 任务调度入口 + +- [ ] 新增手动触发任务 API。 +- [ ] API 参数支持任务资产 ID。 +- [ ] API 调用 Orchestrator,source 为 `schedule`。 +- [ ] Orchestrator 路由到 Hermes。 +- [ ] Hermes 执行结果写入 AgentRun。 +- [ ] 任务执行失败时写入错误。 +- [ ] 任务执行结束后更新任务最近执行时间。 +- [ ] 任务执行结束后更新任务最近执行状态。 + +验收证据: + +- [ ] 可以手动触发一次 Hermes 任务并看到运行结果。 + +## 3. 每日风险巡检 + +- [ ] 实现重复报销巡检。 +- [ ] 实现金额超标巡检。 +- [ ] 实现发票异常巡检占位。 +- [ ] 实现应收逾期巡检。 +- [ ] 实现应付异常付款巡检。 +- [ ] 每个风险项包含风险类型。 +- [ ] 每个风险项包含业务对象。 +- [ ] 每个风险项包含触发规则。 +- [ ] 每个风险项包含建议动作。 +- [ ] 每个风险项包含风险等级。 + +验收证据: + +- [ ] 风险巡检结果可以被用户理解和追溯。 + +## 4. 每日统计 + +- [ ] 统计当日报销单数量。 +- [ ] 统计当日报销金额。 +- [ ] 统计当日报账数量。 +- [ ] 统计当日报账金额。 +- [ ] 统计应收新增金额。 +- [ ] 统计应收逾期金额。 +- [ ] 统计应付待付金额。 +- [ ] 统计应付逾期金额。 +- [ ] 输出日报摘要。 + +验收证据: + +- [ ] Hermes 能生成一份每日财务摘要。 + +## 5. OCR 接入点 + +- [ ] 建立 OCR 识别服务接口。 +- [ ] 定义发票识别输入结构。 +- [ ] 定义发票识别输出结构。 +- [ ] 输出结构包含发票号。 +- [ ] 输出结构包含开票日期。 +- [ ] 输出结构包含金额。 +- [ ] 输出结构包含税额。 +- [ ] 输出结构包含销售方。 +- [ ] 输出结构包含购买方。 +- [ ] 输出结构包含置信度。 +- [ ] 当前阶段允许使用 Mock 结果。 +- [ ] OCR 调用写入 ToolCall。 + +验收证据: + +- [ ] Hermes 风险巡检中可以调用 OCR Mock。 + +## 6. 知识库维护 + +- [ ] 建立知识条目写入服务。 +- [ ] Hermes 可以生成知识候选条目。 +- [ ] 候选条目包含标题。 +- [ ] 候选条目包含正文。 +- [ ] 候选条目包含来源。 +- [ ] 候选条目包含适用场景。 +- [ ] 候选条目默认状态为 `draft`。 +- [ ] 知识条目不能自动发布。 +- [ ] 知识条目写入审计日志。 + +验收证据: + +- [ ] Hermes 可以生成待审核知识条目。 + +## 7. 规则草稿形成 + +- [ ] Hermes 可以根据风险巡检结果生成规则草稿。 +- [ ] 规则草稿保存为 `asset_type=rule`。 +- [ ] 规则草稿状态为 `draft`。 +- [ ] 规则草稿包含 Markdown 内容。 +- [ ] 规则草稿包含生成原因。 +- [ ] 规则草稿包含关联风险样例。 +- [ ] 规则草稿不能自动上线。 +- [ ] 规则草稿需要审核人。 +- [ ] 规则草稿写入审计日志。 + +验收证据: + +- [ ] Hermes 生成的新规则出现在规则列表中,但不是 active。 + +## 8. Hermes 页面或日志展示 + +- [ ] 任务详情能看到最近执行结果。 +- [ ] 任务详情能手动触发执行。 +- [ ] 任务详情能看到风险项数量。 +- [ ] 任务详情能看到日报摘要。 +- [ ] 任务详情能看到知识候选数量。 +- [ ] 任务详情能看到规则草稿数量。 +- [ ] 运行 Trace 能看到 Hermes 步骤。 +- [ ] 错误时展示错误原因。 + +验收证据: + +- [ ] 不查数据库也能判断 Hermes 是否执行成功。 + +## 9. 测试 + +- [ ] 测试手动触发任务。 +- [ ] 测试 Orchestrator 路由到 Hermes。 +- [ ] 测试风险巡检输出。 +- [ ] 测试日报统计输出。 +- [ ] 测试 OCR Mock 调用。 +- [ ] 测试知识候选写入。 +- [ ] 测试规则草稿生成。 +- [ ] 测试 Hermes 异常写入 AgentRun。 + +验收证据: + +- [ ] Hermes 核心测试通过。 + +## 10. Day 6 验收 + +- [ ] Hermes 可被 Orchestrator 调用。 +- [ ] 至少一个任务可以手动触发。 +- [ ] 风险巡检有结构化结果。 +- [ ] 每日统计有结构化结果。 +- [ ] OCR Mock 接入点可用。 +- [ ] 知识候选可生成。 +- [ ] 规则草稿可生成且不能自动上线。 +- [ ] 任务详情或运行日志能展示结果。 +- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 + +## 阻塞记录 + +- [ ] 暂无。 + +## 日终交接 + +- [ ] 写明 Hermes 已支持任务类型。 +- [ ] 写明 OCR 当前是真实还是 Mock。 +- [ ] 写明生成的知识和规则草稿状态。 +- [ ] 写明 Day 7 需要重点回归的路径。 diff --git a/document/development/agent plan/weekly_execution_details/day_7_hardening_demo_acceptance.md b/document/development/agent plan/weekly_execution_details/day_7_hardening_demo_acceptance.md new file mode 100644 index 0000000..ad9845f --- /dev/null +++ b/document/development/agent plan/weekly_execution_details/day_7_hardening_demo_acceptance.md @@ -0,0 +1,225 @@ +# Day 7:加固、演示和验收 TODO + +本文件是周计划 Day 7 的具体执行细则。路线图见 `document/development/agent week plan/day_7_hardening_demo_acceptance.md`。 + +目标:把前 6 天做出的功能整理成可演示、可验收、可继续迭代的基础平台。Day 7 不再大规模扩功能,重点是修缺口、补测试、补日志、补文档、完成演示链路。 + +参考文档: + +- `document/development/agent plan/00_README.md` +- `document/development/agent plan/05_development_roadmap.md` +- `document/development/agent plan/09_observability_and_trace.md` +- `document/development/agent plan/10_evaluation_and_testset.md` + +## 0. 开始前检查 + +- [ ] 汇总 Day 1 未完成项。 +- [ ] 汇总 Day 2 未完成项。 +- [ ] 汇总 Day 3 未完成项。 +- [ ] 汇总 Day 4 未完成项。 +- [ ] 汇总 Day 5 未完成项。 +- [ ] 汇总 Day 6 未完成项。 +- [ ] 标记必须今天修复的问题。 +- [ ] 标记可以进入下一阶段的问题。 +- [ ] 冻结新增需求,只处理验收相关问题。 + +## 1. 核心链路回归 + +- [ ] 回归资产列表接口。 +- [ ] 回归规则详情接口。 +- [ ] 回归 Markdown 保存。 +- [ ] 回归版本列表。 +- [ ] 回归版本切换。 +- [ ] 回归审核接口。 +- [ ] 回归上线拦截。 +- [ ] 回归语义解析接口。 +- [ ] 回归 Orchestrator 路由。 +- [ ] 回归 User Agent 问答。 +- [ ] 回归 Hermes 任务执行。 +- [ ] 回归 AgentRun Trace。 +- [ ] 回归 ToolCall 日志。 +- [ ] 回归 AuditLog 日志。 + +验收证据: + +- [ ] 从前端能完成至少一条端到端演示路径。 + +## 2. 权限和风险边界 + +- [ ] 未审核规则不能上线。 +- [ ] rejected 规则不能上线。 +- [ ] disabled 能力不能被调用。 +- [ ] 用户请求付款必须拦截。 +- [ ] 用户请求审批必须需要确认。 +- [ ] Hermes 生成规则只能是 draft。 +- [ ] Hermes 生成知识只能是 draft。 +- [ ] User Agent 生成处理意见只能是草稿。 +- [ ] 所有高风险动作响应中包含 `requires_confirmation`。 + +验收证据: + +- [ ] 不存在 MVP 期间绕过人工审核的路径。 + +## 3. 审计和 Trace 补齐 + +- [ ] 规则保存写 AuditLog。 +- [ ] 规则审核写 AuditLog。 +- [ ] 规则上线写 AuditLog。 +- [ ] Hermes 生成规则草稿写 AuditLog。 +- [ ] Hermes 生成知识候选写 AuditLog。 +- [ ] User Agent 草稿生成写 AuditLog。 +- [ ] Orchestrator 每次运行有 AgentRun。 +- [ ] 每次工具调用有 ToolCall。 +- [ ] Trace 页面或接口能串起 run_id。 +- [ ] 错误 Trace 包含 error_message。 + +验收证据: + +- [ ] 任意一条演示链路都能追溯到 run_id。 + +## 4. 前端体验修补 + +- [ ] 任务规则中心列表无明显错位。 +- [ ] 详情页无双 title。 +- [ ] Hero title 高度紧凑。 +- [ ] 返回列表栏高度正常。 +- [ ] Markdown 编辑器和版本卡片底部对齐。 +- [ ] 版本卡片不贴右侧。 +- [ ] 当前版本标识不突兀。 +- [ ] 日期列对齐。 +- [ ] 弹窗文案清楚。 +- [ ] 加载态可见。 +- [ ] 错误态可见。 +- [ ] 空态可见。 +- [ ] 按钮禁用态可见。 +- [ ] 窄屏不出现内容重叠。 + +验收证据: + +- [ ] 任务规则中心可以给业务用户演示,不需要解释 UI 异常。 + +## 5. 测试补齐 + +- [ ] 运行后端现有测试。 +- [ ] 运行新增模型测试。 +- [ ] 运行新增 API 测试。 +- [ ] 运行语义解析测试。 +- [ ] 运行 Orchestrator 测试。 +- [ ] 运行 User Agent 测试。 +- [ ] 运行 Hermes 测试。 +- [ ] 运行前端构建。 +- [ ] 如果有前端测试,运行前端测试。 +- [ ] 记录未能运行的测试和原因。 + +验收证据: + +- [ ] 测试结果写入本文件“测试记录”。 + +## 6. 评测集 + +- [ ] 准备 5 条报销问题。 +- [ ] 准备 5 条应收问题。 +- [ ] 准备 5 条应付问题。 +- [ ] 准备 3 条规则解释问题。 +- [ ] 准备 3 条越权动作问题。 +- [ ] 执行语义解析评测。 +- [ ] 执行 User Agent 回答评测。 +- [ ] 执行权限拦截评测。 +- [ ] 记录失败样例。 +- [ ] 为失败样例写下一阶段优化建议。 + +验收证据: + +- [ ] 可以说明 MVP 当前能力边界和准确率风险。 + +## 7. 演示数据 + +- [ ] 准备 active 规则。 +- [ ] 准备 pending 规则。 +- [ ] 准备 rejected 规则。 +- [ ] 准备至少一条报销数据。 +- [ ] 准备至少一条应收数据。 +- [ ] 准备至少一条应付数据。 +- [ ] 准备至少一个 Hermes 任务。 +- [ ] 准备至少一个 MCP Mock。 +- [ ] 准备至少一个知识条目。 +- [ ] 准备至少一个风险样例。 + +验收证据: + +- [ ] 演示不会因为没有数据而中断。 + +## 8. 演示脚本 + +- [ ] 编写演示步骤 1:打开任务规则中心。 +- [ ] 编写演示步骤 2:查看规则详情。 +- [ ] 编写演示步骤 3:编辑 Markdown 并保存。 +- [ ] 编写演示步骤 4:切换版本。 +- [ ] 编写演示步骤 5:尝试上线未审核规则并被拦截。 +- [ ] 编写演示步骤 6:输入用户问题。 +- [ ] 编写演示步骤 7:查看语义本体结果。 +- [ ] 编写演示步骤 8:查看 User Agent 回答。 +- [ ] 编写演示步骤 9:手动触发 Hermes 任务。 +- [ ] 编写演示步骤 10:查看 AgentRun Trace。 +- [ ] 编写演示步骤 11:查看审计日志。 + +验收证据: + +- [ ] 新开发者按脚本可以复现演示。 + +## 9. 文档收尾 + +- [ ] 更新一周计划完成情况。 +- [ ] 更新剩余风险。 +- [ ] 更新下一阶段开发建议。 +- [ ] 更新接口清单。 +- [ ] 更新数据模型清单。 +- [ ] 更新前端页面清单。 +- [ ] 更新评测结果。 +- [ ] 更新演示脚本。 +- [ ] 更新部署或启动说明。 + +验收证据: + +- [ ] 文档能指导下一周继续开发。 + +## 10. 最终验收清单 + +- [ ] 任务规则中心可查看规则、技能、MCP、任务。 +- [ ] 规则详情可编辑 Markdown。 +- [ ] 规则详情可查看最近 5 个版本。 +- [ ] 版本切换有确认弹窗。 +- [ ] 审核者信息可见。 +- [ ] 未审核规则不能上线。 +- [ ] 语义本体 8 字段可返回。 +- [ ] Orchestrator 能路由用户请求。 +- [ ] Orchestrator 能路由定时任务。 +- [ ] User Agent 能回答至少 3 类财务问题。 +- [ ] Hermes 能执行至少 1 个任务。 +- [ ] OCR Mock 接入点可用。 +- [ ] 知识候选可生成。 +- [ ] 规则草稿可生成。 +- [ ] AgentRun Trace 可查。 +- [ ] AuditLog 可查。 +- [ ] 前端构建通过。 +- [ ] 后端核心测试通过。 +- [ ] 演示脚本可执行。 +- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 + +## 测试记录 + +- [ ] 后端测试:未运行。 +- [ ] 前端构建:未运行。 +- [ ] 语义评测:未运行。 +- [ ] 手动验收:未运行。 + +## 阻塞记录 + +- [ ] 暂无。 + +## 日终交接 + +- [ ] 写明本周最终完成内容。 +- [ ] 写明未完成内容。 +- [ ] 写明生产化前必须补齐内容。 +- [ ] 写明下一周建议优先级。 diff --git a/document/development/agent week plan/00_README.md b/document/development/agent week plan/00_README.md index 9289224..5bdd0ef 100644 --- a/document/development/agent week plan/00_README.md +++ b/document/development/agent week plan/00_README.md @@ -1,105 +1,51 @@ -# Agent Week Plan 一周开发计划 +# Agent Week Plan 一周开发路线图 -本目录是在 `document/development/agent plan` 架构文档基础上拆出的 7 天生产标准开发计划。 +本目录是 `agent_weekly_plan` 的路线图层,负责回答“这一周每天大概做什么、为什么这样排、每天交付什么”。 -这版文档不是概念说明,而是给 Codex 或开发人员逐项执行的 TODO 手册。执行时必须按顺序推进,每完成一项就在对应文档中标记。 +具体到字段、接口、服务、前端、测试和验收步骤的执行清单,不放在这里,而是放在: -## 执行标记规则 +[../agent plan/weekly_execution_details/README.md](<../agent plan/weekly_execution_details/README.md>) -未完成: +## 两层文档分工 -```md -- [ ] 建立 AgentAsset 数据模型 -``` +| 目录 | 职责 | 读者 | +| --- | --- | --- | +| `agent week plan` | 7 天路线图,每天只讲大开发点、交付物、验收门槛 | 产品、架构、排期、开发启动前 | +| `agent plan` | 架构设计、协议、流程、每天具体执行 TODO | Codex、开发人员、评审人员 | -完成后: +## 使用方式 -```md -- [x] ~~建立 AgentAsset 数据模型~~ -``` +1. 先读 [MASTER_TODO.md](./MASTER_TODO.md),确认 7 天节奏。 +2. 进入当天 daily 文档,看当天主题、交付结果和验收门槛。 +3. 点击 daily 中的“对应执行细则”,进入 `agent plan/weekly_execution_details`。 +4. 在执行细则中按 `- [ ]` 一项一项开发。 +5. 完成细则后回到 daily 文档,确认当天是否达到交付门槛。 -执行要求: +## 一周总体目标 -- [ ] 每次只处理一个最小 TODO。 -- [ ] 完成后先自测,再改成 `[x]`。 -- [ ] 改成 `[x]` 时,同时用 `~~` 画线。 -- [ ] 不能因为代码写完就标完成,必须满足该 TODO 的验收证据。 -- [ ] 遇到阻塞时,在当天文档的“阻塞记录”下新增一条说明。 -- [ ] 每天收尾时更新当天文档的“日终交接”。 +- Day 1:先把资产、版本、审核、运行日志、审计日志等基础地基建好。 +- Day 2:把任务规则中心和后端资产体系打通。 +- Day 3:建立语义本体 MVP,让用户问题能变成稳定结构。 +- Day 4:建立 Orchestrator,让请求能被统一路由、审计、降级。 +- Day 5:建立 User Agent MVP,处理用户查询、解释和草稿生成。 +- Day 6:建立 Hermes MVP,处理定时巡检、统计、知识和规则草稿。 +- Day 7:做加固、测试、演示、验收和下一阶段交接。 -## 文档顺序 +## 一周暂不完成 -先看总控清单,再进入每天的执行文档: - -1. [MASTER_TODO.md](./MASTER_TODO.md) -2. [day_1_foundation_models.md](./day_1_foundation_models.md) -3. [day_2_rule_center_integration.md](./day_2_rule_center_integration.md) -4. [day_3_semantic_ontology_mvp.md](./day_3_semantic_ontology_mvp.md) -5. [day_4_orchestrator_runtime.md](./day_4_orchestrator_runtime.md) -6. [day_5_user_agent_mvp.md](./day_5_user_agent_mvp.md) -7. [day_6_hermes_mvp.md](./day_6_hermes_mvp.md) -8. [day_7_hardening_demo_acceptance.md](./day_7_hardening_demo_acceptance.md) - -## 一周总目标 - -- [ ] 建立规则、技能、MCP、任务的统一资产模型。 -- [ ] 建立规则 Markdown 内容、版本、审核、上线状态的闭环。 -- [ ] 建立语义本体 8 字段解析接口。 -- [ ] 建立 Orchestrator 路由和 Agent Run Trace。 -- [ ] 建立 User Agent 的查询、解释、流程辅助 MVP。 -- [ ] 建立 Hermes 的定时风险巡检和日报 MVP。 -- [ ] 建立基础权限分级、人工确认、审计日志。 -- [ ] 建立最小评测集、手动验收脚本和演示流程。 - -## 一周暂不做 - -- [ ] 不做完整 OCR 生产识别引擎,只预留标准接口和 Mock 结果。 -- [ ] 不做完整发票验真 MCP 深度接入,只做能力注册和 Mock 调用。 -- [ ] 不做完整 LLM Wiki 向量检索,只做知识条目写入和读取骨架。 -- [ ] 不做所有财务域数据全量打通,只覆盖报销、应收、应付的最小字段。 -- [ ] 不做规则自动上线,规则只能生成草稿,必须人工审核。 -- [ ] 不做完整 CI/CD,只做本地构建、核心测试和验收脚本。 +- 完整 OCR 生产识别引擎。 +- 完整发票验真 MCP 深度接入。 +- 完整 LLM Wiki 向量检索。 +- 全量财务域数据打通。 +- 规则自动上线。 +- 完整 CI/CD 质量门禁。 ## 生产底线 -以下底线不得被 MVP 名义绕过: - -- [ ] 所有写操作必须记录审计日志。 -- [ ] 所有 Agent 执行必须生成 `run_id`。 -- [ ] 所有规则必须有版本。 -- [ ] 未审核规则不能上线。 -- [ ] 高风险动作只能生成草稿或建议,不能自动提交。 -- [ ] 外部服务失败必须有降级结果。 -- [ ] 语义解析结果必须落库或落日志,便于回放。 -- [ ] 前端不能只写静态 UI,必须至少对接 Mock 或真实 API。 - -## 每日固定流程 - -上午: - -- [ ] 读取当天文档。 -- [ ] 检查前一天遗留阻塞。 -- [ ] 确认数据库模型、API、服务边界。 -- [ ] 完成后端主路径。 - -下午: - -- [ ] 完成前端联调。 -- [ ] 接入 Agent 或 Orchestrator 流程。 -- [ ] 完成权限、审计、错误态。 - -傍晚: - -- [ ] 运行测试和构建。 -- [ ] 按当天验收清单逐项验收。 -- [ ] 更新 TODO 完成状态。 -- [ ] 填写日终交接。 - -## Codex 执行约束 - -- [ ] 修改代码前先读相关文件,不凭空创建重复模块。 -- [ ] 优先复用现有 FastAPI、SQLAlchemy、Vue、PrimeVue 写法。 -- [ ] API 命名必须稳定,不能一天一个风格。 -- [ ] 数据模型新增字段必须写清楚用途。 -- [ ] 前端状态、空态、错误态、加载态都要覆盖。 -- [ ] 每天结束必须能给出可运行证据。 +- 所有写操作必须有审计日志。 +- 所有 Agent 执行必须生成 `run_id`。 +- 所有规则必须有版本。 +- 未审核规则不能上线。 +- 高风险动作只能生成草稿或建议,不能自动提交。 +- 外部能力失败必须有降级结果。 +- 语义解析结果必须可回放。 diff --git a/document/development/agent week plan/MASTER_TODO.md b/document/development/agent week plan/MASTER_TODO.md index c66f9de..07b5f29 100644 --- a/document/development/agent week plan/MASTER_TODO.md +++ b/document/development/agent week plan/MASTER_TODO.md @@ -1,119 +1,46 @@ -# Agent Week Plan 总控 TODO +# Agent Week Plan 总览 -本文件用于控制 7 天开发顺序。每个大项完成后,再进入对应天的详细文档。 +本文件只放一周路线图,不放细节 TODO。具体执行步骤在 `agent plan/weekly_execution_details`。 -完成标记规则: +## 快速浏览 -```md -- [x] ~~已完成的任务~~ -``` +- HTML 总览:[agent_week_plan_html/index.html](<../agent_week_plan_html/index.html>) +- Day 1 HTML:[agent_week_plan_html/day-1.html](<../agent_week_plan_html/day-1.html>) -## Day 1:基础模型与工程骨架 +## 一周节奏 -- [ ] 阅读 `document/development/agent plan/01_overall_architecture.md`。 -- [ ] 阅读 `document/development/agent plan/02_semantic_ontology.md`。 -- [ ] 阅读 `document/development/agent plan/06_data_contracts_and_governance.md`。 -- [ ] 阅读 `document/development/agent plan/07_capability_registry.md`。 -- [ ] 阅读 `document/development/agent plan/08_permission_confirmation.md`。 -- [ ] 阅读 `document/development/agent plan/09_observability_and_trace.md`。 -- [ ] 完成统一资产模型 `AgentAsset`。 -- [ ] 完成资产版本模型 `AgentAssetVersion`。 -- [ ] 完成资产审核模型 `AgentAssetReview`。 -- [ ] 完成 Agent 运行日志模型 `AgentRun`。 -- [ ] 完成工具调用日志模型 `AgentToolCall`。 -- [ ] 完成语义解析日志模型 `SemanticParseLog`。 -- [ ] 完成审计日志模型 `AuditLog`。 -- [ ] 完成基础 API 路由骨架。 -- [ ] 完成种子数据。 -- [ ] 完成 Day 1 验收。 +| Day | 状态 | 主题 | 主要交付 | 快速视图 | 对应执行细则 | +| --- | --- | --- | --- | --- | --- | +| Day 1 | 已完成(2026-05-11) | 基础模型与工程骨架 | 资产、版本、审核、运行日志、审计日志、基础 API、最小财务数据源 | [HTML](<../agent_week_plan_html/day-1.html>) | [细则](<../agent plan/weekly_execution_details/day_1_foundation_models.md>) | +| Day 2 | 未开始 | 任务规则中心联调 | 规则/技能/MCP/任务列表与详情、Markdown、版本、审核 | [HTML](<../agent_week_plan_html/day-2.html>) | [细则](<../agent plan/weekly_execution_details/day_2_rule_center_integration.md>) | +| Day 3 | 未开始 | 语义本体 MVP | 8 字段语义解析、日志、评测入口 | [HTML](<../agent_week_plan_html/day-3.html>) | [细则](<../agent plan/weekly_execution_details/day_3_semantic_ontology_mvp.md>) | +| Day 4 | 未开始 | Orchestrator 运行时 | 统一入口、路由、权限、工具调用、Trace | [HTML](<../agent_week_plan_html/day-4.html>) | [细则](<../agent plan/weekly_execution_details/day_4_orchestrator_runtime.md>) | +| Day 5 | 未开始 | User Agent MVP | 用户问答、财务查询、规则解释、草稿生成 | [HTML](<../agent_week_plan_html/day-5.html>) | [细则](<../agent plan/weekly_execution_details/day_5_user_agent_mvp.md>) | +| Day 6 | 未开始 | Hermes MVP | 定时任务、风险巡检、日报、知识候选、规则草稿 | [HTML](<../agent_week_plan_html/day-6.html>) | [细则](<../agent plan/weekly_execution_details/day_6_hermes_mvp.md>) | +| Day 7 | 未开始 | 加固、演示和验收 | 回归、测试、演示脚本、交付说明 | [HTML](<../agent_week_plan_html/day-7.html>) | [细则](<../agent plan/weekly_execution_details/day_7_hardening_demo_acceptance.md>) | -## Day 2:任务规则中心联调 +## 当前完成情况 -- [ ] 阅读 `document/development/agent plan/13_rule_formation_lifecycle.md`。 -- [ ] 阅读 `document/development/agent plan/07_capability_registry.md`。 -- [ ] 对接规则、技能、MCP、任务资产列表 API。 -- [ ] 对接资产详情 API。 -- [ ] 对接规则 Markdown 读取和保存 API。 -- [ ] 对接版本列表和版本切换 API。 -- [ ] 对接审核者信息和审核状态。 -- [ ] 对接规则上线前审核拦截。 -- [ ] 完成前端筛选、搜索、详情、弹窗状态。 -- [ ] 完成 Day 2 验收。 +- Day 1 已完成,后端基础模型、审计和最小财务数据源已可供 Day 2 前端联调使用。 +- Day 2 到 Day 7 保持原排期,下一步直接进入规则中心联调。 -## Day 3:语义本体 MVP +## 关键依赖顺序 -- [ ] 阅读 `document/development/agent plan/02_semantic_ontology.md`。 -- [ ] 阅读 `document/development/agent plan/14_financial_document_canonical_model.md`。 -- [ ] 定义 8 个核心字段的数据结构。 -- [ ] 实现语义解析服务。 -- [ ] 实现语义解析 API。 -- [ ] 实现解析日志保存。 -- [ ] 实现场景、意图、对象、时间、指标、约束、风险、权限字段。 -- [ ] 接入 User Agent 查询入口。 -- [ ] 完成最小评测集。 -- [ ] 完成 Day 3 验收。 +1. Day 1 必须先完成,因为后面所有能力都依赖资产、版本、审核、日志。 +2. Day 2 必须在 Day 3 前完成,因为语义和 Agent 需要读取规则、技能、MCP、任务资产。 +3. Day 3 必须在 Day 4 前完成,因为 Orchestrator 依赖语义本体做路由。 +4. Day 4 必须在 Day 5 / Day 6 前完成,因为 User Agent 和 Hermes 都应该由 Orchestrator 调用。 +5. Day 5 和 Day 6 可以部分并行,但都必须遵守权限、审计、Trace。 +6. Day 7 不新增大功能,只做加固、验收和交接。 -## Day 4:Orchestrator 运行时 +## 最终验收 -- [ ] 阅读 `document/development/agent plan/04_orchestrator_and_runtime_flow.md`。 -- [ ] 阅读 `document/development/agent plan/08_permission_confirmation.md`。 -- [ ] 阅读 `document/development/agent plan/09_observability_and_trace.md`。 -- [ ] 实现 Orchestrator 入口服务。 -- [ ] 实现语义本体到 Agent 路由。 -- [ ] 实现权限级别判断。 -- [ ] 实现工具调用封装。 -- [ ] 实现运行 Trace。 -- [ ] 实现降级和错误返回。 -- [ ] 完成 Day 4 验收。 - -## Day 5:User Agent MVP - -- [ ] 阅读 `document/development/agent plan/03_agent_responsibilities.md`。 -- [ ] 阅读 `document/development/agent plan/12_llm_wiki_knowledge_architecture.md`。 -- [ ] 实现用户自然语言入口。 -- [ ] 实现报销查询解释流程。 -- [ ] 实现应收账款查询解释流程。 -- [ ] 实现应付账款查询解释流程。 -- [ ] 实现规则引用解释。 -- [ ] 实现建议草稿输出。 -- [ ] 完成前端对话或操作入口。 -- [ ] 完成 Day 5 验收。 - -## Day 6:Hermes MVP - -- [ ] 阅读 `document/development/agent plan/03_agent_responsibilities.md`。 -- [ ] 阅读 `document/development/agent plan/11_ocr_invoice_architecture.md`。 -- [ ] 阅读 `document/development/agent plan/15_feedback_learning_loop.md`。 -- [ ] 实现任务资产调度入口。 -- [ ] 实现每日风险巡检任务。 -- [ ] 实现每日报销/报账/账款统计任务。 -- [ ] 实现知识库维护任务。 -- [ ] 实现 OCR Mock 接入点。 -- [ ] 实现 Hermes 运行结果面板或 API。 -- [ ] 完成 Day 6 验收。 - -## Day 7:加固、演示和验收 - -- [ ] 回归 Day 1 到 Day 6 所有核心路径。 -- [ ] 补齐权限拦截。 -- [ ] 补齐审计日志。 -- [ ] 补齐错误态和空态。 -- [ ] 补齐评测用例。 -- [ ] 补齐演示数据。 -- [ ] 完成构建。 -- [ ] 完成一周交付说明。 -- [ ] 完成 Day 7 验收。 - -## 一周最终验收 - -- [ ] 能从任务规则中心看到规则、技能、MCP、任务。 -- [ ] 能打开规则详情,编辑 Markdown,查看版本,切换版本。 -- [ ] 未审核规则不能上线。 -- [ ] 能输入一句自然语言问题并得到语义本体 8 字段结果。 -- [ ] 能由 Orchestrator 路由到 User Agent。 -- [ ] 能由 Hermes 执行一次模拟定时任务。 -- [ ] 能查看 Agent Run Trace。 -- [ ] 能查看工具调用日志。 -- [ ] 能查看审计日志。 -- [ ] 能运行核心测试。 -- [ ] 能完成演示脚本。 +- 任务规则中心能看到规则、技能、MCP、任务。 +- 规则详情能编辑 Markdown、查看最近 5 个版本、切换版本。 +- 未审核规则不能上线。 +- 用户问题能解析出语义本体 8 字段。 +- Orchestrator 能路由到 User Agent 和 Hermes。 +- User Agent 能完成查询、解释、草稿生成。 +- Hermes 能执行一次风险巡检或日报任务。 +- AgentRun、ToolCall、AuditLog 都能追溯。 +- 有演示脚本和下一阶段交接文档。 diff --git a/document/development/agent week plan/day_1_foundation_models.md b/document/development/agent week plan/day_1_foundation_models.md index 23b3dcc..90b8a48 100644 --- a/document/development/agent week plan/day_1_foundation_models.md +++ b/document/development/agent week plan/day_1_foundation_models.md @@ -1,316 +1,74 @@ -# Day 1:基础模型与工程骨架 TODO +# Day 1:基础模型与工程骨架 -目标:建立后续 6 天开发所需的后端地基。Day 1 不做复杂业务逻辑,只做稳定模型、API 骨架、种子数据、基础审计和可运行验证。 +## 当前状态 -参考文档: +- [x] ~~Day 1 已完成(2026-05-11)。~~ +- [x] ~~后端基础模型、API 骨架、种子数据、审计能力和 Day 2 联调入口均已落地。~~ -- `document/development/agent plan/01_overall_architecture.md` -- `document/development/agent plan/02_semantic_ontology.md` -- `document/development/agent plan/06_data_contracts_and_governance.md` -- `document/development/agent plan/07_capability_registry.md` -- `document/development/agent plan/08_permission_confirmation.md` -- `document/development/agent plan/09_observability_and_trace.md` +## 今天的大开发点 -## 0. 开始前检查 +Day 1 只做地基,不做复杂 Agent 智能。 -- [ ] 确认当前分支和工作区状态。 -- [ ] 确认后端目录位置,例如 `/app/server`。 -- [ ] 确认前端目录位置,例如 `/app/web`。 -- [ ] 确认后端使用的框架、ORM、迁移方式。 -- [ ] 找到现有数据库模型目录。 -- [ ] 找到现有 API 路由目录。 -- [ ] 找到现有启动入口。 -- [ ] 找到现有测试目录。 -- [ ] 找到现有种子数据或初始化脚本。 -- [ ] 记录不应修改的无关文件。 +核心是把后面 6 天都会用到的基础对象建出来:资产、版本、审核、运行日志、工具调用日志、语义解析日志、审计日志,以及最小财务业务数据来源。 -## 1. 统一命名和边界 +## 为什么第一天做这个 -- [ ] 确认统一模块名使用 `agent_assets`。 -- [ ] 确认资产类型枚举为 `rule`、`skill`、`mcp`、`task`。 -- [ ] 确认资产状态枚举为 `draft`、`review`、`active`、`disabled`。 -- [ ] 确认审核状态枚举为 `pending`、`approved`、`rejected`。 -- [ ] 确认 Agent 枚举为 `orchestrator`、`user_agent`、`hermes`。 -- [ ] 确认运行来源枚举为 `user_message`、`schedule`、`system_event`。 -- [ ] 确认权限级别枚举为 `read`、`draft_write`、`approval_required`、`forbidden`。 -- [ ] 确认所有主键、外键、时间字段命名符合现有代码风格。 +如果没有稳定的数据模型,后面的任务规则中心、语义本体、Orchestrator、User Agent、Hermes 都会各自临时造结构,后期会很难合并。 -验收证据: +## 今天主要交付 -- [ ] 枚举命名在模型、Schema、服务层保持一致。 -- [ ] 没有同时出现 `schedule` 和 `task` 两套用户可见命名。 +- [x] ~~统一资产模型:规则、技能、MCP、任务。~~ +- [x] ~~版本模型:规则 Markdown 和其他资产配置快照。~~ +- [x] ~~审核模型:未审核不能上线。~~ +- [x] ~~Agent 运行日志:所有 Agent 执行都有 `run_id`。~~ +- [x] ~~工具调用日志:MCP、数据库、LLM、OCR、规则引擎调用都可追踪。~~ +- [x] ~~语义解析日志:后续语义本体结果可回放。~~ +- [x] ~~审计日志:所有写操作可追责。~~ +- [x] ~~最小财务业务数据来源:报销、应收、应付。~~ -## 2. 建立 AgentAsset 模型 +## 实际落地结果 -- [ ] 新增或扩展模型文件,定义 `AgentAsset`。 -- [ ] 增加字段 `id`。 -- [ ] 增加字段 `asset_type`,取值 `rule | skill | mcp | task`。 -- [ ] 增加字段 `code`,作为业务编码。 -- [ ] 增加字段 `name`。 -- [ ] 增加字段 `description`。 -- [ ] 增加字段 `domain`,例如 `expense | ar | ap | knowledge | system`。 -- [ ] 增加字段 `scenario_json`,保存适用场景。 -- [ ] 增加字段 `owner`。 -- [ ] 增加字段 `reviewer`。 -- [ ] 增加字段 `status`。 -- [ ] 增加字段 `current_version`。 -- [ ] 增加字段 `config_json`。 -- [ ] 增加字段 `created_at`。 -- [ ] 增加字段 `updated_at`。 -- [ ] 给 `code` 增加唯一约束。 -- [ ] 给 `asset_type` 增加索引。 -- [ ] 给 `status` 增加索引。 -- [ ] 给 `domain` 增加索引。 +- [x] ~~新增 `AgentAsset`、`AgentAssetVersion`、`AgentAssetReview`、`AgentRun`、`AgentToolCall`、`SemanticParseLog`、`AuditLog`、`ExpenseClaim`、`ExpenseClaimItem`、`AccountsReceivableRecord`、`AccountsPayableRecord`。~~ +- [x] ~~新增 `/api/v1/agent-assets`、`/api/v1/agent-runs`、`/api/v1/audit-logs` 相关接口。~~ +- [x] ~~种子数据已覆盖 3 条规则、2 条技能、2 条 MCP、3 条任务,以及报销 / 应收 / 应付示例数据。~~ +- [x] ~~旧开发库启动时会自动补齐新增资产和版本,不需要手动清库。~~ -验收证据: +## 对应执行细则 -- [ ] 能创建一条规则资产。 -- [ ] 能创建一条技能资产。 -- [ ] 能创建一条 MCP 资产。 -- [ ] 能创建一条任务资产。 +- [Day 1 执行细则](<../agent plan/weekly_execution_details/day_1_foundation_models.md>) -## 3. 建立 AgentAssetVersion 模型 +相关架构文档: -- [ ] 定义 `AgentAssetVersion`。 -- [ ] 增加字段 `id`。 -- [ ] 增加字段 `asset_id`。 -- [ ] 增加字段 `version`,例如 `v1.0.0`。 -- [ ] 增加字段 `content`。 -- [ ] 增加字段 `content_type`,取值 `markdown | json`。 -- [ ] 增加字段 `change_note`。 -- [ ] 增加字段 `created_by`。 -- [ ] 增加字段 `created_at`。 -- [ ] 增加 `asset_id + version` 唯一约束。 -- [ ] 建立 `AgentAsset` 到 `AgentAssetVersion` 的关系。 -- [ ] 约定规则资产的 `content` 保存 Markdown。 -- [ ] 约定技能、MCP、任务资产的 `content` 保存 JSON 快照。 +- [整体架构](<../agent plan/01_overall_architecture.md>) +- [语义本体](<../agent plan/02_semantic_ontology.md>) +- [数据契约与治理](<../agent plan/06_data_contracts_and_governance.md>) +- [能力注册](<../agent plan/07_capability_registry.md>) +- [权限与确认](<../agent plan/08_permission_confirmation.md>) +- [观测与 Trace](<../agent plan/09_observability_and_trace.md>) +- [财务单据标准模型](<../agent plan/14_financial_document_canonical_model.md>) -验收证据: +## 当天验收门槛 -- [ ] 同一个资产不能重复创建同一个版本号。 -- [ ] 资产详情能拿到最近 5 个版本。 +- [x] ~~数据库或等价存储能创建基础对象。~~ +- [x] ~~API 服务能启动。~~ +- [x] ~~资产列表能返回规则、技能、MCP、任务。~~ +- [x] ~~规则资产能关联 Markdown 当前版本。~~ +- [x] ~~未审核规则不能上线。~~ +- [x] ~~AgentRun 能保存一条运行记录。~~ +- [x] ~~AuditLog 能保存一条写操作记录。~~ -## 4. 建立 AgentAssetReview 模型 +## Day 2 联调入口 -- [ ] 定义 `AgentAssetReview`。 -- [ ] 增加字段 `id`。 -- [ ] 增加字段 `asset_id`。 -- [ ] 增加字段 `version`。 -- [ ] 增加字段 `reviewer`。 -- [ ] 增加字段 `review_status`。 -- [ ] 增加字段 `review_note`。 -- [ ] 增加字段 `reviewed_at`。 -- [ ] 增加字段 `created_at`。 -- [ ] 建立资产、版本、审核之间的查询关系。 -- [ ] 增加服务层校验:没有 `approved` 审核时不能把规则置为 `active`。 +- `GET /api/v1/agent-assets` +- `GET /api/v1/agent-assets/{asset_id}` +- `GET /api/v1/agent-assets/{asset_id}/versions?limit=5` +- `POST /api/v1/agent-assets/{asset_id}/reviews` +- `POST /api/v1/agent-assets/{asset_id}/activate` +- `GET /api/v1/audit-logs` -验收证据: +## 今天不做 -- [ ] `pending` 规则上线会被拒绝。 -- [ ] `rejected` 规则上线会被拒绝。 -- [ ] `approved` 规则可以上线。 - -## 5. 建立 AgentRun 模型 - -- [ ] 定义 `AgentRun`。 -- [ ] 增加字段 `id`。 -- [ ] 增加字段 `run_id`。 -- [ ] 增加字段 `agent`。 -- [ ] 增加字段 `source`。 -- [ ] 增加字段 `user_id`。 -- [ ] 增加字段 `task_id`。 -- [ ] 增加字段 `ontology_json`。 -- [ ] 增加字段 `route_json`。 -- [ ] 增加字段 `permission_level`。 -- [ ] 增加字段 `status`,取值 `running | succeeded | failed | blocked`。 -- [ ] 增加字段 `result_summary`。 -- [ ] 增加字段 `error_message`。 -- [ ] 增加字段 `started_at`。 -- [ ] 增加字段 `finished_at`。 -- [ ] 给 `run_id` 增加唯一约束。 -- [ ] 给 `agent`、`status`、`started_at` 增加索引。 - -验收证据: - -- [ ] 创建任意 Agent 执行记录时必须生成 `run_id`。 -- [ ] 失败执行能保存错误信息。 - -## 6. 建立 AgentToolCall 模型 - -- [ ] 定义 `AgentToolCall`。 -- [ ] 增加字段 `id`。 -- [ ] 增加字段 `run_id`。 -- [ ] 增加字段 `tool_type`,例如 `mcp | database | llm | ocr | rule_engine`。 -- [ ] 增加字段 `tool_name`。 -- [ ] 增加字段 `request_json`。 -- [ ] 增加字段 `response_json`。 -- [ ] 增加字段 `status`。 -- [ ] 增加字段 `duration_ms`。 -- [ ] 增加字段 `error_message`。 -- [ ] 增加字段 `created_at`。 -- [ ] 建立 `AgentRun` 到 `AgentToolCall` 的关系。 - -验收证据: - -- [ ] 一个 `run_id` 下可以记录多个工具调用。 -- [ ] 工具调用失败时不影响主运行日志保存。 - -## 7. 建立 SemanticParseLog 模型 - -- [ ] 定义 `SemanticParseLog`。 -- [ ] 增加字段 `id`。 -- [ ] 增加字段 `run_id`。 -- [ ] 增加字段 `user_id`。 -- [ ] 增加字段 `raw_query`。 -- [ ] 增加字段 `scenario`。 -- [ ] 增加字段 `intent`。 -- [ ] 增加字段 `entities_json`。 -- [ ] 增加字段 `time_range_json`。 -- [ ] 增加字段 `metrics_json`。 -- [ ] 增加字段 `constraints_json`。 -- [ ] 增加字段 `risk_flags_json`。 -- [ ] 增加字段 `permission_json`。 -- [ ] 增加字段 `confidence`。 -- [ ] 增加字段 `created_at`。 - -验收证据: - -- [ ] 能保存一条完整 8 字段语义解析日志。 -- [ ] 能按 `run_id` 查询语义解析结果。 - -## 8. 建立 AuditLog 模型 - -- [ ] 定义 `AuditLog`。 -- [ ] 增加字段 `id`。 -- [ ] 增加字段 `actor`。 -- [ ] 增加字段 `action`。 -- [ ] 增加字段 `resource_type`。 -- [ ] 增加字段 `resource_id`。 -- [ ] 增加字段 `before_json`。 -- [ ] 增加字段 `after_json`。 -- [ ] 增加字段 `request_id`。 -- [ ] 增加字段 `created_at`。 -- [ ] 为规则保存、审核、上线、任务执行创建审计记录接口。 - -验收证据: - -- [ ] 保存规则 Markdown 时有审计日志。 -- [ ] 审核规则时有审计日志。 -- [ ] 修改任务状态时有审计日志。 - -## 9. 建立 Schema / DTO - -- [ ] 定义 `AgentAssetCreate`。 -- [ ] 定义 `AgentAssetUpdate`。 -- [ ] 定义 `AgentAssetRead`。 -- [ ] 定义 `AgentAssetListItem`。 -- [ ] 定义 `AgentAssetVersionRead`。 -- [ ] 定义 `AgentAssetReviewRead`。 -- [ ] 定义 `RuleMarkdownUpdate`。 -- [ ] 定义 `AgentRunRead`。 -- [ ] 定义 `AgentToolCallRead`。 -- [ ] 定义 `SemanticParseRead`。 -- [ ] 所有 JSON 字段在 DTO 中保持结构化,不返回字符串化 JSON。 - -验收证据: - -- [ ] OpenAPI 文档能展示新增 Schema。 -- [ ] 列表 DTO 不返回大块 Markdown 内容。 -- [ ] 详情 DTO 返回当前版本内容。 - -## 10. 建立 API 骨架 - -- [ ] 新增 `GET /api/agent-assets`。 -- [ ] 新增 `GET /api/agent-assets/{asset_id}`。 -- [ ] 新增 `POST /api/agent-assets`。 -- [ ] 新增 `PATCH /api/agent-assets/{asset_id}`。 -- [ ] 新增 `GET /api/agent-assets/{asset_id}/versions`。 -- [ ] 新增 `POST /api/agent-assets/{asset_id}/versions`。 -- [ ] 新增 `POST /api/agent-assets/{asset_id}/reviews`。 -- [ ] 新增 `POST /api/agent-assets/{asset_id}/activate`。 -- [ ] 新增 `GET /api/agent-runs`。 -- [ ] 新增 `GET /api/agent-runs/{run_id}`。 -- [ ] 新增 `GET /api/audit-logs`。 -- [ ] 所有接口先返回真实数据库结果,不使用前端硬编码数据作为最终结果。 - -验收证据: - -- [ ] 能调用资产列表接口。 -- [ ] 能调用资产详情接口。 -- [ ] 能调用版本接口。 -- [ ] 能调用运行日志接口。 - -## 11. 建立服务层 - -- [ ] 新增资产查询服务。 -- [ ] 新增资产保存服务。 -- [ ] 新增版本创建服务。 -- [ ] 新增审核服务。 -- [ ] 新增上线校验服务。 -- [ ] 新增 Agent Run 创建服务。 -- [ ] 新增 Tool Call 记录服务。 -- [ ] 新增审计日志服务。 -- [ ] 所有服务函数返回明确错误,不直接把数据库异常暴露给前端。 - -验收证据: - -- [ ] API 路由中不堆业务判断。 -- [ ] 上线校验逻辑在服务层。 -- [ ] 审计日志通过统一服务写入。 - -## 12. 建立种子数据 - -- [ ] 创建至少 3 条规则资产。 -- [ ] 每条规则资产至少有 2 个版本。 -- [ ] 至少 1 条规则为 `active`。 -- [ ] 至少 1 条规则为 `review`。 -- [ ] 至少 1 条规则为 `draft`。 -- [ ] 创建至少 2 条技能资产。 -- [ ] 创建至少 2 条 MCP 资产。 -- [ ] 创建至少 3 条任务资产。 -- [ ] 为 active 规则创建 approved 审核记录。 -- [ ] 为 review 规则创建 pending 审核记录。 - -验收证据: - -- [ ] 资产列表按类型筛选时四类都有数据。 -- [ ] 规则详情能看到版本和审核者。 - -## 13. 最小测试 - -- [ ] 编写资产模型创建测试。 -- [ ] 编写版本唯一约束测试。 -- [ ] 编写未审核不能上线测试。 -- [ ] 编写资产列表接口测试。 -- [ ] 编写资产详情接口测试。 -- [ ] 编写 AgentRun 创建测试。 -- [ ] 编写 AuditLog 写入测试。 - -验收证据: - -- [ ] 后端核心测试通过。 -- [ ] 测试失败时能定位到具体服务。 - -## 14. Day 1 验收 - -- [ ] 数据库能创建所有新增表或等价结构。 -- [ ] API 服务能启动。 -- [ ] OpenAPI 能看到新增接口。 -- [ ] 资产列表接口返回规则、技能、MCP、任务。 -- [ ] 规则资产有 Markdown 当前版本。 -- [ ] 规则资产有最近版本列表。 -- [ ] 未审核规则不能上线。 -- [ ] AgentRun 能保存一条运行记录。 -- [ ] AuditLog 能保存一条写操作记录。 -- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 - -## 阻塞记录 - -- [ ] 暂无。 - -## 日终交接 - -- [ ] 写明已完成模型。 -- [ ] 写明已完成 API。 -- [ ] 写明未完成问题。 -- [ ] 写明 Day 2 前端联调需要使用的接口地址。 +- 不做完整 Agent 对话。 +- 不做完整 Hermes 调度。 +- 不做真实 OCR。 +- 不做复杂规则推理。 diff --git a/document/development/agent week plan/day_2_rule_center_integration.md b/document/development/agent week plan/day_2_rule_center_integration.md index ce33a76..6d151e5 100644 --- a/document/development/agent week plan/day_2_rule_center_integration.md +++ b/document/development/agent week plan/day_2_rule_center_integration.md @@ -1,220 +1,49 @@ -# Day 2:任务规则中心联调 TODO +# Day 2:任务规则中心联调 -目标:把任务规则中心从静态 UI 改成可对接后端的生产形态,覆盖规则、技能、MCP、任务四类资产。重点是规则 Markdown 编辑、版本切换、审核者信息、上线约束。 +## 今天的大开发点 -参考文档: +把任务规则中心从静态页面改成可和后端资产体系联动的生产形态。 -- `document/development/agent plan/07_capability_registry.md` -- `document/development/agent plan/13_rule_formation_lifecycle.md` -- `document/development/agent plan/06_data_contracts_and_governance.md` +重点是规则、技能、MCP、任务四类资产的列表和详情,以及规则 Markdown、版本、审核、上线约束。 -## 0. 开始前检查 +## 为什么第二天做这个 -- [ ] 确认 Day 1 API 已可访问。 -- [ ] 确认前端任务规则中心文件位置。 -- [ ] 确认现有路由名称和导航名称。 -- [ ] 确认现有 UI 风格,不重新做大改版。 -- [ ] 确认当前页面已有页签:规则、技能、MCP、任务。 -- [ ] 确认详情页隐藏顶部 title bar 的逻辑仍然有效。 -- [ ] 确认返回列表栏高度没有被重新拉高。 +任务规则中心是业务人员管理 Agent 能力的入口。后续语义本体、Orchestrator、User Agent、Hermes 都要读取这里注册的规则、技能、MCP 和任务。 -## 1. API Client +## 今天主要交付 -- [ ] 新增或扩展资产列表请求函数。 -- [ ] 新增资产详情请求函数。 -- [ ] 新增版本列表请求函数。 -- [ ] 新增规则 Markdown 保存请求函数。 -- [ ] 新增审核请求函数。 -- [ ] 新增上线请求函数。 -- [ ] 新增运行日志请求函数。 -- [ ] 给所有请求增加加载态。 -- [ ] 给所有请求增加错误态。 -- [ ] 给所有写请求增加成功提示。 +- 规则、技能、MCP、任务四个页签对接资产 API。 +- 列表支持搜索、筛选、状态展示。 +- 规则详情展示 Markdown 内容。 +- 管理员可编辑规则 Markdown。 +- 规则版本展示最近 5 个版本。 +- 版本切换需要弹窗确认。 +- 审核者信息放在标题区域。 +- 右侧只保留版本信息。 +- 未审核规则上线时被后端拦截。 -验收证据: +## 对应执行细则 -- [ ] 前端不再只依赖本地硬编码资产数据。 -- [ ] 后端不可用时页面有明确错误提示。 +- [Day 2 执行细则](<../agent plan/weekly_execution_details/day_2_rule_center_integration.md>) -## 2. 列表页数据接入 +相关架构文档: -- [ ] 规则页签请求 `asset_type=rule`。 -- [ ] 技能页签请求 `asset_type=skill`。 -- [ ] MCP 页签请求 `asset_type=mcp`。 -- [ ] 任务页签请求 `asset_type=task`。 -- [ ] 搜索框传递关键词或本地过滤。 -- [ ] 类型下拉和搜索框可以同时生效。 -- [ ] 状态筛选可以过滤 `draft | review | active | disabled`。 -- [ ] 列表卡片展示名称。 -- [ ] 列表卡片展示摘要。 -- [ ] 列表卡片展示状态。 -- [ ] 列表卡片展示负责人。 -- [ ] 列表卡片展示最近更新时间。 -- [ ] 空数据时展示空态。 -- [ ] 加载中时展示骨架或加载状态。 +- [能力注册](<../agent plan/07_capability_registry.md>) +- [规则形成生命周期](<../agent plan/13_rule_formation_lifecycle.md>) +- [数据契约与治理](<../agent plan/06_data_contracts_and_governance.md>) -验收证据: +## 当天验收门槛 -- [ ] 四个页签都能切换。 -- [ ] 四个页签都有数据或空态。 -- [ ] 搜索和筛选不会互相覆盖。 +- 四个页签可切换并有真实 API 或 Mock API 数据。 +- 规则详情可编辑 Markdown。 +- Markdown 保存后刷新不丢失。 +- 版本卡片可切换版本。 +- 未审核规则不能上线。 +- 前端构建通过。 -## 3. 规则详情页主信息 +## 今天不做 -- [ ] 打开规则资产时请求详情 API。 -- [ ] Hero title 展示规则名称。 -- [ ] Hero title 下方展示审核者。 -- [ ] Hero title 下方展示审核状态。 -- [ ] Hero title 下方展示上线条件。 -- [ ] Hero title 高度保持紧凑。 -- [ ] 详情页不显示外层顶部 title bar。 -- [ ] 返回列表栏高度保持原有紧凑高度。 - -验收证据: - -- [ ] 用户能一眼看到该规则是否已审核。 -- [ ] 用户不会看到两层 title。 - -## 4. Markdown 编辑器 - -- [ ] 从当前版本读取 Markdown 内容。 -- [ ] Markdown 编辑框高度和右侧版本卡片底部对齐。 -- [ ] Markdown 编辑框支持长内容滚动。 -- [ ] Markdown 编辑框保存时调用 API。 -- [ ] 保存后创建新版本或更新草稿版本,按后端约定执行。 -- [ ] 保存成功后刷新版本列表。 -- [ ] 保存失败时保留用户输入。 -- [ ] 编辑器禁用态覆盖 `active` 且无编辑权限的情况。 -- [ ] 编辑器底部展示最后保存时间。 - -验收证据: - -- [ ] 编辑 Markdown 后刷新页面内容仍存在。 -- [ ] 保存失败不会丢内容。 -- [ ] 左右卡片底部视觉对齐。 - -## 5. 版本卡片 - -- [ ] 右侧只保留版本信息卡片。 -- [ ] 版本卡片宽度足够展示版本号、日期、状态。 -- [ ] 展示最近 5 个版本。 -- [ ] 当前版本有明显但不突兀的标识。 -- [ ] 当前版本标识居中显示。 -- [ ] 选中状态只变色,不改变内容对齐。 -- [ ] 日期列和其他版本日期对齐。 -- [ ] 点击非当前版本时弹出确认弹窗。 -- [ ] 弹窗展示目标版本号。 -- [ ] 弹窗展示切换风险提示。 -- [ ] 确认后切换当前展示内容。 -- [ ] 取消后不改变当前版本。 - -验收证据: - -- [ ] 版本切换不会造成列表文字位移。 -- [ ] 当前版本背景能完全覆盖内容区域。 -- [ ] 版本卡片不贴右侧边界。 - -## 6. 审核与上线 - -- [ ] 详情中展示审核者姓名。 -- [ ] 详情中展示审核时间。 -- [ ] 详情中展示审核意见。 -- [ ] 未审核规则显示不能上线原因。 -- [ ] 点击上线时调用后端上线接口。 -- [ ] 后端拒绝时展示拒绝原因。 -- [ ] 审核通过后上线按钮可用。 -- [ ] 审核动作写入审计日志。 -- [ ] 上线动作写入审计日志。 - -验收证据: - -- [ ] pending 规则无法上线。 -- [ ] approved 规则可以上线。 -- [ ] rejected 规则无法上线。 - -## 7. 技能详情 - -- [ ] 技能页签列表展示能力名称。 -- [ ] 技能详情展示能力说明。 -- [ ] 技能详情展示输入参数。 -- [ ] 技能详情展示输出参数。 -- [ ] 技能详情展示依赖能力。 -- [ ] 技能详情展示适用场景。 -- [ ] 技能详情展示负责人。 -- [ ] 技能详情展示版本。 -- [ ] 技能详情不使用规则 Markdown 编辑器。 - -验收证据: - -- [ ] 技能和规则详情不会混用 UI。 - -## 8. MCP 详情 - -- [ ] MCP 页签列表展示外部服务名称。 -- [ ] MCP 详情展示服务类型。 -- [ ] MCP 详情展示调用地址或能力名。 -- [ ] MCP 详情展示鉴权方式。 -- [ ] MCP 详情展示超时配置。 -- [ ] MCP 详情展示降级策略。 -- [ ] MCP 详情展示最近调用状态。 -- [ ] MCP 详情展示负责人。 - -验收证据: - -- [ ] MCP 被定义为外部服务,而不是技能规则。 - -## 9. 任务详情 - -- [ ] 任务页签展示定时任务名称。 -- [ ] 任务详情展示 cron 或调度周期。 -- [ ] 任务详情展示执行 Agent,默认 Hermes。 -- [ ] 任务详情展示任务目标。 -- [ ] 任务详情展示风险等级。 -- [ ] 任务详情展示最近执行时间。 -- [ ] 任务详情展示最近执行结果。 -- [ ] 任务详情展示启停状态。 - -验收证据: - -- [ ] 定时任务用户可见名称为“任务”。 -- [ ] 技术字段可保留 `schedule`,但 UI 不显示“定时任务”。 - -## 10. 前端质量 - -- [ ] 页面在 1366 宽度下无横向滚动。 -- [ ] 页面在 1920 宽度下右侧卡片不过宽。 -- [ ] 页面在窄屏下详情区域可滚动。 -- [ ] 所有按钮有禁用态。 -- [ ] 所有弹窗有取消按钮。 -- [ ] 所有表单错误有提示。 -- [ ] 所有日期格式统一。 -- [ ] 状态颜色和现有系统一致。 - -验收证据: - -- [ ] `npm run build` 通过。 -- [ ] 任务规则中心手动走查通过。 - -## 11. Day 2 验收 - -- [ ] 规则、技能、MCP、任务四个页签可用。 -- [ ] 搜索框和筛选下拉可用。 -- [ ] 规则详情展示 Markdown。 -- [ ] 规则 Markdown 可保存。 -- [ ] 右侧只保留版本信息。 -- [ ] 版本可切换且有弹窗确认。 -- [ ] 审核者信息在标题下方。 -- [ ] 未审核规则不能上线。 -- [ ] 前端构建通过。 -- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 - -## 阻塞记录 - -- [ ] 暂无。 - -## 日终交接 - -- [ ] 写明已接入的 API。 -- [ ] 写明仍然使用 Mock 的字段。 -- [ ] 写明 UI 未完成项。 -- [ ] 写明 Day 3 语义本体需要复用的资产数据。 +- 不做规则自动生成。 +- 不做完整 MCP 真实调用。 +- 不做复杂权限矩阵。 +- 不重做 UI 风格,只在现有风格上微调。 diff --git a/document/development/agent week plan/day_3_semantic_ontology_mvp.md b/document/development/agent week plan/day_3_semantic_ontology_mvp.md index 7a5b1d8..cfd21fc 100644 --- a/document/development/agent week plan/day_3_semantic_ontology_mvp.md +++ b/document/development/agent week plan/day_3_semantic_ontology_mvp.md @@ -1,236 +1,47 @@ -# Day 3:语义本体 MVP TODO +# Day 3:语义本体 MVP -目标:建立用户问题的语义解析层,输出稳定的 8 个核心字段,让 User Agent、Hermes 和 Orchestrator 都能使用同一套语义结构。 +## 今天的大开发点 -参考文档: +建立用户问题的语义解析层,把自然语言问题转换成统一的 8 个核心字段。 -- `document/development/agent plan/02_semantic_ontology.md` -- `document/development/agent plan/14_financial_document_canonical_model.md` -- `document/development/agent plan/06_data_contracts_and_governance.md` +这一天的目标不是做到 LLM 全能理解,而是先让报销、应收、应付、知识和风险相关问题能进入稳定结构。 -## 0. 开始前检查 +## 为什么第三天做这个 -- [ ] 确认 Day 1 的 `SemanticParseLog` 可用。 -- [ ] 确认 Day 1 的 `AgentRun` 可用。 -- [ ] 确认 Day 2 的资产 API 可用。 -- [ ] 找到后端服务层目录。 -- [ ] 找到现有 LLM 调用或 Mock 调用方式。 -- [ ] 确认当前是否允许真实调用 LLM。 -- [ ] 如果不能调用真实 LLM,准备规则解析加 Mock 解析。 +Orchestrator 不能直接根据原始文本做可靠路由。它需要先拿到结构化语义,再决定调用 User Agent、Hermes、规则、MCP 或知识库。 -## 1. 定义 8 个核心字段 +## 今天主要交付 -- [ ] 定义字段 `scenario`,表示业务场景。 -- [ ] 定义字段 `intent`,表示用户意图。 -- [ ] 定义字段 `entities`,表示业务对象。 -- [ ] 定义字段 `time_range`,表示时间范围。 -- [ ] 定义字段 `metrics`,表示指标或金额口径。 -- [ ] 定义字段 `constraints`,表示过滤条件。 -- [ ] 定义字段 `risk_flags`,表示风险信号。 -- [ ] 定义字段 `permission`,表示动作权限。 -- [ ] 为每个字段写清楚类型。 -- [ ] 为每个字段写清楚是否必填。 -- [ ] 为每个字段写清楚默认值。 -- [ ] 为每个字段写清楚示例。 +- 语义本体 8 字段结构。 +- 场景识别:报销、应收、应付、知识、未知。 +- 意图识别:查询、解释、对比、风险检查、草稿、操作。 +- 业务对象提取:员工、客户、供应商、部门、项目、单据、金额。 +- 时间范围解析。 +- 指标和约束解析。 +- 风险信号和权限级别判断。 +- 语义解析 API。 +- 解析日志和最小评测集。 -验收证据: +## 对应执行细则 -- [ ] 8 个字段在 Schema、服务层、日志中名字一致。 +- [Day 3 执行细则](<../agent plan/weekly_execution_details/day_3_semantic_ontology_mvp.md>) -## 2. 设计字段枚举 +相关架构文档: -- [ ] `scenario` 支持 `expense`。 -- [ ] `scenario` 支持 `accounts_receivable`。 -- [ ] `scenario` 支持 `accounts_payable`。 -- [ ] `scenario` 支持 `knowledge`。 -- [ ] `scenario` 支持 `unknown`。 -- [ ] `intent` 支持 `query`。 -- [ ] `intent` 支持 `explain`。 -- [ ] `intent` 支持 `compare`。 -- [ ] `intent` 支持 `risk_check`。 -- [ ] `intent` 支持 `draft`。 -- [ ] `intent` 支持 `operate`。 -- [ ] `permission.level` 支持 `read`。 -- [ ] `permission.level` 支持 `draft_write`。 -- [ ] `permission.level` 支持 `approval_required`。 -- [ ] `permission.level` 支持 `forbidden`。 +- [语义本体](<../agent plan/02_semantic_ontology.md>) +- [财务单据标准模型](<../agent plan/14_financial_document_canonical_model.md>) +- [数据契约与治理](<../agent plan/06_data_contracts_and_governance.md>) -验收证据: +## 当天验收门槛 -- [ ] 未识别的问题不会抛异常,返回 `unknown`。 +- 输入自然语言问题能返回 8 个字段。 +- 低置信度问题能返回澄清问题。 +- 越权动作不会被标记为可直接执行。 +- 解析结果能写入日志。 +- 至少覆盖报销、应收、应付三个场景。 -## 3. 建立 Schema +## 今天不做 -- [ ] 定义 `OntologyParseRequest`。 -- [ ] `OntologyParseRequest` 包含 `query`。 -- [ ] `OntologyParseRequest` 包含 `user_id`。 -- [ ] `OntologyParseRequest` 包含 `context_json`。 -- [ ] 定义 `OntologyParseResult`。 -- [ ] `OntologyParseResult` 包含 8 个核心字段。 -- [ ] `OntologyParseResult` 包含 `confidence`。 -- [ ] `OntologyParseResult` 包含 `clarification_required`。 -- [ ] `OntologyParseResult` 包含 `clarification_question`。 -- [ ] `OntologyParseResult` 包含 `run_id`。 -- [ ] 定义字段级错误结构。 - -验收证据: - -- [ ] OpenAPI 中可以看到语义解析请求和响应。 - -## 4. 实现解析服务 - -- [ ] 新增 `SemanticOntologyService` 或同等服务。 -- [ ] 实现 `parse(query, user_context)` 主函数。 -- [ ] 先做关键词规则解析。 -- [ ] 报销关键词映射到 `expense`。 -- [ ] 应收、回款、客户欠款映射到 `accounts_receivable`。 -- [ ] 应付、供应商、付款映射到 `accounts_payable`。 -- [ ] 风险、异常、重复、超标映射到 `risk_check`。 -- [ ] 为什么、依据、规则映射到 `explain`。 -- [ ] 统计、汇总、多少映射到 `query`。 -- [ ] 生成、创建、发起映射到 `draft` 或 `operate`。 -- [ ] 无法识别时返回低置信度和澄清问题。 - -验收证据: - -- [ ] “查一下本周报销超标风险”能识别为 expense + risk_check。 -- [ ] “客户 A 这个月还有多少应收”能识别为 accounts_receivable + query。 -- [ ] “供应商 B 明天要付多少钱”能识别为 accounts_payable + query。 - -## 5. 解析业务对象 - -- [ ] 从问题中提取员工姓名。 -- [ ] 从问题中提取部门。 -- [ ] 从问题中提取客户。 -- [ ] 从问题中提取供应商。 -- [ ] 从问题中提取项目。 -- [ ] 从问题中提取单据号。 -- [ ] 从问题中提取金额。 -- [ ] 从问题中提取费用类型。 -- [ ] 无法提取时返回空数组,不返回 null。 - -验收证据: - -- [ ] “张三 4 月差旅报销”能提取员工、月份、费用类型。 - -## 6. 解析时间范围 - -- [ ] 支持今天。 -- [ ] 支持昨天。 -- [ ] 支持本周。 -- [ ] 支持上周。 -- [ ] 支持本月。 -- [ ] 支持上月。 -- [ ] 支持本季度。 -- [ ] 支持今年。 -- [ ] 支持明确日期。 -- [ ] 支持日期区间。 -- [ ] 解析结果包含 `start_date` 和 `end_date`。 -- [ ] 日期使用 ISO 格式。 - -验收证据: - -- [ ] “本周”能解析为当前周起止日期。 -- [ ] “2026 年 4 月”能解析为 `2026-04-01` 到 `2026-04-30`。 - -## 7. 解析指标与约束 - -- [ ] 识别金额指标。 -- [ ] 识别数量指标。 -- [ ] 识别超标指标。 -- [ ] 识别逾期指标。 -- [ ] 识别重复报销指标。 -- [ ] 识别部门过滤条件。 -- [ ] 识别状态过滤条件。 -- [ ] 识别金额阈值过滤条件。 -- [ ] 识别排序要求。 -- [ ] 识别 Top N 要求。 - -验收证据: - -- [ ] “列出金额最高的 10 笔报销”能识别排序和 Top 10。 - -## 8. 解析风险与权限 - -- [ ] 重复报销映射到 `duplicate_expense`。 -- [ ] 发票异常映射到 `invoice_anomaly`。 -- [ ] 金额超标映射到 `amount_over_limit`。 -- [ ] 逾期应收映射到 `ar_overdue`。 -- [ ] 逾期应付映射到 `ap_overdue`。 -- [ ] 查询类问题权限为 `read`。 -- [ ] 生成草稿权限为 `draft_write`。 -- [ ] 审批、上线、付款类动作权限为 `approval_required`。 -- [ ] 越权动作权限为 `forbidden`。 - -验收证据: - -- [ ] “帮我直接付款”不能被标为可直接执行。 - -## 9. API 接口 - -- [ ] 新增 `POST /api/ontology/parse`。 -- [ ] 请求参数包含用户问题。 -- [ ] 请求参数包含用户上下文。 -- [ ] 响应包含 8 个字段。 -- [ ] 响应包含 `run_id`。 -- [ ] 响应包含置信度。 -- [ ] 响应包含澄清问题。 -- [ ] 每次调用写入 `SemanticParseLog`。 -- [ ] 每次调用写入 `AgentRun` 或关联已有 `AgentRun`。 - -验收证据: - -- [ ] 连续调用 5 次都能在日志中查到。 - -## 10. 前端调试入口 - -- [ ] 在合适页面增加语义解析调试入口。 -- [ ] 输入框支持自然语言问题。 -- [ ] 点击解析后调用 API。 -- [ ] 展示 8 个字段。 -- [ ] 展示 JSON 原始结果。 -- [ ] 展示置信度。 -- [ ] 展示澄清问题。 -- [ ] 展示 `run_id`。 -- [ ] 错误时展示错误信息。 - -验收证据: - -- [ ] 产品和开发可以直接在页面验证解析结果。 - -## 11. 评测集 - -- [ ] 创建至少 5 条报销问题。 -- [ ] 创建至少 5 条应收问题。 -- [ ] 创建至少 5 条应付问题。 -- [ ] 创建至少 3 条知识库问题。 -- [ ] 创建至少 3 条越权操作问题。 -- [ ] 为每条问题写期望 `scenario`。 -- [ ] 为每条问题写期望 `intent`。 -- [ ] 为每条问题写期望权限级别。 -- [ ] 编写评测脚本或测试。 - -验收证据: - -- [ ] 核心场景识别准确率达到当天设定阈值,例如 80%。 - -## 12. Day 3 验收 - -- [ ] 语义解析 API 可用。 -- [ ] 8 个核心字段完整返回。 -- [ ] 解析日志可查询。 -- [ ] 低置信度问题有澄清问题。 -- [ ] 越权动作不会被标为可执行。 -- [ ] 前端调试入口可用。 -- [ ] 评测集可运行。 -- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 - -## 阻塞记录 - -- [ ] 暂无。 - -## 日终交接 - -- [ ] 写明已支持的关键词。 -- [ ] 写明识别不准的样例。 -- [ ] 写明 Day 4 Orchestrator 可以直接复用的响应结构。 +- 不做复杂多轮对话记忆。 +- 不做完整 LLM 提示词优化。 +- 不做自动执行业务流程。 diff --git a/document/development/agent week plan/day_4_orchestrator_runtime.md b/document/development/agent week plan/day_4_orchestrator_runtime.md index f4f0915..f214744 100644 --- a/document/development/agent week plan/day_4_orchestrator_runtime.md +++ b/document/development/agent week plan/day_4_orchestrator_runtime.md @@ -1,183 +1,47 @@ -# Day 4:Orchestrator 运行时 TODO +# Day 4:Orchestrator 运行时 -目标:建立统一调度层,让用户请求和系统任务都先进入 Orchestrator,再根据语义本体、权限、能力注册路由到 User Agent、Hermes、MCP 或规则引擎。 +## 今天的大开发点 -参考文档: +建立统一调度层。用户请求和系统任务都先进入 Orchestrator,由它完成语义解析、权限判断、能力选择、Agent 路由、工具调用记录和失败降级。 -- `document/development/agent plan/04_orchestrator_and_runtime_flow.md` -- `document/development/agent plan/07_capability_registry.md` -- `document/development/agent plan/08_permission_confirmation.md` -- `document/development/agent plan/09_observability_and_trace.md` +## 为什么第四天做这个 -## 0. 开始前检查 +没有 Orchestrator,User Agent 和 Hermes 会各自直接调用能力,权限、审计、降级、Trace 都会分散。生产系统必须有统一入口。 -- [ ] 确认 Day 3 `POST /api/ontology/parse` 可用。 -- [ ] 确认 `AgentRun` 可创建。 -- [ ] 确认 `AgentToolCall` 可创建。 -- [ ] 确认资产列表能查询技能、MCP、任务。 -- [ ] 确认权限级别枚举已稳定。 -- [ ] 找到后端服务层适合放 Orchestrator 的位置。 +## 今天主要交付 -## 1. Orchestrator 输入输出 +- Orchestrator 请求和响应结构。 +- 用户请求路由到 User Agent。 +- 定时任务路由到 Hermes。 +- 权限级别判断。 +- 高风险动作确认机制。 +- 能力注册查询。 +- 工具调用封装。 +- AgentRun Trace 查询。 +- 失败降级返回。 -- [ ] 定义 `OrchestratorRequest`。 -- [ ] 请求包含 `source`。 -- [ ] 请求包含 `user_id`。 -- [ ] 请求包含 `message`。 -- [ ] 请求包含 `task_id`。 -- [ ] 请求包含 `context_json`。 -- [ ] 定义 `OrchestratorResponse`。 -- [ ] 响应包含 `run_id`。 -- [ ] 响应包含 `selected_agent`。 -- [ ] 响应包含 `route_reason`。 -- [ ] 响应包含 `permission_level`。 -- [ ] 响应包含 `status`。 -- [ ] 响应包含 `result`。 -- [ ] 响应包含 `requires_confirmation`。 -- [ ] 响应包含 `trace_summary`。 +## 对应执行细则 -验收证据: +- [Day 4 执行细则](<../agent plan/weekly_execution_details/day_4_orchestrator_runtime.md>) -- [ ] Orchestrator 响应能直接被前端展示。 +相关架构文档: -## 2. 建立 Orchestrator 服务 +- [Orchestrator 与运行流程](<../agent plan/04_orchestrator_and_runtime_flow.md>) +- [能力注册](<../agent plan/07_capability_registry.md>) +- [权限与确认](<../agent plan/08_permission_confirmation.md>) +- [观测与 Trace](<../agent plan/09_observability_and_trace.md>) -- [ ] 新增 `OrchestratorService`。 -- [ ] 实现 `run(request)` 主入口。 -- [ ] 主入口第一步创建 `AgentRun`。 -- [ ] 主入口第二步调用语义解析。 -- [ ] 主入口第三步执行权限判断。 -- [ ] 主入口第四步选择 Agent。 -- [ ] 主入口第五步调用目标 Agent 或返回阻断结果。 -- [ ] 主入口第六步更新 `AgentRun` 状态。 -- [ ] 所有异常都写入 `AgentRun.error_message`。 +## 当天验收门槛 -验收证据: +- Orchestrator API 可用。 +- 用户消息能路由到 User Agent 占位实现。 +- 定时任务能路由到 Hermes 占位实现。 +- forbidden 请求不会调用下游 Agent。 +- 每次运行都有 `run_id` 和 Trace。 +- 工具调用失败能记录并返回降级结果。 -- [ ] 正常请求状态为 `succeeded`。 -- [ ] 被权限拦截请求状态为 `blocked`。 -- [ ] 异常请求状态为 `failed`。 +## 今天不做 -## 3. 路由规则 - -- [ ] `source=user_message` 默认路由到 User Agent。 -- [ ] `source=schedule` 默认路由到 Hermes。 -- [ ] `intent=risk_check` 且来源为 schedule 时路由到 Hermes。 -- [ ] `intent=query` 且来源为 user_message 时路由到 User Agent。 -- [ ] `intent=explain` 路由到 User Agent。 -- [ ] `intent=draft` 路由到 User Agent,但只允许生成草稿。 -- [ ] `permission.level=approval_required` 时设置 `requires_confirmation=true`。 -- [ ] `permission.level=forbidden` 时不调用下游 Agent。 -- [ ] 无法识别时返回澄清问题。 - -验收证据: - -- [ ] 同一句风险检查,在用户入口和任务入口有不同路由结果。 - -## 4. 权限判断 - -- [ ] 新增权限判断服务或函数。 -- [ ] 查询类请求返回 `read`。 -- [ ] 草稿类请求返回 `draft_write`。 -- [ ] 审批、上线、付款类请求返回 `approval_required`。 -- [ ] 用户无权限时返回 `forbidden`。 -- [ ] 高风险动作不允许自动执行。 -- [ ] 需要确认的动作返回确认提示。 -- [ ] 权限判断结果写入 `AgentRun.permission_level`。 - -验收证据: - -- [ ] “直接上线规则”不会被自动执行。 -- [ ] “直接付款”不会被自动执行。 - -## 5. 能力注册查询 - -- [ ] 从 `AgentAsset` 查询 active 技能。 -- [ ] 从 `AgentAsset` 查询 active MCP。 -- [ ] 从 `AgentAsset` 查询 active 任务。 -- [ ] 过滤 disabled 能力。 -- [ ] 过滤未审核 active 条件不满足的规则。 -- [ ] 为每次能力选择记录 `route_json`。 -- [ ] 找不到能力时返回降级说明。 - -验收证据: - -- [ ] 禁用 MCP 不会被 Orchestrator 调用。 - -## 6. 工具调用封装 - -- [ ] 定义统一工具调用接口。 -- [ ] 工具请求前写入 `AgentToolCall` running 或准备记录。 -- [ ] 工具成功后写入响应和耗时。 -- [ ] 工具失败后写入错误。 -- [ ] 外部 MCP 调用失败时返回降级结果。 -- [ ] 数据库查询失败时返回明确错误。 -- [ ] LLM 调用失败时返回可读提示。 - -验收证据: - -- [ ] 每次 Orchestrator 运行至少可以看到 0 到多条工具调用记录。 - -## 7. API 接口 - -- [ ] 新增 `POST /api/orchestrator/run`。 -- [ ] 请求支持用户消息。 -- [ ] 请求支持任务触发。 -- [ ] 响应返回 `run_id`。 -- [ ] 响应返回路由结果。 -- [ ] 响应返回权限结果。 -- [ ] 新增 `GET /api/orchestrator/runs/{run_id}/trace` 或复用 AgentRun 详情接口。 -- [ ] Trace 接口返回语义解析、路由、工具调用、最终结果。 - -验收证据: - -- [ ] 前端或 curl 可以完整看到一次运行链路。 - -## 8. 前端最小 Trace 查看 - -- [ ] 在合适位置展示最近运行记录。 -- [ ] 点击运行记录能查看 `run_id`。 -- [ ] 展示 selected_agent。 -- [ ] 展示 route_reason。 -- [ ] 展示 permission_level。 -- [ ] 展示工具调用列表。 -- [ ] 展示错误信息。 -- [ ] 展示耗时。 - -验收证据: - -- [ ] 开发调试时不需要直接查数据库才能理解路由结果。 - -## 9. 测试 - -- [ ] 测试用户查询路由到 User Agent。 -- [ ] 测试定时任务路由到 Hermes。 -- [ ] 测试 forbidden 不调用下游 Agent。 -- [ ] 测试 approval_required 返回确认。 -- [ ] 测试工具失败写入 ToolCall。 -- [ ] 测试 Orchestrator 异常写入 AgentRun。 - -验收证据: - -- [ ] Orchestrator 核心测试通过。 - -## 10. Day 4 验收 - -- [ ] Orchestrator API 可用。 -- [ ] 用户请求能路由到 User Agent 占位实现。 -- [ ] 定时任务能路由到 Hermes 占位实现。 -- [ ] 权限阻断有效。 -- [ ] 运行 Trace 可查询。 -- [ ] 工具调用日志可查询。 -- [ ] 降级结果可读。 -- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 - -## 阻塞记录 - -- [ ] 暂无。 - -## 日终交接 - -- [ ] 写明路由规则现状。 -- [ ] 写明权限判断现状。 -- [ ] 写明 Day 5 User Agent 需要实现的接口契约。 +- 不做复杂任务编排 DAG。 +- 不做多 Agent 协商。 +- 不做自动高风险动作。 diff --git a/document/development/agent week plan/day_5_user_agent_mvp.md b/document/development/agent week plan/day_5_user_agent_mvp.md index 682bb05..3667145 100644 --- a/document/development/agent week plan/day_5_user_agent_mvp.md +++ b/document/development/agent week plan/day_5_user_agent_mvp.md @@ -1,183 +1,50 @@ -# Day 5:User Agent MVP TODO +# Day 5:User Agent MVP -目标:实现面向用户的自建 Agent。它负责用户提问、流程辅助、规则解释、查询结果解释和草稿生成,不做自动审批、自动付款、自动上线等高风险动作。 +## 今天的大开发点 -参考文档: +实现面向用户的自建 Agent。它负责用户提问、流程辅助、规则解释、查询结果解释和草稿生成。 -- `document/development/agent plan/03_agent_responsibilities.md` -- `document/development/agent plan/04_orchestrator_and_runtime_flow.md` -- `document/development/agent plan/12_llm_wiki_knowledge_architecture.md` -- `document/development/agent plan/13_rule_formation_lifecycle.md` +User Agent 只能处理用户侧交互,不负责后台定时内循环,也不能自动执行高风险动作。 -## 0. 开始前检查 +## 为什么第五天做这个 -- [ ] 确认 Orchestrator 能把用户请求路由到 User Agent。 -- [ ] 确认语义本体 8 字段可用。 -- [ ] 确认规则资产可查询。 -- [ ] 确认 AgentRun 和 ToolCall 可记录。 -- [ ] 确认是否有现成对话 UI。 -- [ ] 确认财务业务数据是否真实可查。 -- [ ] 如果业务数据不可查,准备最小 Mock 数据服务。 +Day 1 到 Day 4 已经具备资产、语义、路由和日志基础,此时可以把用户自然语言入口接到真实流程上。 -## 1. User Agent 输入输出 +## 今天主要交付 -- [ ] 定义 `UserAgentRequest`。 -- [ ] 请求包含 `run_id`。 -- [ ] 请求包含 `user_id`。 -- [ ] 请求包含 `message`。 -- [ ] 请求包含 `ontology`。 -- [ ] 请求包含 `context_json`。 -- [ ] 定义 `UserAgentResponse`。 -- [ ] 响应包含 `answer`。 -- [ ] 响应包含 `citations`。 -- [ ] 响应包含 `suggested_actions`。 -- [ ] 响应包含 `draft_payload`。 -- [ ] 响应包含 `risk_flags`。 -- [ ] 响应包含 `requires_confirmation`。 +- 用户自然语言入口。 +- 报销查询和解释。 +- 应收查询和解释。 +- 应付查询和解释。 +- 规则引用解释。 +- 风险原因说明。 +- 处理意见草稿。 +- 知识库读取骨架。 +- 前端问答或操作入口。 -验收证据: +## 对应执行细则 -- [ ] User Agent 响应结构能被 Orchestrator 直接包装返回。 +- [Day 5 执行细则](<../agent plan/weekly_execution_details/day_5_user_agent_mvp.md>) -## 2. 查询处理 +相关架构文档: -- [ ] 实现报销查询处理器。 -- [ ] 实现应收查询处理器。 -- [ ] 实现应付查询处理器。 -- [ ] 查询前检查权限级别。 -- [ ] 查询时记录 ToolCall。 -- [ ] 查询失败时返回可读错误。 -- [ ] 查询为空时返回空态解释。 -- [ ] 查询结果限制返回条数,避免一次返回过大。 +- [Agent 职责边界](<../agent plan/03_agent_responsibilities.md>) +- [Orchestrator 与运行流程](<../agent plan/04_orchestrator_and_runtime_flow.md>) +- [LLM Wiki 知识库架构](<../agent plan/12_llm_wiki_knowledge_architecture.md>) +- [规则形成生命周期](<../agent plan/13_rule_formation_lifecycle.md>) -验收证据: +## 当天验收门槛 -- [ ] “查本周报销金额”有可读回答。 -- [ ] “客户 A 本月应收多少”有可读回答。 -- [ ] “供应商 B 待付款多少”有可读回答。 +- 用户能输入自然语言问题。 +- 请求必须经过 Orchestrator。 +- 至少 3 类财务问题有可读回答。 +- 回答能引用规则或知识。 +- 高风险动作只生成草稿或建议。 +- AgentRun Trace 能看到 User Agent 步骤。 -## 3. 规则解释 +## 今天不做 -- [ ] 根据语义场景查询相关规则资产。 -- [ ] 只引用 active 规则。 -- [ ] 读取规则当前版本 Markdown。 -- [ ] 从 Markdown 中提取规则摘要。 -- [ ] 回答中说明使用了哪些规则。 -- [ ] 回答中包含规则版本号。 -- [ ] 回答中包含规则更新时间。 -- [ ] 没有相关规则时说明缺失。 - -验收证据: - -- [ ] “为什么这笔报销有风险”能引用规则。 - -## 4. 风险解释 - -- [ ] 识别重复报销风险。 -- [ ] 识别金额超标风险。 -- [ ] 识别发票异常风险。 -- [ ] 识别逾期应收风险。 -- [ ] 识别逾期应付风险。 -- [ ] 风险回答包含风险类型。 -- [ ] 风险回答包含触发原因。 -- [ ] 风险回答包含建议处理动作。 -- [ ] 高风险建议不能变成自动执行。 - -验收证据: - -- [ ] 风险解释结果不是单纯“有风险”,而是有依据。 - -## 5. 草稿生成 - -- [ ] 支持生成报销处理意见草稿。 -- [ ] 支持生成应收催收建议草稿。 -- [ ] 支持生成应付付款建议草稿。 -- [ ] 草稿中标明“待人工确认”。 -- [ ] 草稿不直接提交业务系统。 -- [ ] 草稿生成写入审计日志。 -- [ ] 草稿生成写入 AgentRun 结果。 - -验收证据: - -- [ ] “帮我生成处理意见”只返回草稿,不执行审批。 - -## 6. 知识库读取骨架 - -- [ ] 建立知识条目查询接口或服务。 -- [ ] 支持按关键词查询知识条目。 -- [ ] 支持按业务场景查询知识条目。 -- [ ] User Agent 回答可以引用知识条目。 -- [ ] 引用中包含知识标题。 -- [ ] 引用中包含更新时间。 -- [ ] 知识库不可用时返回降级说明。 - -验收证据: - -- [ ] 知识库失败不会导致整个回答失败。 - -## 7. 对话或操作入口 - -- [ ] 前端增加用户问题输入框。 -- [ ] 输入框支持回车或按钮提交。 -- [ ] 提交时调用 Orchestrator,而不是绕过 Orchestrator。 -- [ ] 展示 Agent 回答。 -- [ ] 展示引用规则或知识。 -- [ ] 展示建议动作。 -- [ ] 展示需要人工确认的提示。 -- [ ] 展示 `run_id`。 -- [ ] 展示加载态。 -- [ ] 展示错误态。 - -验收证据: - -- [ ] 用户可在页面完成一次问答闭环。 - -## 8. 安全边界 - -- [ ] User Agent 不直接修改规则状态。 -- [ ] User Agent 不直接上线规则。 -- [ ] User Agent 不直接审批报销。 -- [ ] User Agent 不直接付款。 -- [ ] User Agent 不直接删除知识。 -- [ ] 所有高风险动作只返回建议或草稿。 -- [ ] 所有草稿动作标记 `requires_confirmation=true`。 - -验收证据: - -- [ ] 提示词要求“直接付款”时仍被阻断。 - -## 9. 测试 - -- [ ] 测试报销查询。 -- [ ] 测试应收查询。 -- [ ] 测试应付查询。 -- [ ] 测试规则解释。 -- [ ] 测试风险解释。 -- [ ] 测试草稿生成。 -- [ ] 测试越权动作阻断。 -- [ ] 测试知识库降级。 - -验收证据: - -- [ ] User Agent 核心测试通过。 - -## 10. Day 5 验收 - -- [ ] User Agent 服务可被 Orchestrator 调用。 -- [ ] 用户入口可提交自然语言问题。 -- [ ] 至少 3 个财务场景有回答。 -- [ ] 回答能引用规则或知识。 -- [ ] 高风险动作不会自动执行。 -- [ ] AgentRun Trace 能看到 User Agent 步骤。 -- [ ] 前端构建通过。 -- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 - -## 阻塞记录 - -- [ ] 暂无。 - -## 日终交接 - -- [ ] 写明已支持的问题类型。 -- [ ] 写明仍使用 Mock 的数据。 -- [ ] 写明 Day 6 Hermes 可以复用的规则、风险、知识接口。 +- 不做自动审批。 +- 不做自动付款。 +- 不做自动上线规则。 +- 不做完整知识库检索优化。 diff --git a/document/development/agent week plan/day_6_hermes_mvp.md b/document/development/agent week plan/day_6_hermes_mvp.md index fce2990..268f014 100644 --- a/document/development/agent week plan/day_6_hermes_mvp.md +++ b/document/development/agent week plan/day_6_hermes_mvp.md @@ -1,191 +1,47 @@ -# Day 6:Hermes MVP TODO +# Day 6:Hermes MVP -目标:实现 Hermes 数字员工的最小闭环。Hermes 不面向用户即时对话,而是负责定时巡检、统计、风险预警、知识维护和规则草稿形成。 +## 今天的大开发点 -参考文档: +实现 Hermes 数字员工的最小闭环。Hermes 负责后台内循环:定时巡检、统计日报、风险预警、知识维护、规则草稿形成。 -- `document/development/agent plan/03_agent_responsibilities.md` -- `document/development/agent plan/11_ocr_invoice_architecture.md` -- `document/development/agent plan/12_llm_wiki_knowledge_architecture.md` -- `document/development/agent plan/15_feedback_learning_loop.md` +## 为什么第六天做这个 -## 0. 开始前检查 +Hermes 依赖前几天已经建立的资产、规则、语义、Orchestrator、Trace 和权限体系。放在第六天做,可以避免它变成孤立脚本。 -- [ ] 确认任务资产 `asset_type=task` 可查询。 -- [ ] 确认 Orchestrator 能处理 `source=schedule`。 -- [ ] 确认 Hermes 占位服务可被调用。 -- [ ] 确认 AgentRun 和 ToolCall 可记录。 -- [ ] 确认是否已有后台任务框架。 -- [ ] 如果没有后台任务框架,先用手动触发 API 模拟定时执行。 +## 今天主要交付 -## 1. Hermes 输入输出 +- 任务资产调度入口。 +- 手动触发任务 API。 +- 每日风险巡检。 +- 每日报销、报账、账款统计。 +- OCR Mock 接入点。 +- 知识候选条目生成。 +- 规则草稿生成。 +- Hermes 运行结果展示。 -- [ ] 定义 `HermesTaskRequest`。 -- [ ] 请求包含 `run_id`。 -- [ ] 请求包含 `task_asset_id`。 -- [ ] 请求包含 `task_type`。 -- [ ] 请求包含 `schedule_time`。 -- [ ] 请求包含 `context_json`。 -- [ ] 定义 `HermesTaskResult`。 -- [ ] 响应包含 `summary`。 -- [ ] 响应包含 `risk_items`。 -- [ ] 响应包含 `statistics`。 -- [ ] 响应包含 `knowledge_updates`。 -- [ ] 响应包含 `draft_rules`。 -- [ ] 响应包含 `next_actions`。 +## 对应执行细则 -验收证据: +- [Day 6 执行细则](<../agent plan/weekly_execution_details/day_6_hermes_mvp.md>) -- [ ] Hermes 响应能被任务详情或运行日志展示。 +相关架构文档: -## 2. 任务调度入口 +- [Agent 职责边界](<../agent plan/03_agent_responsibilities.md>) +- [OCR 票据识别架构](<../agent plan/11_ocr_invoice_architecture.md>) +- [LLM Wiki 知识库架构](<../agent plan/12_llm_wiki_knowledge_architecture.md>) +- [反馈学习闭环](<../agent plan/15_feedback_learning_loop.md>) -- [ ] 新增手动触发任务 API。 -- [ ] API 参数支持任务资产 ID。 -- [ ] API 调用 Orchestrator,source 为 `schedule`。 -- [ ] Orchestrator 路由到 Hermes。 -- [ ] Hermes 执行结果写入 AgentRun。 -- [ ] 任务执行失败时写入错误。 -- [ ] 任务执行结束后更新任务最近执行时间。 -- [ ] 任务执行结束后更新任务最近执行状态。 +## 当天验收门槛 -验收证据: +- 至少一个 Hermes 任务可以手动触发。 +- 风险巡检有结构化结果。 +- 每日统计有结构化结果。 +- OCR Mock 调用能记录 ToolCall。 +- 知识候选只能是草稿。 +- 规则草稿只能是 draft,不能自动上线。 -- [ ] 可以手动触发一次 Hermes 任务并看到运行结果。 +## 今天不做 -## 3. 每日风险巡检 - -- [ ] 实现重复报销巡检。 -- [ ] 实现金额超标巡检。 -- [ ] 实现发票异常巡检占位。 -- [ ] 实现应收逾期巡检。 -- [ ] 实现应付异常付款巡检。 -- [ ] 每个风险项包含风险类型。 -- [ ] 每个风险项包含业务对象。 -- [ ] 每个风险项包含触发规则。 -- [ ] 每个风险项包含建议动作。 -- [ ] 每个风险项包含风险等级。 - -验收证据: - -- [ ] 风险巡检结果可以被用户理解和追溯。 - -## 4. 每日统计 - -- [ ] 统计当日报销单数量。 -- [ ] 统计当日报销金额。 -- [ ] 统计当日报账数量。 -- [ ] 统计当日报账金额。 -- [ ] 统计应收新增金额。 -- [ ] 统计应收逾期金额。 -- [ ] 统计应付待付金额。 -- [ ] 统计应付逾期金额。 -- [ ] 输出日报摘要。 - -验收证据: - -- [ ] Hermes 能生成一份每日财务摘要。 - -## 5. OCR 接入点 - -- [ ] 建立 OCR 识别服务接口。 -- [ ] 定义发票识别输入结构。 -- [ ] 定义发票识别输出结构。 -- [ ] 输出结构包含发票号。 -- [ ] 输出结构包含开票日期。 -- [ ] 输出结构包含金额。 -- [ ] 输出结构包含税额。 -- [ ] 输出结构包含销售方。 -- [ ] 输出结构包含购买方。 -- [ ] 输出结构包含置信度。 -- [ ] 当前阶段允许使用 Mock 结果。 -- [ ] OCR 调用写入 ToolCall。 - -验收证据: - -- [ ] Hermes 风险巡检中可以调用 OCR Mock。 - -## 6. 知识库维护 - -- [ ] 建立知识条目写入服务。 -- [ ] Hermes 可以生成知识候选条目。 -- [ ] 候选条目包含标题。 -- [ ] 候选条目包含正文。 -- [ ] 候选条目包含来源。 -- [ ] 候选条目包含适用场景。 -- [ ] 候选条目默认状态为 `draft`。 -- [ ] 知识条目不能自动发布。 -- [ ] 知识条目写入审计日志。 - -验收证据: - -- [ ] Hermes 可以生成待审核知识条目。 - -## 7. 规则草稿形成 - -- [ ] Hermes 可以根据风险巡检结果生成规则草稿。 -- [ ] 规则草稿保存为 `asset_type=rule`。 -- [ ] 规则草稿状态为 `draft`。 -- [ ] 规则草稿包含 Markdown 内容。 -- [ ] 规则草稿包含生成原因。 -- [ ] 规则草稿包含关联风险样例。 -- [ ] 规则草稿不能自动上线。 -- [ ] 规则草稿需要审核人。 -- [ ] 规则草稿写入审计日志。 - -验收证据: - -- [ ] Hermes 生成的新规则出现在规则列表中,但不是 active。 - -## 8. Hermes 页面或日志展示 - -- [ ] 任务详情能看到最近执行结果。 -- [ ] 任务详情能手动触发执行。 -- [ ] 任务详情能看到风险项数量。 -- [ ] 任务详情能看到日报摘要。 -- [ ] 任务详情能看到知识候选数量。 -- [ ] 任务详情能看到规则草稿数量。 -- [ ] 运行 Trace 能看到 Hermes 步骤。 -- [ ] 错误时展示错误原因。 - -验收证据: - -- [ ] 不查数据库也能判断 Hermes 是否执行成功。 - -## 9. 测试 - -- [ ] 测试手动触发任务。 -- [ ] 测试 Orchestrator 路由到 Hermes。 -- [ ] 测试风险巡检输出。 -- [ ] 测试日报统计输出。 -- [ ] 测试 OCR Mock 调用。 -- [ ] 测试知识候选写入。 -- [ ] 测试规则草稿生成。 -- [ ] 测试 Hermes 异常写入 AgentRun。 - -验收证据: - -- [ ] Hermes 核心测试通过。 - -## 10. Day 6 验收 - -- [ ] Hermes 可被 Orchestrator 调用。 -- [ ] 至少一个任务可以手动触发。 -- [ ] 风险巡检有结构化结果。 -- [ ] 每日统计有结构化结果。 -- [ ] OCR Mock 接入点可用。 -- [ ] 知识候选可生成。 -- [ ] 规则草稿可生成且不能自动上线。 -- [ ] 任务详情或运行日志能展示结果。 -- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 - -## 阻塞记录 - -- [ ] 暂无。 - -## 日终交接 - -- [ ] 写明 Hermes 已支持任务类型。 -- [ ] 写明 OCR 当前是真实还是 Mock。 -- [ ] 写明生成的知识和规则草稿状态。 -- [ ] 写明 Day 7 需要重点回归的路径。 +- 不做完整生产调度集群。 +- 不做真实 OCR 深度集成。 +- 不做自动发布知识。 +- 不做自动上线规则。 diff --git a/document/development/agent week plan/day_7_hardening_demo_acceptance.md b/document/development/agent week plan/day_7_hardening_demo_acceptance.md index a157439..d99025b 100644 --- a/document/development/agent week plan/day_7_hardening_demo_acceptance.md +++ b/document/development/agent week plan/day_7_hardening_demo_acceptance.md @@ -1,223 +1,47 @@ -# Day 7:加固、演示和验收 TODO +# Day 7:加固、演示和验收 -目标:把前 6 天做出的功能整理成可演示、可验收、可继续迭代的基础平台。Day 7 不再大规模扩功能,重点是修缺口、补测试、补日志、补文档、完成演示链路。 +## 今天的大开发点 -参考文档: +不再大规模扩功能,集中做回归、加固、测试、演示脚本、文档收尾和下一阶段交接。 -- `document/development/agent plan/00_README.md` -- `document/development/agent plan/05_development_roadmap.md` -- `document/development/agent plan/09_observability_and_trace.md` -- `document/development/agent plan/10_evaluation_and_testset.md` +## 为什么第七天做这个 -## 0. 开始前检查 +一周开发不能只停留在“代码写了”。必须能演示、能追溯、能说清楚边界、能交给下一阶段继续开发。 -- [ ] 汇总 Day 1 未完成项。 -- [ ] 汇总 Day 2 未完成项。 -- [ ] 汇总 Day 3 未完成项。 -- [ ] 汇总 Day 4 未完成项。 -- [ ] 汇总 Day 5 未完成项。 -- [ ] 汇总 Day 6 未完成项。 -- [ ] 标记必须今天修复的问题。 -- [ ] 标记可以进入下一阶段的问题。 -- [ ] 冻结新增需求,只处理验收相关问题。 +## 今天主要交付 -## 1. 核心链路回归 +- 核心链路回归。 +- 权限和风险边界复查。 +- 审计日志补齐。 +- AgentRun Trace 补齐。 +- 前端体验修补。 +- 测试和构建记录。 +- 评测集执行记录。 +- 演示数据准备。 +- 演示脚本。 +- 下一阶段开发建议。 -- [ ] 回归资产列表接口。 -- [ ] 回归规则详情接口。 -- [ ] 回归 Markdown 保存。 -- [ ] 回归版本列表。 -- [ ] 回归版本切换。 -- [ ] 回归审核接口。 -- [ ] 回归上线拦截。 -- [ ] 回归语义解析接口。 -- [ ] 回归 Orchestrator 路由。 -- [ ] 回归 User Agent 问答。 -- [ ] 回归 Hermes 任务执行。 -- [ ] 回归 AgentRun Trace。 -- [ ] 回归 ToolCall 日志。 -- [ ] 回归 AuditLog 日志。 +## 对应执行细则 -验收证据: +- [Day 7 执行细则](<../agent plan/weekly_execution_details/day_7_hardening_demo_acceptance.md>) -- [ ] 从前端能完成至少一条端到端演示路径。 +相关架构文档: -## 2. 权限和风险边界 +- [Agent Plan 总览](<../agent plan/00_README.md>) +- [开发路线图](<../agent plan/05_development_roadmap.md>) +- [观测与 Trace](<../agent plan/09_observability_and_trace.md>) +- [评测与测试集](<../agent plan/10_evaluation_and_testset.md>) -- [ ] 未审核规则不能上线。 -- [ ] rejected 规则不能上线。 -- [ ] disabled 能力不能被调用。 -- [ ] 用户请求付款必须拦截。 -- [ ] 用户请求审批必须需要确认。 -- [ ] Hermes 生成规则只能是 draft。 -- [ ] Hermes 生成知识只能是 draft。 -- [ ] User Agent 生成处理意见只能是草稿。 -- [ ] 所有高风险动作响应中包含 `requires_confirmation`。 +## 当天验收门槛 -验收证据: +- 任务规则中心核心路径可演示。 +- 语义本体、Orchestrator、User Agent、Hermes 都能跑通最小链路。 +- 未审核规则、高风险动作、自动付款等边界都被拦截。 +- AgentRun、ToolCall、AuditLog 可追溯。 +- 有测试记录、演示脚本和交接说明。 -- [ ] 不存在 MVP 期间绕过人工审核的路径。 +## 今天不做 -## 3. 审计和 Trace 补齐 - -- [ ] 规则保存写 AuditLog。 -- [ ] 规则审核写 AuditLog。 -- [ ] 规则上线写 AuditLog。 -- [ ] Hermes 生成规则草稿写 AuditLog。 -- [ ] Hermes 生成知识候选写 AuditLog。 -- [ ] User Agent 草稿生成写 AuditLog。 -- [ ] Orchestrator 每次运行有 AgentRun。 -- [ ] 每次工具调用有 ToolCall。 -- [ ] Trace 页面或接口能串起 run_id。 -- [ ] 错误 Trace 包含 error_message。 - -验收证据: - -- [ ] 任意一条演示链路都能追溯到 run_id。 - -## 4. 前端体验修补 - -- [ ] 任务规则中心列表无明显错位。 -- [ ] 详情页无双 title。 -- [ ] Hero title 高度紧凑。 -- [ ] 返回列表栏高度正常。 -- [ ] Markdown 编辑器和版本卡片底部对齐。 -- [ ] 版本卡片不贴右侧。 -- [ ] 当前版本标识不突兀。 -- [ ] 日期列对齐。 -- [ ] 弹窗文案清楚。 -- [ ] 加载态可见。 -- [ ] 错误态可见。 -- [ ] 空态可见。 -- [ ] 按钮禁用态可见。 -- [ ] 窄屏不出现内容重叠。 - -验收证据: - -- [ ] 任务规则中心可以给业务用户演示,不需要解释 UI 异常。 - -## 5. 测试补齐 - -- [ ] 运行后端现有测试。 -- [ ] 运行新增模型测试。 -- [ ] 运行新增 API 测试。 -- [ ] 运行语义解析测试。 -- [ ] 运行 Orchestrator 测试。 -- [ ] 运行 User Agent 测试。 -- [ ] 运行 Hermes 测试。 -- [ ] 运行前端构建。 -- [ ] 如果有前端测试,运行前端测试。 -- [ ] 记录未能运行的测试和原因。 - -验收证据: - -- [ ] 测试结果写入本文件“测试记录”。 - -## 6. 评测集 - -- [ ] 准备 5 条报销问题。 -- [ ] 准备 5 条应收问题。 -- [ ] 准备 5 条应付问题。 -- [ ] 准备 3 条规则解释问题。 -- [ ] 准备 3 条越权动作问题。 -- [ ] 执行语义解析评测。 -- [ ] 执行 User Agent 回答评测。 -- [ ] 执行权限拦截评测。 -- [ ] 记录失败样例。 -- [ ] 为失败样例写下一阶段优化建议。 - -验收证据: - -- [ ] 可以说明 MVP 当前能力边界和准确率风险。 - -## 7. 演示数据 - -- [ ] 准备 active 规则。 -- [ ] 准备 pending 规则。 -- [ ] 准备 rejected 规则。 -- [ ] 准备至少一条报销数据。 -- [ ] 准备至少一条应收数据。 -- [ ] 准备至少一条应付数据。 -- [ ] 准备至少一个 Hermes 任务。 -- [ ] 准备至少一个 MCP Mock。 -- [ ] 准备至少一个知识条目。 -- [ ] 准备至少一个风险样例。 - -验收证据: - -- [ ] 演示不会因为没有数据而中断。 - -## 8. 演示脚本 - -- [ ] 编写演示步骤 1:打开任务规则中心。 -- [ ] 编写演示步骤 2:查看规则详情。 -- [ ] 编写演示步骤 3:编辑 Markdown 并保存。 -- [ ] 编写演示步骤 4:切换版本。 -- [ ] 编写演示步骤 5:尝试上线未审核规则并被拦截。 -- [ ] 编写演示步骤 6:输入用户问题。 -- [ ] 编写演示步骤 7:查看语义本体结果。 -- [ ] 编写演示步骤 8:查看 User Agent 回答。 -- [ ] 编写演示步骤 9:手动触发 Hermes 任务。 -- [ ] 编写演示步骤 10:查看 AgentRun Trace。 -- [ ] 编写演示步骤 11:查看审计日志。 - -验收证据: - -- [ ] 新开发者按脚本可以复现演示。 - -## 9. 文档收尾 - -- [ ] 更新一周计划完成情况。 -- [ ] 更新剩余风险。 -- [ ] 更新下一阶段开发建议。 -- [ ] 更新接口清单。 -- [ ] 更新数据模型清单。 -- [ ] 更新前端页面清单。 -- [ ] 更新评测结果。 -- [ ] 更新演示脚本。 -- [ ] 更新部署或启动说明。 - -验收证据: - -- [ ] 文档能指导下一周继续开发。 - -## 10. 最终验收清单 - -- [ ] 任务规则中心可查看规则、技能、MCP、任务。 -- [ ] 规则详情可编辑 Markdown。 -- [ ] 规则详情可查看最近 5 个版本。 -- [ ] 版本切换有确认弹窗。 -- [ ] 审核者信息可见。 -- [ ] 未审核规则不能上线。 -- [ ] 语义本体 8 字段可返回。 -- [ ] Orchestrator 能路由用户请求。 -- [ ] Orchestrator 能路由定时任务。 -- [ ] User Agent 能回答至少 3 类财务问题。 -- [ ] Hermes 能执行至少 1 个任务。 -- [ ] OCR Mock 接入点可用。 -- [ ] 知识候选可生成。 -- [ ] 规则草稿可生成。 -- [ ] AgentRun Trace 可查。 -- [ ] AuditLog 可查。 -- [ ] 前端构建通过。 -- [ ] 后端核心测试通过。 -- [ ] 演示脚本可执行。 -- [ ] 所有完成项已用 `[x] ~~...~~` 标记。 - -## 测试记录 - -- [ ] 后端测试:未运行。 -- [ ] 前端构建:未运行。 -- [ ] 语义评测:未运行。 -- [ ] 手动验收:未运行。 - -## 阻塞记录 - -- [ ] 暂无。 - -## 日终交接 - -- [ ] 写明本周最终完成内容。 -- [ ] 写明未完成内容。 -- [ ] 写明生产化前必须补齐内容。 -- [ ] 写明下一周建议优先级。 +- 不做新大功能。 +- 不临时扩大范围。 +- 不绕过测试和验收。 diff --git a/document/development/agent_week_plan_html/day-1.html b/document/development/agent_week_plan_html/day-1.html new file mode 100644 index 0000000..c9d05ca --- /dev/null +++ b/document/development/agent_week_plan_html/day-1.html @@ -0,0 +1,137 @@ + + + + + + Day 1 - 基础模型与工程骨架 + + + +
+ + +
+ Day 1 + Day 2 + Day 3 + Day 4 + Day 5 + Day 6 + Day 7 +
+ +
+
Foundation Completed
+

Day 1 基础模型与工程骨架

+

这一天的任务不是做炫目的业务能力,而是把后面 6 天要反复依赖的模型、版本、审核、run trace、审计日志和最小业务数据源一次定稳。Day 1 做虚了,Day 4 到 Day 6 会全部返工。

+
+
当前状态
已完成(2026-05-11),可直接进入 Day 2 联调。
+
上游依赖
无,Day 1 是全周底座。
+
下游交接
Day 2 资产 API,Day 3 解析日志,Day 4 run trace,Day 5/6 业务数据查询。
+
当天关键
先确定统一模型,再接 API 骨架和种子数据。
+
+
+ +
Three-Layer Mapping
+

三层文档映射

+
+
+

路线图

+

周计划里定义这一天要完成“工程地基”,强调只做稳定模型、API 骨架、种子数据、基础审计和可运行验证。

+ +
+
+

执行细则

+

执行层把 Day 1 拆成命名边界、最小财务业务数据模型、Agent 资产模型、版本、审核、Run、ToolCall、SemanticParseLog、AuditLog、Schema、API、服务层。

+ +
+
+

架构依据

+

主要受总体架构、语义本体、数据契约、能力注册、权限确认、可观测性和财务标准模型约束。

+ +
+
+ +
Build Order
+

推荐开发顺序

+
+
Step 1先确认后端目录、ORM、迁移方式、测试目录和不该碰的文件。
+
Step 2统一命名:资产类型、状态、审核状态、Agent、权限级别。
+
Step 3补最小财务业务数据模型:expense_claimsaccounts_receivableaccounts_payable
+
Step 4完成 AgentAsset、Version、Review、Run、ToolCall、ParseLog、AuditLog。
+
Step 5把 Schema、API 骨架、服务层、种子数据接起来。
+
+ +
Must Deliver
+

今天必须产出的东西

+
+
+

平台底座表

+
    +
  • AgentAssetAgentAssetVersionAgentAssetReview
  • +
  • AgentRunAgentToolCallSemanticParseLog
  • +
  • AuditLog
  • +
+
+
+

最小业务数据来源

+
    +
  • 报销至少有时间、地点、理由、金额、员工、部门、状态。
  • +
  • 应收至少有客户、金额、未收金额、到期日、账龄、状态。
  • +
  • 应付至少有供应商、金额、未付金额、到期日、账龄、状态。
  • +
+
+
+

API 骨架

+
    +
  • 资产列表 / 详情 / 版本 / 审核 / 上线。
  • +
  • 运行日志与审计日志查询。
  • +
  • 返回真实数据库结果,不用前端硬编码收尾。
  • +
+
+
+

统一服务边界

+
    +
  • 上线拦截逻辑在服务层,不堆到路由。
  • +
  • 所有写操作要留审计接口。
  • +
  • 任何 Agent 执行记录都必须生成 run_id
  • +
+
+
+ +
Acceptance Snapshot
+

验收快照

+
+
资产模型
已落地 3 条规则、2 条技能、2 条 MCP、3 条任务,并可通过资产接口返回。
+
版本与审核
三条规则都具备版本历史;同一资产版本号不可重复,未审核规则不能上线。
+
运行与错误
`GET /api/v1/agent-runs` 可返回 3 条运行日志,任意新建 Run 自动生成 run_id
+
最小业务表
报销、应收、应付种子数据已就位,后续查询和风险巡检都有明确数据来源。
+
+ +
Common Misses
+

这一天最容易漏掉的点

+ + + +
+ + diff --git a/document/development/agent_week_plan_html/day-2.html b/document/development/agent_week_plan_html/day-2.html new file mode 100644 index 0000000..3bdceeb --- /dev/null +++ b/document/development/agent_week_plan_html/day-2.html @@ -0,0 +1,132 @@ + + + + + + Day 2 - 任务规则中心联调 + + + +
+ + +
+ Day 1 + Day 2 + Day 3 + Day 4 + Day 5 + Day 6 + Day 7 +
+ +
+
Integration
+

Day 2 任务规则中心联调

+

Day 2 的核心不是“把页面做漂亮”,而是让规则、技能、MCP、任务这四类资产第一次脱离本地假数据,真正连到 Day 1 的数据库和 API。最关键的能力是 Markdown、版本、审核和上线约束闭环。

+
+
上游依赖
Day 1 的资产模型、版本模型、审核模型、资产 API。
+
下游交接
Day 3 要复用资产数据,Day 4 要查询 active 技能 / MCP / 任务。
+
当天关键
前端联调不是硬编码演示,而是可对接真实后端。
+
+
+ +
Three-Layer Mapping
+

三层文档映射

+
+
+

路线图

+

周计划要求把任务规则中心从静态 UI 升级到真实数据对接,覆盖规则、技能、MCP、任务四类资产。

+ +
+
+

执行细则

+

执行层拆成 API Client、四类列表、规则详情、Markdown 编辑、版本卡片、审核与上线、技能详情、MCP 详情、任务详情、前端质量和当天验收。

+ +
+
+

架构依据

+

这一天主要受能力注册、规则形成生命周期和数据治理约束,重点在四类资产的统一展示方式和规则上线前审核拦截。

+ +
+
+ +
Build Order
+

推荐开发顺序

+
+
Step 1先补 API Client:列表、详情、版本、保存、审核、上线、运行日志。
+
Step 2把四个页签的真实数据接起来,覆盖筛选、搜索、状态、空态和加载态。
+
Step 3把规则详情的 Hero 区、Markdown 编辑器、版本卡片和审核信息拉通。
+
Step 4补技能 / MCP / 任务的差异化详情,不复用规则编辑器。
+
Step 5最后收 UI 细节、错误态、禁用态、确认弹窗和构建验证。
+
+ +
Must Deliver
+

今天必须产出的东西

+
+
+

规则中心四页签

+
    +
  • 规则、技能、MCP、任务都能切换。
  • +
  • 每个页签都来自真实接口,不再只读本地常量。
  • +
  • 搜索和状态筛选同时生效。
  • +
+
+
+

规则详情闭环

+
    +
  • 能读取当前 Markdown。
  • +
  • 能保存并刷新版本列表。
  • +
  • 能展示审核者、审核状态、上线条件。
  • +
+
+
+

版本与上线约束

+
    +
  • 最近 5 个版本可见。
  • +
  • 切换旧版本必须弹确认框。
  • +
  • 未审核规则不能上线,拒绝原因要可见。
  • +
+
+
+

详情差异化

+
    +
  • 技能详情展示输入输出与依赖。
  • +
  • MCP 详情展示服务地址、鉴权、降级策略。
  • +
  • 任务详情展示 cron、执行 Agent、最近执行结果。
  • +
+
+
+ +
Acceptance Snapshot
+

验收快照

+
+
真实数据
四个页签都能用真实后端数据渲染,后端不可用时有明确错误提示。
+
规则编辑
Markdown 保存后刷新页面仍在,保存失败不丢输入。
+
版本卡片
最近 5 个版本可切换,当前版本标识清楚但不造成布局位移。
+
审核上线
pending / rejected 规则都无法上线,approved 才能放行。
+
+ +
Common Misses
+

这一天最容易漏掉的点

+ + + +
+ + diff --git a/document/development/agent_week_plan_html/day-3.html b/document/development/agent_week_plan_html/day-3.html new file mode 100644 index 0000000..d7c24f0 --- /dev/null +++ b/document/development/agent_week_plan_html/day-3.html @@ -0,0 +1,132 @@ + + + + + + Day 3 - 语义本体 MVP + + + +
+ + +
+ Day 1 + Day 2 + Day 3 + Day 4 + Day 5 + Day 6 + Day 7 +
+ +
+
Ontology
+

Day 3 语义本体 MVP

+

这一天把自然语言问题统一切成 8 个核心字段。Day 3 不是追求大模型多聪明,而是先让结构稳定、可落日志、可被 Orchestrator、User Agent 和 Hermes 共用。

+
+
上游依赖
Day 1 的 SemanticParseLog / AgentRun,Day 2 的资产 API。
+
下游交接
Day 4 路由、Day 5 查询解释、Day 6 风险巡检都直接消费这 8 字段。
+
当天关键
名字统一、类型统一、日志统一、低置信度有澄清问题。
+
+
+ +
Three-Layer Mapping
+

三层文档映射

+
+
+

路线图

+

周计划要求建立用户问题的统一语义解析层,覆盖场景、意图、对象、时间、指标、约束、风险、权限 8 字段。

+ +
+
+

执行细则

+

执行层拆成 8 字段定义、字段枚举、Schema、解析服务、对象提取、时间范围、指标约束、风险权限、API、前端调试入口和评测集。

+ +
+
+

架构依据

+

主要受语义本体、财务标准模型和数据治理约束。应收、应付、报销的对象语义必须能回到最小业务表和标准对象。

+ +
+
+ +
Build Order
+

推荐开发顺序

+
+
Step 1先固定 8 个字段名字、类型、默认值和示例。
+
Step 2scenariointentpermission.level 的枚举定死。
+
Step 3做请求/响应 Schema,再写解析服务。
+
Step 4补对象提取、时间范围、指标约束、风险和权限映射。
+
Step 5接 API、日志、调试入口和最小评测集。
+
+ +
Must Deliver
+

今天必须产出的东西

+
+
+

8 字段统一结构

+
    +
  • scenariointententitiestime_range
  • +
  • metricsconstraintsrisk_flagspermission
  • +
  • 附带 confidenceclarification_requiredrun_id
  • +
+
+
+

规则解析优先版

+
    +
  • 先用关键词和规则解析打底。
  • +
  • 报销 / 应收 / 应付 / 知识 / unknown 场景都能落到结构。
  • +
  • 越权动作能识别为 approval_requiredforbidden
  • +
+
+
+

日志和调试入口

+
    +
  • 每次解析都要落 SemanticParseLog
  • +
  • 前端可直接输入一句话看 8 字段结果。
  • +
  • 低置信度问题必须给澄清问题。
  • +
+
+
+

最小评测集

+
    +
  • 至少覆盖报销、应收、应付、知识、越权动作。
  • +
  • 每条样例要写期望 scenariointent 和权限级别。
  • +
  • 当天目标是可评测,而不是追求完美准确率。
  • +
+
+
+ +
Acceptance Snapshot
+

验收快照

+
+
语义结构
8 字段在 Schema、服务层、日志里名字完全一致。
+
关键识别
“本周报销超标风险”“客户 A 本月应收”“供应商 B 明天要付多少钱”都能落到正确场景和意图。
+
权限结果
“帮我直接付款”不能被识别成可直接执行动作。
+
日志与前端
连续调用多次都能在日志中查到,并能通过调试入口观察结果。
+
+ +
Common Misses
+

这一天最容易漏掉的点

+ + + +
+ + diff --git a/document/development/agent_week_plan_html/day-4.html b/document/development/agent_week_plan_html/day-4.html new file mode 100644 index 0000000..e0dec54 --- /dev/null +++ b/document/development/agent_week_plan_html/day-4.html @@ -0,0 +1,133 @@ + + + + + + Day 4 - Orchestrator 运行时 + + + +
+ + +
+ Day 1 + Day 2 + Day 3 + Day 4 + Day 5 + Day 6 + Day 7 +
+ +
+
Runtime
+

Day 4 Orchestrator 运行时

+

Day 4 把整个系统第一次串成“能跑的链”。用户消息和定时任务都先走 Orchestrator,由它创建 run、调用语义解析、做权限判断、选择 Agent、记录 ToolCall 和 Trace,然后再给下游执行。

+
+
上游依赖
Day 3 的语义解析结果,Day 1 的 Run / ToolCall,Day 2 的 active 资产。
+
下游交接
Day 5 User Agent 和 Day 6 Hermes 都通过它被调度。
+
当天关键
权限拦截和 Trace 必须在 Orchestrator 层,而不是散落在各 Agent。
+
+
+ +
Three-Layer Mapping
+

三层文档映射

+
+
+

路线图

+

周计划要求建立统一调度层,让用户请求和系统任务都先进入 Orchestrator,再根据语义、权限、能力注册路由到 User Agent、Hermes、MCP 或规则引擎。

+ +
+
+

执行细则

+

执行层拆成输入输出、Orchestrator 服务、路由规则、权限判断、能力查询、工具调用封装、API、最小 Trace 查看和测试。

+ +
+
+

架构依据

+

主要受运行时流程、能力注册、权限确认和可观测性约束。Day 4 的输出要能直接给前端展示,并支持 Day 5/6 的占位实现接入。

+ +
+
+ +
Build Order
+

推荐开发顺序

+
+
Step 1先定 OrchestratorRequestOrchestratorResponse
+
Step 2run(request) 主流程:创建 Run、解析语义、判权限、选 Agent、更新状态。
+
Step 3把用户入口 / 任务入口的路由规则固化下来。
+
Step 4封装工具调用记录和降级策略。
+
Step 5暴露 API 和最小 Trace 页面或接口。
+
+ +
Must Deliver
+

今天必须产出的东西

+
+
+

统一入口

+
    +
  • source=user_messagesource=schedule 都能进同一入口。
  • +
  • 请求返回 run_idselected_agentroute_reasonpermission_level
  • +
  • 返回结果要能被前端直接展示。
  • +
+
+
+

权限与路由

+
    +
  • 查询类走 User Agent,定时风险类走 Hermes。
  • +
  • approval_required 只返回确认,不直接执行。
  • +
  • forbidden 直接阻断,不调下游 Agent。
  • +
+
+
+

能力与工具调用

+
    +
  • 只查询 active 技能 / MCP / 任务。
  • +
  • 禁用能力不允许被调用。
  • +
  • 每次工具调用都能落 AgentToolCall
  • +
+
+
+

Trace 与降级

+
    +
  • Trace 能串起语义解析、路由、工具调用和最终结果。
  • +
  • 外部 MCP 失败要返回降级说明,不让前端拿到不可读错误。
  • +
  • 异常都要写进 AgentRun.error_message
  • +
+
+
+ +
Acceptance Snapshot
+

验收快照

+
+
路由结果
同一句风险检查,在用户入口和任务入口会有不同路由结果。
+
权限边界
“直接上线规则”和“直接付款”都不会被自动执行。
+
日志完整度
每次运行至少有一条 AgentRun,工具调用有 0 到多条 AgentToolCall
+
可观察性
前端或 curl 可以完整看到一次运行链路,不需要直接查数据库猜过程。
+
+ +
Common Misses
+

这一天最容易漏掉的点

+ + + +
+ + diff --git a/document/development/agent_week_plan_html/day-5.html b/document/development/agent_week_plan_html/day-5.html new file mode 100644 index 0000000..c7e01ef --- /dev/null +++ b/document/development/agent_week_plan_html/day-5.html @@ -0,0 +1,133 @@ + + + + + + Day 5 - User Agent MVP + + + +
+ + +
+ Day 1 + Day 2 + Day 3 + Day 4 + Day 5 + Day 6 + Day 7 +
+ +
+
User Agent
+

Day 5 User Agent MVP

+

这一天开始让“用户真的能问问题”。但 User Agent 只负责查询、解释、规则引用和草稿生成,绝不绕过权限做审批、付款、上线这类高风险动作。

+
+
上游依赖
Day 4 Orchestrator、Day 3 语义结构、Day 1 业务数据与日志模型、Day 2 规则资产。
+
下游交接
Day 7 要拿它做问答演示、规则解释演示和草稿生成演示。
+
当天关键
回答可读、引用可追溯、草稿可确认、高风险不自动执行。
+
+
+ +
Three-Layer Mapping
+

三层文档映射

+
+
+

路线图

+

周计划要求做用户自然语言入口、报销 / 应收 / 应付查询解释、规则引用解释、建议草稿和前端入口。

+ +
+
+

执行细则

+

执行层拆成输入输出、查询处理、规则解释、风险解释、草稿生成、知识库读取骨架、对话入口、安全边界和测试。

+ +
+
+

架构依据

+

主要受 Agent 职责划分、运行时流程、知识架构和规则形成生命周期约束。所有高风险动作只能停留在建议或草稿层。

+ +
+
+ +
Build Order
+

推荐开发顺序

+
+
Step 1先定 UserAgentRequest / UserAgentResponse 协议。
+
Step 2优先实现报销、应收、应付查询处理器。
+
Step 3补规则解释和风险解释,让回答有依据而不是只给一句话。
+
Step 4补草稿生成与知识读取骨架。
+
Step 5最后接前端问答入口、加载态、错误态和确认提示。
+
+ +
Must Deliver
+

今天必须产出的东西

+
+
+

三类财务查询

+
    +
  • 报销查询可读,能查金额、状态或进度。
  • +
  • 应收查询可读,能查客户未收金额或账龄。
  • +
  • 应付查询可读,能查供应商待付款或付款状态。
  • +
+
+
+

解释能力

+
    +
  • 规则解释能引用 active 规则、版本号和更新时间。
  • +
  • 风险解释能说明风险类型、原因和建议动作。
  • +
  • 知识库不可用时要优雅降级。
  • +
+
+
+

草稿而非执行

+
    +
  • 可生成报销处理意见草稿、应收催收建议草稿、应付付款建议草稿。
  • +
  • 草稿必须写明“待人工确认”。
  • +
  • 草稿行为写入审计日志和 AgentRun 结果。
  • +
+
+
+

用户入口

+
    +
  • 前端输入框走 Orchestrator,不绕行。
  • +
  • 显示回答、引用、建议动作、确认提示和 run_id
  • +
  • 有加载态和错误态。
  • +
+
+
+ +
Acceptance Snapshot
+

验收快照

+
+
问答闭环
用户在页面上能完成一次自然语言提问、拿到回答、看到引用和 run_id。
+
三类场景
至少报销、应收、应付三类财务问题都有结构化回答。
+
引用能力
“为什么这笔报销有风险”这类问题能引用规则,而不是只给模糊判断。
+
安全边界
“直接付款”“直接审批”类提示不会自动执行,只能变成建议或草稿。
+
+ +
Common Misses
+

这一天最容易漏掉的点

+ + + +
+ + diff --git a/document/development/agent_week_plan_html/day-6.html b/document/development/agent_week_plan_html/day-6.html new file mode 100644 index 0000000..753a044 --- /dev/null +++ b/document/development/agent_week_plan_html/day-6.html @@ -0,0 +1,133 @@ + + + + + + Day 6 - Hermes MVP + + + +
+ + +
+ Day 1 + Day 2 + Day 3 + Day 4 + Day 5 + Day 6 + Day 7 +
+ +
+
Hermes
+

Day 6 Hermes MVP

+

Hermes 是后台数字员工,不做即时对话,而是负责定时巡检、风险预警、日报统计、知识候选和规则草稿。它的关键不是“会不会说”,而是“任务能不能跑、结果能不能追”。

+
+
上游依赖
Day 4 的 Orchestrator 路由,Day 1 的任务与日志表,Day 3 的语义结构,Day 5 可复用的风险/规则/知识接口。
+
下游交接
Day 7 要用它做手动触发任务、查看结果、展示规则草稿和知识候选。
+
当天关键
任务入口、风险项结构、OCR Mock、知识候选和规则草稿都必须可追溯。
+
+
+ +
Three-Layer Mapping
+

三层文档映射

+
+
+

路线图

+

周计划要求实现 Hermes 调度入口、每日风险巡检、统计任务、知识库维护、OCR Mock 和运行结果面板或 API。

+ +
+
+

执行细则

+

执行层拆成输入输出、任务调度入口、风险巡检、每日统计、OCR 接入点、知识库维护、规则草稿形成、结果展示和测试。

+ +
+
+

架构依据

+

主要受 Agent 职责、OCR 架构、知识库架构和反馈学习闭环约束。Hermes 能生成候选和草稿,但不能自动发布正式结果。

+ +
+
+ +
Build Order
+

推荐开发顺序

+
+
Step 1先定 HermesTaskRequest / HermesTaskResult
+
Step 2建立手动触发任务 API,经 Orchestrator 路由到 Hermes。
+
Step 3补风险巡检和每日统计的结构化输出。
+
Step 4接入 OCR Mock、知识候选生成、规则草稿生成。
+
Step 5补任务详情展示、错误信息和测试。
+
+ +
Must Deliver
+

今天必须产出的东西

+
+
+

任务调度入口

+
    +
  • 可手动触发至少一个任务资产。
  • +
  • 任务经 Orchestrator 进入 Hermes。
  • +
  • 结束后能更新最近执行时间和状态。
  • +
+
+
+

风险与统计

+
    +
  • 重复报销、金额超标、应收逾期、应付异常付款等风险有结构化输出。
  • +
  • 日报包含报销、报账、应收、应付的关键统计口径。
  • +
  • 每个风险项都要能被业务人员理解和追溯。
  • +
+
+
+

知识候选与规则草稿

+
    +
  • 知识候选默认是 draft,不能自动发布。
  • +
  • 规则草稿保存为 asset_type=rule,状态为 draft
  • +
  • 两类生成都要写审计日志。
  • +
+
+
+

OCR Mock 与结果展示

+
    +
  • OCR 服务接口和输入输出结构定下来。
  • +
  • 当前阶段允许完全使用 Mock 结果。
  • +
  • 任务详情或运行日志中能直接看到 Hermes 的执行结果。
  • +
+
+
+ +
Acceptance Snapshot
+

验收快照

+
+
任务可触发
至少一个任务可以手动触发,并能查到结构化结果。
+
风险巡检
输出里能看到风险类型、业务对象、触发规则、建议动作和风险等级。
+
候选与草稿
知识候选和规则草稿都能生成,但都不是 active / published 正式状态。
+
可观察性
不用查数据库,也能从任务详情或运行日志判断 Hermes 是否执行成功。
+
+ +
Common Misses
+

这一天最容易漏掉的点

+ + + +
+ + diff --git a/document/development/agent_week_plan_html/day-7.html b/document/development/agent_week_plan_html/day-7.html new file mode 100644 index 0000000..c1c1442 --- /dev/null +++ b/document/development/agent_week_plan_html/day-7.html @@ -0,0 +1,132 @@ + + + + + + Day 7 - 加固、演示和验收 + + + +
+ + +
+ Day 1 + Day 2 + Day 3 + Day 4 + Day 5 + Day 6 + Day 7 +
+ +
+
Hardening
+

Day 7 加固、演示和验收

+

Day 7 不再追求新增大功能,而是把 Day 1 到 Day 6 的链路整理成“可演示、可验收、可继续接手”的状态。没有这一层收口,前面做出来的东西很容易停在“只有作者自己懂”的阶段。

+
+
上游依赖
Day 1 到 Day 6 的全部核心路径。
+
当天输出
回归记录、权限边界、审计和 Trace 补齐、测试记录、演示脚本、交接说明。
+
当天关键
冻结新增需求,只收验收相关缺口。
+
+
+ +
Three-Layer Mapping
+

三层文档映射

+
+
+

路线图

+

周计划要求完成回归、权限补齐、审计补齐、错误态和空态、评测、演示数据、构建和交付说明。

+ +
+
+

执行细则

+

执行层拆成核心链路回归、权限和风险边界、审计和 Trace、前端体验修补、测试补齐、评测集、演示数据、演示脚本和文档收尾。

+ +
+
+

架构依据

+

主要受整体 README、开发路线图、可观测性和评测集约束。Day 7 的本质是把所有边界和证据讲清楚。

+ +
+
+ +
Build Order
+

推荐收口顺序

+
+
Step 1先汇总 Day 1 到 Day 6 未完成项,冻结新增需求。
+
Step 2回归核心链路:资产、规则、语义解析、Orchestrator、User Agent、Hermes、Trace、AuditLog。
+
Step 3补权限边界与高风险动作拦截。
+
Step 4补测试、评测、演示数据和前端体验问题。
+
Step 5写演示脚本和交接说明,形成最终交付。
+
+ +
Must Deliver
+

今天必须产出的东西

+
+
+

回归与边界

+
    +
  • 未审核规则不能上线。
  • +
  • 付款、审批、上线等高风险动作都不能绕过确认。
  • +
  • disabled 能力不能被调用。
  • +
+
+
+

审计与 Trace

+
    +
  • 规则保存、审核、上线都能看到 AuditLog。
  • +
  • Hermes 生成知识候选 / 规则草稿有审计。
  • +
  • 任意演示路径都能追到 run_id
  • +
+
+
+

测试、评测、演示数据

+
    +
  • 后端测试、前端构建、语义评测至少有执行记录。
  • +
  • 报销 / 应收 / 应付 / 风险 / 知识都准备好演示数据。
  • +
  • 失败样例和已知边界要明确写出。
  • +
+
+
+

演示脚本与交接

+
    +
  • 从任务规则中心、规则详情、版本切换、上线拦截,到 User Agent 问答、Hermes 任务、Trace 和审计,都有明确步骤。
  • +
  • 新开发者按脚本能走通一遍。
  • +
+
+
+ +
Acceptance Snapshot
+

最终验收快照

+
+
端到端链路
从规则中心到 User Agent,再到 Hermes 和 Trace,至少有一条完整演示路径可复现。
+
证据完整
AgentRun、ToolCall、AuditLog、测试记录、评测结果和演示脚本都存在。
+
风险边界
MVP 期间不存在绕过人工审核、自动付款、自动上线的暗门路径。
+
可交接性
下一位开发或 Codex 打开文档就能知道已完成、未完成和生产化前必补项。
+
+ +
Common Misses
+

这一天最容易漏掉的点

+ + + +
+ + diff --git a/document/development/agent_week_plan_html/index.html b/document/development/agent_week_plan_html/index.html new file mode 100644 index 0000000..bce99ea --- /dev/null +++ b/document/development/agent_week_plan_html/index.html @@ -0,0 +1,181 @@ + + + + + + Agent Week Plan HTML + + + +
+ + +
+
Static Map
+

把 7 天周计划变成可直接浏览的开发视图

+

这一套 HTML 页面不是替代 Markdown,而是把 agent week planweekly_execution_detailsagent plan 的对应关系收成一个稳定入口。Codex 可以按日推进,开发人员也能按目标、依赖、验收和风险快速定位。

+
+
+
阅读顺序
+
先总览,再选 Day,再跳转到具体 Markdown 落地执行。
+
+
+
核心视图
+
路线图、执行细则、架构依据三层同时可见。
+
+
+
适用对象
+
Codex 开发、后端开发、前端开发、项目 owner、验收人员。
+
+
+
+ +
How To Use
+

怎么用这套页面

+
+
+

Codex 开发视角

+
    +
  1. 先看今天在哪一天,确认上游依赖和下游交接。
  2. +
  3. 用“三层映射”定位:周计划看目标,执行细则看步骤,架构文档看约束。
  4. +
  5. 按“推荐开发顺序”推进,不跳天,不跨层乱做。
  6. +
  7. 完成后回到原始 Markdown,把 TODO、阻塞、交接更新回文档。
  8. +
+
+
+

人工开发与验收视角

+
    +
  1. 先看每一天的“今日定位”,知道这一天到底产出什么。
  2. +
  3. 再看“今天必须产出的东西”和“验收快照”,确认完成标准。
  4. +
  5. 最后跳转到对应 Markdown,逐条执行或验收。
  6. +
  7. 如果发现跨天阻塞,优先回前一天补地基,而不是在当前天临时兜底。
  8. +
+
+
+ +
Three Layers
+

文档结构一眼看清

+
+
+

1. 周计划路线图

+

定义每天的大方向、交付物和验收门槛。用于排期、对齐和验收。核心入口是 MASTER_TODO.md 和 Day 1 到 Day 7 daily 文档。

+ +
+
+

2. 执行细则

+

把每天的开发目标拆到模型、字段、接口、服务、前端、测试和验收证据。这里是 Codex 和研发的直接执行层。

+ +
+
+

3. 架构依据

+

提供为什么要这么做、协议怎么定、权限和审计边界是什么。它不直接当 TODO,但所有实现都要受它约束。

+ +
+
+ +
Seven Days
+

7 天总览

+
+
+

Day 1 基础模型与工程骨架

+

当前状态:已完成(2026-05-11)。先把 Agent 资产、版本、审核、运行日志、审计日志,以及报销 / 应收 / 应付的最小业务数据来源定下来。后面所有能力都站在这一天的模型上。

+ +
+
+

Day 2 任务规则中心联调

+

把规则、技能、MCP、任务从静态 UI 拉到真实后端数据。重点是规则 Markdown、版本切换、审核和上线拦截。

+ +
+
+

Day 3 语义本体 MVP

+

建立 8 字段语义解析协议,让报销、应收、应付、知识查询进入同一结构,给 Orchestrator、User Agent、Hermes 统一消费。

+ +
+
+

Day 4 Orchestrator 运行时

+

把用户消息和定时任务统一接到 Orchestrator,完成 run_id、权限拦截、Agent 路由、ToolCall 和 Trace。

+ +
+
+

Day 5 User Agent MVP

+

面向用户的问答和流程辅助层。做查询、解释、规则引用、草稿生成,但严格不碰自动审批、自动付款和自动上线。

+ +
+
+

Day 6 Hermes MVP

+

后台数字员工层。做任务触发、风险巡检、日报统计、OCR Mock、知识候选、规则草稿,结果都必须可追溯。

+ +
+
+

Day 7 加固、演示和验收

+

不再大扩功能,只做回归、权限边界、审计、Trace、测试、演示脚本和交接收口,让整周产出可跑、可演示、可继续接手。

+ +
+
+ +
Dependency Chain
+

跨天依赖链

+
+
Day 1模型、审计、运行日志、最小业务数据源
+
Day 2把 Day 1 的资产 API 接进规则中心 UI
+
Day 3在 Day 1/2 基础上产出统一语义结构
+
Day 4用 Day 3 的语义结果完成路由与权限
+
Day 5接入 User Agent 问答、解释和草稿
+
Day 6接入 Hermes 任务、巡检和知识/规则候选
+
Day 7统一回归、补日志、做演示和交接
+
+ + +
+ + diff --git a/document/development/agent_week_plan_html/styles.css b/document/development/agent_week_plan_html/styles.css new file mode 100644 index 0000000..0db90b9 --- /dev/null +++ b/document/development/agent_week_plan_html/styles.css @@ -0,0 +1,426 @@ +:root { + --bg: #f3ead9; + --bg-deep: #e7d8bc; + --panel: rgba(255, 250, 241, 0.9); + --panel-strong: #fff8ee; + --ink: #1f2a24; + --muted: #64655d; + --line: #dbc8a9; + --accent: #bb5b2c; + --accent-strong: #8d3d1b; + --accent-soft: #f4d9bf; + --teal: #20656d; + --teal-soft: #d8ecee; + --olive: #5f6b3a; + --olive-soft: #e6ecd7; + --shadow: 0 24px 60px rgba(84, 59, 30, 0.12); + --radius-xl: 28px; + --radius-lg: 20px; + --radius-md: 14px; + --max: 1240px; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + font-family: "Trebuchet MS", "Gill Sans", "Lucida Grande", sans-serif; + color: var(--ink); + background: + radial-gradient(circle at top left, rgba(32, 101, 109, 0.14), transparent 26%), + radial-gradient(circle at top right, rgba(187, 91, 44, 0.15), transparent 30%), + linear-gradient(180deg, #f8f0e2 0%, var(--bg) 40%, #efe2cb 100%); +} + +a { + color: inherit; +} + +.shell { + width: min(100% - 40px, var(--max)); + margin: 0 auto; + padding: 28px 0 56px; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 18px; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 12px; + text-decoration: none; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--accent-strong); +} + +.brand-mark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border-radius: 50%; + background: linear-gradient(135deg, var(--accent), #df9a44); + color: #fff7ef; + box-shadow: 0 14px 30px rgba(187, 91, 44, 0.28); +} + +.quick-links, +.day-nav { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.pill { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 38px; + padding: 10px 14px; + border-radius: 999px; + border: 1px solid rgba(143, 114, 74, 0.22); + background: rgba(255, 248, 238, 0.75); + text-decoration: none; + color: var(--muted); + font-size: 14px; + transition: transform 180ms ease, border-color 180ms ease, background 180ms ease; +} + +.pill:hover, +.pill:focus-visible { + transform: translateY(-1px); + border-color: rgba(187, 91, 44, 0.4); + background: rgba(255, 251, 245, 0.96); + outline: none; +} + +.pill.active { + color: #fff6ef; + border-color: transparent; + background: linear-gradient(135deg, var(--accent-strong), var(--accent)); + box-shadow: 0 14px 24px rgba(141, 61, 27, 0.24); +} + +.hero { + position: relative; + overflow: hidden; + margin-bottom: 22px; + padding: 30px; + border: 1px solid rgba(128, 109, 82, 0.18); + border-radius: var(--radius-xl); + background: + linear-gradient(135deg, rgba(255, 248, 238, 0.95), rgba(247, 236, 216, 0.88)), + var(--panel); + box-shadow: var(--shadow); +} + +.hero::after { + content: ""; + position: absolute; + right: -50px; + top: -50px; + width: 220px; + height: 220px; + border-radius: 50%; + background: radial-gradient(circle, rgba(32, 101, 109, 0.16), transparent 68%); +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + padding: 7px 12px; + border-radius: 999px; + background: var(--accent-soft); + color: var(--accent-strong); + font-size: 13px; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.hero h1 { + margin: 0; + font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif; + font-size: clamp(34px, 5vw, 62px); + line-height: 1.03; +} + +.hero p { + max-width: 880px; + margin: 14px 0 0; + color: var(--muted); + font-size: 18px; + line-height: 1.65; +} + +.hero-meta { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 14px; + margin-top: 20px; +} + +.meta-card { + padding: 14px 16px; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.55); + border: 1px solid rgba(132, 109, 83, 0.16); +} + +.meta-label { + margin-bottom: 6px; + color: var(--muted); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.meta-value { + font-size: 16px; + line-height: 1.45; +} + +.grid { + display: grid; + gap: 18px; +} + +.grid.two { + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); +} + +.grid.three { + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); +} + +.card { + padding: 22px; + border: 1px solid rgba(132, 109, 83, 0.15); + border-radius: var(--radius-lg); + background: var(--panel); + box-shadow: 0 16px 36px rgba(78, 58, 32, 0.08); + animation: rise 420ms ease both; +} + +.card:nth-child(2) { animation-delay: 60ms; } +.card:nth-child(3) { animation-delay: 120ms; } +.card:nth-child(4) { animation-delay: 180ms; } +.card:nth-child(5) { animation-delay: 240ms; } + +.card h2, +.card h3 { + margin: 0 0 10px; + font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif; +} + +.card h2 { + font-size: 28px; +} + +.card h3 { + font-size: 22px; +} + +.card p { + margin: 0; + color: var(--muted); + line-height: 1.7; +} + +.section-title { + margin: 28px 0 14px; + font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif; + font-size: 28px; +} + +.section-kicker { + margin: 30px 0 8px; + color: var(--accent-strong); + font-size: 13px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.list, +.compact-list { + margin: 12px 0 0; + padding-left: 18px; + color: var(--ink); + line-height: 1.72; +} + +.compact-list { + font-size: 15px; +} + +.list li + li, +.compact-list li + li { + margin-top: 8px; +} + +.card-links { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 16px; +} + +.link-chip { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 13px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.76); + border: 1px solid rgba(132, 109, 83, 0.18); + text-decoration: none; + font-size: 14px; +} + +.tone-warm { + background: linear-gradient(180deg, rgba(244, 217, 191, 0.55), rgba(255, 250, 241, 0.9)); +} + +.tone-teal { + background: linear-gradient(180deg, rgba(216, 236, 238, 0.76), rgba(255, 250, 241, 0.92)); +} + +.tone-olive { + background: linear-gradient(180deg, rgba(230, 236, 215, 0.82), rgba(255, 250, 241, 0.92)); +} + +.tone-accent { + background: linear-gradient(160deg, rgba(141, 61, 27, 0.94), rgba(187, 91, 44, 0.92)); + color: #fff8f1; +} + +.tone-accent p, +.tone-accent .meta-label, +.tone-accent .meta-value, +.tone-accent li { + color: rgba(255, 248, 241, 0.92); +} + +.tone-accent .link-chip, +.tone-accent .pill { + background: rgba(255, 255, 255, 0.14); + border-color: rgba(255, 255, 255, 0.18); + color: #fff8f1; +} + +.timeline { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; +} + +.timeline-step { + position: relative; + padding: 16px; + border-radius: var(--radius-md); + border: 1px solid rgba(132, 109, 83, 0.16); + background: rgba(255, 252, 247, 0.84); +} + +.timeline-step strong { + display: block; + margin-bottom: 8px; + font-size: 15px; +} + +.footer { + margin-top: 26px; + padding: 20px 4px 0; + color: var(--muted); + font-size: 14px; +} + +.muted { + color: var(--muted); +} + +.table-like { + display: grid; + gap: 12px; +} + +.row { + display: grid; + grid-template-columns: minmax(120px, 0.9fr) minmax(0, 2.3fr); + gap: 14px; + padding: 14px 16px; + border-radius: var(--radius-md); + border: 1px solid rgba(132, 109, 83, 0.15); + background: rgba(255, 255, 255, 0.56); +} + +.row-label { + font-size: 13px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--accent-strong); +} + +.row-value { + line-height: 1.68; +} + +code { + padding: 1px 6px; + border-radius: 8px; + background: rgba(32, 101, 109, 0.08); + color: var(--teal); + font-family: "Lucida Console", "Courier New", monospace; + font-size: 0.92em; +} + +@keyframes rise { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 760px) { + .shell { + width: min(100% - 24px, var(--max)); + padding-top: 18px; + } + + .hero { + padding: 22px; + } + + .hero p { + font-size: 16px; + } + + .row { + grid-template-columns: 1fr; + } +} diff --git a/server/src/app/api/v1/endpoints/agent_assets.py b/server/src/app/api/v1/endpoints/agent_assets.py new file mode 100644 index 0000000..b37baac --- /dev/null +++ b/server/src/app/api/v1/endpoints/agent_assets.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends, Header, HTTPException, Query, status +from sqlalchemy.orm import Session + +from app.api.deps import get_db +from app.schemas.agent_asset import ( + AgentAssetCreate, + AgentAssetListItem, + AgentAssetRead, + AgentAssetReviewCreate, + AgentAssetReviewRead, + AgentAssetUpdate, + AgentAssetVersionCreate, + AgentAssetVersionRead, +) +from app.services.agent_assets import AgentAssetService + +router = APIRouter(prefix="/agent-assets") +DbSession = Annotated[Session, Depends(get_db)] + + +def _handle_asset_error(exc: Exception) -> None: + if isinstance(exc, LookupError): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc + if isinstance(exc, PermissionError): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc + if isinstance(exc, ValueError): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc + raise exc + + +@router.get("", response_model=list[AgentAssetListItem]) +def list_agent_assets( + db: DbSession, + asset_type: str | None = Query(default=None), + status_value: str | None = Query(default=None, alias="status"), + domain: str | None = Query(default=None), + keyword: str | None = Query(default=None), +) -> list[AgentAssetListItem]: + return AgentAssetService(db).list_assets( + asset_type=asset_type, + status=status_value, + domain=domain, + keyword=keyword, + ) + + +@router.get("/{asset_id}", response_model=AgentAssetRead) +def get_agent_asset(asset_id: str, db: DbSession) -> AgentAssetRead: + asset = AgentAssetService(db).get_asset(asset_id) + if asset is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Asset not found") + return asset + + +@router.post("", response_model=AgentAssetRead, status_code=status.HTTP_201_CREATED) +def create_agent_asset( + payload: AgentAssetCreate, + db: DbSession, + x_actor: Annotated[str | None, Header()] = None, + x_request_id: Annotated[str | None, Header()] = None, +) -> AgentAssetRead: + try: + return AgentAssetService(db).create_asset( + payload, + actor=(x_actor or payload.owner).strip() or "system", + request_id=x_request_id, + ) + except Exception as exc: + _handle_asset_error(exc) + + +@router.patch("/{asset_id}", response_model=AgentAssetRead) +def update_agent_asset( + asset_id: str, + payload: AgentAssetUpdate, + db: DbSession, + x_actor: Annotated[str | None, Header()] = None, + x_request_id: Annotated[str | None, Header()] = None, +) -> AgentAssetRead: + try: + return AgentAssetService(db).update_asset( + asset_id, + payload, + actor=(x_actor or "system").strip() or "system", + request_id=x_request_id, + ) + except Exception as exc: + _handle_asset_error(exc) + + +@router.get("/{asset_id}/versions", response_model=list[AgentAssetVersionRead]) +def list_agent_asset_versions( + asset_id: str, db: DbSession, limit: int = Query(default=20, ge=1, le=100) +) -> list[AgentAssetVersionRead]: + try: + return AgentAssetService(db).list_versions(asset_id, limit=limit) + except Exception as exc: + _handle_asset_error(exc) + + +@router.post( + "/{asset_id}/versions", + response_model=AgentAssetVersionRead, + status_code=status.HTTP_201_CREATED, +) +def create_agent_asset_version( + asset_id: str, + payload: AgentAssetVersionCreate, + db: DbSession, + x_actor: Annotated[str | None, Header()] = None, + x_request_id: Annotated[str | None, Header()] = None, +) -> AgentAssetVersionRead: + try: + return AgentAssetService(db).create_version( + asset_id, + payload, + actor=(x_actor or payload.created_by).strip() or "system", + request_id=x_request_id, + ) + except Exception as exc: + _handle_asset_error(exc) + + +@router.post( + "/{asset_id}/reviews", response_model=AgentAssetReviewRead, status_code=status.HTTP_201_CREATED +) +def create_agent_asset_review( + asset_id: str, + payload: AgentAssetReviewCreate, + db: DbSession, + x_actor: Annotated[str | None, Header()] = None, + x_request_id: Annotated[str | None, Header()] = None, +) -> AgentAssetReviewRead: + try: + return AgentAssetService(db).create_review( + asset_id, + payload, + actor=(x_actor or payload.reviewer).strip() or "system", + request_id=x_request_id, + ) + except Exception as exc: + _handle_asset_error(exc) + + +@router.post("/{asset_id}/activate", response_model=AgentAssetRead) +def activate_agent_asset( + asset_id: str, + db: DbSession, + x_actor: Annotated[str | None, Header()] = None, + x_request_id: Annotated[str | None, Header()] = None, +) -> AgentAssetRead: + try: + return AgentAssetService(db).activate_asset( + asset_id, + actor=(x_actor or "system").strip() or "system", + request_id=x_request_id, + ) + except Exception as exc: + _handle_asset_error(exc) diff --git a/server/src/app/api/v1/endpoints/agent_runs.py b/server/src/app/api/v1/endpoints/agent_runs.py new file mode 100644 index 0000000..d98739b --- /dev/null +++ b/server/src/app/api/v1/endpoints/agent_runs.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + +from app.api.deps import get_db +from app.schemas.agent_run import AgentRunRead +from app.services.agent_runs import AgentRunService + +router = APIRouter(prefix="/agent-runs") +DbSession = Annotated[Session, Depends(get_db)] + + +@router.get("", response_model=list[AgentRunRead]) +def list_agent_runs( + db: DbSession, + agent: str | None = Query(default=None), + status_value: str | None = Query(default=None, alias="status"), + source: str | None = Query(default=None), + limit: int = Query(default=20, ge=1, le=100), +) -> list[AgentRunRead]: + return AgentRunService(db).list_runs( + agent=agent, status=status_value, source=source, limit=limit + ) + + +@router.get("/{run_id}", response_model=AgentRunRead) +def get_agent_run(run_id: str, db: DbSession) -> AgentRunRead: + run = AgentRunService(db).get_run(run_id) + if run is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Run not found") + return run diff --git a/server/src/app/api/v1/endpoints/audit_logs.py b/server/src/app/api/v1/endpoints/audit_logs.py new file mode 100644 index 0000000..24b1db1 --- /dev/null +++ b/server/src/app/api/v1/endpoints/audit_logs.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session + +from app.api.deps import get_db +from app.schemas.audit_log import AuditLogRead +from app.services.audit import AuditLogService + +router = APIRouter(prefix="/audit-logs") +DbSession = Annotated[Session, Depends(get_db)] + + +@router.get("", response_model=list[AuditLogRead]) +def list_audit_logs( + db: DbSession, + resource_type: str | None = Query(default=None), + resource_id: str | None = Query(default=None), + action: str | None = Query(default=None), + limit: int = Query(default=50, ge=1, le=200), +) -> list[AuditLogRead]: + return AuditLogService(db).list_logs( + resource_type=resource_type, + resource_id=resource_id, + action=action, + limit=limit, + ) diff --git a/server/src/app/api/v1/router.py b/server/src/app/api/v1/router.py index 565dd68..911ebb8 100644 --- a/server/src/app/api/v1/router.py +++ b/server/src/app/api/v1/router.py @@ -1,18 +1,24 @@ -from fastapi import APIRouter - -from app.api.v1.endpoints.auth import router as auth_router -from app.api.v1.endpoints.bootstrap import router as bootstrap_router -from app.api.v1.endpoints.employees import router as employees_router -from app.api.v1.endpoints.health import router as health_router -from app.api.v1.endpoints.knowledge import router as knowledge_router +from fastapi import APIRouter + +from app.api.v1.endpoints.agent_assets import router as agent_assets_router +from app.api.v1.endpoints.agent_runs import router as agent_runs_router +from app.api.v1.endpoints.audit_logs import router as audit_logs_router +from app.api.v1.endpoints.auth import router as auth_router +from app.api.v1.endpoints.bootstrap import router as bootstrap_router +from app.api.v1.endpoints.employees import router as employees_router +from app.api.v1.endpoints.health import router as health_router +from app.api.v1.endpoints.knowledge import router as knowledge_router from app.api.v1.endpoints.reimbursements import router as reimbursements_router from app.api.v1.endpoints.settings import router as settings_router -router = APIRouter() -router.include_router(health_router, tags=["health"]) -router.include_router(bootstrap_router, tags=["bootstrap"]) -router.include_router(auth_router, tags=["auth"]) -router.include_router(knowledge_router, tags=["knowledge"]) -router.include_router(employees_router, prefix="/employees", tags=["employees"]) -router.include_router(reimbursements_router, prefix="/reimbursements", tags=["reimbursements"]) -router.include_router(settings_router, tags=["settings"]) +router = APIRouter() +router.include_router(health_router, tags=["health"]) +router.include_router(bootstrap_router, tags=["bootstrap"]) +router.include_router(auth_router, tags=["auth"]) +router.include_router(agent_assets_router, tags=["agent-assets"]) +router.include_router(agent_runs_router, tags=["agent-runs"]) +router.include_router(audit_logs_router, tags=["audit-logs"]) +router.include_router(knowledge_router, tags=["knowledge"]) +router.include_router(employees_router, prefix="/employees", tags=["employees"]) +router.include_router(reimbursements_router, prefix="/reimbursements", tags=["reimbursements"]) +router.include_router(settings_router, tags=["settings"]) diff --git a/server/src/app/core/agent_enums.py b/server/src/app/core/agent_enums.py new file mode 100644 index 0000000..2522884 --- /dev/null +++ b/server/src/app/core/agent_enums.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from enum import StrEnum + + +class AgentAssetType(StrEnum): + RULE = "rule" + SKILL = "skill" + MCP = "mcp" + TASK = "task" + + +class AgentAssetStatus(StrEnum): + DRAFT = "draft" + REVIEW = "review" + ACTIVE = "active" + DISABLED = "disabled" + + +class AgentReviewStatus(StrEnum): + PENDING = "pending" + APPROVED = "approved" + REJECTED = "rejected" + + +class AgentName(StrEnum): + ORCHESTRATOR = "orchestrator" + USER_AGENT = "user_agent" + HERMES = "hermes" + + +class AgentRunSource(StrEnum): + USER_MESSAGE = "user_message" + SCHEDULE = "schedule" + SYSTEM_EVENT = "system_event" + + +class AgentPermissionLevel(StrEnum): + READ = "read" + DRAFT_WRITE = "draft_write" + APPROVAL_REQUIRED = "approval_required" + FORBIDDEN = "forbidden" + + +class AgentAssetContentType(StrEnum): + MARKDOWN = "markdown" + JSON = "json" + + +class AgentRunStatus(StrEnum): + RUNNING = "running" + SUCCEEDED = "succeeded" + FAILED = "failed" + BLOCKED = "blocked" + + +class AgentToolType(StrEnum): + MCP = "mcp" + DATABASE = "database" + LLM = "llm" + OCR = "ocr" + RULE_ENGINE = "rule_engine" + + +class AgentAssetDomain(StrEnum): + EXPENSE = "expense" + AR = "ar" + AP = "ap" + KNOWLEDGE = "knowledge" + SYSTEM = "system" diff --git a/server/src/app/db/base.py b/server/src/app/db/base.py index 04a2783..0db5e8d 100644 --- a/server/src/app/db/base.py +++ b/server/src/app/db/base.py @@ -1,23 +1,43 @@ -from app.db.base_class import Base -from app.models.approval import ApprovalRecord -from app.models.employee_change_log import EmployeeChangeLog -from app.models.employee import Employee -from app.models.organization import OrganizationUnit -from app.models.reimbursement import ReimbursementRequest -from app.models.role import Role -from app.models.system_model_setting import SystemModelSetting -from app.models.system_setting import SystemSetting -from app.models.system_setting_secret import SystemSettingSecret - -__all__ = [ - "Base", - "ApprovalRecord", - "Employee", - "EmployeeChangeLog", - "OrganizationUnit", - "ReimbursementRequest", - "Role", - "SystemModelSetting", - "SystemSetting", - "SystemSettingSecret", -] +from app.db.base_class import Base +from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion +from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog +from app.models.approval import ApprovalRecord +from app.models.audit_log import AuditLog +from app.models.employee_change_log import EmployeeChangeLog +from app.models.employee import Employee +from app.models.financial_record import ( + AccountsPayableRecord, + AccountsReceivableRecord, + ExpenseClaim, + ExpenseClaimItem, +) +from app.models.organization import OrganizationUnit +from app.models.reimbursement import ReimbursementRequest +from app.models.role import Role +from app.models.system_model_setting import SystemModelSetting +from app.models.system_setting import SystemSetting +from app.models.system_setting_secret import SystemSettingSecret + +__all__ = [ + "Base", + "AccountsPayableRecord", + "AccountsReceivableRecord", + "AgentAsset", + "AgentAssetReview", + "AgentAssetVersion", + "AgentRun", + "AgentToolCall", + "ApprovalRecord", + "AuditLog", + "Employee", + "EmployeeChangeLog", + "ExpenseClaim", + "ExpenseClaimItem", + "OrganizationUnit", + "ReimbursementRequest", + "Role", + "SemanticParseLog", + "SystemModelSetting", + "SystemSetting", + "SystemSettingSecret", +] diff --git a/server/src/app/main.py b/server/src/app/main.py index 45f91c5..63c7e1c 100644 --- a/server/src/app/main.py +++ b/server/src/app/main.py @@ -3,12 +3,13 @@ from __future__ import annotations from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.api.router import api_router -from app.core.config import get_settings -from app.core.logging import get_logger, setup_logging -from app.middleware.logging import AccessLogMiddleware -from app.services.employee import prepare_employee_directory -from app.services.knowledge import prepare_knowledge_library +from app.api.router import api_router +from app.core.config import get_settings +from app.core.logging import get_logger, setup_logging +from app.middleware.logging import AccessLogMiddleware +from app.services.agent_foundation import prepare_agent_foundation +from app.services.employee import prepare_employee_directory +from app.services.knowledge import prepare_knowledge_library def create_app() -> FastAPI: @@ -49,11 +50,12 @@ def create_app() -> FastAPI: return {"message": f"{settings.app_name} is running"} @app.on_event("startup") - def _on_startup() -> None: - prepare_employee_directory() - prepare_knowledge_library() - logger.info( - "Server ready - host=%s port=%s prefix=%s", + def _on_startup() -> None: + prepare_employee_directory() + prepare_agent_foundation() + prepare_knowledge_library() + logger.info( + "Server ready - host=%s port=%s prefix=%s", settings.app_host, settings.app_port, settings.api_v1_prefix, diff --git a/server/src/app/models/__init__.py b/server/src/app/models/__init__.py index 6aad399..8a18eba 100644 --- a/server/src/app/models/__init__.py +++ b/server/src/app/models/__init__.py @@ -1,21 +1,41 @@ -from app.models.approval import ApprovalRecord -from app.models.employee_change_log import EmployeeChangeLog -from app.models.employee import Employee -from app.models.organization import OrganizationUnit -from app.models.reimbursement import ReimbursementRequest -from app.models.role import Role -from app.models.system_model_setting import SystemModelSetting -from app.models.system_setting import SystemSetting -from app.models.system_setting_secret import SystemSettingSecret - -__all__ = [ - "ApprovalRecord", - "Employee", - "EmployeeChangeLog", - "OrganizationUnit", - "ReimbursementRequest", - "Role", - "SystemModelSetting", - "SystemSetting", - "SystemSettingSecret", -] +from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion +from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog +from app.models.approval import ApprovalRecord +from app.models.audit_log import AuditLog +from app.models.employee_change_log import EmployeeChangeLog +from app.models.employee import Employee +from app.models.financial_record import ( + AccountsPayableRecord, + AccountsReceivableRecord, + ExpenseClaim, + ExpenseClaimItem, +) +from app.models.organization import OrganizationUnit +from app.models.reimbursement import ReimbursementRequest +from app.models.role import Role +from app.models.system_model_setting import SystemModelSetting +from app.models.system_setting import SystemSetting +from app.models.system_setting_secret import SystemSettingSecret + +__all__ = [ + "AccountsPayableRecord", + "AccountsReceivableRecord", + "AgentAsset", + "AgentAssetReview", + "AgentAssetVersion", + "AgentRun", + "AgentToolCall", + "ApprovalRecord", + "AuditLog", + "Employee", + "EmployeeChangeLog", + "ExpenseClaim", + "ExpenseClaimItem", + "OrganizationUnit", + "ReimbursementRequest", + "Role", + "SemanticParseLog", + "SystemModelSetting", + "SystemSetting", + "SystemSettingSecret", +] diff --git a/server/src/app/models/agent_asset.py b/server/src/app/models/agent_asset.py new file mode 100644 index 0000000..8002ff1 --- /dev/null +++ b/server/src/app/models/agent_asset.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import uuid +from datetime import datetime +from typing import Any + +from sqlalchemy import DateTime, ForeignKey, String, Text, UniqueConstraint, func +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.types import JSON + +from app.db.base_class import Base + + +class AgentAsset(Base): + __tablename__ = "agent_assets" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + asset_type: Mapped[str] = mapped_column(String(20), index=True) + code: Mapped[str] = mapped_column(String(100), unique=True, index=True) + name: Mapped[str] = mapped_column(String(200)) + description: Mapped[str] = mapped_column(Text(), default="") + domain: Mapped[str] = mapped_column(String(50), index=True) + scenario_json: Mapped[list[Any]] = mapped_column(JSON, default=list) + owner: Mapped[str] = mapped_column(String(100)) + reviewer: Mapped[str | None] = mapped_column(String(100), nullable=True) + status: Mapped[str] = mapped_column(String(20), index=True, default="draft") + current_version: Mapped[str | None] = mapped_column(String(30), nullable=True) + config_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + versions = relationship( + "AgentAssetVersion", + back_populates="asset", + cascade="all, delete-orphan", + order_by="desc(AgentAssetVersion.created_at)", + ) + reviews = relationship( + "AgentAssetReview", + back_populates="asset", + cascade="all, delete-orphan", + order_by="desc(AgentAssetReview.created_at)", + ) + scheduled_runs = relationship("AgentRun", back_populates="task_asset") + + +class AgentAssetVersion(Base): + __tablename__ = "agent_asset_versions" + __table_args__ = ( + UniqueConstraint("asset_id", "version", name="uq_agent_asset_versions_asset_version"), + ) + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + asset_id: Mapped[str] = mapped_column(ForeignKey("agent_assets.id"), index=True) + version: Mapped[str] = mapped_column(String(30)) + content: Mapped[str] = mapped_column(Text()) + content_type: Mapped[str] = mapped_column(String(20)) + change_note: Mapped[str | None] = mapped_column(Text(), nullable=True) + created_by: Mapped[str] = mapped_column(String(100)) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + asset = relationship("AgentAsset", back_populates="versions") + + +class AgentAssetReview(Base): + __tablename__ = "agent_asset_reviews" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + asset_id: Mapped[str] = mapped_column(ForeignKey("agent_assets.id"), index=True) + version: Mapped[str] = mapped_column(String(30)) + reviewer: Mapped[str] = mapped_column(String(100)) + review_status: Mapped[str] = mapped_column(String(20), index=True) + review_note: Mapped[str | None] = mapped_column(Text(), nullable=True) + reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + asset = relationship("AgentAsset", back_populates="reviews") diff --git a/server/src/app/models/agent_run.py b/server/src/app/models/agent_run.py new file mode 100644 index 0000000..714c5bb --- /dev/null +++ b/server/src/app/models/agent_run.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import uuid +from datetime import datetime +from typing import Any + +from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.types import JSON + +from app.db.base_class import Base + + +class AgentRun(Base): + __tablename__ = "agent_runs" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + run_id: Mapped[str] = mapped_column(String(50), unique=True, index=True) + agent: Mapped[str] = mapped_column(String(30), index=True) + source: Mapped[str] = mapped_column(String(30)) + user_id: Mapped[str | None] = mapped_column(String(100), nullable=True) + task_id: Mapped[str | None] = mapped_column( + ForeignKey("agent_assets.id"), nullable=True, index=True + ) + ontology_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) + route_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) + permission_level: Mapped[str] = mapped_column(String(30), default="read") + status: Mapped[str] = mapped_column(String(20), index=True) + result_summary: Mapped[str | None] = mapped_column(Text(), nullable=True) + error_message: Mapped[str | None] = mapped_column(Text(), nullable=True) + started_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), index=True + ) + finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + task_asset = relationship("AgentAsset", back_populates="scheduled_runs") + tool_calls = relationship( + "AgentToolCall", + back_populates="run", + cascade="all, delete-orphan", + order_by="asc(AgentToolCall.created_at)", + ) + semantic_parse_logs = relationship( + "SemanticParseLog", + back_populates="run", + cascade="all, delete-orphan", + order_by="asc(SemanticParseLog.created_at)", + ) + + +class AgentToolCall(Base): + __tablename__ = "agent_tool_calls" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + run_id: Mapped[str] = mapped_column(ForeignKey("agent_runs.run_id"), index=True) + tool_type: Mapped[str] = mapped_column(String(30)) + tool_name: Mapped[str] = mapped_column(String(100)) + request_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) + response_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) + status: Mapped[str] = mapped_column(String(20)) + duration_ms: Mapped[int] = mapped_column(Integer, default=0) + error_message: Mapped[str | None] = mapped_column(Text(), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + run = relationship("AgentRun", back_populates="tool_calls") + + +class SemanticParseLog(Base): + __tablename__ = "semantic_parse_logs" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + run_id: Mapped[str] = mapped_column(ForeignKey("agent_runs.run_id"), index=True) + user_id: Mapped[str | None] = mapped_column(String(100), nullable=True) + raw_query: Mapped[str] = mapped_column(Text()) + scenario: Mapped[str] = mapped_column(String(50), index=True) + intent: Mapped[str] = mapped_column(String(50), index=True) + entities_json: Mapped[list[Any]] = mapped_column(JSON, default=list) + time_range_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) + metrics_json: Mapped[list[Any]] = mapped_column(JSON, default=list) + constraints_json: Mapped[list[Any]] = mapped_column(JSON, default=list) + risk_flags_json: Mapped[list[Any]] = mapped_column(JSON, default=list) + permission_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) + confidence: Mapped[float] = mapped_column(Float, default=0.0) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + run = relationship("AgentRun", back_populates="semantic_parse_logs") diff --git a/server/src/app/models/audit_log.py b/server/src/app/models/audit_log.py new file mode 100644 index 0000000..0bd31a4 --- /dev/null +++ b/server/src/app/models/audit_log.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import uuid +from datetime import datetime +from typing import Any + +from sqlalchemy import DateTime, String, func +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.types import JSON + +from app.db.base_class import Base + + +class AuditLog(Base): + __tablename__ = "audit_logs" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + actor: Mapped[str] = mapped_column(String(100)) + action: Mapped[str] = mapped_column(String(100), index=True) + resource_type: Mapped[str] = mapped_column(String(50), index=True) + resource_id: Mapped[str] = mapped_column(String(100), index=True) + before_json: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) + after_json: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) + request_id: Mapped[str] = mapped_column(String(64), index=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) diff --git a/server/src/app/models/financial_record.py b/server/src/app/models/financial_record.py new file mode 100644 index 0000000..3d492a3 --- /dev/null +++ b/server/src/app/models/financial_record.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +import uuid +from datetime import date, datetime +from decimal import Decimal +from typing import Any + +from sqlalchemy import Date, DateTime, ForeignKey, Integer, Numeric, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.types import JSON + +from app.db.base_class import Base + + +class ExpenseClaim(Base): + __tablename__ = "expense_claims" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + claim_no: Mapped[str] = mapped_column(String(50), unique=True, index=True) + employee_id: Mapped[str | None] = mapped_column( + ForeignKey("employees.id"), nullable=True, index=True + ) + employee_name: Mapped[str] = mapped_column(String(100), index=True) + department_id: Mapped[str | None] = mapped_column( + ForeignKey("organization_units.id"), nullable=True, index=True + ) + department_name: Mapped[str] = mapped_column(String(100), index=True) + project_code: Mapped[str | None] = mapped_column(String(50), nullable=True) + expense_type: Mapped[str] = mapped_column(String(50), index=True) + reason: Mapped[str] = mapped_column(Text()) + location: Mapped[str] = mapped_column(String(100)) + amount: Mapped[Decimal] = mapped_column(Numeric(12, 2)) + currency: Mapped[str] = mapped_column(String(10), default="CNY") + invoice_count: Mapped[int] = mapped_column(Integer, default=0) + occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) + submitted_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True, index=True + ) + status: Mapped[str] = mapped_column(String(30), index=True) + approval_stage: Mapped[str | None] = mapped_column(String(50), nullable=True) + risk_flags_json: Mapped[list[Any]] = mapped_column(JSON, default=list) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + items = relationship( + "ExpenseClaimItem", + back_populates="claim", + cascade="all, delete-orphan", + order_by="asc(ExpenseClaimItem.item_date)", + ) + + +class ExpenseClaimItem(Base): + __tablename__ = "expense_claim_items" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + claim_id: Mapped[str] = mapped_column(ForeignKey("expense_claims.id"), index=True) + item_date: Mapped[date] = mapped_column(Date(), index=True) + item_type: Mapped[str] = mapped_column(String(50)) + item_reason: Mapped[str] = mapped_column(Text()) + item_location: Mapped[str] = mapped_column(String(100)) + item_amount: Mapped[Decimal] = mapped_column(Numeric(12, 2)) + invoice_id: Mapped[str | None] = mapped_column(String(100), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + claim = relationship("ExpenseClaim", back_populates="items") + + +class AccountsReceivableRecord(Base): + __tablename__ = "accounts_receivable" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + receivable_no: Mapped[str] = mapped_column(String(50), unique=True, index=True) + customer_id: Mapped[str] = mapped_column(String(64), index=True) + customer_name: Mapped[str] = mapped_column(String(120), index=True) + contract_no: Mapped[str | None] = mapped_column(String(100), nullable=True) + invoice_no: Mapped[str | None] = mapped_column(String(100), nullable=True) + amount_receivable: Mapped[Decimal] = mapped_column(Numeric(12, 2)) + amount_received: Mapped[Decimal] = mapped_column(Numeric(12, 2)) + amount_outstanding: Mapped[Decimal] = mapped_column(Numeric(12, 2)) + currency: Mapped[str] = mapped_column(String(10), default="CNY") + posting_date: Mapped[date] = mapped_column(Date(), index=True) + due_date: Mapped[date] = mapped_column(Date(), index=True) + aging_days: Mapped[int] = mapped_column(Integer, default=0) + status: Mapped[str] = mapped_column(String(30), index=True) + risk_flags_json: Mapped[list[Any]] = mapped_column(JSON, default=list) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + +class AccountsPayableRecord(Base): + __tablename__ = "accounts_payable" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + payable_no: Mapped[str] = mapped_column(String(50), unique=True, index=True) + vendor_id: Mapped[str] = mapped_column(String(64), index=True) + vendor_name: Mapped[str] = mapped_column(String(120), index=True) + invoice_no: Mapped[str | None] = mapped_column(String(100), nullable=True) + amount_payable: Mapped[Decimal] = mapped_column(Numeric(12, 2)) + amount_paid: Mapped[Decimal] = mapped_column(Numeric(12, 2)) + amount_outstanding: Mapped[Decimal] = mapped_column(Numeric(12, 2)) + currency: Mapped[str] = mapped_column(String(10), default="CNY") + posting_date: Mapped[date] = mapped_column(Date(), index=True) + due_date: Mapped[date] = mapped_column(Date(), index=True) + aging_days: Mapped[int] = mapped_column(Integer, default=0) + status: Mapped[str] = mapped_column(String(30), index=True) + risk_flags_json: Mapped[list[Any]] = mapped_column(JSON, default=list) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) diff --git a/server/src/app/repositories/agent_asset.py b/server/src/app/repositories/agent_asset.py new file mode 100644 index 0000000..96d1197 --- /dev/null +++ b/server/src/app/repositories/agent_asset.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from sqlalchemy import or_, select +from sqlalchemy.orm import Session + +from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion + + +class AgentAssetRepository: + def __init__(self, db: Session) -> None: + self.db = db + + def list( + self, + *, + asset_type: str | None = None, + status: str | None = None, + domain: str | None = None, + keyword: str | None = None, + ) -> list[AgentAsset]: + stmt = select(AgentAsset) + + if asset_type: + stmt = stmt.where(AgentAsset.asset_type == asset_type) + if status: + stmt = stmt.where(AgentAsset.status == status) + if domain: + stmt = stmt.where(AgentAsset.domain == domain) + if keyword: + like_keyword = f"%{keyword.strip()}%" + stmt = stmt.where( + or_( + AgentAsset.name.ilike(like_keyword), + AgentAsset.code.ilike(like_keyword), + AgentAsset.description.ilike(like_keyword), + ) + ) + + stmt = stmt.order_by(AgentAsset.updated_at.desc(), AgentAsset.created_at.desc()) + return list(self.db.scalars(stmt).all()) + + def get(self, asset_id: str) -> AgentAsset | None: + return self.db.get(AgentAsset, asset_id) + + def get_by_code(self, code: str) -> AgentAsset | None: + stmt = select(AgentAsset).where(AgentAsset.code == code) + return self.db.scalar(stmt) + + def list_versions(self, asset_id: str, *, limit: int | None = None) -> list[AgentAssetVersion]: + stmt = ( + select(AgentAssetVersion) + .where(AgentAssetVersion.asset_id == asset_id) + .order_by(AgentAssetVersion.created_at.desc()) + ) + if limit is not None: + stmt = stmt.limit(limit) + return list(self.db.scalars(stmt).all()) + + def get_version(self, asset_id: str, version: str) -> AgentAssetVersion | None: + stmt = select(AgentAssetVersion).where( + AgentAssetVersion.asset_id == asset_id, + AgentAssetVersion.version == version, + ) + return self.db.scalar(stmt) + + def list_reviews(self, asset_id: str, *, limit: int | None = None) -> list[AgentAssetReview]: + stmt = ( + select(AgentAssetReview) + .where(AgentAssetReview.asset_id == asset_id) + .order_by(AgentAssetReview.created_at.desc()) + ) + if limit is not None: + stmt = stmt.limit(limit) + return list(self.db.scalars(stmt).all()) + + def get_review( + self, asset_id: str, version: str, review_status: str | None = None + ) -> AgentAssetReview | None: + stmt = select(AgentAssetReview).where( + AgentAssetReview.asset_id == asset_id, + AgentAssetReview.version == version, + ) + if review_status: + stmt = stmt.where(AgentAssetReview.review_status == review_status) + stmt = stmt.order_by(AgentAssetReview.created_at.desc()) + return self.db.scalar(stmt) + + def create_asset(self, asset: AgentAsset) -> AgentAsset: + self.db.add(asset) + self.db.commit() + self.db.refresh(asset) + return asset + + def save_asset(self, asset: AgentAsset) -> AgentAsset: + self.db.add(asset) + self.db.commit() + self.db.refresh(asset) + return asset + + def create_version(self, version: AgentAssetVersion) -> AgentAssetVersion: + self.db.add(version) + self.db.commit() + self.db.refresh(version) + return version + + def create_review(self, review: AgentAssetReview) -> AgentAssetReview: + self.db.add(review) + self.db.commit() + self.db.refresh(review) + return review diff --git a/server/src/app/repositories/agent_run.py b/server/src/app/repositories/agent_run.py new file mode 100644 index 0000000..2a4aa99 --- /dev/null +++ b/server/src/app/repositories/agent_run.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog + + +class AgentRunRepository: + def __init__(self, db: Session) -> None: + self.db = db + + def list( + self, + *, + agent: str | None = None, + status: str | None = None, + source: str | None = None, + limit: int = 20, + ) -> list[AgentRun]: + stmt = select(AgentRun) + if agent: + stmt = stmt.where(AgentRun.agent == agent) + if status: + stmt = stmt.where(AgentRun.status == status) + if source: + stmt = stmt.where(AgentRun.source == source) + stmt = stmt.order_by(AgentRun.started_at.desc()).limit(limit) + return list(self.db.scalars(stmt).all()) + + def get_by_run_id(self, run_id: str) -> AgentRun | None: + stmt = select(AgentRun).where(AgentRun.run_id == run_id) + return self.db.scalar(stmt) + + def create_run(self, run: AgentRun) -> AgentRun: + self.db.add(run) + self.db.commit() + self.db.refresh(run) + return run + + def save_run(self, run: AgentRun) -> AgentRun: + self.db.add(run) + self.db.commit() + self.db.refresh(run) + return run + + def create_tool_call(self, tool_call: AgentToolCall) -> AgentToolCall: + self.db.add(tool_call) + self.db.commit() + self.db.refresh(tool_call) + return tool_call + + def create_semantic_parse(self, semantic_parse: SemanticParseLog) -> SemanticParseLog: + self.db.add(semantic_parse) + self.db.commit() + self.db.refresh(semantic_parse) + return semantic_parse diff --git a/server/src/app/repositories/audit_log.py b/server/src/app/repositories/audit_log.py new file mode 100644 index 0000000..0636b11 --- /dev/null +++ b/server/src/app/repositories/audit_log.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.models.audit_log import AuditLog + + +class AuditLogRepository: + def __init__(self, db: Session) -> None: + self.db = db + + def list( + self, + *, + resource_type: str | None = None, + resource_id: str | None = None, + action: str | None = None, + limit: int = 50, + ) -> list[AuditLog]: + stmt = select(AuditLog) + if resource_type: + stmt = stmt.where(AuditLog.resource_type == resource_type) + if resource_id: + stmt = stmt.where(AuditLog.resource_id == resource_id) + if action: + stmt = stmt.where(AuditLog.action == action) + stmt = stmt.order_by(AuditLog.created_at.desc()).limit(limit) + return list(self.db.scalars(stmt).all()) + + def create(self, log: AuditLog) -> AuditLog: + self.db.add(log) + self.db.commit() + self.db.refresh(log) + return log diff --git a/server/src/app/schemas/agent_asset.py b/server/src/app/schemas/agent_asset.py new file mode 100644 index 0000000..8f31b1e --- /dev/null +++ b/server/src/app/schemas/agent_asset.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field + +from app.core.agent_enums import ( + AgentAssetContentType, + AgentAssetDomain, + AgentAssetStatus, + AgentAssetType, + AgentReviewStatus, +) + + +class AgentAssetCreate(BaseModel): + asset_type: AgentAssetType + code: str = Field(min_length=1, max_length=100) + name: str = Field(min_length=1, max_length=200) + description: str = "" + domain: AgentAssetDomain + scenario_json: list[Any] = Field(default_factory=list) + owner: str = Field(min_length=1, max_length=100) + reviewer: str | None = Field(default=None, max_length=100) + status: AgentAssetStatus = AgentAssetStatus.DRAFT + config_json: dict[str, Any] = Field(default_factory=dict) + + +class AgentAssetUpdate(BaseModel): + name: str | None = Field(default=None, min_length=1, max_length=200) + description: str | None = None + domain: AgentAssetDomain | None = None + scenario_json: list[Any] | None = None + owner: str | None = Field(default=None, min_length=1, max_length=100) + reviewer: str | None = Field(default=None, max_length=100) + status: AgentAssetStatus | None = None + current_version: str | None = Field(default=None, max_length=30) + config_json: dict[str, Any] | None = None + + +class AgentAssetVersionCreate(BaseModel): + version: str = Field(min_length=1, max_length=30) + content: Any + content_type: AgentAssetContentType + change_note: str | None = None + created_by: str = Field(min_length=1, max_length=100) + + +class RuleMarkdownUpdate(BaseModel): + version: str = Field(min_length=1, max_length=30) + content: str + change_note: str | None = None + created_by: str = Field(min_length=1, max_length=100) + + +class AgentAssetReviewCreate(BaseModel): + version: str = Field(min_length=1, max_length=30) + reviewer: str = Field(min_length=1, max_length=100) + review_status: AgentReviewStatus + review_note: str | None = None + + +class AgentAssetReviewRead(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + asset_id: str + version: str + reviewer: str + review_status: str + review_note: str | None + reviewed_at: datetime | None + created_at: datetime + + +class AgentAssetVersionRead(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + asset_id: str + version: str + content: Any + content_type: str + change_note: str | None + created_by: str + created_at: datetime + is_current: bool = False + + +class AgentAssetListItem(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + asset_type: str + code: str + name: str + description: str + domain: str + scenario_json: list[Any] + owner: str + reviewer: str | None + status: str + current_version: str | None + config_json: dict[str, Any] + created_at: datetime + updated_at: datetime + + +class AgentAssetRead(AgentAssetListItem): + current_version_content: Any | None = None + current_version_content_type: str | None = None + current_version_change_note: str | None = None + recent_versions: list[AgentAssetVersionRead] = Field(default_factory=list) + latest_review: AgentAssetReviewRead | None = None diff --git a/server/src/app/schemas/agent_run.py b/server/src/app/schemas/agent_run.py new file mode 100644 index 0000000..840ad7c --- /dev/null +++ b/server/src/app/schemas/agent_run.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field + + +class AgentToolCallRead(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + run_id: str + tool_type: str + tool_name: str + request_json: dict[str, Any] + response_json: dict[str, Any] + status: str + duration_ms: int + error_message: str | None + created_at: datetime + + +class SemanticParseRead(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + run_id: str + user_id: str | None + raw_query: str + scenario: str + intent: str + entities_json: list[Any] + time_range_json: dict[str, Any] + metrics_json: list[Any] + constraints_json: list[Any] + risk_flags_json: list[Any] + permission_json: dict[str, Any] + confidence: float + created_at: datetime + + +class AgentRunRead(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + run_id: str + agent: str + source: str + user_id: str | None + task_id: str | None + ontology_json: dict[str, Any] + route_json: dict[str, Any] + permission_level: str + status: str + result_summary: str | None + error_message: str | None + started_at: datetime + finished_at: datetime | None + tool_calls: list[AgentToolCallRead] = Field(default_factory=list) + semantic_parse: SemanticParseRead | None = None diff --git a/server/src/app/schemas/audit_log.py b/server/src/app/schemas/audit_log.py new file mode 100644 index 0000000..144ba32 --- /dev/null +++ b/server/src/app/schemas/audit_log.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from pydantic import BaseModel, ConfigDict + + +class AuditLogRead(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + actor: str + action: str + resource_type: str + resource_id: str + before_json: dict[str, Any] | None + after_json: dict[str, Any] | None + request_id: str + created_at: datetime diff --git a/server/src/app/services/agent_assets.py b/server/src/app/services/agent_assets.py new file mode 100644 index 0000000..a2a320f --- /dev/null +++ b/server/src/app/services/agent_assets.py @@ -0,0 +1,407 @@ +from __future__ import annotations + +import json +from datetime import UTC, datetime +from typing import Any + +from sqlalchemy.orm import Session + +from app.core.agent_enums import ( + AgentAssetContentType, + AgentAssetStatus, + AgentAssetType, + AgentReviewStatus, +) +from app.core.logging import get_logger +from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion +from app.repositories.agent_asset import AgentAssetRepository +from app.schemas.agent_asset import ( + AgentAssetCreate, + AgentAssetListItem, + AgentAssetRead, + AgentAssetReviewCreate, + AgentAssetReviewRead, + AgentAssetUpdate, + AgentAssetVersionCreate, + AgentAssetVersionRead, +) +from app.services.agent_foundation import AgentFoundationService +from app.services.audit import AuditLogService + +logger = get_logger("app.services.agent_assets") + + +class AgentAssetService: + def __init__(self, db: Session) -> None: + self.db = db + self.repository = AgentAssetRepository(db) + self.audit_service = AuditLogService(db) + + def list_assets( + self, + *, + asset_type: str | None = None, + status: str | None = None, + domain: str | None = None, + keyword: str | None = None, + ) -> list[AgentAssetListItem]: + self._ensure_ready() + items = self.repository.list( + asset_type=asset_type, status=status, domain=domain, keyword=keyword + ) + return [AgentAssetListItem.model_validate(item) for item in items] + + def get_asset(self, asset_id: str) -> AgentAssetRead | None: + self._ensure_ready() + asset = self.repository.get(asset_id) + if asset is None: + return None + + recent_versions = self._sort_versions( + self.repository.list_versions(asset_id, limit=5), + asset.current_version, + ) + latest_review = next(iter(self.repository.list_reviews(asset_id, limit=1)), None) + current_version = ( + self.repository.get_version(asset_id, asset.current_version) + if asset.current_version + else None + ) + return AgentAssetRead( + **AgentAssetListItem.model_validate(asset).model_dump(), + current_version_content=self._deserialize_content(current_version) + if current_version + else None, + current_version_content_type=current_version.content_type if current_version else None, + current_version_change_note=current_version.change_note if current_version else None, + recent_versions=[ + self._serialize_version(item, asset.current_version) for item in recent_versions + ], + latest_review=AgentAssetReviewRead.model_validate(latest_review) + if latest_review + else None, + ) + + def create_asset( + self, + payload: AgentAssetCreate, + *, + actor: str, + request_id: str | None = None, + ) -> AgentAssetRead: + self._ensure_ready() + if self.repository.get_by_code(payload.code): + raise ValueError(f"资产编码 {payload.code} 已存在") + if payload.status == AgentAssetStatus.ACTIVE: + raise ValueError("请先创建资产并完成审核,再通过上线接口激活。") + + asset = AgentAsset( + asset_type=payload.asset_type.value, + code=payload.code, + name=payload.name, + description=payload.description, + domain=payload.domain.value, + scenario_json=payload.scenario_json, + owner=payload.owner, + reviewer=payload.reviewer, + status=payload.status.value, + config_json=payload.config_json, + ) + created = self.repository.create_asset(asset) + self.audit_service.log_action( + actor=actor, + action="create_agent_asset", + resource_type=created.asset_type, + resource_id=created.id, + before_json=None, + after_json=self._asset_snapshot(created), + request_id=request_id, + ) + logger.info("Created agent asset id=%s code=%s", created.id, created.code) + return self.get_asset(created.id) # type: ignore[return-value] + + def update_asset( + self, + asset_id: str, + payload: AgentAssetUpdate, + *, + actor: str, + request_id: str | None = None, + ) -> AgentAssetRead: + self._ensure_ready() + asset = self.repository.get(asset_id) + if asset is None: + raise LookupError("Asset not found") + + before = self._asset_snapshot(asset) + + if payload.status == AgentAssetStatus.ACTIVE: + raise ValueError("请使用上线接口激活资产。") + + for field_name in ( + "name", + "description", + "owner", + "reviewer", + "current_version", + "config_json", + "scenario_json", + ): + value = getattr(payload, field_name) + if value is not None: + setattr(asset, field_name, value) + + if payload.domain is not None: + asset.domain = payload.domain.value + if payload.status is not None: + asset.status = payload.status.value + if payload.current_version is not None and not self.repository.get_version( + asset_id, payload.current_version + ): + raise LookupError(f"版本 {payload.current_version} 不存在") + + updated = self.repository.save_asset(asset) + self.audit_service.log_action( + actor=actor, + action="update_agent_asset", + resource_type=updated.asset_type, + resource_id=updated.id, + before_json=before, + after_json=self._asset_snapshot(updated), + request_id=request_id, + ) + logger.info("Updated agent asset id=%s code=%s", updated.id, updated.code) + return self.get_asset(updated.id) # type: ignore[return-value] + + def list_versions(self, asset_id: str, *, limit: int = 20) -> list[AgentAssetVersionRead]: + self._ensure_ready() + asset = self.repository.get(asset_id) + if asset is None: + raise LookupError("Asset not found") + versions = self._sort_versions( + self.repository.list_versions(asset_id, limit=limit), + asset.current_version, + ) + return [self._serialize_version(item, asset.current_version) for item in versions] + + def create_version( + self, + asset_id: str, + payload: AgentAssetVersionCreate, + *, + actor: str, + request_id: str | None = None, + ) -> AgentAssetVersionRead: + self._ensure_ready() + asset = self.repository.get(asset_id) + if asset is None: + raise LookupError("Asset not found") + if self.repository.get_version(asset_id, payload.version): + raise ValueError(f"版本号 {payload.version} 已存在") + + self._validate_version_payload(asset, payload) + serialized_content = self._serialize_content(payload.content, payload.content_type.value) + version = AgentAssetVersion( + asset_id=asset_id, + version=payload.version, + content=serialized_content, + content_type=payload.content_type.value, + change_note=payload.change_note, + created_by=payload.created_by, + ) + created = self.repository.create_version(version) + + before = self._asset_snapshot(asset) + asset.current_version = payload.version + if ( + asset.asset_type == AgentAssetType.RULE.value + and asset.status == AgentAssetStatus.ACTIVE.value + ): + asset.status = AgentAssetStatus.REVIEW.value + updated_asset = self.repository.save_asset(asset) + + self.audit_service.log_action( + actor=actor, + action="save_agent_asset_version", + resource_type=updated_asset.asset_type, + resource_id=updated_asset.id, + before_json=before, + after_json={ + "current_version": updated_asset.current_version, + "status": updated_asset.status, + }, + request_id=request_id, + ) + logger.info("Created agent asset version asset_id=%s version=%s", asset_id, payload.version) + return self._serialize_version(created, updated_asset.current_version) + + def create_review( + self, + asset_id: str, + payload: AgentAssetReviewCreate, + *, + actor: str, + request_id: str | None = None, + ) -> AgentAssetReviewRead: + self._ensure_ready() + asset = self.repository.get(asset_id) + if asset is None: + raise LookupError("Asset not found") + if self.repository.get_version(asset_id, payload.version) is None: + raise LookupError(f"版本 {payload.version} 不存在") + + review = AgentAssetReview( + asset_id=asset_id, + version=payload.version, + reviewer=payload.reviewer, + review_status=payload.review_status.value, + review_note=payload.review_note, + reviewed_at=None + if payload.review_status == AgentReviewStatus.PENDING + else datetime.now(UTC), + ) + created = self.repository.create_review(review) + + before = self._asset_snapshot(asset) + asset.reviewer = payload.reviewer + if payload.review_status == AgentReviewStatus.PENDING: + asset.status = AgentAssetStatus.REVIEW.value + elif payload.review_status == AgentReviewStatus.REJECTED: + asset.status = AgentAssetStatus.DRAFT.value + elif asset.status != AgentAssetStatus.ACTIVE.value: + asset.status = AgentAssetStatus.REVIEW.value + self.repository.save_asset(asset) + + self.audit_service.log_action( + actor=actor, + action="review_agent_asset", + resource_type=asset.asset_type, + resource_id=asset.id, + before_json=before, + after_json={ + "review_version": payload.version, + "review_status": payload.review_status.value, + "asset_status": asset.status, + }, + request_id=request_id, + ) + logger.info( + "Created review asset_id=%s version=%s status=%s", + asset_id, + payload.version, + payload.review_status.value, + ) + return AgentAssetReviewRead.model_validate(created) + + def activate_asset( + self, + asset_id: str, + *, + actor: str, + request_id: str | None = None, + ) -> AgentAssetRead: + self._ensure_ready() + asset = self.repository.get(asset_id) + if asset is None: + raise LookupError("Asset not found") + if not asset.current_version: + raise ValueError("资产尚未设置当前版本,无法上线。") + + if asset.asset_type == AgentAssetType.RULE.value: + review = self.repository.get_review( + asset.id, asset.current_version, AgentReviewStatus.APPROVED.value + ) + if review is None: + raise PermissionError("规则当前版本尚未审核通过,不能上线。") + + before = self._asset_snapshot(asset) + asset.status = AgentAssetStatus.ACTIVE.value + updated = self.repository.save_asset(asset) + self.audit_service.log_action( + actor=actor, + action="activate_agent_asset", + resource_type=updated.asset_type, + resource_id=updated.id, + before_json=before, + after_json=self._asset_snapshot(updated), + request_id=request_id, + ) + logger.info("Activated agent asset id=%s code=%s", updated.id, updated.code) + return self.get_asset(updated.id) # type: ignore[return-value] + + def _ensure_ready(self) -> None: + AgentFoundationService(self.db).ensure_foundation_ready() + + def _validate_version_payload( + self, asset: AgentAsset, payload: AgentAssetVersionCreate + ) -> None: + if ( + asset.asset_type == AgentAssetType.RULE.value + and payload.content_type != AgentAssetContentType.MARKDOWN + ): + raise ValueError("规则资产版本内容必须使用 markdown。") + if ( + asset.asset_type != AgentAssetType.RULE.value + and payload.content_type != AgentAssetContentType.JSON + ): + raise ValueError("技能、MCP、任务资产版本内容必须使用 json。") + if payload.content_type == AgentAssetContentType.MARKDOWN and not isinstance( + payload.content, str + ): + raise ValueError("Markdown 内容必须是字符串。") + if payload.content_type == AgentAssetContentType.JSON and not isinstance( + payload.content, (dict, list) + ): + raise ValueError("JSON 内容必须是对象或数组。") + + def _serialize_version( + self, version: AgentAssetVersion, current_version: str | None + ) -> AgentAssetVersionRead: + return AgentAssetVersionRead( + id=version.id, + asset_id=version.asset_id, + version=version.version, + content=self._deserialize_content(version), + content_type=version.content_type, + change_note=version.change_note, + created_by=version.created_by, + created_at=version.created_at, + is_current=version.version == current_version, + ) + + @staticmethod + def _sort_versions( + versions: list[AgentAssetVersion], current_version: str | None + ) -> list[AgentAssetVersion]: + return sorted( + versions, + key=lambda item: (item.version == current_version, item.created_at), + reverse=True, + ) + + @staticmethod + def _serialize_content(content: Any, content_type: str) -> str: + if content_type == AgentAssetContentType.MARKDOWN.value: + return str(content) + return json.dumps(content, ensure_ascii=False, sort_keys=True, indent=2) + + @staticmethod + def _deserialize_content(version: AgentAssetVersion | None) -> Any: + if version is None: + return None + if version.content_type == AgentAssetContentType.MARKDOWN.value: + return version.content + return json.loads(version.content) + + @staticmethod + def _asset_snapshot(asset: AgentAsset) -> dict[str, Any]: + return { + "asset_type": asset.asset_type, + "code": asset.code, + "name": asset.name, + "status": asset.status, + "current_version": asset.current_version, + "domain": asset.domain, + "owner": asset.owner, + "reviewer": asset.reviewer, + } diff --git a/server/src/app/services/agent_foundation.py b/server/src/app/services/agent_foundation.py new file mode 100644 index 0000000..e65de48 --- /dev/null +++ b/server/src/app/services/agent_foundation.py @@ -0,0 +1,977 @@ +from __future__ import annotations + +import json +from datetime import UTC, date, datetime +from decimal import Decimal + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.core.agent_enums import ( + AgentAssetContentType, + AgentAssetDomain, + AgentAssetStatus, + AgentAssetType, + AgentName, + AgentPermissionLevel, + AgentReviewStatus, + AgentRunSource, + AgentRunStatus, + AgentToolType, +) +from app.core.config import get_settings +from app.core.logging import get_logger +from app.db.base import Base +from app.db.session import get_session_factory +from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion +from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog +from app.models.audit_log import AuditLog +from app.models.financial_record import ( + AccountsPayableRecord, + AccountsReceivableRecord, + ExpenseClaim, + ExpenseClaimItem, +) + +logger = get_logger("app.services.agent_foundation") + + +def prepare_agent_foundation() -> None: + settings = get_settings() + if not settings.setup_completed: + logger.info("Agent foundation bootstrap skipped because setup is incomplete") + return + + session_factory = get_session_factory() + with session_factory() as db: + AgentFoundationService(db).ensure_foundation_ready() + + +class AgentFoundationService: + def __init__(self, db: Session) -> None: + self.db = db + + def ensure_foundation_ready(self) -> None: + try: + Base.metadata.create_all(bind=self.db.get_bind()) + self._seed_agent_assets() + self._seed_financial_records() + self._seed_runs_and_logs() + self.db.commit() + except Exception: + self.db.rollback() + logger.exception("Failed to prepare agent foundation") + raise + + def _seed_agent_assets(self) -> None: + existing_codes = set(self.db.scalars(select(AgentAsset.code)).all()) + if existing_codes: + self._top_up_agent_assets(existing_codes) + return + + approved_rule = AgentAsset( + asset_type=AgentAssetType.RULE.value, + code="rule.expense.duplicate_expense_check", + name="重复报销识别规则", + description="识别同一员工短时间内同金额、同地点、同理由的重复报销风险。", + domain=AgentAssetDomain.EXPENSE.value, + scenario_json=["expense", "risk_check", "duplicate_expense"], + owner="财务共享中心", + reviewer="张晓晴", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.1.0", + config_json={"severity": "high", "enabled": True}, + ) + pending_rule = AgentAsset( + asset_type=AgentAssetType.RULE.value, + code="rule.expense.travel_receipt_requirements", + name="差旅票据完整性规则", + description="检查差旅报销是否附齐发票、行程单和住宿凭证。", + domain=AgentAssetDomain.EXPENSE.value, + scenario_json=["expense", "explain", "invoice_anomaly"], + owner="费用运营组", + reviewer="高嘉禾", + status=AgentAssetStatus.REVIEW.value, + current_version="v1.0.0", + config_json={"severity": "medium", "enabled": False}, + ) + rejected_rule = AgentAsset( + asset_type=AgentAssetType.RULE.value, + code="rule.ap.payment_dual_review", + name="付款双人复核规则", + description="大额付款必须由两名财务人员复核后再进入付款建议。", + domain=AgentAssetDomain.AP.value, + scenario_json=["accounts_payable", "approval_required"], + owner="付款管理组", + reviewer="孙楠", + status=AgentAssetStatus.DRAFT.value, + current_version="v0.9.0", + config_json={"amount_threshold": 50000}, + ) + skill_expense_asset = AgentAsset( + asset_type=AgentAssetType.SKILL.value, + code="skill.expense.summary_lookup", + name="报销汇总查询技能", + description="根据时间、员工和部门汇总报销金额与单据数量。", + domain=AgentAssetDomain.EXPENSE.value, + scenario_json=["expense", "query", "summary"], + owner="平台研发组", + reviewer="陈硕", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.0.0", + config_json={"input_schema": ["time_range", "employee", "department"]}, + ) + skill_ar_asset = AgentAsset( + asset_type=AgentAssetType.SKILL.value, + code="skill.ar.aging_summary", + name="应收账龄汇总技能", + description="按客户、账龄和逾期状态汇总应收风险分布。", + domain=AgentAssetDomain.AR.value, + scenario_json=["accounts_receivable", "query", "aging_summary"], + owner="平台研发组", + reviewer="陈硕", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.0.0", + config_json={"input_schema": ["customer", "aging_bucket", "status"]}, + ) + invoice_mcp_asset = AgentAsset( + asset_type=AgentAssetType.MCP.value, + code="mcp.invoice.verify_mock", + name="发票验真 Mock 服务", + description="模拟发票验真、发票状态查询和异常降级说明。", + domain=AgentAssetDomain.SYSTEM.value, + scenario_json=["expense", "invoice_validation"], + owner="平台研发组", + reviewer="周悦宁", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.0.0", + config_json={"endpoint": "mock://invoice/verify", "timeout_ms": 1200}, + ) + ledger_mcp_asset = AgentAsset( + asset_type=AgentAssetType.MCP.value, + code="mcp.ledger.snapshot_mock", + name="总账快照 Mock 服务", + description="模拟返回应收、应付和费用汇总快照,供 Agent 查询和巡检。", + domain=AgentAssetDomain.SYSTEM.value, + scenario_json=["expense", "accounts_receivable", "accounts_payable"], + owner="平台研发组", + reviewer="周悦宁", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.0.0", + config_json={"endpoint": "mock://ledger/snapshot", "timeout_ms": 1500}, + ) + task_asset = AgentAsset( + asset_type=AgentAssetType.TASK.value, + code="task.hermes.daily_risk_scan", + name="Hermes 每日风险巡检", + description="每天早上巡检重复报销、金额超标、逾期应收和异常付款。", + domain=AgentAssetDomain.SYSTEM.value, + scenario_json=["schedule", "risk_check"], + owner="风控与审计部", + reviewer="顾承宇", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.0.0", + config_json={"cron": "0 9 * * *", "agent": AgentName.HERMES.value}, + ) + ar_summary_task = AgentAsset( + asset_type=AgentAssetType.TASK.value, + code="task.hermes.weekly_ar_summary", + name="Hermes 每周应收账龄汇总", + description="每周汇总逾期应收、账龄分布和客户风险变化。", + domain=AgentAssetDomain.SYSTEM.value, + scenario_json=["schedule", "accounts_receivable", "summary"], + owner="风控与审计部", + reviewer="顾承宇", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.0.0", + config_json={"cron": "0 10 * * 1", "agent": AgentName.HERMES.value}, + ) + rule_digest_task = AgentAsset( + asset_type=AgentAssetType.TASK.value, + code="task.hermes.rule_review_digest", + name="Hermes 规则待审摘要", + description="每天汇总待审规则、待补样例和被拒规则修订建议。", + domain=AgentAssetDomain.SYSTEM.value, + scenario_json=["schedule", "rule_center", "review_digest"], + owner="风控与审计部", + reviewer="顾承宇", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.0.0", + config_json={"cron": "0 18 * * *", "agent": AgentName.HERMES.value}, + ) + + self.db.add_all( + [ + approved_rule, + pending_rule, + rejected_rule, + skill_expense_asset, + skill_ar_asset, + invoice_mcp_asset, + ledger_mcp_asset, + task_asset, + ar_summary_task, + rule_digest_task, + ] + ) + self.db.flush() + + self.db.add_all( + [ + AgentAssetVersion( + asset=approved_rule, + version="v1.0.0", + content=self._markdown_content( + "# 重复报销识别规则\n\n" + "- 检查员工、金额、地点、发生日期是否高度重复。\n" + "- 命中后输出 `duplicate_expense` 风险标签。" + ), + content_type=AgentAssetContentType.MARKDOWN.value, + change_note="初始化生产规则版本。", + created_by="系统初始化", + ), + AgentAssetVersion( + asset=approved_rule, + version="v1.1.0", + content=self._markdown_content( + "# 重复报销识别规则\n\n" + "- 检查员工、金额、地点、发生日期是否高度重复。\n" + "- 新增对同项目、同金额、跨单重复提交的识别。\n" + "- 命中后输出 `duplicate_expense` 风险标签。" + ), + content_type=AgentAssetContentType.MARKDOWN.value, + change_note="补充跨单重复提交判断。", + created_by="系统初始化", + ), + AgentAssetVersion( + asset=pending_rule, + version="v0.9.0", + content=self._markdown_content( + "# 差旅票据完整性规则\n\n" + "- 差旅报销必须具备发票、行程单、住宿凭证。\n" + "- 缺失时输出 `invoice_anomaly`。" + ), + content_type=AgentAssetContentType.MARKDOWN.value, + change_note="首版草稿。", + created_by="高嘉禾", + ), + AgentAssetVersion( + asset=pending_rule, + version="v1.0.0", + content=self._markdown_content( + "# 差旅票据完整性规则\n\n" + "- 差旅报销必须具备发票、行程单、住宿凭证。\n" + "- 新增高铁改签和住宿分拆票据的补件说明。\n" + "- 缺失时输出 `invoice_anomaly`。" + ), + content_type=AgentAssetContentType.MARKDOWN.value, + change_note="补充差旅特殊票据口径,待审核。", + created_by="高嘉禾", + ), + AgentAssetVersion( + asset=rejected_rule, + version="v0.8.0", + content=self._markdown_content( + "# 付款双人复核规则\n\n" + "- 单笔付款超过阈值时必须双人复核。\n" + "- 本版本规则口径过宽,待修订。" + ), + content_type=AgentAssetContentType.MARKDOWN.value, + change_note="首版方案。", + created_by="孙楠", + ), + AgentAssetVersion( + asset=rejected_rule, + version="v0.9.0", + content=self._markdown_content( + "# 付款双人复核规则\n\n" + "- 单笔付款超过阈值时必须双人复核。\n" + "- 新增跨币种付款也进入复核队列。\n" + "- 当前阈值定义仍不清晰,需继续修订。" + ), + content_type=AgentAssetContentType.MARKDOWN.value, + change_note="补充跨币种场景,但阈值仍待明确。", + created_by="孙楠", + ), + AgentAssetVersion( + asset=skill_expense_asset, + version="v1.0.0", + content=self._json_content( + { + "inputs": ["time_range", "employee", "department"], + "outputs": ["total_amount", "claim_count"], + "dependencies": ["database.expense_claims"], + } + ), + content_type=AgentAssetContentType.JSON.value, + change_note="初始化技能快照。", + created_by="系统初始化", + ), + AgentAssetVersion( + asset=skill_ar_asset, + version="v1.0.0", + content=self._json_content( + { + "inputs": ["customer", "aging_bucket", "status"], + "outputs": ["receivable_total", "overdue_total", "customer_count"], + "dependencies": ["database.accounts_receivable"], + } + ), + content_type=AgentAssetContentType.JSON.value, + change_note="初始化应收账龄技能快照。", + created_by="系统初始化", + ), + AgentAssetVersion( + asset=invoice_mcp_asset, + version="v1.0.0", + content=self._json_content( + { + "service_type": "mock", + "auth_mode": "none", + "degrade_strategy": "return_stub_with_warning", + } + ), + content_type=AgentAssetContentType.JSON.value, + change_note="初始化 MCP 快照。", + created_by="系统初始化", + ), + AgentAssetVersion( + asset=ledger_mcp_asset, + version="v1.0.0", + content=self._json_content( + { + "service_type": "mock", + "auth_mode": "service_account", + "degrade_strategy": "return_cached_snapshot_with_warning", + } + ), + content_type=AgentAssetContentType.JSON.value, + change_note="初始化总账快照 MCP。", + created_by="系统初始化", + ), + AgentAssetVersion( + asset=task_asset, + version="v1.0.0", + content=self._json_content( + { + "task_type": "daily_risk_scan", + "schedule": "0 9 * * *", + "target_agent": AgentName.HERMES.value, + } + ), + content_type=AgentAssetContentType.JSON.value, + change_note="初始化任务快照。", + created_by="系统初始化", + ), + AgentAssetVersion( + asset=ar_summary_task, + version="v1.0.0", + content=self._json_content( + { + "task_type": "weekly_ar_summary", + "schedule": "0 10 * * 1", + "target_agent": AgentName.HERMES.value, + } + ), + content_type=AgentAssetContentType.JSON.value, + change_note="初始化应收账龄汇总任务。", + created_by="系统初始化", + ), + AgentAssetVersion( + asset=rule_digest_task, + version="v1.0.0", + content=self._json_content( + { + "task_type": "rule_review_digest", + "schedule": "0 18 * * *", + "target_agent": AgentName.HERMES.value, + } + ), + content_type=AgentAssetContentType.JSON.value, + change_note="初始化规则待审摘要任务。", + created_by="系统初始化", + ), + ] + ) + self.db.add_all( + [ + AgentAssetReview( + asset=approved_rule, + version="v1.1.0", + reviewer="张晓晴", + review_status=AgentReviewStatus.APPROVED.value, + review_note="规则口径清晰,可上线。", + reviewed_at=datetime.now(UTC), + ), + AgentAssetReview( + asset=pending_rule, + version="v1.0.0", + reviewer="高嘉禾", + review_status=AgentReviewStatus.PENDING.value, + review_note="等待补充票据异常样例。", + reviewed_at=None, + ), + AgentAssetReview( + asset=rejected_rule, + version="v0.9.0", + reviewer="孙楠", + review_status=AgentReviewStatus.REJECTED.value, + review_note="阈值定义不清,暂不通过。", + reviewed_at=datetime.now(UTC), + ), + ] + ) + + def _seed_financial_records(self) -> None: + if self.db.scalar(select(ExpenseClaim.id).limit(1)) is not None: + return + + claim_1 = ExpenseClaim( + claim_no="EXP-202605-001", + employee_name="张三", + department_name="财务共享中心", + project_code="PRJ-EXP-01", + expense_type="travel", + reason="华南客户拜访差旅报销", + location="深圳", + amount=Decimal("3280.00"), + currency="CNY", + invoice_count=3, + occurred_at=datetime(2026, 5, 6, 9, 0, tzinfo=UTC), + submitted_at=datetime(2026, 5, 7, 10, 20, tzinfo=UTC), + status="submitted", + approval_stage="finance_review", + risk_flags_json=["amount_over_limit"], + ) + claim_1.items = [ + ExpenseClaimItem( + item_date=date(2026, 5, 5), + item_type="hotel", + item_reason="客户拜访住宿", + item_location="深圳", + item_amount=Decimal("1880.00"), + invoice_id="INV-HOTEL-001", + ), + ExpenseClaimItem( + item_date=date(2026, 5, 6), + item_type="transport", + item_reason="往返交通", + item_location="深圳", + item_amount=Decimal("1400.00"), + invoice_id="INV-TRANS-009", + ), + ] + + claim_2 = ExpenseClaim( + claim_no="EXP-202605-002", + employee_name="李四", + department_name="华东销售部", + project_code="PRJ-SALES-02", + expense_type="meal", + reason="客户路演餐费", + location="上海", + amount=Decimal("860.00"), + currency="CNY", + invoice_count=1, + occurred_at=datetime(2026, 5, 8, 12, 0, tzinfo=UTC), + submitted_at=datetime(2026, 5, 8, 18, 30, tzinfo=UTC), + status="approved", + approval_stage="completed", + risk_flags_json=[], + ) + + claim_3 = ExpenseClaim( + claim_no="EXP-202605-003", + employee_name="王五", + department_name="市场品牌部", + project_code="PRJ-MKT-08", + expense_type="travel", + reason="市场活动会务差旅", + location="北京", + amount=Decimal("3280.00"), + currency="CNY", + invoice_count=2, + occurred_at=datetime(2026, 5, 6, 11, 30, tzinfo=UTC), + submitted_at=datetime(2026, 5, 8, 9, 10, tzinfo=UTC), + status="review", + approval_stage="risk_check", + risk_flags_json=["duplicate_expense"], + ) + + ar_records = [ + AccountsReceivableRecord( + receivable_no="AR-202605-001", + customer_id="CUS-A", + customer_name="客户A", + contract_no="CTR-AR-1001", + invoice_no="INV-AR-9001", + amount_receivable=Decimal("120000.00"), + amount_received=Decimal("70000.00"), + amount_outstanding=Decimal("50000.00"), + currency="CNY", + posting_date=date(2026, 4, 1), + due_date=date(2026, 4, 30), + aging_days=11, + status="partial", + risk_flags_json=[], + ), + AccountsReceivableRecord( + receivable_no="AR-202605-002", + customer_id="CUS-B", + customer_name="客户B", + contract_no="CTR-AR-1002", + invoice_no="INV-AR-9002", + amount_receivable=Decimal("88000.00"), + amount_received=Decimal("10000.00"), + amount_outstanding=Decimal("78000.00"), + currency="CNY", + posting_date=date(2026, 3, 15), + due_date=date(2026, 4, 15), + aging_days=26, + status="overdue", + risk_flags_json=["ar_overdue"], + ), + ] + + ap_records = [ + AccountsPayableRecord( + payable_no="AP-202605-001", + vendor_id="VEN-A", + vendor_name="供应商A", + invoice_no="INV-AP-5001", + amount_payable=Decimal("43000.00"), + amount_paid=Decimal("10000.00"), + amount_outstanding=Decimal("33000.00"), + currency="CNY", + posting_date=date(2026, 4, 20), + due_date=date(2026, 5, 12), + aging_days=0, + status="scheduled", + risk_flags_json=[], + ), + AccountsPayableRecord( + payable_no="AP-202605-002", + vendor_id="VEN-B", + vendor_name="供应商B", + invoice_no="INV-AP-5002", + amount_payable=Decimal("96000.00"), + amount_paid=Decimal("0.00"), + amount_outstanding=Decimal("96000.00"), + currency="CNY", + posting_date=date(2026, 4, 10), + due_date=date(2026, 5, 5), + aging_days=6, + status="overdue", + risk_flags_json=["ap_overdue"], + ), + ] + + self.db.add_all([claim_1, claim_2, claim_3, *ar_records, *ap_records]) + + def _seed_runs_and_logs(self) -> None: + if self.db.scalar(select(AgentRun.id).limit(1)) is not None: + return + + task_asset = self.db.scalar( + select(AgentAsset).where(AgentAsset.code == "task.hermes.daily_risk_scan") + ) + + user_run = AgentRun( + run_id="run_user_20260511_001", + agent=AgentName.USER_AGENT.value, + source=AgentRunSource.USER_MESSAGE.value, + user_id="emp_001", + task_id=None, + ontology_json={"scenario": "expense", "intent": "query"}, + route_json={"selected_agent": AgentName.USER_AGENT.value, "route_reason": "user query"}, + permission_level=AgentPermissionLevel.READ.value, + status=AgentRunStatus.SUCCEEDED.value, + result_summary="已返回本周报销金额和风险摘要。", + started_at=datetime(2026, 5, 11, 8, 35, tzinfo=UTC), + finished_at=datetime(2026, 5, 11, 8, 35, 2, tzinfo=UTC), + ) + hermes_run = AgentRun( + run_id="run_hermes_20260511_001", + agent=AgentName.HERMES.value, + source=AgentRunSource.SCHEDULE.value, + user_id=None, + task_id=task_asset.id if task_asset else None, + ontology_json={"scenario": "expense", "intent": "risk_check"}, + route_json={ + "selected_agent": AgentName.HERMES.value, + "route_reason": "scheduled risk scan", + }, + permission_level=AgentPermissionLevel.READ.value, + status=AgentRunStatus.SUCCEEDED.value, + result_summary="Hermes 已生成今日风险巡检摘要。", + started_at=datetime(2026, 5, 11, 9, 0, tzinfo=UTC), + finished_at=datetime(2026, 5, 11, 9, 0, 4, tzinfo=UTC), + ) + blocked_run = AgentRun( + run_id="run_user_20260511_002", + agent=AgentName.ORCHESTRATOR.value, + source=AgentRunSource.USER_MESSAGE.value, + user_id="emp_002", + task_id=None, + ontology_json={"scenario": "accounts_payable", "intent": "operate"}, + route_json={ + "selected_agent": AgentName.USER_AGENT.value, + "route_reason": "payment request", + }, + permission_level=AgentPermissionLevel.APPROVAL_REQUIRED.value, + status=AgentRunStatus.BLOCKED.value, + result_summary="动作需要人工确认。", + error_message="直接付款属于高风险动作,已阻断自动执行。", + started_at=datetime(2026, 5, 11, 10, 5, tzinfo=UTC), + finished_at=datetime(2026, 5, 11, 10, 5, 1, tzinfo=UTC), + ) + self.db.add_all([user_run, hermes_run, blocked_run]) + self.db.flush() + + self.db.add_all( + [ + AgentToolCall( + run_id=user_run.run_id, + tool_type=AgentToolType.DATABASE.value, + tool_name="expense_claims.lookup", + request_json={"time_range": "this_week", "employee": "all"}, + response_json={"claim_count": 3, "total_amount": "7420.00"}, + status="succeeded", + duration_ms=48, + ), + AgentToolCall( + run_id=hermes_run.run_id, + tool_type=AgentToolType.MCP.value, + tool_name="invoice.verify_mock", + request_json={"claim_no": "EXP-202605-003"}, + response_json={ + "warning": "external service degraded", + "fallback": "used mock response", + }, + status="failed", + duration_ms=132, + error_message="mock upstream timeout", + ), + AgentToolCall( + run_id=blocked_run.run_id, + tool_type=AgentToolType.RULE_ENGINE.value, + tool_name="permission.guard", + request_json={"action": "direct_payment"}, + response_json={"requires_confirmation": True}, + status="succeeded", + duration_ms=5, + ), + SemanticParseLog( + run_id=user_run.run_id, + user_id="emp_001", + raw_query="查一下本周报销超标风险", + scenario="expense", + intent="risk_check", + entities_json=[], + time_range_json={"start_date": "2026-05-11", "end_date": "2026-05-17"}, + metrics_json=["amount"], + constraints_json=[], + risk_flags_json=["amount_over_limit"], + permission_json={"level": AgentPermissionLevel.READ.value}, + confidence=0.93, + ), + SemanticParseLog( + run_id=blocked_run.run_id, + user_id="emp_002", + raw_query="帮我直接付款给供应商B", + scenario="accounts_payable", + intent="operate", + entities_json=[{"type": "vendor", "value": "供应商B"}], + time_range_json={}, + metrics_json=["amount"], + constraints_json=[], + risk_flags_json=["ap_overdue"], + permission_json={"level": AgentPermissionLevel.APPROVAL_REQUIRED.value}, + confidence=0.96, + ), + ] + ) + + if self.db.scalar(select(AuditLog.id).limit(1)) is None: + self.db.add_all( + [ + AuditLog( + actor="系统初始化", + action="save_rule_markdown", + resource_type="rule", + resource_id="rule.expense.duplicate_expense_check", + before_json=None, + after_json={"version": "v1.0.0"}, + request_id="seed-audit-001", + ), + AuditLog( + actor="张晓晴", + action="review_rule", + resource_type="rule", + resource_id="rule.expense.duplicate_expense_check", + before_json={"review_status": "pending"}, + after_json={"review_status": "approved"}, + request_id="seed-audit-002", + ), + AuditLog( + actor="系统初始化", + action="activate_rule", + resource_type="rule", + resource_id="rule.expense.duplicate_expense_check", + before_json={"status": "review"}, + after_json={"status": "active"}, + request_id="seed-audit-003", + ), + AuditLog( + actor="Hermes", + action="update_task_status", + resource_type="task", + resource_id="task.hermes.daily_risk_scan", + before_json={"status": "idle"}, + after_json={"status": "succeeded"}, + request_id="seed-audit-004", + ), + ] + ) + + def _top_up_agent_assets(self, existing_codes: set[str]) -> None: + approved_rule = self.db.scalar( + select(AgentAsset).where(AgentAsset.code == "rule.expense.duplicate_expense_check") + ) + pending_rule = self.db.scalar( + select(AgentAsset).where(AgentAsset.code == "rule.expense.travel_receipt_requirements") + ) + rejected_rule = self.db.scalar( + select(AgentAsset).where(AgentAsset.code == "rule.ap.payment_dual_review") + ) + + if approved_rule is not None: + self._ensure_asset_version( + approved_rule, + version="v1.1.0", + content=self._markdown_content( + "# 重复报销识别规则\n\n" + "- 检查员工、金额、地点、发生日期是否高度重复。\n" + "- 新增对同项目、同金额、跨单重复提交的识别。\n" + "- 命中后输出 `duplicate_expense` 风险标签。" + ), + content_type=AgentAssetContentType.MARKDOWN.value, + change_note="补充跨单重复提交判断。", + created_by="系统初始化", + ) + + if pending_rule is not None: + self._ensure_asset_version( + pending_rule, + version="v1.0.0", + content=self._markdown_content( + "# 差旅票据完整性规则\n\n" + "- 差旅报销必须具备发票、行程单、住宿凭证。\n" + "- 新增高铁改签和住宿分拆票据的补件说明。\n" + "- 缺失时输出 `invoice_anomaly`。" + ), + content_type=AgentAssetContentType.MARKDOWN.value, + change_note="补充差旅特殊票据口径,待审核。", + created_by="高嘉禾", + ) + + if rejected_rule is not None: + self._ensure_asset_version( + rejected_rule, + version="v0.9.0", + content=self._markdown_content( + "# 付款双人复核规则\n\n" + "- 单笔付款超过阈值时必须双人复核。\n" + "- 新增跨币种付款也进入复核队列。\n" + "- 当前阈值定义仍不清晰,需继续修订。" + ), + content_type=AgentAssetContentType.MARKDOWN.value, + change_note="补充跨币种场景,但阈值仍待明确。", + created_by="孙楠", + ) + + if "skill.ar.aging_summary" not in existing_codes: + asset = self._create_seed_asset( + asset_type=AgentAssetType.SKILL.value, + code="skill.ar.aging_summary", + name="应收账龄汇总技能", + description="按客户、账龄和逾期状态汇总应收风险分布。", + domain=AgentAssetDomain.AR.value, + scenario_json=["accounts_receivable", "query", "aging_summary"], + owner="平台研发组", + reviewer="陈硕", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.0.0", + config_json={"input_schema": ["customer", "aging_bucket", "status"]}, + ) + self._ensure_asset_version( + asset, + version="v1.0.0", + content=self._json_content( + { + "inputs": ["customer", "aging_bucket", "status"], + "outputs": ["receivable_total", "overdue_total", "customer_count"], + "dependencies": ["database.accounts_receivable"], + } + ), + content_type=AgentAssetContentType.JSON.value, + change_note="初始化应收账龄技能快照。", + created_by="系统初始化", + ) + + if "mcp.ledger.snapshot_mock" not in existing_codes: + asset = self._create_seed_asset( + asset_type=AgentAssetType.MCP.value, + code="mcp.ledger.snapshot_mock", + name="总账快照 Mock 服务", + description="模拟返回应收、应付和费用汇总快照,供 Agent 查询和巡检。", + domain=AgentAssetDomain.SYSTEM.value, + scenario_json=["expense", "accounts_receivable", "accounts_payable"], + owner="平台研发组", + reviewer="周悦宁", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.0.0", + config_json={"endpoint": "mock://ledger/snapshot", "timeout_ms": 1500}, + ) + self._ensure_asset_version( + asset, + version="v1.0.0", + content=self._json_content( + { + "service_type": "mock", + "auth_mode": "service_account", + "degrade_strategy": "return_cached_snapshot_with_warning", + } + ), + content_type=AgentAssetContentType.JSON.value, + change_note="初始化总账快照 MCP。", + created_by="系统初始化", + ) + + if "task.hermes.weekly_ar_summary" not in existing_codes: + asset = self._create_seed_asset( + asset_type=AgentAssetType.TASK.value, + code="task.hermes.weekly_ar_summary", + name="Hermes 每周应收账龄汇总", + description="每周汇总逾期应收、账龄分布和客户风险变化。", + domain=AgentAssetDomain.SYSTEM.value, + scenario_json=["schedule", "accounts_receivable", "summary"], + owner="风控与审计部", + reviewer="顾承宇", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.0.0", + config_json={"cron": "0 10 * * 1", "agent": AgentName.HERMES.value}, + ) + self._ensure_asset_version( + asset, + version="v1.0.0", + content=self._json_content( + { + "task_type": "weekly_ar_summary", + "schedule": "0 10 * * 1", + "target_agent": AgentName.HERMES.value, + } + ), + content_type=AgentAssetContentType.JSON.value, + change_note="初始化应收账龄汇总任务。", + created_by="系统初始化", + ) + + if "task.hermes.rule_review_digest" not in existing_codes: + asset = self._create_seed_asset( + asset_type=AgentAssetType.TASK.value, + code="task.hermes.rule_review_digest", + name="Hermes 规则待审摘要", + description="每天汇总待审规则、待补样例和被拒规则修订建议。", + domain=AgentAssetDomain.SYSTEM.value, + scenario_json=["schedule", "rule_center", "review_digest"], + owner="风控与审计部", + reviewer="顾承宇", + status=AgentAssetStatus.ACTIVE.value, + current_version="v1.0.0", + config_json={"cron": "0 18 * * *", "agent": AgentName.HERMES.value}, + ) + self._ensure_asset_version( + asset, + version="v1.0.0", + content=self._json_content( + { + "task_type": "rule_review_digest", + "schedule": "0 18 * * *", + "target_agent": AgentName.HERMES.value, + } + ), + content_type=AgentAssetContentType.JSON.value, + change_note="初始化规则待审摘要任务。", + created_by="系统初始化", + ) + + def _create_seed_asset( + self, + *, + asset_type: str, + code: str, + name: str, + description: str, + domain: str, + scenario_json: list[str], + owner: str, + reviewer: str, + status: str, + current_version: str, + config_json: dict[str, object], + ) -> AgentAsset: + asset = AgentAsset( + asset_type=asset_type, + code=code, + name=name, + description=description, + domain=domain, + scenario_json=scenario_json, + owner=owner, + reviewer=reviewer, + status=status, + current_version=current_version, + config_json=config_json, + ) + self.db.add(asset) + self.db.flush() + return asset + + def _ensure_asset_version( + self, + asset: AgentAsset, + *, + version: str, + content: str, + content_type: str, + change_note: str, + created_by: str, + ) -> None: + existing = self.db.scalar( + select(AgentAssetVersion).where( + AgentAssetVersion.asset_id == asset.id, + AgentAssetVersion.version == version, + ) + ) + if existing is not None: + return + + self.db.add( + AgentAssetVersion( + asset_id=asset.id, + version=version, + content=content, + content_type=content_type, + change_note=change_note, + created_by=created_by, + ) + ) + + @staticmethod + def _markdown_content(content: str) -> str: + return content + + @staticmethod + def _json_content(content: dict[str, object]) -> str: + return json.dumps(content, ensure_ascii=False, sort_keys=True, indent=2) diff --git a/server/src/app/services/agent_runs.py b/server/src/app/services/agent_runs.py new file mode 100644 index 0000000..3f526a5 --- /dev/null +++ b/server/src/app/services/agent_runs.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +import uuid +from datetime import UTC, datetime +from typing import Any + +from sqlalchemy.orm import Session + +from app.core.agent_enums import AgentPermissionLevel, AgentRunStatus +from app.core.logging import get_logger +from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog +from app.repositories.agent_run import AgentRunRepository +from app.schemas.agent_run import AgentRunRead, AgentToolCallRead, SemanticParseRead +from app.services.agent_foundation import AgentFoundationService + +logger = get_logger("app.services.agent_runs") + + +class AgentRunService: + def __init__(self, db: Session) -> None: + self.db = db + self.repository = AgentRunRepository(db) + + def list_runs( + self, + *, + agent: str | None = None, + status: str | None = None, + source: str | None = None, + limit: int = 20, + ) -> list[AgentRunRead]: + self._ensure_ready() + runs = self.repository.list(agent=agent, status=status, source=source, limit=limit) + return [self._serialize_run(item) for item in runs] + + def get_run(self, run_id: str) -> AgentRunRead | None: + self._ensure_ready() + run = self.repository.get_by_run_id(run_id) + if run is None: + return None + return self._serialize_run(run) + + def create_run( + self, + *, + agent: str, + source: str, + user_id: str | None = None, + task_id: str | None = None, + ontology_json: dict[str, Any] | None = None, + route_json: dict[str, Any] | None = None, + permission_level: str = AgentPermissionLevel.READ.value, + status: str = AgentRunStatus.RUNNING.value, + result_summary: str | None = None, + error_message: str | None = None, + started_at: datetime | None = None, + finished_at: datetime | None = None, + ) -> AgentRunRead: + self._ensure_ready() + run = AgentRun( + run_id=f"run_{uuid.uuid4().hex[:16]}", + agent=agent, + source=source, + user_id=user_id, + task_id=task_id, + ontology_json=ontology_json or {}, + route_json=route_json or {}, + permission_level=permission_level, + status=status, + result_summary=result_summary, + error_message=error_message, + started_at=started_at or datetime.now(UTC), + finished_at=finished_at, + ) + created = self.repository.create_run(run) + logger.info("Created agent run id=%s run_id=%s", created.id, created.run_id) + return self._serialize_run(created) + + def record_tool_call( + self, + *, + run_id: str, + tool_type: str, + tool_name: str, + request_json: dict[str, Any] | None = None, + response_json: dict[str, Any] | None = None, + status: str, + duration_ms: int = 0, + error_message: str | None = None, + ) -> AgentToolCallRead: + self._ensure_ready() + tool_call = AgentToolCall( + run_id=run_id, + tool_type=tool_type, + tool_name=tool_name, + request_json=request_json or {}, + response_json=response_json or {}, + status=status, + duration_ms=duration_ms, + error_message=error_message, + ) + created = self.repository.create_tool_call(tool_call) + logger.info("Recorded tool call run_id=%s tool=%s", run_id, tool_name) + return AgentToolCallRead.model_validate(created) + + def record_semantic_parse( + self, + *, + run_id: str, + user_id: str | None, + raw_query: str, + scenario: str, + intent: str, + entities_json: list[Any] | None = None, + time_range_json: dict[str, Any] | None = None, + metrics_json: list[Any] | None = None, + constraints_json: list[Any] | None = None, + risk_flags_json: list[Any] | None = None, + permission_json: dict[str, Any] | None = None, + confidence: float = 0.0, + ) -> SemanticParseRead: + self._ensure_ready() + semantic_parse = SemanticParseLog( + run_id=run_id, + user_id=user_id, + raw_query=raw_query, + scenario=scenario, + intent=intent, + entities_json=entities_json or [], + time_range_json=time_range_json or {}, + metrics_json=metrics_json or [], + constraints_json=constraints_json or [], + risk_flags_json=risk_flags_json or [], + permission_json=permission_json or {}, + confidence=confidence, + ) + created = self.repository.create_semantic_parse(semantic_parse) + logger.info( + "Recorded semantic parse run_id=%s scenario=%s intent=%s", run_id, scenario, intent + ) + return SemanticParseRead.model_validate(created) + + def _ensure_ready(self) -> None: + AgentFoundationService(self.db).ensure_foundation_ready() + + @staticmethod + def _serialize_run(run: AgentRun) -> AgentRunRead: + semantic_parse = run.semantic_parse_logs[0] if run.semantic_parse_logs else None + return AgentRunRead( + id=run.id, + run_id=run.run_id, + agent=run.agent, + source=run.source, + user_id=run.user_id, + task_id=run.task_id, + ontology_json=run.ontology_json, + route_json=run.route_json, + permission_level=run.permission_level, + status=run.status, + result_summary=run.result_summary, + error_message=run.error_message, + started_at=run.started_at, + finished_at=run.finished_at, + tool_calls=[AgentToolCallRead.model_validate(item) for item in run.tool_calls], + semantic_parse=SemanticParseRead.model_validate(semantic_parse) + if semantic_parse + else None, + ) diff --git a/server/src/app/services/audit.py b/server/src/app/services/audit.py new file mode 100644 index 0000000..cabbb89 --- /dev/null +++ b/server/src/app/services/audit.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import uuid +from typing import Any + +from sqlalchemy.orm import Session + +from app.core.logging import get_logger +from app.models.audit_log import AuditLog +from app.repositories.audit_log import AuditLogRepository +from app.schemas.audit_log import AuditLogRead +from app.services.agent_foundation import AgentFoundationService + +logger = get_logger("app.services.audit") + + +class AuditLogService: + def __init__(self, db: Session) -> None: + self.db = db + self.repository = AuditLogRepository(db) + + def list_logs( + self, + *, + resource_type: str | None = None, + resource_id: str | None = None, + action: str | None = None, + limit: int = 50, + ) -> list[AuditLogRead]: + self._ensure_ready() + items = self.repository.list( + resource_type=resource_type, + resource_id=resource_id, + action=action, + limit=limit, + ) + return [AuditLogRead.model_validate(item) for item in items] + + def log_action( + self, + *, + actor: str, + action: str, + resource_type: str, + resource_id: str, + before_json: dict[str, Any] | None = None, + after_json: dict[str, Any] | None = None, + request_id: str | None = None, + ) -> AuditLog: + log = AuditLog( + actor=actor, + action=action, + resource_type=resource_type, + resource_id=resource_id, + before_json=before_json, + after_json=after_json, + request_id=request_id or uuid.uuid4().hex, + ) + created = self.repository.create(log) + logger.info( + "Created audit log id=%s action=%s resource=%s:%s", + created.id, + created.action, + created.resource_type, + created.resource_id, + ) + return created + + def _ensure_ready(self) -> None: + AgentFoundationService(self.db).ensure_foundation_ready() diff --git a/server/src/x_financial_server.egg-info/SOURCES.txt b/server/src/x_financial_server.egg-info/SOURCES.txt index f14e484..bb8c7c0 100644 --- a/server/src/x_financial_server.egg-info/SOURCES.txt +++ b/server/src/x_financial_server.egg-info/SOURCES.txt @@ -8,6 +8,9 @@ src/app/api/router.py src/app/api/v1/__init__.py src/app/api/v1/router.py src/app/api/v1/endpoints/__init__.py +src/app/api/v1/endpoints/agent_assets.py +src/app/api/v1/endpoints/agent_runs.py +src/app/api/v1/endpoints/audit_logs.py src/app/api/v1/endpoints/auth.py src/app/api/v1/endpoints/bootstrap.py src/app/api/v1/endpoints/employees.py @@ -17,6 +20,7 @@ src/app/api/v1/endpoints/reimbursements.py src/app/api/v1/endpoints/settings.py src/app/core/__init__.py src/app/core/admin_secret.py +src/app/core/agent_enums.py src/app/core/bootstrap.py src/app/core/config.py src/app/core/logging.py @@ -29,9 +33,13 @@ src/app/db/session.py src/app/middleware/__init__.py src/app/middleware/logging.py src/app/models/__init__.py +src/app/models/agent_asset.py +src/app/models/agent_run.py src/app/models/approval.py +src/app/models/audit_log.py src/app/models/employee.py src/app/models/employee_change_log.py +src/app/models/financial_record.py src/app/models/organization.py src/app/models/reimbursement.py src/app/models/role.py @@ -39,10 +47,16 @@ src/app/models/system_model_setting.py src/app/models/system_setting.py src/app/models/system_setting_secret.py src/app/repositories/__init__.py +src/app/repositories/agent_asset.py +src/app/repositories/agent_run.py +src/app/repositories/audit_log.py src/app/repositories/employee.py src/app/repositories/reimbursement.py src/app/repositories/settings.py src/app/schemas/__init__.py +src/app/schemas/agent_asset.py +src/app/schemas/agent_run.py +src/app/schemas/audit_log.py src/app/schemas/auth.py src/app/schemas/bootstrap.py src/app/schemas/employee.py @@ -50,9 +64,14 @@ src/app/schemas/knowledge.py src/app/schemas/reimbursement.py src/app/schemas/settings.py src/app/services/__init__.py +src/app/services/agent_assets.py +src/app/services/agent_foundation.py +src/app/services/agent_runs.py +src/app/services/audit.py src/app/services/auth.py src/app/services/employee.py src/app/services/employee_seed.py +src/app/services/hermes_sync.py src/app/services/knowledge.py src/app/services/model_connectivity.py src/app/services/reimbursement.py @@ -62,9 +81,14 @@ src/x_financial_server.egg-info/SOURCES.txt src/x_financial_server.egg-info/dependency_links.txt src/x_financial_server.egg-info/requires.txt src/x_financial_server.egg-info/top_level.txt +tests/test_agent_asset_service.py +tests/test_agent_foundation_endpoints.py tests/test_auth_service.py +tests/test_config_settings_reload.py tests/test_employee_service.py +tests/test_env_file_precedence.py tests/test_imports.py +tests/test_knowledge_onlyoffice_config.py tests/test_server_start_dependencies.py tests/test_settings_persistence.py -tests/test_settings_service.py +tests/test_settings_service.py \ No newline at end of file diff --git a/server/tests/test_agent_asset_service.py b/server/tests/test_agent_asset_service.py new file mode 100644 index 0000000..08d4be5 --- /dev/null +++ b/server/tests/test_agent_asset_service.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import uuid + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.pool import StaticPool + +from app.core.agent_enums import ( + AgentAssetContentType, + AgentAssetDomain, + AgentAssetStatus, + AgentAssetType, + AgentName, + AgentReviewStatus, + AgentRunSource, + AgentRunStatus, +) +from app.db.base import Base +from app.schemas.agent_asset import ( + AgentAssetCreate, + AgentAssetReviewCreate, + AgentAssetVersionCreate, +) +from app.services.agent_assets import AgentAssetService +from app.services.agent_runs import AgentRunService +from app.services.audit import AuditLogService + + +def build_session() -> Session: + engine = create_engine( + "sqlite+pysqlite:///:memory:", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + Base.metadata.create_all(bind=engine) + session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False) + return session_factory() + + +def test_agent_asset_service_seeds_assets_and_enforces_review_before_activation() -> None: + with build_session() as db: + service = AgentAssetService(db) + + rules = service.list_assets(asset_type=AgentAssetType.RULE.value) + assert len(rules) >= 3 + + pending_rule = next(item for item in rules if item.status == AgentAssetStatus.REVIEW.value) + + with pytest.raises(PermissionError): + service.activate_asset(pending_rule.id, actor="pytest") + + +def test_agent_asset_service_seeds_all_foundation_asset_types() -> None: + with build_session() as db: + service = AgentAssetService(db) + + assert len(service.list_assets(asset_type=AgentAssetType.RULE.value)) >= 3 + assert len(service.list_assets(asset_type=AgentAssetType.SKILL.value)) >= 2 + assert len(service.list_assets(asset_type=AgentAssetType.MCP.value)) >= 2 + assert len(service.list_assets(asset_type=AgentAssetType.TASK.value)) >= 3 + + +def test_agent_asset_service_can_activate_rule_after_review() -> None: + with build_session() as db: + service = AgentAssetService(db) + + created = service.create_asset( + AgentAssetCreate( + asset_type=AgentAssetType.RULE, + code=f"rule.test.{uuid.uuid4().hex[:8]}", + name="测试规则", + description="用于测试审核和上线流程。", + domain=AgentAssetDomain.EXPENSE, + scenario_json=["expense", "risk_check"], + owner="pytest", + reviewer="reviewer", + status=AgentAssetStatus.DRAFT, + config_json={"enabled": False}, + ), + actor="pytest", + ) + service.create_version( + created.id, + AgentAssetVersionCreate( + version="v1.0.0", + content="# 测试规则\n\n- 仅用于测试。", + content_type=AgentAssetContentType.MARKDOWN, + change_note="初始化版本", + created_by="pytest", + ), + actor="pytest", + ) + service.create_review( + created.id, + AgentAssetReviewCreate( + version="v1.0.0", + reviewer="reviewer", + review_status=AgentReviewStatus.APPROVED, + review_note="可以上线", + ), + actor="reviewer", + ) + + activated = service.activate_asset(created.id, actor="reviewer") + + assert activated.status == AgentAssetStatus.ACTIVE.value + assert activated.current_version == "v1.0.0" + assert activated.latest_review is not None + assert activated.latest_review.review_status == AgentReviewStatus.APPROVED.value + + +def test_agent_asset_service_returns_recent_versions_for_rule_detail() -> None: + with build_session() as db: + service = AgentAssetService(db) + + rule = next( + item + for item in service.list_assets(asset_type=AgentAssetType.RULE.value) + if item.code == "rule.expense.duplicate_expense_check" + ) + detail = service.get_asset(rule.id) + + assert detail is not None + assert detail.current_version == "v1.1.0" + assert detail.current_version_content_type == AgentAssetContentType.MARKDOWN.value + assert isinstance(detail.current_version_content, str) + assert len(detail.recent_versions) >= 2 + assert any(item.is_current for item in detail.recent_versions) + assert {item.version for item in detail.recent_versions} >= {"v1.0.0", "v1.1.0"} + + +def test_agent_run_service_lists_seeded_trace_data() -> None: + with build_session() as db: + service = AgentRunService(db) + + runs = service.list_runs() + + assert len(runs) >= 3 + assert any(item.tool_calls for item in runs) + assert any(item.semantic_parse is not None for item in runs) + + +def test_agent_run_service_creates_run_and_persists_error_message() -> None: + with build_session() as db: + service = AgentRunService(db) + + created = service.create_run( + agent=AgentName.ORCHESTRATOR.value, + source=AgentRunSource.SYSTEM_EVENT.value, + status=AgentRunStatus.FAILED.value, + error_message="simulated failure", + result_summary="failed to route request", + ) + fetched = service.get_run(created.run_id) + + assert fetched is not None + assert fetched.run_id.startswith("run_") + assert fetched.status == AgentRunStatus.FAILED.value + assert fetched.error_message == "simulated failure" + assert fetched.result_summary == "failed to route request" + + +def test_agent_asset_creation_writes_audit_log() -> None: + with build_session() as db: + service = AgentAssetService(db) + + created = service.create_asset( + AgentAssetCreate( + asset_type=AgentAssetType.SKILL, + code=f"skill.test.{uuid.uuid4().hex[:8]}", + name="测试技能", + description="用于测试审计日志写入。", + domain=AgentAssetDomain.KNOWLEDGE, + scenario_json=["knowledge", "query"], + owner="pytest", + reviewer="reviewer", + status=AgentAssetStatus.DRAFT, + config_json={"enabled": True}, + ), + actor="pytest", + ) + logs = AuditLogService(db).list_logs(resource_id=created.id) + + assert any(item.action == "create_agent_asset" for item in logs) diff --git a/server/tests/test_agent_foundation_endpoints.py b/server/tests/test_agent_foundation_endpoints.py new file mode 100644 index 0000000..90eb49d --- /dev/null +++ b/server/tests/test_agent_foundation_endpoints.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from collections.abc import Generator + +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.pool import StaticPool + +from app.api.deps import get_db +from app.core.agent_enums import AgentAssetStatus +from app.db.base import Base +from app.main import create_app +from app.services.agent_assets import AgentAssetService + + +def build_client() -> tuple[TestClient, sessionmaker[Session]]: + engine = create_engine( + "sqlite+pysqlite:///:memory:", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + Base.metadata.create_all(bind=engine) + session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False) + + app = create_app() + + def override_db() -> Generator[Session, None, None]: + db = session_factory() + try: + yield db + finally: + db.close() + + app.dependency_overrides[get_db] = override_db + return TestClient(app), session_factory + + +def test_list_agent_assets_endpoint_returns_seeded_items() -> None: + client, _ = build_client() + + response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"}) + + assert response.status_code == 200 + payload = response.json() + assert payload + assert all(item["asset_type"] == "rule" for item in payload) + + +def test_get_agent_asset_detail_endpoint_returns_version_history() -> None: + client, _ = build_client() + + list_response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"}) + asset_id = list_response.json()[0]["id"] + + response = client.get(f"/api/v1/agent-assets/{asset_id}") + + assert response.status_code == 200 + payload = response.json() + assert payload["recent_versions"] + assert payload["current_version_content_type"] == "markdown" + assert len(payload["recent_versions"]) >= 2 + + +def test_activate_pending_rule_endpoint_is_blocked() -> None: + client, session_factory = build_client() + + with session_factory() as db: + pending_rule = next( + item + for item in AgentAssetService(db).list_assets(asset_type="rule") + if item.status == AgentAssetStatus.REVIEW.value + ) + + response = client.post( + f"/api/v1/agent-assets/{pending_rule.id}/activate", + headers={"x-actor": "pytest"}, + ) + + assert response.status_code == 400 + assert "审核" in response.json()["detail"] + + +def test_list_audit_logs_endpoint_returns_seeded_logs() -> None: + client, _ = build_client() + + response = client.get("/api/v1/audit-logs") + + assert response.status_code == 200 + payload = response.json() + assert payload + assert any(item["action"] == "review_rule" for item in payload)