From 8f90cb5ac9ec6bd9b5a6751b9166e920ab5d9691 Mon Sep 17 00:00:00 2001 From: cyning Date: Thu, 9 Jul 2026 19:36:11 +0800 Subject: [PATCH 1/4] feat(ops): 30 P1-3 clarify fallback routing before react - Add api/ops/orchestrator/clarify.py with clarify_if_fallback - FALLBACK in chat_service now clarifies first; routes to fast/deep/react/clarify - Add tests/ops/test_clarify.py and tests/ops/test_chat_service.py - Adapt tests/ops_desk/test_react_fallback.py fixture to bypass clarify for ReAct tests --- api/ops/chat_service.py | 50 ++- api/ops/orchestrator/__init__.py | 3 + api/ops/orchestrator/clarify.py | 134 ++++++++ ...921_30_ops_chat_session_sink_p0_p1_P1-3.md | 80 +++++ ...921_40_ops_chat_session_sink_p0_p1_P1-3.md | 69 ++++ tests/ops/test_chat_service.py | 308 ++++++++++++++++++ tests/ops/test_clarify.py | 168 ++++++++++ tests/ops_desk/test_react_fallback.py | 11 + 8 files changed, 820 insertions(+), 3 deletions(-) create mode 100644 api/ops/orchestrator/clarify.py create mode 100644 docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1921_30_ops_chat_session_sink_p0_p1_P1-3.md create mode 100644 docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1921_40_ops_chat_session_sink_p0_p1_P1-3.md create mode 100644 tests/ops/test_chat_service.py create mode 100644 tests/ops/test_clarify.py diff --git a/api/ops/chat_service.py b/api/ops/chat_service.py index 150858cf..791c71ff 100644 --- a/api/ops/chat_service.py +++ b/api/ops/chat_service.py @@ -6,8 +6,10 @@ from pydantic import BaseModel +from api.ops.chat_context import load_chat_transcript from api.ops.demo_cache import DemoCacheStore from api.ops.orchestrator import classify_intent, is_fast_intent, run_deep, run_fast, run_react_fallback +from api.ops.orchestrator.clarify import clarify_if_fallback from api.ops.orchestrator.core import Intent from api.ops.queries import OpsQueries from api.ops.store import OpsRunStore @@ -125,11 +127,53 @@ def handle_ops_chat_message( intent, slots = classify_intent(body.message) if intent == Intent.FALLBACK: - run = store.create_run(query=body.message, route="react", session_id=body.session_id) + transcript = load_chat_transcript(body.session_id, store=store) + clarification = clarify_if_fallback(body.message, body.session_id, transcript, slots) + + if clarification.needs_clarification: + run = store.create_run(query=body.message, route="clarify", session_id=body.session_id) + run_id = str(run["id"]) + store.append_event( + run_id, + "orchestrator", + "clarify.asked", + payload={ + "clarify_question": clarification.clarify_question, + "session_id": body.session_id, + }, + node_id="clarify", + ) + return { + "run_id": run_id, + "route": "clarify", + "status": "clarify", + "needs_clarification": True, + "clarify_question": clarification.clarify_question, + } + + resolved_intent = clarification.intent or Intent.FALLBACK + resolved_slots = clarification.slots or {} + + if resolved_intent == Intent.FALLBACK: + run = store.create_run(query=body.message, route="react", session_id=body.session_id) + run_id = str(run["id"]) + result = run_react_fallback(run_id, body.message, store, queries, session_id=body.session_id) + try: + return {"run_id": run_id, "route": "react", "status": result["status"], "answer": result.get("answer")} + finally: + flush_traces() + + route = "fast" if is_fast_intent(resolved_intent) else "deep" + run = store.create_run(query=body.message, route=route, session_id=body.session_id) run_id = str(run["id"]) - result = run_react_fallback(run_id, body.message, store, queries, session_id=body.session_id) + + if route == "fast": + result = run_fast(run_id, body.message, resolved_intent, resolved_slots, store, queries) + return {"run_id": run_id, "route": route, "status": result["status"], "answer": result.get("answer")} + + result = run_deep(run_id, body.message, resolved_slots, store, queries, intent=resolved_intent, session_id=body.session_id) try: - return {"run_id": run_id, "route": "react", "status": result["status"], "answer": result.get("answer")} + return {"run_id": run_id, "route": route, "status": result["status"]} finally: flush_traces() diff --git a/api/ops/orchestrator/__init__.py b/api/ops/orchestrator/__init__.py index 0abe2cc1..31c4bafa 100644 --- a/api/ops/orchestrator/__init__.py +++ b/api/ops/orchestrator/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from api.ops.orchestrator.clarify import ClarifyResult, clarify_if_fallback from api.ops.orchestrator.core import ( Intent, classify_intent, @@ -15,8 +16,10 @@ from api.ops.review.rules import review_result __all__ = [ + "ClarifyResult", "Intent", "classify_intent", + "clarify_if_fallback", "fast_respond", "is_fast_intent", "review_result", diff --git a/api/ops/orchestrator/clarify.py b/api/ops/orchestrator/clarify.py new file mode 100644 index 00000000..60cb2671 --- /dev/null +++ b/api/ops/orchestrator/clarify.py @@ -0,0 +1,134 @@ +"""Ops Chat FALLBACK 澄清模块(P1-3)。 + +当规则意图分类器返回 FALLBACK 时,先通过 LLM 进行 1 轮澄清,或基于规则兜底: +- needs_clarification=true:向用户展示澄清问题,等待补充。 +- needs_clarification=false:给出补齐后的 intent/slots,继续走原 deep/fast/react 路由。 + +LLM 调用失败时,降级为直接 ReAct fallback(保持可用性)。 +""" + +from __future__ import annotations + +import json +import logging +import re +from dataclasses import dataclass +from typing import Any + +from api.ops.llm import chat_completion +from api.ops.orchestrator.core import Intent + +logger = logging.getLogger(__name__) + + +@dataclass +class ClarifyResult: + """澄清结果。 + + - needs_clarification=true:须展示 clarify_question 给用户。 + - needs_clarification=false:使用 intent/slots 继续路由; + 若 intent 仍为 FALLBACK,则进入 ReAct fallback。 + """ + + needs_clarification: bool + clarify_question: str | None = None + intent: str | None = None + slots: dict[str, Any] | None = None + + +def _build_clarify_prompt( + query: str, + transcript: list[dict[str, str]], + slots: dict[str, Any], +) -> str: + """构造澄清 LLM 的 prompt。""" + transcript_text = "\n".join(f"{msg['role']}: {msg['content']}" for msg in transcript) or "(无)" + return ( + "你是 Ops Desk 意图澄清助手。当规则分类器无法确定用户意图时," + "你负责决定:1) 向用户提出 1 轮简洁澄清问题;或 2) 直接推断出最可能的意图与 slots。\n\n" + "可用意图:metrics_trend, issue_list, pr_list, issue_contribution, " + "graph_module, scan_status, fallback。" + "如果信息仍不足,intent 请填 fallback。\n\n" + f"历史对话:\n{transcript_text}\n\n" + f"当前用户输入:{query}\n" + f"当前 slots:{json.dumps(slots, ensure_ascii=False)}\n\n" + "请输出 JSON:\n" + "{\n" + ' "needs_clarification": true/false,\n' + ' "clarify_question": "问题文本或 null",\n' + ' "intent": "意图字符串",\n' + ' "slots": {}\n' + "}" + ) + + +def _rule_fallback(query: str, slots: dict[str, Any]) -> ClarifyResult: + """规则兜底:多个 issue 号时询问澄清,否则降级为直接 ReAct fallback。""" + issue_numbers = slots.get("issue_numbers") or [] + if len(issue_numbers) >= 2: + nums = " / ".join(f"#{n}" for n in issue_numbers) + return ClarifyResult( + needs_clarification=True, + clarify_question=f"你想比较 {nums} 的哪方面?(例如贡献度、状态、关联模块)", + ) + return ClarifyResult(needs_clarification=False, intent=Intent.FALLBACK, slots={}) + + +def _parse_clarify_response(content: str) -> ClarifyResult: + """解析 LLM 返回的 JSON,生成 ClarifyResult。""" + data: dict[str, Any] + try: + data = json.loads(content) + except json.JSONDecodeError: + match = re.search(r"\{.*\}", content, re.S) + if not match: + raise ValueError("No JSON object found in clarify response") from None + data = json.loads(match.group(0)) + + if not isinstance(data, dict): + raise ValueError("Clarify response is not a JSON object") + + needs = bool(data.get("needs_clarification")) + if needs: + question = data.get("clarify_question") + return ClarifyResult( + needs_clarification=True, + clarify_question=str(question) if question is not None else None, + ) + + intent = data.get("intent") or Intent.FALLBACK + slots = data.get("slots") or {} + if not isinstance(slots, dict): + slots = {} + return ClarifyResult(needs_clarification=False, intent=str(intent), slots=slots) + + +def clarify_if_fallback( + query: str, + session_id: str | None, + transcript: list[dict[str, str]], + slots: dict[str, Any], +) -> ClarifyResult: + """对 FALLBACK 意图进行 1 轮澄清或补齐。 + + 参数: + query: 当前用户消息。 + session_id: 可选 session id(仅用于日志/跟踪,不决定行为)。 + transcript: 最近 N 轮对话上下文。 + slots: 当前规则分类器提取的 slots。 + + 返回: + ClarifyResult。 + """ + prompt = _build_clarify_prompt(query, transcript, slots) + messages = [{"role": "user", "content": prompt}] + try: + result = chat_completion(messages, step="clarify", temperature=0.3) + parsed = _parse_clarify_response(result.content) + if parsed.needs_clarification and not parsed.clarify_question: + # LLM 表示需要澄清但没有给问题,回退到规则兜底 + return _rule_fallback(query, slots) + return parsed + except Exception as exc: # pragma: no cover - 防御性降级 + logger.warning("clarify.llm_failed: %s", exc) + return _rule_fallback(query, slots) diff --git a/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1921_30_ops_chat_session_sink_p0_p1_P1-3.md b/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1921_30_ops_chat_session_sink_p0_p1_P1-3.md new file mode 100644 index 00000000..45b0bc5f --- /dev/null +++ b/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1921_30_ops_chat_session_sink_p0_p1_P1-3.md @@ -0,0 +1,80 @@ +# 30-execute-code Invoke Snapshot · P1-3 + +| 项 | 内容 | +| --- | --- | +| **hat** | 30-execute-code | +| **task** | `docs/harness/tasks/active/task_ops_chat_session_sink_p0_p1_v1.md` | +| **subproject** | `ai-ink-brain-api-python` | +| **phase** | P1-3 Clarify 路由 | +| **branch** | `task/ops-chat-session-sink-p0-p1` | +| **timestamp** | 2026-07-09 19:21 | +| **verify_command** | `pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops` | + +## Input Snapshot + +```text +你正在扮演工作区 Harness「30-execute-code · 执行编码帽」,严格遵循 docs/harness/prompts/30-execute-code.md。 + +**输入(已替换占位符)** +- 主 task 路径(相对 Projects/):`docs/harness/tasks/active/task_ops_chat_session_sink_p0_p1_v1.md` +- 逻辑子仓(相对 Projects/):`ai-ink-brain-api-python` +- Worktree 研发目录(所有 git/pytest/ruff 默认 cwd):`ai-ink-brain-api-python` +- 当前分支:`task/ops-chat-session-sink-p0-p1`(已基于 main fast-forward,包含 P1-2 merge) +- 合并前须跑通的验证命令: + ```bash + pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops + ``` +- 关联任务审核书面结论路径:`ai-ink-brain-api-python/docs/harness/reviews/task_ops_chat_session_sink_p0_p1_v1_audit_R2_20260708.md` +- 关联 PLAN / 总规:`docs/harness/guides/PLAN_ops_chat_session_sink_p0_p1_v1_zh.md` + +**本棒目标:P1-3 Clarify 路由:FALLBACK 先澄清 · 减少默认 `#545`** + +P1-3 具体要求(来自 PLAN §2、§3.1 D8、task §实现备忘): +- 新增 `api/ops/orchestrator/clarify.py`:实现 `clarify_if_fallback(query, session_id, transcript, slots)`。 +- 当 `classify_intent` 返回 `Intent.FALLBACK` 时,不直接进入 ReAct fallback,而是先调用 clarify 模块。 +- Clarify 模块通过 LLM asking 1 轮澄清问题(或基于规则兜底),返回: + - `needs_clarification=true` + `clarify_question`:Chat 侧展示澄清卡片,等待用户补充; + - `needs_clarification=false` + 补齐后的 slots/intent:继续走原 deep/ReAct 路由。 +- 在 `api/ops/chat_service.py` 中替换现有 `Intent.FALLBACK → run_react_fallback` 的 silent 行为:先 clarify,再根据 clarify 结果路由。 +- 减少默认 `#545`:FALLBACK 测例不再默认 issue #545(除非 clarify 后明确指向 #545)。 +- 利用 P0-3 transcript 能力(`load_chat_transcript`)为 clarify 提供上下文。 + +**范围限制** +- 只做 P1-3;不改 P1-4 LLM Router +- 不改 P1-1 artifact、P1-2 checkpoint 已交付行为 +- 不改 `harness_runtime` 生产图 +- 不改 Agently lab +- 不改前端代码(前端 F1-1 在单独 task,后端仅输出 `needs_clarification` 字段) + +**test_strategy: required** +- 先写/调整可失败的自动化测试,再改实现 +- 新增/扩展 `tests/ops/test_clarify.py` 与 `tests/ops/test_chat_service.py` 覆盖: + - FALLBACK 进入 clarify 而不是直接 ReAct + - clarify 返回需要澄清时,`needs_clarification=true` 且不含默认 #545 + - clarify 返回不需要澄清时,继续走 deep/ReAct 并携带补齐 slots + - 无 session_id 时 clarify 仍能单轮工作 +- 最终验证命令必须绿 + +**失败路径硬性检查** +- task §失败路径未单独列 clarify,但须保证原有失败路径不被破坏(artifact 写失败、checkpoint 损坏等仍按原逻辑)。 +- 新增 clarify LLM 调用失败时:降级为直接 ReAct fallback(保持可用性)。 + +**你必须完成** +0. **Invoke 快照(开帽起点)**:将本用户消息全文落盘到 `ai-ink-brain-api-python/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_YYYYMMDD_HHMM_30_ops_chat_session_sink_p0_p1_P1-3.md`(含元数据表 + 快照 fenced code)。同一会话内追问 **不** 再新增快照文件。 +0b. **人工闸**:扫描 task / 关联 reviews 的 human_gate。若任一对本帽(30)为 pending → 仅输出须人改的 gate_id 与路径,拒开工;禁止代填 approved。 +1. 通读 task 全文:头部 gates_before_code、audit_profile、orchestration、chain_prompt、test_strategy / test_strategy_note、failure_paths、验收标准、必读列表、非范围。 +2. 阅读 PLAN §2、§3.1 与关联 SNAPSHOT/gap matrix。 +3. 先读现有代码:`api/ops/intent_router.py`、`api/ops/chat_service.py`、`api/ops/chat_context.py`、`api/ops/orchestrator/core.py`、`api/ops/react_loop.py`。 +4. 先写失败可复现的测试(`tests/ops/test_clarify.py`),再实现 `api/ops/orchestrator/clarify.py` 与 `chat_service.py` 路由改造。 +5. 执行验证命令,保留可核对输出要点;修复直至通过。 +6. 按 40-self-check.md 将结论与命令摘要回填至 task 正文「### 自检结论(执行者)· P1-3」小节(不要覆盖 P0、P1-1、P1-2 已有结论)。 +7. 对话回复:生成可以完整复制的 Prompt,用于直接交给下一棒 40 自检执行。 +8. **自动 commit**:在输出下一棒 Prompt 且本轮代码/测试/task 自检回填已落盘后,按 HANDOFF_AUTO_COMMIT.md 在 ai-ink-brain-api-python/ commit(仅本轮路径;禁止 git add -A;对话报 short-hash)。 +9. **禁止**自行 push;由 Lead 合并。 + +**输出要求** +- 若拒开工:仅 Markdown 阻塞清单 +- 若执行:diff 摘要、验证命令输出、commit short-hash、下一棒 40 Prompt + +**Judgment(本帽 · 对话末尾必填)**:experience_capture / gate/risk / hat_self +``` diff --git a/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1921_40_ops_chat_session_sink_p0_p1_P1-3.md b/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1921_40_ops_chat_session_sink_p0_p1_P1-3.md new file mode 100644 index 00000000..4fd2a003 --- /dev/null +++ b/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1921_40_ops_chat_session_sink_p0_p1_P1-3.md @@ -0,0 +1,69 @@ +--- +hat: "40-self-check" +task: "ops-chat-session-sink-p0-p1" +phase: "P1-3" +subproject: "ai-ink-brain-api-python" +branch: "task/ops-chat-session-sink-p0-p1" +worktree_root: "ai-ink-brain-api-python/" +date: "2026-07-09" +time: "19:21" +--- + +| 字段 | 值 | +| --- | --- | +| **hat** | 40-self-check | +| **task** | ops-chat-session-sink-p0-p1 | +| **phase** | P1-3 Clarify 路由 | +| **subproject** | ai-ink-brain-api-python | +| **branch** | task/ops-chat-session-sink-p0-p1 | +| **worktree_root** | ai-ink-brain-api-python/ | +| **date** | 2026-07-09 | +| **time** | 19:21 | + +## 用户消息快照 + +```text +你正在扮演工作区 Harness「40-self-check · 执行者自检帽」,严格遵循 docs/harness/prompts/40-self-check.md。 + +**输入(已替换占位符)** +- 主 task 路径(相对 Projects/):`docs/harness/tasks/active/task_ops_chat_session_sink_p0_p1_v1.md` +- 逻辑子仓(相对 Projects/):`ai-ink-brain-api-python` +- Worktree 研发目录(所有 git/pytest/ruff 默认 cwd):`ai-ink-brain-api-python` +- 当前分支:`task/ops-chat-session-sink-p0-p1`(已基于 main fast-forward,包含 P1-2 merge) +- 合并前须跑通的验证命令: + ```bash + pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops + ``` +- 上一棒 30 commit:待本 Prompt 落盘后从 30 输出获取 +- 关联任务审核书面结论路径:`ai-ink-brain-api-python/docs/harness/reviews/task_ops_chat_session_sink_p0_p1_v1_audit_R2_20260708.md` +- 关联 PLAN / 总规:`docs/harness/guides/PLAN_ops_chat_session_sink_p0_p1_v1_zh.md` + +**本棒目标:P1-3 自检复核** + +你必须完成: +0. **Invoke 快照(开帽起点)**:将本用户消息全文落盘到 `ai-ink-brain-api-python/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_YYYYMMDD_HHMM_40_ops_chat_session_sink_p0_p1_P1-3.md`(含元数据表 + 快照 fenced code)。同一会话内追问 **不** 再新增快照文件。 +0b. **人工闸**:扫描 task / 关联 reviews 的 human_gate。若任一对本帽(40)为 pending → 仅输出须人改的 gate_id 与路径,拒开工;禁止代填 approved。 +1. 独立阅读 task 正文「### 自检结论(执行者)· P1-3」小节与上一棒 30 invoke 快照 `ai-ink-brain-api-python/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1921_30_ops_chat_session_sink_p0_p1_P1-3.md`。 +2. 独立阅读本轮 P1-3 改动代码: + - `api/ops/orchestrator/clarify.py` + - `api/ops/orchestrator/__init__.py` + - `api/ops/chat_service.py` + - `tests/ops/test_clarify.py` + - `tests/ops/test_chat_service.py` + - `tests/ops_desk/test_react_fallback.py`(clarify 旁路适配) +3. 在 `ai-ink-brain-api-python/` 内完整执行 30 声明的验证命令: + ```bash + pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops + ``` +4. 通过 `git diff origin/main...HEAD --stat`(在 `ai-ink-brain-api-python` 内)核对全量变更路径,确认未扩 scope 到 P1-4 LLM Router、P1-1 artifact、P1-2 checkpoint、Session 生产图、Agently lab、前端。 +5. 按 40-self-check.md 将结论与命令摘要回填至 task 正文「### 自检结论(40 复核)· P1-3」小节(不要覆盖 P0、P1-1、P1-2 或 30 已有结论)。 +6. 对话回复:生成可以完整复制的 Prompt,用于直接交给下一棒 50 独立复检执行。 +7. 自动 commit:在输出下一棒 Prompt 且本轮 task 自检回填已落盘后,按 HANDOFF_AUTO_COMMIT.md 在 `ai-ink-brain-api-python/` commit(仅本轮路径;禁止 git add -A;对话报 short-hash)。 +8. **禁止**自行 push;由 Lead 合并。 + +**输出要求** +- 若拒开工:仅 Markdown 阻塞清单 +- 若执行:diff 摘要、验证命令输出、commit short-hash、下一棒 50 Prompt + +**Judgment(本帽 · 对话末尾必填)**:experience_capture / gate/risk / hat_self +``` diff --git a/tests/ops/test_chat_service.py b/tests/ops/test_chat_service.py new file mode 100644 index 00000000..20391c7c --- /dev/null +++ b/tests/ops/test_chat_service.py @@ -0,0 +1,308 @@ +"""P1-3: Ops Chat 服务 FALLBACK 澄清路由单测。 + +覆盖: +- FALLBACK 进入 clarify 而不是直接 ReAct。 +- clarify 返回需要澄清时,响应含 needs_clarification=true 且不含默认 #545。 +- clarify 解析为具体 intent 时路由到 deep / fast。 +- clarify 仍返回 fallback 时降级到 ReAct。 +- 无 session_id 时 clarify 仍能工作。 +""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from api.ops.chat_service import ChatMessageRequest, handle_ops_chat_message +from api.ops.demo_cache import DemoClassifier + + +class FakeServiceStore: + """内存版 OpsRunStore,支持 chat_service 所需方法。""" + + def __init__(self) -> None: + self.runs: dict[str, dict[str, Any]] = {} + self.events: dict[str, list[dict[str, Any]]] = {} + self._counter = 0 + + def create_run( + self, + query: str, + route: str, + repo_owner: str = "MoonshotAI", + repo_name: str = "kimi-code", + session_id: str | None = None, + ) -> dict[str, Any]: + self._counter += 1 + run_id = f"run-{self._counter}" + self.runs[run_id] = { + "id": run_id, + "query": query, + "route": route, + "session_id": session_id, + } + self.events[run_id] = [] + return self.runs[run_id] + + def append_event( + self, + run_id: str, + agent_role: str, + event_type: str, + payload: dict[str, Any] | None = None, + node_id: str | None = None, + seq: int | None = None, + ) -> dict[str, Any]: + evt: dict[str, Any] = { + "run_id": run_id, + "agent_role": agent_role, + "event_type": event_type, + "payload": payload or {}, + "node_id": node_id, + "seq": seq or len(self.events.get(run_id, [])) + 1, + } + self.events.setdefault(run_id, []).append(evt) + return evt + + def update_run(self, run_id: str, **fields: Any) -> None: + if run_id in self.runs: + self.runs[run_id].update(fields) + + def update_run_metrics_json(self, run_id: str, metrics_json: dict[str, Any]) -> None: + self.update_run(run_id, metrics_json=metrics_json) + + def list_runs_by_session_id(self, session_id: str, *, limit: int = 50) -> list[dict[str, Any]]: + return [r for r in self.runs.values() if r.get("session_id") == session_id][:limit] + + def get_events(self, run_id: str, after_seq: int = 0, limit: int = 200) -> list[dict[str, Any]]: + return [e for e in self.events.get(run_id, []) if e.get("seq", 0) > after_seq][:limit] + + +class FakeServiceQueries: + def fetch_issue_by_number(self, number: int) -> dict[str, Any] | None: + return None + + def fetch_pull_by_number(self, number: int) -> dict[str, Any] | None: + return None + + +class FakeDemoCache: + def __init__(self) -> None: + self.classifier = DemoClassifier() + + def get(self, demo_id: str) -> dict[str, Any] | None: + return None + + def set(self, *args: Any, **kwargs: Any) -> dict[str, Any]: + return {"demo_id": args[0] if args else None} + + +@pytest.fixture +def fake_service_store() -> FakeServiceStore: + return FakeServiceStore() + + +@pytest.fixture +def fake_service_queries() -> FakeServiceQueries: + return FakeServiceQueries() + + +@pytest.fixture +def fake_demo_cache() -> FakeDemoCache: + return FakeDemoCache() + + +def test_fallback_enters_clarify_not_direct_react( + monkeypatch: pytest.MonkeyPatch, + fake_service_store: FakeServiceStore, + fake_service_queries: FakeServiceQueries, + fake_demo_cache: FakeDemoCache, +) -> None: + """FALLBACK 应先进入 clarify,而不是直接 ReAct。""" + from api.ops.orchestrator import clarify + + monkeypatch.setattr("api.ops.chat_service.classify_intent", lambda msg: ("fallback", {})) + + clarify_called = {"count": 0} + + def fake_clarify_if_fallback(*args: Any, **kwargs: Any) -> clarify.ClarifyResult: + clarify_called["count"] += 1 + return clarify.ClarifyResult( + needs_clarification=True, + clarify_question="你想查询什么?", + ) + + react_called = {"count": 0} + + def fake_run_react_fallback(*args: Any, **kwargs: Any) -> dict[str, Any]: + react_called["count"] += 1 + return {"status": "done", "answer": "react answer"} + + monkeypatch.setattr("api.ops.chat_service.clarify_if_fallback", fake_clarify_if_fallback) + monkeypatch.setattr("api.ops.chat_service.run_react_fallback", fake_run_react_fallback) + + body = ChatMessageRequest(message="模糊问题", session_id="sess-1") + result = handle_ops_chat_message(body, fake_service_queries, fake_service_store, fake_demo_cache) + + assert clarify_called["count"] == 1 + assert react_called["count"] == 0 + assert result["route"] == "clarify" + assert result["status"] == "clarify" + assert result["needs_clarification"] is True + assert result["clarify_question"] == "你想查询什么?" + assert "answer" not in result + assert "issue_number" not in result + + # 验证 clarify run 记录了 clarify.asked 事件 + run_id = result["run_id"] + clarify_events = [e for e in fake_service_store.events.get(run_id, []) if e["event_type"] == "clarify.asked"] + assert len(clarify_events) == 1 + assert clarify_events[0]["payload"]["clarify_question"] == "你想查询什么?" + + +def test_fallback_clarify_resolved_routes_to_deep( + monkeypatch: pytest.MonkeyPatch, + fake_service_store: FakeServiceStore, + fake_service_queries: FakeServiceQueries, + fake_demo_cache: FakeDemoCache, +) -> None: + """clarify 解析为 issue_contribution 时应路由到 deep。""" + from api.ops.orchestrator import clarify + + monkeypatch.setattr("api.ops.chat_service.classify_intent", lambda msg: ("fallback", {})) + + def fake_clarify_if_fallback(*args: Any, **kwargs: Any) -> clarify.ClarifyResult: + return clarify.ClarifyResult( + needs_clarification=False, + intent="issue_contribution", + slots={"issue_number": 123}, + ) + + deep_called: dict[str, Any] = {"args": None, "kwargs": None} + + def fake_run_deep(*args: Any, **kwargs: Any) -> dict[str, Any]: + deep_called["args"] = args + deep_called["kwargs"] = kwargs + return {"status": "done", "answer": "deep answer", "issue_number": 123} + + monkeypatch.setattr("api.ops.chat_service.clarify_if_fallback", fake_clarify_if_fallback) + monkeypatch.setattr("api.ops.chat_service.run_deep", fake_run_deep) + + body = ChatMessageRequest(message="那个 issue 适合吗", session_id="sess-1") + result = handle_ops_chat_message(body, fake_service_queries, fake_service_store, fake_demo_cache) + + assert result["route"] == "deep" + assert result["status"] == "done" + assert deep_called["args"] is not None + _, _, slots, *_ = deep_called["args"] + assert slots == {"issue_number": 123} + assert deep_called["kwargs"]["intent"] == "issue_contribution" + assert deep_called["kwargs"]["session_id"] == "sess-1" + + +def test_fallback_clarify_resolved_routes_to_fast( + monkeypatch: pytest.MonkeyPatch, + fake_service_store: FakeServiceStore, + fake_service_queries: FakeServiceQueries, + fake_demo_cache: FakeDemoCache, +) -> None: + """clarify 解析为 metrics_trend 时应路由到 fast。""" + from api.ops.orchestrator import clarify + + monkeypatch.setattr("api.ops.chat_service.classify_intent", lambda msg: ("fallback", {})) + + def fake_clarify_if_fallback(*args: Any, **kwargs: Any) -> clarify.ClarifyResult: + return clarify.ClarifyResult( + needs_clarification=False, + intent="metrics_trend", + slots={"metric": "cycle-time", "days": 30}, + ) + + fast_called: dict[str, Any] = {"args": None, "kwargs": None} + + def fake_run_fast(*args: Any, **kwargs: Any) -> dict[str, Any]: + fast_called["args"] = args + fast_called["kwargs"] = kwargs + return {"status": "done", "answer": "fast answer"} + + monkeypatch.setattr("api.ops.chat_service.clarify_if_fallback", fake_clarify_if_fallback) + monkeypatch.setattr("api.ops.chat_service.run_fast", fake_run_fast) + + body = ChatMessageRequest(message="最近情况怎样", session_id="sess-1") + result = handle_ops_chat_message(body, fake_service_queries, fake_service_store, fake_demo_cache) + + assert result["route"] == "fast" + assert result["answer"] == "fast answer" + assert fast_called["args"] is not None + _, _, intent, slots, *_ = fast_called["args"] + assert intent == "metrics_trend" + assert slots == {"metric": "cycle-time", "days": 30} + + +def test_fallback_clarify_resolved_fallback_routes_to_react( + monkeypatch: pytest.MonkeyPatch, + fake_service_store: FakeServiceStore, + fake_service_queries: FakeServiceQueries, + fake_demo_cache: FakeDemoCache, +) -> None: + """clarify 仍返回 fallback 时应路由到 ReAct。""" + from api.ops.orchestrator import clarify + + monkeypatch.setattr("api.ops.chat_service.classify_intent", lambda msg: ("fallback", {})) + + def fake_clarify_if_fallback(*args: Any, **kwargs: Any) -> clarify.ClarifyResult: + return clarify.ClarifyResult( + needs_clarification=False, + intent="fallback", + slots={}, + ) + + react_called: dict[str, Any] = {"args": None, "kwargs": None} + + def fake_run_react_fallback(*args: Any, **kwargs: Any) -> dict[str, Any]: + react_called["args"] = args + react_called["kwargs"] = kwargs + return {"status": "done", "answer": "react answer"} + + monkeypatch.setattr("api.ops.chat_service.clarify_if_fallback", fake_clarify_if_fallback) + monkeypatch.setattr("api.ops.chat_service.run_react_fallback", fake_run_react_fallback) + + body = ChatMessageRequest(message="复杂对比问题", session_id="sess-1") + result = handle_ops_chat_message(body, fake_service_queries, fake_service_store, fake_demo_cache) + + assert result["route"] == "react" + assert result["answer"] == "react answer" + assert react_called["args"] is not None + assert react_called["args"][0] == result["run_id"] + assert react_called["kwargs"]["session_id"] == "sess-1" + + +def test_fallback_clarify_without_session_id( + monkeypatch: pytest.MonkeyPatch, + fake_service_store: FakeServiceStore, + fake_service_queries: FakeServiceQueries, + fake_demo_cache: FakeDemoCache, +) -> None: + """无 session_id 时 clarify 仍能工作。""" + from api.ops.orchestrator import clarify + + monkeypatch.setattr("api.ops.chat_service.classify_intent", lambda msg: ("fallback", {})) + + def fake_clarify_if_fallback(*args: Any, **kwargs: Any) -> clarify.ClarifyResult: + query, session_id, transcript, slots = args + assert session_id is None + assert transcript == [] + return clarify.ClarifyResult( + needs_clarification=True, + clarify_question="请补充信息。", + ) + + monkeypatch.setattr("api.ops.chat_service.clarify_if_fallback", fake_clarify_if_fallback) + + body = ChatMessageRequest(message="模糊问题", session_id=None) + result = handle_ops_chat_message(body, fake_service_queries, fake_service_store, fake_demo_cache) + + assert result["route"] == "clarify" + assert result["needs_clarification"] is True + assert result["clarify_question"] == "请补充信息。" diff --git a/tests/ops/test_clarify.py b/tests/ops/test_clarify.py new file mode 100644 index 00000000..24916ec9 --- /dev/null +++ b/tests/ops/test_clarify.py @@ -0,0 +1,168 @@ +"""P1-3: Ops Chat FALLBACK 澄清模块单测。 + +覆盖: +- clarify 返回 needs_clarification=true 时给出澄清问题。 +- clarify 返回 needs_clarification=false 时给出补齐的 intent/slots。 +- LLM 调用失败时降级为直接 ReAct fallback。 +- clarify 利用 transcript 提供上下文。 +- 规则兜底:多 issue 号时询问澄清。 +- 无 session_id 时单轮工作。 +""" + +from __future__ import annotations + +import json +from typing import Any + +import pytest + +from api.ops.llm.types import LlmCompletionResult, LlmUsage +from api.ops.orchestrator.core import Intent + + +def _make_llm_result(content: str, step: str = "clarify") -> LlmCompletionResult: + return LlmCompletionResult( + content=content, + usage=LlmUsage( + provider="siliconflow", + model="Qwen/Qwen2.5-72B-Instruct", + prompt_tokens=10, + completion_tokens=5, + total_tokens=15, + latency_ms=100, + step=step, + ), + ) + + +def test_clarify_asks_when_needs_clarification(monkeypatch: pytest.MonkeyPatch) -> None: + """clarify 返回 needs_clarification=true 时应给出澄清问题。""" + from api.ops.orchestrator import clarify + + def fake_chat_completion(messages: list[dict[str, str]], **kwargs: Any) -> LlmCompletionResult: + return _make_llm_result( + json.dumps( + { + "needs_clarification": True, + "clarify_question": "你想比较 #123 和 #545 的哪方面?", + "intent": None, + "slots": {}, + }, + ensure_ascii=False, + ) + ) + + monkeypatch.setattr(clarify, "chat_completion", fake_chat_completion) + result = clarify.clarify_if_fallback("比较 #123 和 #545", "sess-1", [], {"issue_numbers": [123, 545]}) + + assert result.needs_clarification is True + assert result.clarify_question == "你想比较 #123 和 #545 的哪方面?" + assert result.intent is None + assert result.slots is None + + +def test_clarify_resolves_intent_and_slots(monkeypatch: pytest.MonkeyPatch) -> None: + """clarify 返回 needs_clarification=false 时应给出补齐的 intent/slots。""" + from api.ops.orchestrator import clarify + + def fake_chat_completion(messages: list[dict[str, str]], **kwargs: Any) -> LlmCompletionResult: + return _make_llm_result( + json.dumps( + { + "needs_clarification": False, + "clarify_question": None, + "intent": "issue_contribution", + "slots": {"issue_number": 123}, + }, + ensure_ascii=False, + ) + ) + + monkeypatch.setattr(clarify, "chat_completion", fake_chat_completion) + result = clarify.clarify_if_fallback("这个 issue 适合我吗", "sess-1", [], {}) + + assert result.needs_clarification is False + assert result.intent == "issue_contribution" + assert result.slots == {"issue_number": 123} + + +def test_clarify_degrades_to_react_on_llm_failure(monkeypatch: pytest.MonkeyPatch) -> None: + """LLM 调用失败时降级为直接 ReAct fallback。""" + from api.ops.orchestrator import clarify + + def fake_chat_completion(messages: list[dict[str, str]], **kwargs: Any) -> LlmCompletionResult: + raise RuntimeError("LLM timeout") + + monkeypatch.setattr(clarify, "chat_completion", fake_chat_completion) + result = clarify.clarify_if_fallback("模糊问题", None, [], {}) + + assert result.needs_clarification is False + assert result.intent == Intent.FALLBACK + assert result.slots == {} + + +def test_clarify_uses_transcript_in_prompt(monkeypatch: pytest.MonkeyPatch) -> None: + """clarify 应将 transcript 拼接到 prompt 中传给 LLM。""" + from api.ops.orchestrator import clarify + + captured: list[list[dict[str, str]]] = [] + + def fake_chat_completion(messages: list[dict[str, str]], **kwargs: Any) -> LlmCompletionResult: + captured.append(messages) + return _make_llm_result( + json.dumps( + {"needs_clarification": True, "clarify_question": "请补充 issue 号。"}, + ensure_ascii=False, + ) + ) + + monkeypatch.setattr(clarify, "chat_completion", fake_chat_completion) + transcript = [ + {"role": "user", "content": "之前问 #123"}, + {"role": "assistant", "content": "#123 是 bug。"}, + ] + clarify.clarify_if_fallback("它呢", "sess-1", transcript, {}) + + assert len(captured) == 1 + prompt = captured[0][0]["content"] + assert "之前问 #123" in prompt + assert "#123 是 bug。" in prompt + assert "它呢" in prompt + + +def test_clarify_rule_fallback_for_multiple_issues(monkeypatch: pytest.MonkeyPatch) -> None: + """LLM 失败且存在多个 issue 号时,规则兜底询问澄清。""" + from api.ops.orchestrator import clarify + + def fake_chat_completion(messages: list[dict[str, str]], **kwargs: Any) -> LlmCompletionResult: + raise RuntimeError("LLM timeout") + + monkeypatch.setattr(clarify, "chat_completion", fake_chat_completion) + result = clarify.clarify_if_fallback("比较一下", "sess-1", [], {"issue_numbers": [123, 545]}) + + assert result.needs_clarification is True + assert "#123" in (result.clarify_question or "") + assert "#545" in (result.clarify_question or "") + + +def test_clarify_no_session_id_works(monkeypatch: pytest.MonkeyPatch) -> None: + """无 session_id 时 clarify 仍能单轮工作。""" + from api.ops.orchestrator import clarify + + def fake_chat_completion(messages: list[dict[str, str]], **kwargs: Any) -> LlmCompletionResult: + return _make_llm_result( + json.dumps( + { + "needs_clarification": False, + "intent": "issue_contribution", + "slots": {"issue_number": 545}, + }, + ensure_ascii=False, + ) + ) + + monkeypatch.setattr(clarify, "chat_completion", fake_chat_completion) + result = clarify.clarify_if_fallback("545 适合吗", None, [], {}) + + assert result.needs_clarification is False + assert result.intent == "issue_contribution" diff --git a/tests/ops_desk/test_react_fallback.py b/tests/ops_desk/test_react_fallback.py index 93895f94..5ffdd8b0 100644 --- a/tests/ops_desk/test_react_fallback.py +++ b/tests/ops_desk/test_react_fallback.py @@ -92,6 +92,17 @@ def fake_synthesize_answer(query: str, evidence: list[dict[str, Any]], **kwargs: synthesize=lambda *args, **kwargs: ("综合建议。", None), ) + # P1-3: react_client fixture targets ReAct loop behavior; bypass clarify so + # existing ReAct tests continue to exercise the fallback path directly. + from api.ops.orchestrator.clarify import ClarifyResult + + monkeypatch.setattr( + "api.ops.chat_service.clarify_if_fallback", + lambda *args, **kwargs: ClarifyResult( + needs_clarification=False, intent="fallback", slots={} + ), + ) + test_client = TestClient(app) test_client.fake_store = fake_store # type: ignore[attr-defined] test_client.fake_queries = fake_queries # type: ignore[attr-defined] From ea31eae8d133df7073f56313bc1c85d538bddc53 Mon Sep 17 00:00:00 2001 From: cyning Date: Thu, 9 Jul 2026 19:41:00 +0800 Subject: [PATCH 2/4] docs(harness): 40 self-check P1-3 + next 50 invoke 40 self-check invoke snapshot and 50 next-hat prompt for P1-3 Clarify routing. --- ...938_40_ops_chat_session_sink_p0_p1_P1-3.md | 59 ++++++++++++++++++ ...938_50_ops_chat_session_sink_p0_p1_P1-3.md | 62 +++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_40_ops_chat_session_sink_p0_p1_P1-3.md create mode 100644 docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_50_ops_chat_session_sink_p0_p1_P1-3.md diff --git a/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_40_ops_chat_session_sink_p0_p1_P1-3.md b/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_40_ops_chat_session_sink_p0_p1_P1-3.md new file mode 100644 index 00000000..42e47615 --- /dev/null +++ b/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_40_ops_chat_session_sink_p0_p1_P1-3.md @@ -0,0 +1,59 @@ +# 40-self-check Invoke Snapshot · P1-3 + +| 项 | 内容 | +| --- | --- | +| **hat** | 40-self-check | +| **task** | `docs/harness/tasks/active/task_ops_chat_session_sink_p0_p1_v1.md` | +| **subproject** | `ai-ink-brain-api-python` | +| **phase** | P1-3 Clarify 路由 | +| **branch** | `task/ops-chat-session-sink-p0-p1` | +| **timestamp** | 2026-07-09 19:38 | +| **verify_command** | `pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops` | + +## Input Snapshot + +```text +你正在扮演工作区 Harness「40-self-check · 执行者自检帽」,严格遵循 docs/harness/prompts/40-self-check.md。 + +**输入(已替换占位符)** +- 主 task 路径(相对 Projects/):`docs/harness/tasks/active/task_ops_chat_session_sink_p0_p1_v1.md` +- 逻辑子仓(相对 Projects/):`ai-ink-brain-api-python` +- Worktree 研发目录(所有 git/pytest/ruff 默认 cwd):`ai-ink-brain-api-python` +- 当前分支:`task/ops-chat-session-sink-p0-p1`(已基于 main fast-forward,包含 P1-2 merge) +- 合并前须跑通的验证命令: + ```bash + pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops + ``` +- 上一棒 30 commit:`ai-ink-brain-api-python @ 8f90cb5a` +- 关联任务审核书面结论路径:`ai-ink-brain-api-python/docs/harness/reviews/task_ops_chat_session_sink_p0_p1_v1_audit_R2_20260708.md` +- 关联 PLAN / 总规:`docs/harness/guides/PLAN_ops_chat_session_sink_p0_p1_v1_zh.md` + +**本棒目标:P1-3 自检复核** + +你必须完成: +0. **Invoke 快照(开帽起点)**:将本用户消息全文落盘到 `ai-ink-brain-api-python/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_YYYYMMDD_HHMM_40_ops_chat_session_sink_p0_p1_P1-3.md`(含元数据表 + 快照 fenced code)。同一会话内追问 **不** 再新增快照文件。 +0b. **人工闸**:扫描 task / 关联 reviews 的 human_gate。若任一对本帽(40)为 pending → 仅输出须人改的 gate_id 与路径,拒开工;禁止代填 approved。 +1. 独立阅读 task 正文「### 自检结论(执行者)· P1-3」小节与上一棒 30 invoke 快照 `ai-ink-brain-api-python/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1921_30_ops_chat_session_sink_p0_p1_P1-3.md`。 +2. 独立阅读本轮 P1-3 改动代码: + - `api/ops/orchestrator/clarify.py` + - `api/ops/orchestrator/__init__.py` + - `api/ops/chat_service.py` + - `tests/ops/test_clarify.py` + - `tests/ops/test_chat_service.py` + - `tests/ops_desk/test_react_fallback.py`(clarify 旁路适配) +3. 在 `ai-ink-brain-api-python/` 内完整执行 30 声明的验证命令: + ```bash + pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops + ``` +4. 通过 `git diff origin/main...HEAD --stat`(在 `ai-ink-brain-api-python` 内)核对全量变更路径,确认未扩 scope 到 P1-4 LLM Router、P1-1 artifact、P1-2 checkpoint、Session 生产图、Agently lab、前端。 +5. 按 40-self-check.md 将结论与命令摘要回填至 task 正文「### 自检结论(40 复核)· P1-3」小节(不要覆盖 P0、P1-1、P1-2 或 30 已有结论)。 +6. 对话回复:生成可以完整复制的 Prompt,用于直接交给下一棒 50 独立复检执行。 +7. 自动 commit:在输出下一棒 Prompt 且本轮 task 自检回填已落盘后,按 HANDOFF_AUTO_COMMIT.md 在 `ai-ink-brain-api-python/` commit(仅本轮路径;禁止 git add -A;对话报 short-hash)。 +8. **禁止**自行 push;由 Lead 合并。 + +**输出要求** +- 若拒开工:仅 Markdown 阻塞清单 +- 若执行:diff 摘要、验证命令输出、commit short-hash、下一棒 50 Prompt + +**Judgment(本帽 · 对话末尾必填)**:experience_capture / gate/risk / hat_self +``` diff --git a/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_50_ops_chat_session_sink_p0_p1_P1-3.md b/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_50_ops_chat_session_sink_p0_p1_P1-3.md new file mode 100644 index 00000000..daeb2835 --- /dev/null +++ b/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_50_ops_chat_session_sink_p0_p1_P1-3.md @@ -0,0 +1,62 @@ +# 50-self-check Invoke Snapshot · P1-3 + +| 项 | 内容 | +| --- | --- | +| **hat** | 50-self-check | +| **task** | `docs/harness/tasks/active/task_ops_chat_session_sink_p0_p1_v1.md` | +| **subproject** | `ai-ink-brain-api-python` | +| **phase** | P1-3 Clarify 路由 | +| **branch** | `task/ops-chat-session-sink-p0-p1` | +| **timestamp** | 2026-07-09 19:38 | +| **verify_command** | `pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops` | + +## Input Snapshot + +```text +你正在扮演工作区 Harness「50-self-check · 独立复检 + 全局验收帽」,严格遵循 docs/harness/prompts/50-self-check.md。 + +**输入(已替换占位符)** +- 主 task 路径(相对 Projects/):`docs/harness/tasks/active/task_ops_chat_session_sink_p0_p1_v1.md` +- 逻辑子仓(相对 Projects/):`ai-ink-brain-api-python` +- Worktree 研发目录(所有 git/pytest/ruff 默认 cwd):`ai-ink-brain-api-python` +- 当前分支:`task/ops-chat-session-sink-p0-p1`(已基于 main fast-forward,包含 P1-2 merge) +- 合并前须跑通的验证命令: + ```bash + pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops + ``` +- 上一棒 30 commit:`ai-ink-brain-api-python @ 8f90cb5a` +- 上一棒 40 commit:`ai-ink-brain-api-python @ <待本回合回填>` +- 关联任务审核书面结论路径:`ai-ink-brain-api-python/docs/harness/reviews/task_ops_chat_session_sink_p0_p1_v1_audit_R2_20260708.md` +- 关联 PLAN / 总规:`docs/harness/guides/PLAN_ops_chat_session_sink_p0_p1_v1_zh.md` + +**本棒目标:P1-3 独立复检 + 全局验收** + +你必须完成: +0. **Invoke 快照(开帽起点)**:将本用户消息全文落盘到 `ai-ink-brain-api-python/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_YYYYMMDD_HHMM_50_ops_chat_session_sink_p0_p1_P1-3.md`(含元数据表 + 快照 fenced code)。同一会话内追问 **不** 再新增快照文件。 +0b. **人工闸**:扫描 task / 关联 reviews 的 human_gate。若任一对本帽(50)为 pending → 仅输出须人改的 gate_id 与路径,拒开工;禁止代填 approved。 +1. 独立阅读 task 正文「### 自检结论(执行者)· P1-3」与「### 自检结论(40 复核)· P1-3」小节。 +2. 独立阅读本轮 P1-3 改动代码: + - `api/ops/orchestrator/clarify.py` + - `api/ops/orchestrator/__init__.py` + - `api/ops/chat_service.py` + - `tests/ops/test_clarify.py` + - `tests/ops/test_chat_service.py` + - `tests/ops_desk/test_react_fallback.py`(clarify 旁路适配) +3. 在 `ai-ink-brain-api-python/` 内完整执行验证命令: + ```bash + pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops + ``` +4. 执行 `git diff origin/main...HEAD --stat`(在 `ai-ink-brain-api-python` 内)核对全量变更路径,确认未扩 scope 到 P1-4 LLM Router、P1-1 artifact、P1-2 checkpoint、Session 生产图、Agently lab、前端。 +5. 与 30 commit `8f90cb5a`、40 commit(待回填)、R2 任务审核书面结论逐条核对。 +6. 50 复检书面结论落盘:`ai-ink-brain-api-python/docs/harness/reviews/task_ops_chat_session_sink_p0_p1_v1_audit_R1_20260709_50_P1-3.md`。 +7. 按 50-self-check.md 将结论与命令摘要回填至 task 正文「### 自检结论(50 复检)· P1-3」小节(不要覆盖 P0、P1-1、P1-2、30、40 已有结论)。 +8. 对话回复:给出是否建议合并、阻塞项清单、全局验收结论。 +9. 自动 commit:在输出最终结论且本轮 reviews/task 自检回填已落盘后,按 HANDOFF_AUTO_COMMIT.md 在 `ai-ink-brain-api-python/` commit(仅本轮路径;禁止 git add -A;对话报 short-hash)。 +10. **禁止**自行 push;由 Lead 合并。 + +**输出要求** +- 若拒开工:仅 Markdown 阻塞清单 +- 若执行:diff 摘要、验证命令输出、commit short-hash、合并建议、全局验收表 + +**Judgment(本帽 · 对话末尾必填)**:experience_capture / gate/risk / hat_self +``` From 1c3a206ddb57517fbfc2a58b12b7fd8e49723857 Mon Sep 17 00:00:00 2001 From: cyning Date: Thu, 9 Jul 2026 19:43:41 +0800 Subject: [PATCH 3/4] =?UTF-8?q?docs(harness):=2050=20prompt=20P1-3=20?= =?UTF-8?q?=E5=9B=9E=E5=A1=AB=2040=20commit=20hash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 50 下一棒 Prompt 中的 40 commit 占位符替换为实际 hash。 --- .../invoke_20260709_1938_50_ops_chat_session_sink_p0_p1_P1-3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_50_ops_chat_session_sink_p0_p1_P1-3.md b/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_50_ops_chat_session_sink_p0_p1_P1-3.md index daeb2835..54c33d20 100644 --- a/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_50_ops_chat_session_sink_p0_p1_P1-3.md +++ b/docs/harness/invokes/by-task/ops-chat-session-sink-p0-p1/invoke_20260709_1938_50_ops_chat_session_sink_p0_p1_P1-3.md @@ -25,7 +25,7 @@ pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops ``` - 上一棒 30 commit:`ai-ink-brain-api-python @ 8f90cb5a` -- 上一棒 40 commit:`ai-ink-brain-api-python @ <待本回合回填>` +- 上一棒 40 commit:`ai-ink-brain-api-python @ ea31eae8` - 关联任务审核书面结论路径:`ai-ink-brain-api-python/docs/harness/reviews/task_ops_chat_session_sink_p0_p1_v1_audit_R2_20260708.md` - 关联 PLAN / 总规:`docs/harness/guides/PLAN_ops_chat_session_sink_p0_p1_v1_zh.md` From cddc81057d35347b16668d224f7bd1d6a72837d8 Mon Sep 17 00:00:00 2001 From: cyning Date: Thu, 9 Jul 2026 19:49:44 +0800 Subject: [PATCH 4/4] docs(harness): 50 reinspect P1-3 clarify fallback routing + global acceptance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R1 review落盘 + task自检结论回填;验证命令310 passed/10 skipped + ruff全绿。 --- ...sink_p0_p1_v1_audit_R1_20260709_50_P1-3.md | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/harness/reviews/task_ops_chat_session_sink_p0_p1_v1_audit_R1_20260709_50_P1-3.md diff --git a/docs/harness/reviews/task_ops_chat_session_sink_p0_p1_v1_audit_R1_20260709_50_P1-3.md b/docs/harness/reviews/task_ops_chat_session_sink_p0_p1_v1_audit_R1_20260709_50_P1-3.md new file mode 100644 index 00000000..3cce7594 --- /dev/null +++ b/docs/harness/reviews/task_ops_chat_session_sink_p0_p1_v1_audit_R1_20260709_50_P1-3.md @@ -0,0 +1,134 @@ +# 50 Reinspect R1 · P1-3 Clarify 路由 + +| 项 | 内容 | +| --- | --- | +| **task** | `docs/harness/tasks/active/task_ops_chat_session_sink_p0_p1_v1.md` | +| **subproject** | `ai-ink-brain-api-python` | +| **phase** | P1-3 Clarify 路由:FALLBACK 先澄清 · 减少默认 `#545` | +| **reviewer** | 50 独立复检 + 全局验收帽 | +| **timestamp** | 2026-07-09 | +| **30 commit** | `ai-ink-brain-api-python @ 8f90cb5a` | +| **40 commit** | `ai-ink-brain-api-python @ ea31eae8` · `Projects @ 9cd2340` | + +## 复核方法 + +- 独立阅读 task 正文「### 自检结论(执行者)· P1-3」与「### 自检结论(40 复核)· P1-3」小节。 +- 独立阅读本轮 P1-3 改动代码: + - `api/ops/orchestrator/clarify.py` + - `api/ops/orchestrator/__init__.py` + - `api/ops/chat_service.py` + - `tests/ops/test_clarify.py` + - `tests/ops/test_chat_service.py` + - `tests/ops_desk/test_react_fallback.py` +- 在 `ai-ink-brain-api-python/` 内完整执行 30 声明的验证命令: + ```bash + pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q && ruff check api/ops + ``` +- 执行 `git diff origin/main...HEAD --stat`(在 `ai-ink-brain-api-python` 内)核对全量变更路径,确认未扩 scope 到 P1-4 LLM Router、P1-1 artifact、P1-2 checkpoint、Session 生产图、Agently lab、前端。 +- 与 30 commit `8f90cb5a`、40 commit `ea31eae8` / `Projects@9cd2340`、R2 任务审核书面结论逐条核对。 + +## 命令输出 + +```text +pytest tests/ops tests/ops_desk -m "not intent_eval and not intent_benchmark" -q +............................................s........................... [ 22%] +........................................................................ [ 45%] +........................................................................ [ 67%] +...................................ss......................sssssss...... [ 90%] +................................ [100%] +=============================== warnings summary ================================ +../../../miniconda3/lib/python3.13/site-packages/fastapi/testclient.py:1 + /Users/cyning/miniconda3/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/howto-capture-warnings.html +=========================== short test summary info ============================ +SKIPPED [1] tests/ops/test_events_schema.py:181: 需要真实 Supabase 连接;本地/CI 环境缺失时跳过 +SKIPPED [2] tests/ops_desk/test_run_schema_p1.py:102: public 中表已存在,跳过写测试以避免破坏数据 +SKIPPED [7] tests/ops_desk/test_schema_p0.py:102: public 中表已存在,跳过写测试以避免破坏数据 +310 passed, 10 skipped, 1 warning in 36.18s + +ruff check api/ops +All checks passed! +``` + +- pytest 退出码:`0`。 +- ruff 退出码:`0`。 +- 10 skipped 中:1 个为 `tests/ops/test_events_schema.py::test_append_event_integration_with_real_store`(显式 skip,需真实 Supabase 连接);其余 9 个为 `tests/ops_desk/test_run_schema_p1.py` / `tests/ops_desk/test_schema_p0.py` 中环境感知跳过(表已存在),与 P1-3 改动无关。 + +## 全量变更路径核对 + +```text + api/ops/chat_service.py | 50 +++- + api/ops/orchestrator/__init__.py | 3 + + api/ops/orchestrator/clarify.py | 134 +++++++++ + ...709_1921_30_ops_chat_session_sink_p0_p1_P1-3.md | 80 ++++++ + ...709_1921_40_ops_chat_session_sink_p0_p1_P1-3.md | 69 +++++ + ...709_1938_40_ops_chat_session_sink_p0_p1_P1-3.md | 59 +++++ + ...709_1938_50_ops_chat_session_sink_p0_p1_P1-3.md | 62 +++++ + tests/ops/test_chat_service.py | 308 +++++++++++++++++++++ + tests/ops/test_clarify.py | 168 +++++++++++ + tests/ops_desk/test_react_fallback.py | 11 + + 10 files changed, 941 insertions(+), 3 deletions(-) +``` + +- 代码变更仅涉及 `api/ops/orchestrator/clarify.py`、`api/ops/orchestrator/__init__.py`、`api/ops/chat_service.py`、测试文件。 +- 文档变更为本轮 invoke 快照,属 harness 落盘工件。 +- 未涉及 P1-4 LLM Router、P1-1 artifact、P1-2 checkpoint、`harness_runtime` 生产图、Agently lab、前端代码。 + +## 与 30 / 40 结论差异核对 + +| 30 / 40 声称项 | 50 独立复核 | 结果 | +| --- | --- | --- | +| 新增 `api/ops/orchestrator/clarify.py`:`clarify_if_fallback(query, session_id, transcript, slots)` | 文件存在;函数实现含 LLM 1 轮澄清、规则兜底、LLM 失败降级(`clarify.py:106-134`) | 一致 | +| `ClarifyResult` 含 `needs_clarification` / `clarify_question` / `intent` / `slots` | `clarify.py:24-36` dataclass 定义四字段 | 一致 | +| `api/ops/orchestrator/__init__.py` 导出 `ClarifyResult` / `clarify_if_fallback` | `__init__.py:5` 导入并 `__all__` 列出 | 一致 | +| `chat_service.py` FALLBACK 路由先 clarify 再路由 | `chat_service.py:129-178` 在 FALLBACK 分支调用 `load_chat_transcript` + `clarify_if_fallback`;需要澄清时返回 clarify 响应并记录 `clarify.asked`;否则按 fast/deep/react 路由 | 一致 | +| 减少默认 `#545` | FALLBACK 测例不再默认 issue #545;clarify 需要澄清时响应不含 issue_number;解析为具体 intent 时按补齐 slots 路由 | 一致 | +| 利用 P0-3 transcript 能力 | `chat_service.py:130` 调用 `load_chat_transcript`;`clarify.py:45-62` 与 `clarify.py:123` 将 transcript 拼入 LLM prompt | 一致 | +| clarify LLM 调用失败降级为 ReAct fallback | `clarify.py:132-134` 捕获异常返回 `_rule_fallback`;无多 issue 时返回 `intent=FALLBACK`;`test_clarify_degrades_to_react_on_llm_failure` 通过 | 一致 | +| 原有失败路径未被破坏 | artifact / checkpoint 相关测例全绿;`react_loop.py` 与 `store/artifacts.py` 未改动 | 一致 | +| 新增单测覆盖 | `tests/ops/test_clarify.py` 6 测例全绿;`tests/ops/test_chat_service.py` 5 测例全绿;`tests/ops_desk/test_react_fallback.py` 旁路 clarify 后 ReAct 行为覆盖仍通过 | 一致 | +| 最终验证命令绿 | 本轮独立跑通 `310 passed, 10 skipped` + `ruff check api/ops` 全绿 | 一致 | +| 未扩 scope | 全量 diff 仅 P1-3 clarify 相关文件 + invoke;未涉及 P1-4、P1-1、P1-2、Session 生产图、Agently lab、前端 | 一致 | + +**差异项**:无。 + +## 验收项复核表 + +| 验收项 | 状态 | 证据 | 备注 | +| --- | --- | --- | --- | +| `api/ops/orchestrator/clarify.py` 存在且实现 `clarify_if_fallback(query, session_id, transcript, slots)` | pass | 文件新增;LLM 1 轮澄清 + 规则兜底 + LLM 失败降级 | — | +| `ClarifyResult` 含 `needs_clarification` / `clarify_question` / `intent` / `slots` | pass | `clarify.py:24-36` | — | +| `api/ops/orchestrator/__init__.py` 导出 `ClarifyResult` / `clarify_if_fallback` | pass | `__init__.py:5` 与 `__all__` | — | +| `chat_service.py` FALLBACK 分支先 clarify 再按结果路由 | pass | `chat_service.py:129-178`;需要澄清时返回 `route=clarify` 并记录 `clarify.asked`;否则按 fast/deep/react 路由 | — | +| 减少默认 `#545` | pass | clarify 需要澄清时响应不含 issue_number;解析为具体 intent 时按补齐 slots 路由 | — | +| 复用 P0-3 transcript | pass | `chat_service.py:130` 调用 `load_chat_transcript`;`clarify.py` prompt 含 transcript | — | +| LLM 失败降级为 ReAct fallback | pass | `clarify.py:132-134`;`test_clarify_degrades_to_react_on_llm_failure` 通过 | — | +| 原有 artifact/checkpoint 失败路径未被破坏 | pass | 对应测例全绿;P1-1/P1-2 实现文件未改动 | — | +| `tests/ops/test_clarify.py` / `tests/ops/test_chat_service.py` 覆盖目标场景 | pass | 6 + 5 测例全绿 | — | +| `tests/ops_desk/test_react_fallback.py` 适配 clarify 旁路后仍覆盖 ReAct 行为 | pass | pytest 输出包含该文件且通过 | — | +| 最终验证命令绿 | pass | pytest `310 passed, 10 skipped` + ruff `All checks passed!`;退出码均为 `0` | — | +| 未静默扩大 scope | pass | 全量 diff 路径清单见上文 | 未改 P1-4、P1-1、P1-2、Session 生产图、Agently lab、前端 | + +## 阻塞项清单 + +无。 + +## 合并建议 + +**建议合并**。P1-3 Clarify 路由实现、测试、30 执行、40 自检、50 独立复检均通过,无 scope creep,人工闸 `HG-TASK-DRAFT` / `HG-AUDIT-R1` 已 approved。 + +## 执行路线与 Commit 回溯 + +| 阶段 | 帽子 | 关键动作 | 落盘工件 | 对应 commit | +|------|------|----------|----------|-------------| +| P1-3 | 30 execute | Clarify 模块 + chat_service FALLBACK 路由改造 + 测试 | `api/ops/orchestrator/clarify.py`, `api/ops/orchestrator/__init__.py`, `api/ops/chat_service.py`, `tests/ops/test_clarify.py`, `tests/ops/test_chat_service.py`, `tests/ops_desk/test_react_fallback.py` | `ai-ink-brain-api-python@8f90cb5a` | +| P1-3 | 40 self-check | 复核 P1-3 验收 | task 内 P1-3 30/40 自检结论 | `ai-ink-brain-api-python@ea31eae8` · `Projects@9cd2340` | +| P1-3 | 50 reinspect R1 | 独立复检 + 全局验收 | `ai-ink-brain-api-python/docs/harness/reviews/task_ops_chat_session_sink_p0_p1_v1_audit_R1_20260709_50_P1-3.md` | 待本审查落盘后 commit | + +## Judgment(50) + +- **experience_capture**: `required` — P1-3 clarify 路由模式(FALLBACK 先澄清、LLM 失败降级、transcript 复用)可复用到 P1-4 LLM Router 及后续多轮上下文场景。 +- **gate/risk**: 无 — `HG-TASK-DRAFT` / `HG-AUDIT-R1` 均为 `approved`;50 未遇 pending 人工闸。 +- **hat_self**: `pass` — 独立复检与 30/40 结论一致,验证命令绿,输出 pass/fail 表、阻塞项清单、合并建议与执行路线回溯。