From 2747bb4e64d3e17529f2a3b5fad9eedc85c9d237 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Mon, 29 Jun 2026 20:25:03 +0800 Subject: [PATCH] chore(prior): malformed tool call handling, auth whitelist, dev scripts, wave1 plan --- CONCEPTS.md | 6 + ...ced-agent-gap-optimization-requirements.md | 186 +++++++ ...29-002-feat-agent-wave1-quick-wins-plan.md | 275 ++++++++++ scripts/dev-start.sh | 509 ++++++++++++++++++ scripts/dev-stop.sh | 30 ++ scripts/prod-start.sh | 218 ++++++++ scripts/prod-stop.sh | 19 + scripts/test-start.sh | 192 +++++++ src/agentkit/core/react.py | 152 ++++++ src/agentkit/server/auth/middleware.py | 14 +- src/agentkit/server/frontend/components.d.ts | 1 + .../src/components/bitable/BitableGrid.vue | 2 +- src/agentkit/server/static/index.html | 4 +- tests/unit/test_react_engine.py | 106 ++++ 14 files changed, 1705 insertions(+), 9 deletions(-) create mode 100644 docs/brainstorms/2026-06-29-advanced-agent-gap-optimization-requirements.md create mode 100644 docs/plans/2026-06-29-002-feat-agent-wave1-quick-wins-plan.md create mode 100755 scripts/dev-start.sh create mode 100755 scripts/dev-stop.sh create mode 100755 scripts/prod-start.sh create mode 100755 scripts/prod-stop.sh create mode 100755 scripts/test-start.sh diff --git a/CONCEPTS.md b/CONCEPTS.md index 7a0eb61..95bf397 100644 --- a/CONCEPTS.md +++ b/CONCEPTS.md @@ -33,6 +33,12 @@ A phase dynamically inserted into a team plan when divergence is detected betwee ### Resume The act of rebuilding a crashed pipeline's runtime state from persisted checkpoints. Restores completed and failed phase statuses, rebuilds runtime counters, and re-persists any dynamically inserted phases so the restored plan matches what was executing at crash time. +### Verify Re-injection +The feedback loop triggered when a verification check fails after a final answer is produced. Errors are injected as a new user message into the conversation and the ReAct loop continues for one retry; a second verification failure interrupts execution and returns the error to the user with the verify log. Bounded to one retry to balance auto-correction against token cost, rather than looping until `max_steps`. + +### Three-tier Degradation Chain +The agent-level fallback sequence when the primary agent fails: main agent → Recovery tier (reuses `ReflexionEngine` for Evaluate→Reflect→Retry) → Emergency tier (rule-based fallback returning a structured error with suggestions). Each tier is independently configurable; the Recovery tier avoids new infrastructure by reusing the existing reflection engine, and the Emergency tier replaces the previous static-text fallback with actionable error structure. + ## Channels & Caching ### Per-User Cache Namespace diff --git a/docs/brainstorms/2026-06-29-advanced-agent-gap-optimization-requirements.md b/docs/brainstorms/2026-06-29-advanced-agent-gap-optimization-requirements.md new file mode 100644 index 0000000..e7deb4e --- /dev/null +++ b/docs/brainstorms/2026-06-29-advanced-agent-gap-optimization-requirements.md @@ -0,0 +1,186 @@ +--- +date: 2026-06-29 +topic: advanced-agent-gap-optimization +type: feature +origin: "2026-06-24-004-feat-long-horizon-reliability-optimization-plan.md(增补);Qoder/Codex/Hermes/Trae Work 架构对比(2026-06-29)" +--- + +## Summary + +为 2026-06-24-004 长程可靠性 plan 补齐 9 个真新缺口——对比 Qoder/Codex/Hermes/Trae Work 后未被覆盖的执行/反馈/效率短板。分 3 波按 ROI/风险交付:Wave 1 自包含快速补强(W1-1~W1-4),Wave 2 中等耦合(W2-1~W2-3),Wave 3 战略级重构(W3-1~W3-2)。每波独立 plan、独立验证、独立发布。 + +--- + +## Problem Frame + +2026-06-24-004 plan 已落地循环检测(U1)、Headroom 压缩(U3)、SharedWorkspace Redis 化(U4)、中间件管道(U6)、阶段级 checkpoint+resume(U7)等长程可靠性护栏。但对照 Qoder(Spec→Coding→Verify 闭环)、Codex(apply_patch+OS 沙箱)、Hermes(三级降级链+4层记忆)、Trae Work(SOLO 四阶段状态机+Trajectory 持久化)的工程实践,agentkit 仍存在 9 个**已在生产/测试中观察到痛点**的短板,横跨三个维度: + +- **反馈稳定性**:verify 失败不回灌 ReAct 直接退出、工具调用无 schema 校验、主 agent 失败后仅静态 fallback、子任务失败不回滚已写文件 +- **响应效率**:prompt cache 命中率低(记忆注入破坏前缀)、摘要/压缩用主模型、文件读取无函数级分片、token chunk 无节流 +- **执行能力**:ReAct 循环无阶段约束(think 阶段即可 write_file) + +验证器已核对 10 项仓库声明,9 项 confirmed,2 处修正(C5:agentkit 无独立 `read_file` 工具,文件读取走 `tools/shell.py` 的 `cat`;C6:`PlanPhase` 定义在 `experts/plan.py:148` 非 `team.py`)。 + +--- + +## Key Decisions + +**KTD1:G1 verify 回灌一次后中断** + +verify 失败时自动把 errors 注入 conversation 继续 ReAct,但仅重试一次;二次失败则中断返回错误给用户(附 verify log)。平衡自动纠正与 token 成本,而非全自动循环到 max_steps 烧 token。 + +**KTD2:3 波按 ROI/风险分次** + +Wave 1(G1/G2/G3/G8)自包含、低风险、可独立验证;Wave 2(G4/G7/G9)中等耦合,需触碰 LLM 层与编排层;Wave 3(G5/G6)战略级,引入新依赖或触 ReAct 核心。优先交付自包含补强,而非按维度或 LLM 层依赖分波。 + +**KTD3:G2 三层 prompt 结构跨 provider,记忆注入移到 volatile** + +system prompt 重构为 stable(技能配置/系统指令)/ context(会话上下文)/ volatile(记忆检索+时间戳)三层。`memory_retriever` 当前在 `react.py:1042-1059` 把"## 参考信息"拼到 system prompt 末尾,每次随 query 变化破坏 cache 前缀——移到 volatile 层,stable 层保持不变以命中 prompt cache。cache 策略跨 provider 统一:Anthropic 用原生 `cache_control` 断点(`system_and_3`),OpenAI 等依赖自动前缀缓存(stable 层前置即命中),无需为每个 provider 单独适配。 + +**KTD4:G7 复用 ReflexionEngine 作为 Recovery 层** + +主 agent 失败后触发 Recovery 层,复用现有 `ReflexionEngine`(`core/reflexion.py:58`,Evaluate→Reflect→Retry),而非新建 Recovery Agent。Recovery 仍失败则进入 Emergency 层(规则化 fallback,返回结构化错误+建议)。避免新基础设施,最大化复用现有反思机制。 + +**KTD5:G9 阶段级 rollback,git checkout 机制** + +`PlanPhase` 增加 `validation_command`/`rollback_command` 可选字段,阶段失败时执行 rollback(默认 `git checkout`),与 U7 checkpoint 协同——checkpoint save 必须在 rollback validation 通过后。不做步骤级 rollback(粒度过细,实现复杂度高)。 + +**KTD6:G5 函数级分片引入 tree-sitter(Wave 3 决策可延后)** + +Wave 3 的 G5 需要按 symbol/函数粒度分片读取文件,引入 tree-sitter 作为新依赖。具体集成方式(tree-sitter vs ANTLR4 vs 复用 `quality` 模块)延后到 Wave 3 自己的 plan 决策。 + +**KTD7:G6 扩展 PLAN_EXEC 而非新建模式** + +G6 SOLO 四阶段状态机约束通过扩展现有 `ExecutionMode.PLAN_EXEC` 实现,而非新建独立模式。阶段约束(Planning 阶段只允许 think/search,Building 才允许 write_file)作为 PLAN_EXEC 的配置项。 + +--- + +## Requirements + +### Wave 1 — 自包含快速补强(P0,低风险) + +**G1 Verify 失败回灌 ReAct** + +- R1. verify 失败时,系统自动把 errors 作为新 user 消息注入 conversation,继续 ReAct 循环,而非直接抛 `TaskResult.error_message` 退出。 +- R2. 回灌后若二次 verify 仍失败,系统中断执行并返回错误给用户,附 verify log(测试输出/schema 错误明细)。 +- R3. 回灌最大重试次数可配置(默认 1 次),受 `max_steps` 上限约束。 + +**G2 prompt cache 断点策略** + +- R4. system prompt 重构为三层:stable(技能配置/系统指令)、context(会话上下文)、volatile(记忆检索+时间戳)。 +- R5. 记忆检索注入从 system prompt 末尾移到 volatile 层,stable 层保持不变以命中 prompt cache。 +- R6. 跨 provider 统一 cache 策略:Anthropic 显式插入 `cache_control` 断点(`system_and_3`,最多 4 个);OpenAI 等无原生断点的 provider 依赖自动前缀缓存,通过 stable 层前置保证命中。 +- R7. 多轮对话输入 token 成本降低(目标降低 ~50% 输入 token,跨 provider 均受益)。 + +**G3 工具调用 schema 校验** + +- R8. `_execute_tool` 调用工具前,基于 `tool.input_schema` 校验 LLM 传入参数(类型/必填)。 +- R9. 校验失败时返回类型化错误码(`tool_call_invalid`/`schema_mismatch`),不执行工具。 +- R10. 错误以 tool 角色消息回灌 conversation,给 LLM 自我修正机会。 + +**G8 delta_flush_interval 调速** + +- R11. `execute_stream` 的 token chunk yield 加可配置节流(默认 `flush_interval_ms`,如 50ms)。 +- R12. 节流配置化(`agentkit.yaml` 或 runtime 配置),允许客户端调高降低渲染开销。 + +### Wave 2 — 中等耦合(P1) + +**G4 辅助 LLM 分流** + +- R13. `ContextCompressor` 摘要任务路由到 auxiliary model(便宜模型,如 Gemini Flash/Doubao lite),而非主 model。 +- R14. `auxiliary_model` 配置化,与主 `model` 分离。 +- R15. 摘要质量不降级:auxiliary model 失败时回退主 model。 + +**G7 三级降级链** + +- R16. 主 agent 失败后触发 Recovery 层(复用 `ReflexionEngine` 做 Evaluate→Reflect→Retry)。 +- R17. Recovery 仍失败触发 Emergency 层(规则化 fallback,返回结构化错误+建议)。 +- R18. 降级链可配置(每层最大重试次数、是否启用 Recovery/Emergency)。 + +**G9 原子化子任务 + rollback 绑定** + +- R19. `PlanPhase`(`experts/plan.py:148`)增加 `validation_command` 和 `rollback_command` 可选字段。 +- R20. 阶段失败时自动执行 `rollback_command`(默认 `git checkout`),与 U7 checkpoint 协同。 +- R21. checkpoint save 必须在 rollback validation 通过后(避免持久化失败状态)。 + +### Wave 3 — 战略级重构(P2,高风险) + +**G5 函数级代码分片** + +- R22. 文件读取支持按 symbol/函数粒度分片(需引入 tree-sitter 或类似,具体方式延后到 Wave 3 plan)。 +- R23. 分片能力作为工具参数(如 `symbol="function_name"`),向后兼容整文件读取。 + +**G6 SOLO 四阶段状态机约束** + +- R24. ReAct 循环加阶段约束:Planning 阶段只允许 think/search,Building 阶段才允许 `write_file`。 +- R25. 阶段状态可配置(扩展 `ExecutionMode.PLAN_EXEC`,非新建独立模式)。 + +### Cross-cutting + +- R26. 所有优化项配置化(`agentkit.yaml` 新增对应配置节,遵循 `ServerConfig.from_dict` 模式)。 +- R27. 每个优化项附最小自检测试(ponytail 规则,参考 `test_pipeline_state.py` 的 `TestPipelineStateRedis` 模式)。 + +--- + +## Acceptance Examples + +- **AE1**(Covers R1, R2, R3):用户发起 ReAct 任务,verify 失败(测试不通过)→ 系统自动把 errors 注入 conversation 继续 ReAct → LLM 修正后二次 verify 通过 → 任务完成。若二次 verify 仍失败 → 中断返回错误,附 verify log。 +- **AE2**(Covers R4, R5, R6, R7):用户发起 50 轮长对话 → stable 层(技能配置)保持不变,volatile 层(记忆检索)随 query 变化 → Anthropic prompt cache 命中 stable 前缀 → 输入 token 成本降低 ~50%。 +- **AE3**(Covers R8, R9, R10):LLM 调用工具时传错参数类型(如 `count: "abc"` 应为 int)→ schema 校验失败 → 返回 `tool_call_invalid` 错误码 → 错误回灌 conversation → LLM 修正参数类型后重试成功。 +- **AE4**(Covers R16, R17, R18):主 agent 连续 3 次失败 → 触发 Recovery 层(ReflexionEngine 反思重试)→ Recovery 仍失败 → 触发 Emergency 层(规则化 fallback,返回结构化错误+建议)→ 用户收到清晰错误而非静态文案。 +- **AE5**(Covers R19, R20, R21):`@team` 任务阶段 3 失败 → 自动执行 `rollback_command`(git checkout 阶段 3 写入的文件)→ rollback validation 通过后才 save checkpoint → 阶段 4 从干净状态继续。 +- **AE6**(Covers R11, R12):弱网客户端连接 `execute_stream` → token chunk 按 50ms 节流批量 yield → 前端渲染开销降低,无卡顿。 + +--- + +## Scope Boundaries + +### Deferred for later + +- Wave 3(G5/G6)的具体实现设计延后到 Wave 3 自己的 plan,本文档只锁定战略方向(KTD6/KTD7)。 +- G5 的具体集成方式(tree-sitter vs ANTLR4 vs 复用 `quality` 模块)延后到 Wave 3 plan 决策。 +- G7 Emergency 层的具体规则模板(返回什么错误结构、建议什么动作)延后到 Wave 2 plan。 +- 全局 LLM 并发限制(`LLMGateway` 内 semaphore)——本期只做工具层/编排层,LLM 层并发延后。 + +### Outside this product's identity + +- 重写现有编排逻辑(拓扑排序/Board 辩论/4 层记忆保持不变)——继承自 2026-06-24-004 plan。 +- 节点级 checkpoint(ReAct 循环单步)——继承自 2026-06-24-004 KTD3,阶段级已满足核心需求。 +- DeerFlow 式磁盘文件系统——继承自 2026-06-24-004 KTD4,复用 Redis。 +- 全盘迁移 LangGraph——继承自 2026-06-24-004,自研架构保持灵活。 +- Docker 沙箱默认引入——仅文档化命令级安全边界,作为可选插件未来考虑。 + +--- + +## Dependencies / Assumptions + +- **依赖**:Wave 2 的 G9 rollback 依赖现有 U7 checkpoint 已落地(`orchestrator/checkpoint.py` + `experts/orchestrator.py:265-268,335`)。 +- **依赖**:Wave 1 的 G2 prompt cache 断点依赖 Anthropic provider 已支持 `cache_control`(LiteLLM 适配层 U15)。 +- **假设**:G4 auxiliary model 可用——`agentkit.yaml` 的 llm 段可配置多个 model,auxiliary 复用现有 provider 注册机制。若实际不可用,回退主 model(R15)。 +- **假设**:G6 阶段约束不影响现有 DIRECT_CHAT/REACT 模式——仅作用于 PLAN_EXEC 模式,向后兼容。 + +--- + +## Sources & Research + +- **Qoder 架构研究**(2026-06-29):Spec→Coding→Verify 闭环、SSE 事件成对约束、`delta_flush_interval_ms`、prompt cache、自动模型路由 +- **Codex CLI 架构研究**(2026-06-29):apply_patch 协议、Approval Policy 三层决策、OS 级沙箱、prompt caching 前缀匹配、`/responses/compact` 压缩 +- **Hermes Agent 架构研究**(2026-06-29):Pydantic+JSON Schema 双校验、`validate_function_call_schema`、三级降级链(主→Recovery→Emergency)、`cache_control: system_and_3` 断点(4 个,~75% token 节省)、auxiliary_client 分流 +- **Trae Work 架构研究**(2026-06-29):SOLO 四阶段状态机(Planning→Building→Verification→Delivery)、TrajectoryRecorder JSON 持久化、断点续跑、原子化子任务+rollback、函数级分片(ANTLR4+LLVM) +- **agentkit 现有架构**(2026-06-29 验证器核对):`verification_loop.py:111-145`、`react.py:1042-1059,1118-1134,1897-1916`、`compressor.py:45,123-154`、`reflexion.py:58-702`、`fallback.py:1-19`、`gateway.py:405-407`、`experts/plan.py:148`、`orchestrator/checkpoint.py`、`experts/orchestrator.py:265-268,335` +- **2026-06-24-004 plan**(增补来源):U1-U7 长程可靠性护栏,KTD1-KTD5 决策,U7 阶段级 checkpoint+resume 已落地 + +--- + +## Outstanding Questions + +### Resolve Before Planning + +(已全部解决——OQ1 确认同 plan 内顺序执行 G2→G8;OQ2 确认跨 provider 统一 cache 策略。) + +### Deferred to Planning + +- G5 的具体集成方式(tree-sitter vs ANTLR4 vs 复用 `quality` 模块)。 +- G7 Emergency 层的具体规则模板与错误结构。 +- G6 阶段约束的精确边界(Planning 阶段允许哪些工具、Building 阶段允许哪些)。 +- G9 rollback_command 的默认值(git checkout 整文件 vs patch 级)。 diff --git a/docs/plans/2026-06-29-002-feat-agent-wave1-quick-wins-plan.md b/docs/plans/2026-06-29-002-feat-agent-wave1-quick-wins-plan.md new file mode 100644 index 0000000..1746120 --- /dev/null +++ b/docs/plans/2026-06-29-002-feat-agent-wave1-quick-wins-plan.md @@ -0,0 +1,275 @@ +--- +title: "feat: Agent Wave 1 快速补强 (verify 回灌/prompt cache/schema 校验/delta_flush)" +type: feat +date: 2026-06-29 +origin: docs/brainstorms/2026-06-29-advanced-agent-gap-optimization-requirements.md +--- + +## Summary + +落地 brainstorm Wave 1 的 4 项自包含快速补强:G1 verify 失败回灌 ReAct、G2 prompt cache 三层结构跨 provider、G3 工具调用 schema 校验、G8 delta_flush_interval 调速。四项均作用于 ReAct 引擎层,同 plan 内 G2→G8 顺序执行(共享 `execute_stream` 改动),每项附最小自检测试。 + +--- + +## Problem Frame + +agentkit 对比 Qoder/Codex/Hermes/Trae 后发现 9 个真新缺口(已在生产/测试观察到痛点),brainstorm 分 3 波交付。本 plan 实现 Wave 1——自包含、低风险的 4 项快速补强,覆盖反馈稳定性(verify 不回灌、工具无 schema 校验)与响应效率(prompt cache 命中率低、token chunk 无节流)。 + +验证器已核对仓库现状(见 origin Sources & Research):`verification_loop.py:111-145` verify 失败仅调 `fix_callback` 不回灌 ReAct;`react.py:1042-1059` 记忆注入拼到 system prompt 末尾破坏 cache 前缀;`react.py:1118-1134` 每个 token chunk 都 yield 无节流;`tools/base.py:50-77` `safe_execute` 无 schema 校验直接 `await execute()`。 + +--- + +## Requirements + +### G1 Verify 失败回灌 ReAct (origin R1-R3) + +- R1. verify 失败时,系统自动把 errors 作为新 user 消息注入 conversation,继续 ReAct 循环,而非直接退出。 +- R2. 回灌后若二次 verify 仍失败,系统中断执行并返回错误给用户,附 verify log。 +- R3. 回灌最大重试次数可配置(默认 1 次),受 `max_steps` 上限约束。 + +### G2 Prompt Cache 三层结构 (origin R4-R7) + +- R4. system prompt 重构为双块结构:stable(技能配置/系统指令)+volatile(记忆检索+时间戳)。原 context(会话上下文)层由 conversation messages 承载,不进 system prompt。 +- R5. 记忆检索注入从 system prompt 末尾移到 volatile 层,stable 层保持不变。 +- R6. 跨 provider 统一 cache 策略:Anthropic 显式插入 `cache_control` 断点;OpenAI 等依赖自动前缀缓存。 +- R7. 多轮对话输入 token 成本降低(目标 ~50%)。 + +### G3 工具调用 Schema 校验 (origin R8-R10) + +- R8. `safe_execute` 调用 `execute()` 前,基于 `tool.input_schema` 校验参数(类型/必填)。 +- R9. 校验失败时返回类型化错误码(`tool_call_invalid`/`schema_mismatch`),不执行工具。 +- R10. 错误以 tool 角色消息回灌 conversation,给 LLM 自我修正机会。 + +### G8 delta_flush_interval 调速 (origin R11-R12) + +- R11. `execute_stream` 的 token chunk yield 加可配置节流(默认 `flush_interval_ms`)。 +- R12. 节流配置化,允许客户端调高降低渲染开销。 + +### Cross-cutting (origin R26-R27) + +- R13. 所有优化项配置化(`agentkit.yaml` 新增对应配置节)。 +- R14. 每个优化项附最小自检测试(ponytail 规则)。 + +--- + +## Key Technical Decisions + +**KTD1:G3 schema 校验放在 `Tool.safe_execute`(base.py),而非 `_execute_tool`(react.py)** + +校验在工具基类层,所有调用方(ReActEngine、ExpertTeam、StandaloneAgent)统一受益。以 `tool.input_schema`(JSON Schema dict)为契约源,`input_schema=None` 时跳过校验保持向后兼容。用 `jsonschema` 库(已是 Python 生态标准,无新依赖)。 + +**KTD2:G2 双块结构用 content blocks + cache_control 标记,Anthropic provider 需改 _convert_messages** + +system message 的 `content` 从字符串改为 content blocks 列表(`[{"type":"text","text":stable,"cache_control":{"type":"ephemeral"}},{"type":"text","text":volatile}]`)。**注意**:`src/agentkit/llm/providers/anthropic.py` 是 httpx 直连实现(非 LiteLLM),其 `_convert_messages`(`:102-197`)假设 system content 为字符串(`:116`),需修改以支持 list-type system content 并透传 `cache_control` blocks。OpenAI 等 provider 的 chat completions API 不支持 list-type system content,`_build_system_message` 需按 provider 能力检测:Anthropic 返回 blocks,其余返回字符串拼接(stable+volatile),依赖 stable 前缀命中自动前缀缓存。不改 gateway 方法签名。 + +**KTD3:G1 verify 回灌包进 ReAct 主循环,而非外层 wrapper** + +verify 当前在循环外 final answer 后运行(`react.py:887` execute / `:1603` execute_stream)。回灌改为:检测到 final answer(无 tool_calls)→ 运行 verify → 失败则把 errors 作为 user 消息 append 到 conversation → `continue` 主循环(LLM 自纠正)→ 二次 final answer 再 verify → 仍失败则 break 带 verify log。保留现有 `VerificationLoop` 类与 `verify_and_retry` 方法不动(向后兼容),回灌逻辑在 ReActEngine 内。 + +**KTD4:G8 delta_flush 用 time.monotonic 节流,非计数器** + +在 `execute_stream` chunk 循环内累积 chunks,按 `flush_interval_ms` 间隔批量 yield。`flush_interval_ms=0` 时退化为逐 chunk yield(向后兼容)。流结束 mid-interval 时最终 flush。用 `time.monotonic()`(不受系统时钟跳变影响)。 + +**KTD5:G2→G8 同 plan 内顺序执行,共享 execute_stream 改动** + +G2 改 system prompt 构造(循环前),G8 改 chunk yield 逻辑(循环内),两者不冲突但都触 `execute_stream`。G2 先落地确保 stable 前缀结构稳定,G8 再加节流避免在未稳定结构上叠加。 + +**KTD6:ServerConfig 到 ReActEngine 的接线用独立构造参数(带默认值)** + +`ReActEngine.__init__`(`react.py:154-198`)不接受 `ServerConfig` 对象,采用独立构造参数:`prompt_cache_enable: bool = True`、`flush_interval_ms: int = 0`、`max_reinjections: int = 1`(默认值保当前行为,向后兼容)。`ServerConfig` 在 agent 工厂/handler 层(`chat/handler.py` 等 ReActEngine 构造点)读取并传入。实现时需列出所有构造 ReActEngine 的调用点并更新。 + +--- + +## Implementation Units + +### U1. G3 工具调用 Schema 校验 + +**Goal:** 在 `Tool.safe_execute` 调用 `execute()` 前校验参数,失败返回类型化错误。 + +**Requirements:** R8, R9, R10, R14 + +**Dependencies:** 无(独立,奠基性——定义错误回灌模式) + +**Files:** +- 修改: `src/agentkit/tools/base.py`(`safe_execute` 加校验 + `ToolValidationError`) +- 修改: `src/agentkit/core/react.py`(`_execute_tool` `:1897-1916` 捕获 `ToolValidationError` 并 append tool 角色消息) +- 测试: `tests/unit/test_tool_schema_validation.py` + +**Approach:** +- 新增 `ToolValidationError(Exception)`,带 `error_code`(`tool_call_invalid`/`schema_mismatch`)与 `details`。 +- `safe_execute` 在 `before_execute` 后、`execute` 前:若 `self.input_schema` 非 None,用 `jsonschema.validate(kwargs, self.input_schema)`;校验失败抛 `ToolValidationError`。 +- `input_schema=None` → 跳过(向后兼容,旧工具无 schema)。 +- `_execute_tool`(react.py:1897-1916)在现有 `except Exception` **之前**加 `except ToolValidationError as e:` 优先捕获,返回 `{"error": str(e), "error_code": e.error_code, "details": e.details}`(保留类型化错误码,不被通用 except 平坦化为字符串)。现有调用方 `_build_tool_result_message` 把返回 dict 转为 tool 角色消息 append 到 conversation,给 LLM 自纠正机会。 + +**Patterns to follow:** `VerificationResult` 的类型化错误模式(`verification_loop.py:18-24`);`jsonschema` 标准用法。 + +**Test scenarios:** +- Covers R8. Happy path: tool 有 `input_schema={"type":"object","properties":{"count":{"type":"integer"}},"required":["count"]}`,传 `count=5` → 校验通过,execute 正常执行。 +- Covers R9. Edge: `input_schema=None` → 跳过校验,execute 正常(向后兼容)。 +- Covers R9. Error: 传 `count="abc"`(类型错)→ 抛 `ToolValidationError(error_code="tool_call_invalid")`,execute 不调用。 +- Covers R9. Error: 缺 `count`(必填)→ 抛 `ToolValidationError(error_code="schema_mismatch")`。 +- Covers R10. Integration: `_execute_tool` 捕获 `ToolValidationError` → conversation append tool 角色消息 → LLM 下一轮看到错误并修正参数 → 重试成功。 + +**Verification:** `python3 -m pytest tests/unit/test_tool_schema_validation.py -x -q` 通过;现有工具测试不回归。 + +--- + +### U2. G2 Prompt Cache 双块结构 + +**Goal:** system prompt 重构为 stable/volatile 双块结构,记忆注入移到 volatile,加 cache_control 断点。 + +**Requirements:** R4, R5, R6, R7, R13, R14 + +**Dependencies:** 无(独立,与 U1 不触同代码区) + +**Files:** +- 修改: `src/agentkit/core/react.py`(execute_stream `:1042-1059` 记忆注入 + system message 构造;execute 同路径若有,可选 — 见 Scope Boundaries) +- 修改: `src/agentkit/llm/providers/anthropic.py`(`_convert_messages` `:102-197` 支持 list-type system content,透传 cache_control blocks) +- 配置: `src/agentkit/config.py` 或 `ServerConfig`(`prompt_cache.enable: bool`) +- 测试: `tests/unit/test_prompt_cache_layers.py` + +**Approach:** +- 新增 `_build_system_message(base_prompt, memory_context, enable_cache, provider)` 工具方法:Anthropic provider 返回 content blocks 列表,stable 块在前(带 `cache_control: {"type":"ephemeral"}`),volatile 块(记忆+时间戳)在后;非 Anthropic provider 返回字符串拼接(stable+volatile),依赖 stable 前缀命中自动前缀缓存。 +- execute_stream `:1042-1059`:记忆注入从 `system_prompt += "## 参考信息"` 改为收集 `memory_context`,传给 `_build_system_message`。 +- conversation 的 system 消息 content:Anthropic 用 blocks 列表,其余用字符串;gateway `chat_stream` 经 `**kwargs` 透传。 +- `anthropic.py` 的 `_convert_messages`(`:102-197`)需修改:`:116` 从 `system_prompt = content` 改为支持 list-type content 直接透传(payload `["system"]` 接受字符串或 content blocks)。 +- 配置 `prompt_cache.enable: bool`(默认 True)。断点数硬编码为 1(stable 层),不暴露配置(YAGNI — 2 块结构下 >1 无语义)。 +- `enable_cache=False` 或 provider 非 Anthropic → 退化为字符串拼接(向后兼容)。 + +**Patterns to follow:** LiteLLM/Anthropic `cache_control` content block 规范;现有 `memory_retriever.get_context_string` 调用不变。 + +**Test scenarios:** +- Covers R4, R5. Happy path: 多轮对话,stable 层(技能配置)跨轮不变,volatile 层(记忆)随 query 变 → system message content blocks 结构正确,stable 在前 volatile 在后。 +- Covers R6. Integration: Anthropic provider 收到带 `cache_control` 的 content blocks → `_convert_messages` 透传 → cache 命中 stable 前缀。 +- Covers R6. Edge: OpenAI provider(provider != anthropic)→ `_build_system_message` 返回字符串拼接,不报错;stable 前缀命中自动前缀缓存。 +- Covers R5. Edge: `memory_retriever` 返回空 → 无 volatile 块,system message 仅 stable。 +- Covers R13. Config: `prompt_cache.enable=False` → 退化为字符串拼接,行为同改动前。 + +**Verification:** `python3 -m pytest tests/unit/test_prompt_cache_layers.py -x -q` 通过;多轮对话 system message 结构符合预期。 + +--- + +### U3. G8 delta_flush_interval 调速 + +**Goal:** execute_stream token chunk yield 加时间节流。 + +**Requirements:** R11, R12, R13, R14 + +**Dependencies:** U2(共享 execute_stream,G2 先改 system prompt 结构,G8 后改 yield 逻辑) + +**Files:** +- 修改: `src/agentkit/core/react.py`(`execute_stream` chunk 循环 `:1118-1134`;execute 同路径若有) +- 配置: `ServerConfig`(`streaming.flush_interval_ms`) +- 测试: `tests/unit/test_delta_flush.py` + +**Approach:** +- chunk 循环内:累积 `stream_content_chunks` 同时累积 `_flush_buffer`;用 `time.monotonic()` 记 `_last_flush_ts`。 +- 当 `now - _last_flush_ts >= flush_interval_ms/1000` 时:yield 合并后的 buffer,清空,更新 ts。 +- `flush_interval_ms=0` → 逐 chunk yield(向后兼容,当前行为)。 +- 流结束(for 循环退出)→ 最终 flush 剩余 buffer。 + +**Patterns to follow:** `time.monotonic()` 用法(已在 `:1080` `_stream_start` 使用);现有 `ReActEvent(event_type="token")` 结构不变。 + +**Test scenarios:** +- Covers R11. Happy path: `flush_interval_ms=50`,模拟连续 chunks → 按 50ms 间隔批量 yield,合并 content。 +- Covers R12. Config: `flush_interval_ms=0` → 逐 chunk yield(向后兼容)。 +- Edge: 流结束 mid-interval → 最终 flush 剩余 buffer,不丢 content。 +- Edge: 单个 chunk 后流结束 → 立即 flush。 +- Covers R14. Self-check: 断言 yield 的合并 content 等于原始 chunks 拼接(不丢字符)。 + +**Verification:** `python3 -m pytest tests/unit/test_delta_flush.py -x -q` 通过;token 流无字符丢失。 + +--- + +### U4. G1 Verify 失败回灌 ReAct + +**Goal:** verify 失败时把 errors 注入 conversation 继续 ReAct,二次失败中断带 log。 + +**Requirements:** R1, R2, R3, R13, R14 + +**Dependencies:** U1(复用错误回灌模式;U1 的 `ToolValidationError` 回灌与 G1 verify 回灌同模式) + +**Files:** +- 修改: `src/agentkit/core/react.py`(`execute` `:886-907` verify 块;`execute_stream` `:1601-1629` verify 块) +- 配置: `ServerConfig`(`verification.max_reinjections: int = 1`) +- 测试: `tests/unit/test_verify_reinjection.py` + +**Approach:** +- 把 verify 从"循环后一次性运行"改为"final answer 检测点 + 回灌重试"。 +- 主循环内检测到 final answer(无 tool_calls)时:`if self._verification_enabled` → 运行 verify。 +- verify 通过 → `break`,正常结束。 +- verify 失败且 `reinjections < max_reinjections`:append `{"role":"user","content":f"验证失败,错误如下:\n{vresult.errors}"}` 到 conversation,`continue` 主循环(LLM 见 errors 自纠正)。 +- verify 失败且 `reinjections >= max`:记录 verify log 到 trajectory,`break` 返回失败结果。 +- 保留现有 `VerificationLoop` 类与 `verify_and_retry` 不动(向后兼容,外部调用方仍可用)。 + +**Patterns to follow:** 现有 verify 块的 trajectory/event 记录模式;U1 的 `ToolValidationError` 回灌模式。 + +**Execution note:** 先加 characterization 测试覆盖现有 verify 行为(失败仅记录),再改实现确保不回归。 + +**Test scenarios:** +- Covers R1. Happy path: verify 首次失败 → errors 注入 conversation → LLM 自纠正 → 二次 verify 通过 → 任务完成。 +- Covers R2. Error: verify 二次失败 → 中断,返回错误附 verify log(测试输出 + errors 列表)。 +- Covers R3. Config: `max_reinjections=0` → 等价于不回灌(当前行为),verify 失败直接退出。 +- Covers R3. Edge: 回灌期间达到 `max_steps` → 中断(不无限循环)。 +- Covers R1. Integration: 回灌的 user 消息出现在 conversation,LLM 下一轮 input 含 errors 文本。 +- Covers R14. Self-check: `max_reinjections` 默认值为 1。 + +**Verification:** `python3 -m pytest tests/unit/test_verify_reinjection.py -x -q` 通过;`pytest tests/unit/ -x -q` 全量不回归。 + +--- + +## Scope Boundaries + +### Deferred for later + +- Wave 2(G4 辅助 LLM 分流 / G7 三级降级链 / G9 原子化 rollback)——见 origin Wave 2 section,单独 plan。 +- Wave 3(G5 函数级代码分片 / G6 SOLO 状态机)——见 origin Wave 3 section,单独 plan。 +- G7 Emergency 层规则模板、G5 tree-sitter 集成方式——origin Deferred to Planning。 + +### Deferred to Follow-Up Work + +- `execute()`(非流式)的 G2/G8 改动:本 plan 优先 `execute_stream`(WebSocket 主路径),`execute()` 同步改动可顺带做但非必须,若拆 PR 则归 follow-up。 +- prompt cache 命中率指标化(R7 的 ~50% 目标):本 plan 落结构,埋点量化归 follow-up。 + +### Outside this product's identity + +- 重写编排逻辑/拓扑排序/Board 辩论——继承自 2026-06-24-004。 +- 节点级 checkpoint——继承自 2026-06-24-004 KTD3。 +- 全盘迁移 LangGraph——继承自 2026-06-24-004。 + +--- + +## Risks & Dependencies + +- **风险:G2 content blocks 改动破坏现有 message 序列化。** gateway/messages 假设 `content: str`,改为 blocks 列表需核对所有序列化路径(WebSocket、日志、trace)。缓解:仅 system 消息用 blocks,user/assistant 保持 str;`enable_cache=False` 退化路径保底。 +- **风险:G1 回灌增加 token 消耗。** 二次 verify 循环会多跑一轮 LLM。缓解:`max_reinjections` 默认 1,受 `max_steps` 上限约束;KTD3 设计为循环内 continue 而非递归,无栈溢出风险。 +- **依赖:U3 依赖 U2 完成。** 两者共享 `execute_stream`,G2 先改 system prompt 结构稳定后再加 G8 节流(KTD5)。 +- **依赖:G2 需修改 `anthropic.py` 的 `_convert_messages` 支持 list-type system content。** `anthropic.py` 是 httpx 直连实现(非 LiteLLM),需手动改 `:116` 从 `system_prompt = content` 改为支持 content blocks 透传。非 Anthropic provider 走字符串拼接退化路径,不报错。 +- **依赖:G3 的 `jsonschema` 库。** `jsonschema>=4.0` 已在 `pyproject.toml` 核心依赖中(line 22),无需新增依赖。 + +--- + +## Acceptance Examples + +- **AE1**(Covers R1, R2, R3):ReAct 任务 final answer 后 verify 失败 → errors 注入 conversation 继续 ReAct → LLM 修正后二次 verify 通过 → 完成。若二次失败 → 中断返回错误附 verify log。 +- **AE2**(Covers R4, R5, R6, R7):50 轮长对话 → stable 层不变,volatile 随 query 变 → Anthropic cache 命中 stable 前缀,OpenAI 命中自动前缀缓存 → 输入 token 降低。 +- **AE3**(Covers R8, R9, R10):LLM 传错参数类型 → schema 校验失败 → `tool_call_invalid` → 错误回灌 conversation → LLM 修正后重试成功。 +- **AE4**(Covers R11, R12):弱网客户端 → token chunk 按 50ms 批量 yield → 前端渲染无卡顿,无字符丢失。 + +--- + +## Sources & Research + +- **Origin:** `docs/brainstorms/2026-06-29-advanced-agent-gap-optimization-requirements.md` — Wave 1 需求文档(KTD1-KTD7、R1-R27) +- **上游 plan:** `docs/plans/2026-06-24-004-feat-long-horizon-reliability-optimization-plan.md` — U1-U7 长程可靠性护栏(本 plan 增补) +- **Learnings:** `docs/solutions/logic-errors/long-horizon-reliability-code-review-fixes.md` — 14 个 finding 教训(新字段默认值保契约、跨模块契约显式化、清理方法接入生命周期) +- **代码 grounding(验证器核对):** + - `src/agentkit/core/verification_loop.py:111-145` — `verify_and_retry` 接 `fix_callback`,不回灌 ReAct + - `src/agentkit/core/react.py:1042-1059` — 记忆注入拼到 system prompt 末尾 + - `src/agentkit/core/react.py:1118-1134` — chunk 循环逐 token yield 无节流 + - `src/agentkit/core/react.py:886-907, 1601-1629` — verify 在循环后运行,失败仅记录 trajectory + - `src/agentkit/core/react.py:1897-1916` — `_execute_tool` 无 schema 校验 + - `src/agentkit/tools/base.py:21,28,50-77` — `Tool.input_schema`(JSON Schema dict,可选),`safe_execute` 无校验 + - `src/agentkit/llm/gateway.py:268-281` — `chat_stream(**kwargs)` 可透传 cache_control + - `src/agentkit/llm/providers/anthropic.py:316` — Anthropic provider chat_stream +- **外部研究(brainstorm 阶段):** Qoder(Spec→Verify 闭环、delta_flush_interval)、Hermes(cache_control system_and_3、validate_function_call_schema)、Codex(prompt caching 前缀匹配) diff --git a/scripts/dev-start.sh b/scripts/dev-start.sh new file mode 100755 index 0000000..1c11e45 --- /dev/null +++ b/scripts/dev-start.sh @@ -0,0 +1,509 @@ +#!/usr/bin/env bash +# ============================================================================= +# Fischer AgentKit — 本地开发环境一键启动 +# ============================================================================= +# +# 启动:后端服务(+ 可选 Tauri 桌面客户端) +# 停止:Ctrl+C 或运行 scripts/dev-stop.sh +# +# 用法: +# bash scripts/dev-start.sh # Web 模式(agentkit gui) +# bash scripts/dev-start.sh --tauri # Web + Tauri 桌面客户端 +# bash scripts/dev-start.sh --serve # 仅后端 API +# bash scripts/dev-start.sh --init # 首次运行(初始化 DB) +# bash scripts/dev-start.sh --help # 帮助 +# +# 依赖: +# Python >= 3.11, Node.js >= 18, Redis, PostgreSQL (均自动检查) +# --tauri 需要:Rust 工具链(rustup / brew install rust) +# +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_ROOT" + +# ── 颜色 ──────────────────────────────────────────────────────────────────── + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +# ── 帮助 ──────────────────────────────────────────────────────────────────── + +show_help() { + cat <<-'EOF' +Fischer AgentKit — 本地开发环境启动 + +用法: bash scripts/dev-start.sh [选项] + +选项: + --tauri 启动 Tauri 桌面客户端(Vite 热重载 + 原生窗口) + --serve 仅启动后端 API(无前端) + --init 首次运行,初始化数据库和测试用户 + --help 显示此帮助 + +模式说明: + 默认 Web 模式:agentkit gui (前后端 + 内置静态服务) + --tauri Tauri 模式:后端 API + Vite (:5173) + Tauri 桌面窗口 + +端口映射: + 8000 — 后端 API + 8002 — Web GUI / 前端静态服务 + 5173 — Vite 开发服务器(--tauri 模式) +EOF +} + +# ── 参数解析 ──────────────────────────────────────────────────────────────── + +MODE="gui" +INIT_DB=0 + +while [[ $# -gt 0 ]]; do + case $1 in + --tauri) MODE="tauri"; shift ;; + --serve) MODE="serve"; shift ;; + --init) INIT_DB=1; shift ;; + --help|-h) show_help; exit 0 ;; + *) shift ;; + esac +done + +# ── 日志函数 ──────────────────────────────────────────────────────────────── + +ok() { echo -e " ${GREEN}✓${NC} $*"; } +info() { echo -e " ${CYAN}→${NC} $*"; } +warn() { echo -e " ${YELLOW}!${NC} $*"; } +fail() { echo -e " ${RED}✗${NC} $*" >&2; } + +section() { + echo "" + echo -e "${CYAN}────────────────────────────────────────${NC}" + echo -e "${CYAN} $*${NC}" + echo -e "${CYAN}────────────────────────────────────────${NC}" +} + +# ── 进度状态机 ────────────────────────────────────────────────────────────── + +# 0=未开始, 1=进行中, 2=成功, 3=失败 +S_DEPS=0 S_ENV=0 S_REDIS=0 S_PG=0 S_BACKEND=0 S_FRONTEND=0 S_TAURI=0 + +set_status() { + local step=$1 val=$2 + case $step in + deps) S_DEPS=$val ;; + env) S_ENV=$val ;; + redis) S_REDIS=$val ;; + pg) S_PG=$val ;; + backend) S_BACKEND=$val ;; + frontend) S_FRONTEND=$val ;; + tauri) S_TAURI=$val ;; + esac +} + +print_status() { + echo "" + echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" + echo -e "${CYAN} 启动状态总览${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" + echo -e "$([[ $S_DEPS -eq 2 ]] && echo " ${GREEN}✓${NC}" || [[ $S_DEPS -eq 3 ]] && echo " ${RED}✗${NC}" || echo " ${YELLOW}○${NC}") 依赖检查" + echo -e "$([[ $S_ENV -eq 2 ]] && echo " ${GREEN}✓${NC}" || [[ $S_ENV -eq 3 ]] && echo " ${RED}✗${NC}" || echo " ${YELLOW}○${NC}") 环境配置" + echo -e "$([[ $S_REDIS -eq 2 ]] && echo " ${GREEN}✓${NC}" || [[ $S_REDIS -eq 3 ]] && echo " ${RED}✗${NC}" || echo " ${YELLOW}○${NC}") Redis" + echo -e "$([[ $S_PG -eq 2 ]] && echo " ${GREEN}✓${NC}" || [[ $S_PG -eq 3 ]] && echo " ${RED}✗${NC}" || echo " ${YELLOW}○${NC}") PostgreSQL" + echo -e "$([[ $S_BACKEND -eq 2 ]] && echo " ${GREEN}✓${NC}" || [[ $S_BACKEND -eq 3 ]] && echo " ${RED}✗${NC}" || echo " ${YELLOW}○${NC}") 后端服务 (:8000)" + if [[ $MODE == "gui" || $MODE == "tauri" ]]; then + echo -e "$([[ $S_FRONTEND -eq 2 ]] && echo " ${GREEN}✓${NC}" || [[ $S_FRONTEND -eq 3 ]] && echo " ${RED}✗${NC}" || echo " ${YELLOW}○${NC}") 前端服务 (:8002)" + fi + if [[ $MODE == "tauri" ]]; then + echo -e "$([[ $S_TAURI -eq 2 ]] && echo " ${GREEN}✓${NC}" || [[ $S_TAURI -eq 3 ]] && echo " ${RED}✗${NC}" || echo " ${YELLOW}○${NC}") Tauri 客户端" + fi +} + +# ── 前置检查 ──────────────────────────────────────────────────────────────── + +check_deps() { + section "检查依赖" + set_status deps 1 + + for cmd in python3 node npm; do + if ! command -v "$cmd" &>/dev/null; then + fail "缺少依赖: $cmd" + set_status deps 3 + return 1 + fi + done + ok "Python $(python3 --version 2>&1 | awk '{print $2}')" + ok "Node $(node --version 2>&1)" + ok "npm $(npm --version 2>&1)" + + # Python 版本 >= 3.11 + if ! python3 -c 'import sys; sys.exit(0 if (sys.version_info.major, sys.version_info.minor) >= (3, 11) else 1)'; then + fail "Python 版本需 >= 3.11" + set_status deps 3 + return 1 + fi + + # Tauri 模式需要 Rust + if [[ $MODE == "tauri" ]]; then + if ! command -v rustc &>/dev/null; then + warn "未检测到 Rust(Tauri 需要):请运行 brew install rust 或 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" + else + ok "Rust $(rustc --version 2>&1 | awk '{print $2}')" + fi + fi + + set_status deps 2 +} + +check_env() { + section "检查环境配置" + set_status env 1 + + if [[ ! -f .env ]]; then + warn "未找到 .env,将使用环境变量默认值" + if [[ -f .env.example ]]; then + cp .env.example .env + ok "已从 .env.example 生成 .env" + fi + else + ok ".env 存在" + fi + + set_status env 2 +} + +check_redis() { + section "检查 Redis" + set_status redis 1 + + if command -v redis-cli &>/dev/null && redis-cli ping 2>/dev/null | grep -q PONG; then + ok "Redis 运行中" + set_status redis 2 + return 0 + fi + + # Docker 方式 + local name="fischer-redis-dev" + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then + ok "Redis 容器运行中" + set_status redis 2 + return 0 + fi + + warn "Redis 未运行,尝试启动 Docker 容器..." + if docker run -d --name "$name" -p 6379:6379 redis:7-alpine &>/dev/null; then + sleep 2 + if docker exec "$name" redis-cli ping 2>/dev/null | grep -q PONG; then + ok "Redis Docker 容器启动成功 (:6379)" + set_status redis 2 + return 0 + fi + fi + fail "Redis 启动失败(请确保 Docker 运行中,或手动启动 Redis)" + set_status redis 3 + return 1 +} + +check_postgres() { + section "检查 PostgreSQL" + set_status pg 1 + + if lsof -i :5432 2>/dev/null | grep -q LISTEN; then + ok "PostgreSQL 已在 :5432 监听" + set_status pg 2 + return 0 + fi + + # Docker 方式 + local name="fischer-pg-dev" + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then + ok "PostgreSQL Docker 容器运行中" + set_status pg 2 + return 0 + fi + + warn "PostgreSQL 未在 :5432 运行,尝试启动 Docker 容器..." + if docker run -d --name "$name" \ + -p 5432:5432 \ + -e POSTGRES_USER=agentkit \ + -e POSTGRES_PASSWORD=agentkit \ + -e POSTGRES_DB=agentkit \ + pgvector/pgvector:pg15 &>/dev/null; then + sleep 3 + if docker exec "$name" pg_isready -U agentkit &>/dev/null; then + ok "PostgreSQL Docker 容器启动成功 (:5432)" + set_status pg 2 + return 0 + fi + fi + warn "PostgreSQL 启动失败(bitable 等功能可能受限,继续启动后端...)" + set_status pg 2 # 不阻塞,继续 + return 0 +} + +# ── 安装依赖 ──────────────────────────────────────────────────────────────── + +install_deps() { + section "安装依赖" + info "后端 Python 依赖..." + + # 虚拟环境 + if [[ ! -d .venv ]]; then + python3 -m venv .venv + ok "虚拟环境 .venv 创建完成" + fi + + source .venv/bin/activate + pip install -q -U pip + pip install -e ".[dev]" -q + ok "后端依赖安装完成" + + # 前端 npm 依赖 + local FE_DIR="$PROJECT_ROOT/src/agentkit/server/frontend" + if [[ ! -d "$FE_DIR/node_modules" ]]; then + info "前端 npm 依赖..." + cd "$FE_DIR" + npm install + cd "$PROJECT_ROOT" + ok "前端依赖安装完成" + else + ok "前端 node_modules 已存在" + fi +} + +# ── 启动后端 ──────────────────────────────────────────────────────────────── + +start_backend() { + section "启动后端服务" + set_status backend 1 + + info "启动后端 API (:8000)..." + source .venv/bin/activate + agentkit serve --port 8000 & + BACKEND_PID=$! + + # 等待健康检查就绪(最多 60 秒) + info "等待后端就绪..." + local attempt=0 + while [[ $attempt -lt 60 ]]; do + if curl -sf http://127.0.0.1:8000/api/v1/health &>/dev/null; then + ok "后端 API 就绪 (http://127.0.0.1:8000, PID $BACKEND_PID)" + set_status backend 2 + return 0 + fi + # 检查进程是否还活着 + if ! kill -0 $BACKEND_PID 2>/dev/null; then + fail "后端进程意外退出" + set_status backend 3 + return 1 + fi + sleep 1 + ((attempt++)) + [[ $((attempt % 10)) -eq 0 ]] && info "等待中... (${attempt}s)" + done + + fail "后端启动超时(60s 内未响应健康检查)" + kill $BACKEND_PID 2>/dev/null || true + set_status backend 3 + return 1 +} + +# ── 启动 Web GUI ──────────────────────────────────────────────────────────── + +start_gui() { + section "启动 Web GUI" + set_status frontend 1 + + info "启动 Web GUI (:8002)..." + source .venv/bin/activate + agentkit gui --port 8002 & + GUI_PID=$! + + # 等待就绪 + info "等待 Web GUI 就绪..." + local attempt=0 + while [[ $attempt -lt 60 ]]; do + if curl -sf http://127.0.0.1:8002/api/v1/health &>/dev/null; then + ok "Web GUI 就绪 (http://127.0.0.1:8002, PID $GUI_PID)" + set_status frontend 2 + return 0 + fi + if ! kill -0 $GUI_PID 2>/dev/null; then + fail "Web GUI 进程意外退出" + set_status frontend 3 + return 1 + fi + sleep 1 + ((attempt++)) + [[ $((attempt % 10)) -eq 0 ]] && info "等待中... (${attempt}s)" + done + + fail "Web GUI 启动超时" + kill $GUI_PID 2>/dev/null || true + set_status frontend 3 + return 1 +} + +# ── 启动 Tauri ───────────────────────────────────────────────────────────── + +start_tauri() { + section "启动 Tauri 桌面客户端" + set_status tauri 1 + + local FE_DIR="$PROJECT_ROOT/src/agentkit/server/frontend" + + if ! command -v tauri &>/dev/null; then + warn "Tauri CLI 未安装,跳过桌面客户端" + info "安装方式:npm install -g @tauri-apps/cli" + set_status tauri 2 + return 0 + fi + + info "启动 Tauri 桌面客户端..." + info " (Vite → :5173, 后端 API → :8000)" + cd "$FE_DIR" + npm run tauri dev & + TAURI_PID=$! + + info "Tauri 启动中(首次运行需要编译 Rust,可能需要几分钟)..." + ok "Tauri 进程已启动 (PID $TAURI_PID)" + info "桌面窗口将自动打开,如未打开请手动查看终端输出" + + set_status tauri 2 + cd "$PROJECT_ROOT" +} + +# ── 数据库初始化 ──────────────────────────────────────────────────────────── + +init_db() { + section "初始化数据库" + info "创建测试用户..." + + local SETUP_SCRIPT="$PROJECT_ROOT/src/agentkit/server/frontend/e2e/setup-test-user.py" + if [[ -f "$SETUP_SCRIPT" ]]; then + source .venv/bin/activate + if python3 "$SETUP_SCRIPT" 2>/dev/null; then + ok "测试用户创建完成" + else + warn "测试用户创建失败(可能已存在或数据库未就绪)" + fi + fi +} + +# ── 停止服务 ──────────────────────────────────────────────────────────────── + +stop_services() { + echo "" + info "正在停止所有服务..." + + for port in 8000 8001 8002 5173; do + local pid=$(lsof -ti :$port 2>/dev/null || true) + if [[ -n "$pid" ]]; then + kill $pid 2>/dev/null && ok "端口 $port 已停止" || true + fi + done + + # Vite 进程 + pkill -f "vite" 2>/dev/null && ok "Vite 进程已停止" || true + + echo "" + ok "所有服务已停止。感谢使用!" +} + +# ── 主流程 ───────────────────────────────────────────────────────────────── + +echo "" +echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" +echo -e "${CYAN} Fischer AgentKit — 本地开发环境启动${NC}" +echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" +echo "" + +# 检查依赖 +if ! check_deps; then + echo "" + fail "依赖检查失败,请先安装缺失的依赖" + exit 1 +fi + +check_env + +# 初始化数据库 +[[ $INIT_DB -eq 1 ]] && init_db + +# 安装依赖 +install_deps + +# 检查中间件 +check_redis || true +check_postgres || true + +echo "" + +# ── 启动服务 ──────────────────────────────────────────────────────────────── + +FAILED=0 + +case $MODE in + serve) + if ! start_backend; then FAILED=1; fi + ;; + gui) + if ! start_backend; then FAILED=1; fi + if [[ $FAILED -eq 0 ]] && ! start_gui; then FAILED=1; fi + ;; + tauri) + if ! start_backend; then FAILED=1; fi + if [[ $FAILED -eq 0 ]]; then + start_tauri + fi + ;; +esac + +# ── 状态总览 + 启动完成 ────────────────────────────────────────────────────── + +print_status + +echo "" +echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" +if [[ $FAILED -eq 0 ]]; then + echo -e "${GREEN} 所有服务启动成功!${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" + echo "" + if [[ $MODE == "gui" ]]; then + echo -e " Web GUI: ${GREEN}http://localhost:8002${NC}" + echo " (在浏览器中打开,或直接在 http://localhost:8002 访问)" + elif [[ $MODE == "tauri" ]]; then + echo -e " 后端 API: ${GREEN}http://localhost:8000${NC}" + echo -e " Vite 热重载: ${GREEN}http://localhost:5173${NC}" + echo " Tauri 桌面窗口应已自动打开" + elif [[ $MODE == "serve" ]]; then + echo -e " 后端 API: ${GREEN}http://localhost:8000${NC}" + fi + echo "" + echo -e " ${YELLOW}按 Ctrl+C 停止所有服务${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" +else + echo -e "${RED} 服务启动失败,请查看上方错误信息${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" + echo "" + echo -e " 诊断命令:" + echo -e " 查看日志: ${CYAN}curl http://127.0.0.1:8000/api/v1/health${NC}" + echo -e " 停止服务: ${CYAN}bash scripts/dev-stop.sh${NC}" +fi +echo "" + +# 注册退出钩子 +trap stop_services EXIT INT TERM + +# 保持脚本运行 +if [[ $MODE == "tauri" ]]; then + # Tauri 模式:等待 Tauri 进程 + wait +elif [[ $MODE == "gui" || $MODE == "serve" ]]; then + # 等待后端进程 + wait $BACKEND_PID 2>/dev/null || true +fi diff --git a/scripts/dev-stop.sh b/scripts/dev-stop.sh new file mode 100755 index 0000000..4dbda15 --- /dev/null +++ b/scripts/dev-stop.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# ============================================================================= +# Fischer AgentKit — 本地开发环境一键停止脚本 +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_ROOT" + +echo "停止 Fischer AgentKit 开发服务..." + +# 停止后端/API/GUI 进程(按端口) +for port in 8000 8001 8002 5173; do + PID=$(lsof -ti :$port 2>/dev/null || true) + if [[ -n "$PID" ]]; then + kill $PID 2>/dev/null && echo " [OK] 端口 $port 已停止 (PID $PID)" || true + fi +done + +# 停止 Vite/Node 进程 +pkill -f "vite" 2>/dev/null && echo " [OK] Vite 进程已停止" || true + +# 停止 Docker 容器(开发专用) +for container in fischer-redis-dev fischer-pg-dev; do + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${container}$"; then + docker stop "$container" 2>/dev/null && echo " [OK] 容器 $container 已停止" || true + fi +done + +echo "停止完成。" diff --git a/scripts/prod-start.sh b/scripts/prod-start.sh new file mode 100755 index 0000000..d262f0d --- /dev/null +++ b/scripts/prod-start.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# ============================================================================= +# Fischer AgentKit — 生产环境一键启动脚本 +# ============================================================================= +# +# 启动:Docker Compose(agentkit + Redis + PostgreSQL) +# 要求:.env 文件存在(含 LLM API Key、JWT Secret) +# +# 用法: +# bash scripts/prod-start.sh # 启动所有服务 +# bash scripts/prod-start.sh --init # 首次启动(初始化 DB) +# bash scripts/prod-start.sh --build # 构建镜像后启动 +# bash scripts/prod-start.sh --help # 帮助 +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_ROOT" + +: "${COMPOSE_FILE:=docker-compose.yaml}" + +# ── 颜色 ──────────────────────────────────────────────────────────────────── + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +# ── 帮助 ──────────────────────────────────────────────────────────────────── + +show_help() { + cat <<-'EOF' +Fischer AgentKit — 生产环境启动 + +用法: bash scripts/prod-start.sh [选项] + +选项: + --init 首次启动,初始化数据库表 + --build 先构建/拉取镜像,再启动 + --pull 仅拉取最新镜像(不启动) + --help 显示此帮助 + +Docker Compose 文件: docker-compose.yaml + +服务端口: + 8001 — AgentKit API 服务 + 6379 — Redis + 5432 — PostgreSQL + +访问: http://localhost:8001 +EOF +} + +# ── 参数解析 ──────────────────────────────────────────────────────────────── + +INIT_DB=0 +BUILD=0 +PULL_ONLY=0 + +while [[ $# -gt 0 ]]; do + case $1 in + --init) INIT_DB=1; shift ;; + --build) BUILD=1; shift ;; + --pull) PULL_ONLY=1; shift ;; + --help|-h) show_help; exit 0 ;; + *) shift ;; + esac +done + +# ── 日志函数 ──────────────────────────────────────────────────────────────── + +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +fail() { echo -e "${RED}[FAIL]${NC} $*"; } + +# ── 前置检查 ──────────────────────────────────────────────────────────────── + +check_deps() { + info "检查依赖..." + for cmd in docker; do + if ! command -v "$cmd" &>/dev/null; then + fail "缺少依赖: $cmd" + exit 1 + fi + done + # docker compose v2+ (no separate docker-compose binary needed) + if ! command -v docker-compose &>/dev/null && docker compose version &>/dev/null; then + ok "docker compose 可用(v2+)" + elif command -v docker-compose &>/dev/null; then + ok "docker-compose 可用" + else + fail "缺少 docker 或 docker compose v2" + exit 1 + fi + ok "依赖检查通过" +} + +check_env() { + info "检查环境配置..." + if [[ ! -f .env ]]; then + fail "未找到 .env 文件" + echo " 请先创建 .env 文件(参考 .env.example)" + exit 1 + fi + # 检查关键变量 + if ! grep -q "DASHSCOPE_API_KEY\|DEEPSEEK_API_KEY" .env 2>/dev/null; then + warn ".env 中可能缺少 LLM API Key" + fi + ok ".env 存在" +} + +# ── 数据库初始化 ──────────────────────────────────────────────────────────── + +init_db() { + info "初始化数据库..." + docker compose -f "$COMPOSE_FILE" exec -T postgres \ + psql -U agentkit -d agentkit -c "SELECT 1" &>/dev/null || { + fail "PostgreSQL 容器未就绪,等待后重试..." + sleep 5 + } + + # 等待 PostgreSQL 完全就绪 + local attempt=0 + while [[ $attempt -lt 30 ]]; do + if docker compose -f "$COMPOSE_FILE" exec -T postgres \ + pg_isready -U agentkit &>/dev/null; then + break + fi + sleep 1 + ((attempt++)) + done + + ok "数据库就绪" +} + +# ── 主流程 ───────────────────────────────────────────────────────────────── + +echo "" +echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" +echo -e "${CYAN} Fischer AgentKit — 生产环境启动${NC}" +echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" +echo "" + +check_deps +check_env + +if [[ $PULL_ONLY -eq 1 ]]; then + info "拉取最新镜像..." + docker compose -f "$COMPOSE_FILE" pull + ok "镜像拉取完成" + exit 0 +fi + +echo "" +echo -e "${GREEN}─────────────────────────────────────────────────${NC}" +echo -e "${GREEN} 启动 Docker 服务...${NC}" +echo -e "${GREEN}─────────────────────────────────────────────────${NC}" +echo "" + +if [[ $BUILD -eq 1 ]]; then + info "构建/拉取镜像..." + docker compose -f "$COMPOSE_FILE" build --pull + ok "镜像构建完成" +fi + +# 创建网络(如不存在) +docker network ls 2>/dev/null | grep -q "fischer-agentkit_default" || \ + docker network create fischer-agentkit_default &>/dev/null || true + +info "启动服务(docker compose up -d)..." +docker compose -f "$COMPOSE_FILE" up -d --remove-orphans + +# 等待健康检查 +info "等待服务就绪(健康检查最多 120s)..." +local attempt=0 +while [[ $attempt -lt 60 ]]; do + if curl -sf http://127.0.0.1:8001/api/v1/health &>/dev/null; then + break + fi + sleep 2 + ((attempt++)) + echo -n "." +done +echo "" + +# 显示状态 +echo "" +echo -e "${GREEN}─────────────────────────────────────────────────${NC}" +echo -e "${GREEN} Docker 服务状态${NC}" +echo -e "${GREEN}─────────────────────────────────────────────────${NC}" +docker compose -f "$COMPOSE_FILE" ps + +# 状态检查 +if curl -sf http://127.0.0.1:8001/api/v1/health &>/dev/null; then + echo "" + ok "AgentKit API 就绪 -> http://127.0.0.1:8001" + echo "" + echo -e "${GREEN}═══════════════════════════════════════════════════${NC}" + echo -e "${GREEN} 生产服务全部启动成功!${NC}" + echo -e "${GREEN}─────────────────────────────────────────────────${NC}" + echo -e " AgentKit API: ${GREEN}http://localhost:8001${NC}" + echo -e " Redis: ${GREEN}localhost:6379${NC}" + echo -e " PostgreSQL: ${GREEN}localhost:5432${NC}" + echo -e "${GREEN}─────────────────────────────────────────────────${NC}" + echo -e " 查看日志: ${CYAN}docker compose -f $COMPOSE_FILE logs -f${NC}" + echo -e " 停止服务: ${CYAN}bash scripts/prod-stop.sh${NC}" + echo -e "${GREEN}═══════════════════════════════════════════════════${NC}" +else + echo "" + fail "AgentKit API 启动超时,查看日志诊断:" + echo -e " ${CYAN}docker compose -f $COMPOSE_FILE logs --tail=50${NC}" + exit 1 +fi diff --git a/scripts/prod-stop.sh b/scripts/prod-stop.sh new file mode 100755 index 0000000..277dc86 --- /dev/null +++ b/scripts/prod-stop.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# ============================================================================= +# Fischer AgentKit — 生产环境一键停止脚本 +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_ROOT" + +COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yaml}" + +echo "停止 Fischer AgentKit 生产服务..." + +docker compose -f "$COMPOSE_FILE" down 2>/dev/null && echo " [OK] 容器已停止并移除" || echo " [OK] 无运行中的容器" + +echo "" +echo "注意:数据卷(Redis/PostgreSQL 数据)已保留。" +echo "如需清除所有数据运行:docker volume rm fischer-agentkit_*" +echo "停止完成。" diff --git a/scripts/test-start.sh b/scripts/test-start.sh new file mode 100755 index 0000000..77c724c --- /dev/null +++ b/scripts/test-start.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# ============================================================================= +# Fischer AgentKit — 一键测试环境脚本 +# ============================================================================= +# +# 启动完整测试环境:后端 API + E2E 测试(使用独立端口,不影响开发环境) +# +# 用法: +# bash scripts/test-start.sh # 启动测试环境并运行全部测试 +# bash scripts/test-start.sh --unit # 仅单元测试 +# bash scripts/test-start.sh --e2e # 仅 E2E 测试 +# bash scripts/test-start.sh --bitable # 仅 bitable 模块测试 +# bash scripts/test-start.sh --skip-server # 跳过服务启动(使用已有服务) +# bash scripts/test-start.sh --help # 帮助 +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_ROOT" + +: "${E2E_PORT:=18765}" +: "${E2E_API_KEY:=ak_live_e2e_test_key_000000000000000000000000000000000000000000000000}" + +# ── 颜色 ──────────────────────────────────────────────────────────────────── + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +# ── 帮助 ──────────────────────────────────────────────────────────────────── + +show_help() { + cat <<-'EOF' +Fischer AgentKit — 测试环境 + +用法: bash scripts/test-start.sh [选项] + +选项: + --unit 仅运行单元测试(pytest -m "not integration") + --e2e 仅运行 E2E 测试 + --bitable 仅运行 bitable 模块测试 + --skip-server 跳过服务启动(使用已有服务) + --help 显示帮助 + +环境变量: + E2E_PORT 测试服务器端口(默认: 18765) + E2E_API_KEY 测试 API Key(默认: 内置测试 Key) +EOF +} + +# ── 参数解析 ──────────────────────────────────────────────────────────────── + +RUN_MODE="all" +SKIP_SERVER=0 + +while [[ $# -gt 0 ]]; do + case $1 in + --unit) RUN_MODE="unit"; shift ;; + --e2e) RUN_MODE="e2e"; shift ;; + --bitable) RUN_MODE="bitable"; shift ;; + --skip-server) SKIP_SERVER=1; shift ;; + --help|-h) show_help; exit 0 ;; + *) shift ;; + esac +done + +# ── 日志函数 ──────────────────────────────────────────────────────────────── + +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +fail() { echo -e "${RED}[FAIL]${NC} $*"; } + +# ── 服务管理 ──────────────────────────────────────────────────────────────── + +start_test_server() { + if [[ $SKIP_SERVER -eq 1 ]]; then + info "SKIP_SERVER=1,跳过服务启动" + return 0 + fi + + info "启动测试服务器(端口 $E2E_PORT)..." + if lsof -i :$E2E_PORT 2>/dev/null | grep -q LISTEN; then + ok "测试服务器已在端口 $E2E_PORT 运行" + return 0 + fi + + export AGENTKIT_E2E_MODE=1 + export AGENTKIT_WS_TIMEOUT=0 + export AGENTKIT_API_KEY="$E2E_API_KEY" + + source .venv/bin/activate 2>/dev/null || true + python3 -m agentkit.cli.main serve --host 127.0.0.1 --port "$E2E_PORT" & + SERVER_PID=$! + + # 等待就绪 + local attempt=0 + while [[ $attempt -lt 60 ]]; do + if curl -sf "http://127.0.0.1:$E2E_PORT/api/v1/health" &>/dev/null; then + ok "测试服务器就绪 (PID $SERVER_PID,端口 $E2E_PORT)" + return 0 + fi + sleep 0.5 + ((attempt++)) + done + + fail "测试服务器启动超时" + kill $SERVER_PID 2>/dev/null || true + exit 1 +} + +stop_test_server() { + if [[ $SKIP_SERVER -eq 1 ]]; then return 0; fi + PID=$(lsof -ti :$E2E_PORT 2>/dev/null || true) + if [[ -n "$PID" ]]; then + kill $PID 2>/dev/null && ok "测试服务器已停止" || true + fi +} + +# ── 测试执行 ──────────────────────────────────────────────────────────────── + +run_unit_tests() { + info "运行单元测试..." + source .venv/bin/activate 2>/dev/null || true + pytest -m "not integration" -q --tb=short + ok "单元测试完成" +} + +run_bitable_tests() { + info "运行 bitable 模块测试..." + source .venv/bin/activate 2>/dev/null || true + pytest tests/unit/bitable/ -v --tb=short + ok "bitable 测试完成" +} + +run_e2e_tests() { + info "运行 E2E 测试..." + FE_DIR="$PROJECT_ROOT/src/agentkit/server/frontend" + cd "$FE_DIR" + + if [[ ! -d node_modules ]]; then + info "安装前端依赖..." + npm install + fi + + export AGENTKIT_SERVER_URL="http://127.0.0.1:$E2E_PORT" + export AGENTKIT_API_KEY="$E2E_API_KEY" + + npm run test:e2e + ok "E2E 测试完成" + cd "$PROJECT_ROOT" +} + +# ── 主流程 ───────────────────────────────────────────────────────────────── + +echo "" +echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" +echo -e "${CYAN} Fischer AgentKit — 测试环境${NC}" +echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" +echo "" + +trap stop_test_server EXIT INT TERM + +case $RUN_MODE in + unit) + run_unit_tests + ;; + e2e) + start_test_server + run_e2e_tests + ;; + bitable) + run_bitable_tests + ;; + all) + info "运行全部测试..." + run_unit_tests + echo "" + run_bitable_tests + echo "" + start_test_server + run_e2e_tests + ;; +esac + +echo "" +ok "测试完成!" diff --git a/src/agentkit/core/react.py b/src/agentkit/core/react.py index 0209dab..b0bb29b 100644 --- a/src/agentkit/core/react.py +++ b/src/agentkit/core/react.py @@ -837,6 +837,31 @@ class ReActEngine: except Exception as e: logger.warning(f"Incremental compression failed: {e}") else: + # ponytail: 检查是否为畸形工具调用(含 但解析失败) + # 如果是,注入纠正消息让模型重试,而不是把原始 XML 作为最终答案泄漏 + if "" in (response.content or ""): + logger.warning( + f"Step {step}: content contains but " + f"parsing failed — injecting correction" + ) + conversation.append( + {"role": "assistant", "content": response.content} + ) + conversation.append( + { + "role": "user", + "content": ( + "你上一次的工具调用格式有误,无法解析。" + "请使用正确的格式重新调用工具:\n" + '\n' + '{"name": "工具名", "arguments": {"参数名": "参数值"}}\n' + "\n" + "确保 JSON 完整且不要混入其他标签。" + ), + } + ) + continue + # Final answer: LLM 没有调用工具,返回最终答案 react_step = ReActStep( step=step, @@ -1513,6 +1538,36 @@ class ReActEngine: except Exception as e: logger.warning(f"Incremental compression failed: {e}") else: + # ponytail: 检查是否为畸形工具调用(含 但解析失败) + # 如果是,注入纠正消息让模型重试,而不是把原始 XML 作为最终答案泄漏 + if "" in (response.content or ""): + logger.warning( + f"Step {step}: content contains but " + f"parsing failed — injecting correction (stream)" + ) + conversation.append( + {"role": "assistant", "content": response.content} + ) + conversation.append( + { + "role": "user", + "content": ( + "你上一次的工具调用格式有误,无法解析。" + "请使用正确的格式重新调用工具:\n" + '\n' + '{"name": "工具名", "arguments": {"参数名": "参数值"}}\n' + "\n" + "确保 JSON 完整且不要混入其他标签。" + ), + } + ) + yield ReActEvent( + event_type="step", + step=step, + data={"message": "工具调用格式异常,已注入纠正消息"}, + ) + continue + # Final answer react_step = ReActStep( step=step, @@ -2052,4 +2107,101 @@ class ReActEngine: else: logger.warning(f"Failed to parse tool_use block: {json_str[:200]}") + if calls: + return calls + + # 格式 4: 畸形 — 缺少闭合标签或 JSON 被截断/混入杂标签 + # 兜底解析:从 后提取 JSON 片段,用大括号匹配法恢复完整 JSON + open_pattern = re.compile(r"\s*", re.IGNORECASE) + for match in open_pattern.finditer(content): + remainder = content[match.end():] + parsed = self._extract_tool_call_from_malformed(remainder) + if parsed: + calls.append(parsed) + return calls + + @staticmethod + def _extract_tool_call_from_malformed(text: str) -> dict[str, Any] | None: + """从畸形文本中尝试提取工具调用。 + + 处理场景: + 1. JSON 被截断(缺少闭合大括号) + 2. JSON 中混入 等 XML 标签 + 3. 完全无法解析时返回 None + """ + # 尝试用大括号匹配提取第一个 JSON 对象 + brace_start = text.find("{") + if brace_start == -1: + return None + + depth = 0 + json_end = -1 + in_string = False + escape = False + for i in range(brace_start, len(text)): + ch = text[i] + if escape: + escape = False + continue + if ch == "\\": + escape = True + continue + if ch == '"': + in_string = not in_string + continue + if in_string: + continue + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + json_end = i + 1 + break + + if json_end == -1: + # JSON 被截断 — 尝试补全大括号后解析 + json_str = text[brace_start:].strip() + # 截断掉非 JSON 尾部(如 , 等) + cut = json_str.find("}") + if cut != -1: + json_str = json_str[: cut + 1] + else: + # 补全缺失的大括号 + open_braces = json_str.count("{") - json_str.count("}") + json_str = json_str + "}" * max(open_braces, 0) + else: + json_str = text[brace_start:json_end] + + try: + parsed = json.loads(json_str) + name = parsed.get("name", "") + arguments = parsed.get("arguments", {}) + if name: + return {"name": name, "arguments": arguments} + except (json.JSONDecodeError, TypeError): + pass + + # 最终兜底:用正则提取 name 和已知的参数字段 + name_match = re.search(r'"name"\s*:\s*"([^"]+)"', text) + if not name_match: + return None + name = name_match.group(1) + + arguments: dict[str, Any] = {} + # 提取 "key": "value" 模式 + for kv_match in re.finditer(r'"(\w+)"\s*:\s*"([^"]*)"', text): + key = kv_match.group(1) + if key in ("name",): + continue + arguments[key] = kv_match.group(2) + + # 提取 value 模式 + for pm in re.finditer(r"\s*(.*?)\s*", text, re.DOTALL): + arguments[pm.group(1)] = pm.group(2).strip() + + if name: + return {"name": name, "arguments": arguments} + + return None diff --git a/src/agentkit/server/auth/middleware.py b/src/agentkit/server/auth/middleware.py index 82484b5..de6397f 100644 --- a/src/agentkit/server/auth/middleware.py +++ b/src/agentkit/server/auth/middleware.py @@ -43,6 +43,9 @@ class AuthMiddleware(BaseHTTPMiddleware): """ WHITELIST_PATHS = ( + "/", + "/login", + "/assets", # Static assets (exact match covers root; prefix below covers sub-paths) "/api/v1/health", "/api/v1/auth/login", "/api/v1/auth/refresh", @@ -74,16 +77,15 @@ class AuthMiddleware(BaseHTTPMiddleware): """Return True if ``path`` matches a whitelisted route. Uses exact match for auth routes (so ``/auth/logout`` does NOT - whitelist ``/auth/logout-others``) and prefix match for docs. + whitelist ``/auth/logout-others``) and prefix match for docs and assets. """ for prefix in self.WHITELIST_PATHS: if path == prefix: return True - # Prefix match only for documentation paths (trailing slash - # or sub-path is fine). Auth paths require exact match to - # avoid accidentally whitelisting sibling routes like - # /auth/logout-others under /auth/logout. - if prefix in ("/docs", "/openapi.json", "/redoc") and path.startswith(prefix): + # Prefix match for documentation paths and static assets. + # Auth paths require exact match to avoid accidentally whitelisting + # sibling routes like /auth/logout-others under /auth/logout. + if path.startswith(prefix) and prefix in ("/docs", "/openapi.json", "/redoc", "/assets"): return True return False diff --git a/src/agentkit/server/frontend/components.d.ts b/src/agentkit/server/frontend/components.d.ts index 61721ca..3b4be9c 100644 --- a/src/agentkit/server/frontend/components.d.ts +++ b/src/agentkit/server/frontend/components.d.ts @@ -43,6 +43,7 @@ declare module 'vue' { AListItem: typeof import('ant-design-vue/es')['ListItem'] AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta'] AMenu: typeof import('ant-design-vue/es')['Menu'] + AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider'] AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] AModal: typeof import('ant-design-vue/es')['Modal'] APageHeader: typeof import('ant-design-vue/es')['PageHeader'] diff --git a/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue b/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue index cf3ef30..fddc186 100644 --- a/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue +++ b/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue @@ -40,7 +40,7 @@