Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions api/ops/chat_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
3 changes: 3 additions & 0 deletions api/ops/orchestrator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down
134 changes: 134 additions & 0 deletions api/ops/orchestrator/clarify.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
```
Original file line number Diff line number Diff line change
@@ -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
```
Loading
Loading