feat(roundtable): xdfx 行动名喂入 summary/抽取 + 第三步意图理解增强
- xdfx:把本次「行动名称」喂进 summary 材料(buildMaterials 加 actionName) 与结构化抽取 材料(flow_extract._material 加【本次行动】抬头),让报告/流程图都贴着该行动展开 - 第三步意图识别(classify_flow_intent)增强:新增 can_ingest 上下文 + ingest 意图;重写 _INTENT_SYS 走语义理解——「介绍/解释/为什么/总结一下」等对内容的提问→qa(文字答); 「帮我入库」(可入库时)→ingest→直接入库;能判断就给具体意图不轻易 unclear; 不可入库时误判 ingest 自动降级 qa。前端 classifyFlowIntent 传 canIngest,ask 新增 ingest 分支 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
fba417918f
commit
ae2706dad3
3 files changed
+73
-21
No files matched your search
@@ -12,7 +12,7 @@ import { getBackendBaseURL } from "@/core/config";
|
||||
import type { FlowGraphData } from "@/roundtable-planning/lib/flow-graph";
|
||||
|
||||
/** 第三步输入框意图分类结果(与后端 /intent 对应)。 */
|
||||
export type FlowIntent = "regenerate_report" | "modify_flow" | "regenerate_flow" | "qa" | "unclear";
|
||||
export type FlowIntent = "regenerate_report" | "modify_flow" | "regenerate_flow" | "ingest" | "qa" | "unclear";
|
||||
|
||||
/**
|
||||
* 识别第三步输入框消息的意图(一次模型调用)。失败回退 "unclear"(前端发起澄清)。
|
||||
@@ -22,6 +22,8 @@ export async function classifyFlowIntent(req: {
|
||||
message: string;
|
||||
hasReport: boolean;
|
||||
hasFlow: boolean;
|
||||
/** 当前是否可入库(入库按钮已出现、未入库);为真才允许识别出 ingest 意图。 */
|
||||
canIngest?: boolean;
|
||||
model?: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<{ intent: FlowIntent; reason: string }> {
|
||||
@@ -33,6 +35,7 @@ export async function classifyFlowIntent(req: {
|
||||
message: req.message,
|
||||
has_report: req.hasReport,
|
||||
has_flow: req.hasFlow,
|
||||
can_ingest: req.canIngest ?? false,
|
||||
model: req.model,
|
||||
}),
|
||||
signal: req.signal,
|
||||
|
||||
@@ -180,10 +180,19 @@ function buildMaterials(opts: {
|
||||
seatDeliveries: ReportSeatDelivery[];
|
||||
consensusText: string;
|
||||
coordinatorPrompt?: string | null;
|
||||
/** xdfx:本次「行动名称」,作为材料抬头喂进总结(报告围绕该行动展开)。 */
|
||||
actionName?: string | null;
|
||||
}): string {
|
||||
const { intentReady, seatDeliveries, consensusText, coordinatorPrompt } = opts;
|
||||
const { intentReady, seatDeliveries, consensusText, coordinatorPrompt, actionName } = opts;
|
||||
const parts: string[] = [];
|
||||
|
||||
const an = actionName?.trim();
|
||||
if (an) {
|
||||
parts.push("# 本次行动");
|
||||
parts.push(an);
|
||||
parts.push("");
|
||||
}
|
||||
|
||||
const cp = coordinatorPrompt?.trim();
|
||||
if (cp) {
|
||||
parts.push("# 业务链总控编排提示(最高优先级,按它的关注点/结构/口径来组织报告)");
|
||||
@@ -295,6 +304,7 @@ function buildSummaryPrompt(opts: {
|
||||
seatDeliveries: ReportSeatDelivery[];
|
||||
consensusText: string;
|
||||
coordinatorPrompt?: string | null;
|
||||
actionName?: string | null;
|
||||
flowMode?: boolean;
|
||||
}): string {
|
||||
const parts: string[] = [];
|
||||
@@ -1262,6 +1272,9 @@ export function useStep3Summary(
|
||||
seatDeliveries,
|
||||
consensusText,
|
||||
coordinatorPrompt,
|
||||
// xdfx(7BF):把本次行动名称喂进总结材料,让报告围绕该行动展开。
|
||||
actionName:
|
||||
ingestBusinessCodeRef.current === "7BF" ? readActionDetail()?.actionName ?? null : null,
|
||||
flowMode,
|
||||
});
|
||||
await streamOnce({ bubbleId, prompt, aborter, flowRound: flowMode });
|
||||
@@ -1518,10 +1531,17 @@ export function useStep3Summary(
|
||||
if (!intent) {
|
||||
// **立刻**显示「智能体调度中」气泡(不等意图接口返回),消除连接卡顿时的"卡住"感。
|
||||
dispatchBubbleId = appendStreamingDialogue({ sender: "智能体", avatarType: "blue" });
|
||||
// 入库可用(按钮已出现、未入库 / 未在跑)时把 canIngest 传给意图识别——让「帮我入库」被识别为
|
||||
// ingest 而不是落到 qa/unclear(之前主分类器不懂入库,只有「稍后入库待命」时才识别入库,是个缺口)。
|
||||
const canIngestNow =
|
||||
ingestAvailableRef.current &&
|
||||
ingestStatusRef.current !== "running" &&
|
||||
ingestStatusRef.current !== "done";
|
||||
const r = await classifyFlowIntent({
|
||||
message: text,
|
||||
hasReport: !!currentMdRef.current.trim(),
|
||||
hasFlow: !!flowGraphRef.current,
|
||||
canIngest: canIngestNow,
|
||||
model: selectedModel || undefined,
|
||||
});
|
||||
intent = r.intent;
|
||||
@@ -1533,6 +1553,12 @@ export function useStep3Summary(
|
||||
setPendingFlowClarification(text); // 组件渲染选项让用户自己定
|
||||
return;
|
||||
}
|
||||
if (intent === "ingest") {
|
||||
// 识别为入库(后端已保证仅 can_ingest 时才会返回 ingest)→ 直接入库;复用调度气泡。
|
||||
if (dispatchBubbleId != null) finalizeDialogue(dispatchBubbleId, "正在入库…", "入库中");
|
||||
await runIngest();
|
||||
return;
|
||||
}
|
||||
if (intent === "regenerate_flow" || intent === "modify_flow") {
|
||||
const flowAborter = new AbortController();
|
||||
aborterRef.current = flowAborter;
|
||||
|
||||
@@ -78,11 +78,14 @@ class FlowIntentRequest(BaseModel):
|
||||
message: str = ""
|
||||
has_report: bool = False
|
||||
has_flow: bool = False
|
||||
# 当前是否可入库(结构化抽取智能体已判定有适配本场景的入库技能、入库按钮已出现且未入库)。
|
||||
# 为真时才允许识别出 ingest 意图——否则「帮我入库」回退为 qa/unclear。
|
||||
can_ingest: bool = False
|
||||
model: str | None = Field(default=None)
|
||||
|
||||
|
||||
# 意图枚举(与前端路由一一对应)。
|
||||
_INTENTS = {"regenerate_report", "modify_flow", "regenerate_flow", "qa", "unclear"}
|
||||
# 意图枚举(与前端路由一一对应)。ingest 仅在 can_ingest=true 时可用。
|
||||
_INTENTS = {"regenerate_report", "modify_flow", "regenerate_flow", "ingest", "qa", "unclear"}
|
||||
|
||||
|
||||
# ── flow-json 容错解析(Python 版,对齐前端 lib/flow-graph.ts)──────────────
|
||||
@@ -434,13 +437,16 @@ _STRUCTURE = (
|
||||
|
||||
|
||||
def _material(req: FlowExtractRequest) -> str:
|
||||
# xdfx:把本次「行动名称」作为材料抬头喂进去(根节点就是它),让抽取贴着这条行动展开。
|
||||
action = (req.root_label or "").strip()
|
||||
prefix = f"【本次行动】{action}\n\n" if action else ""
|
||||
report = (req.report_md or "").strip()
|
||||
if report:
|
||||
return "===== 方案总结报告全文(数据来源,逐条抽取,勿改写)=====\n" + report + "\n===== 报告结束 ====="
|
||||
return prefix + "===== 方案总结报告全文(数据来源,逐条抽取,勿改写)=====\n" + report + "\n===== 报告结束 ====="
|
||||
seats = [s for s in req.seat_deliveries if (s.content or "").strip()]
|
||||
if seats:
|
||||
body = "\n\n".join(f"## 席位{i + 1}:{s.sender or '(未具名席位)'}\n{s.content.strip()}" for i, s in enumerate(seats))
|
||||
return "===== 各席位交付产物(数据来源,逐条抽取,勿改写)=====\n" + body + "\n===== 交付结束 ====="
|
||||
return prefix + "===== 各席位交付产物(数据来源,逐条抽取,勿改写)=====\n" + body + "\n===== 交付结束 ====="
|
||||
return ""
|
||||
|
||||
|
||||
@@ -703,23 +709,33 @@ def _parse_intent_json(text: str) -> dict[str, Any] | None:
|
||||
|
||||
|
||||
_INTENT_SYS = (
|
||||
"你是圆桌「方案总结」第三步输入框的**意图识别器**。第三步有两类产物:方案总结报告(Markdown) 和 结构化流程图。"
|
||||
"用户在输入框发了一条消息,请判断他的**真实意图**,只输出一个 JSON 对象(不要解释、不要 markdown 代码块、用英文双引号):\n"
|
||||
"你是圆桌「方案总结」第三步对话的**意图识别器**,也是个能帮用户的助手。第三步已产出:方案总结报告(Markdown)、"
|
||||
"结构化流程图,可能还可把成果入库到知识库。用户在对话框发了一条消息,请**按语义理解他真正想干什么**"
|
||||
"(不要只做关键词匹配),只输出一个 JSON 对象(不要解释、不要 markdown 代码块、用英文双引号):\n"
|
||||
'{"intent":"<枚举>","reason":"<一句话依据>"}\n'
|
||||
"intent 只能取其一:\n"
|
||||
"- regenerate_report:想重新分析 / 重写【方案总结报告】(如「重新生成报告」「报告再写一版」「重新分析一下」);\n"
|
||||
"- modify_flow:想修改**当前流程图**的某些节点/连线(如「把目的1改成X」「删掉第3个行为体」「加个验收节点」「流程图这里调整下」);\n"
|
||||
"intent 取值:\n"
|
||||
"- regenerate_report:想**重做/重写整份**方案总结报告(如「重新生成报告」「报告再写一版」「重新分析一下」);\n"
|
||||
"- modify_flow:想**修改当前流程图**的节点/连线(如「把目的1改成X」「删掉第3个行为体」「加个验收节点」「这一层调整下」);\n"
|
||||
"- regenerate_flow:想**重新生成整张**流程图(如「重新画流程图」「流程图重来」「重新结构化输出」);\n"
|
||||
"- qa:只是提问 / 咨询,不需要改任何产物(如「为什么选 OTA 平台」「这个目的什么意思」);\n"
|
||||
"- unclear:意图不明确、无法判断要改报告还是改流程图还是提问(如只说「改一下」「不对」「重来」而没指明对象)。\n"
|
||||
"判断要点:结合下面给出的上下文(是否已有报告 / 是否已有流程图);当前已有流程图时,含「节点/连线/这一层/这个目的/行为体…改/删/加」之类多半是 modify_flow;"
|
||||
"明确提到「报告」才是 regenerate_report;只有完全无法判断对象时才用 unclear。\n"
|
||||
"- ingest:想把本次成果**入库/沉淀/存进知识库**(如「帮我入库」「入库吧」「把方案存进知识库」「沉淀一下」);\n"
|
||||
"- qa:**提问 / 要求解释、介绍、概述、答疑**,只需用文字回答、不改任何产物(如「介绍一下这报告的内容」"
|
||||
"「这方案主要讲了啥」「为什么选 OTA 平台」「风险最高的是哪条」「帮我看看有没有遗漏」「总结成几句话」);\n"
|
||||
"- unclear:**实在**判断不出他想做哪件事(如只说「改一下」「不对」「重来」「再说」且无任何指向)。\n"
|
||||
"判断要点:\n"
|
||||
"1. **优先识别出明确意图,不要动不动就 unclear**——只要能合理判断就给具体意图;尤其『介绍/讲讲/解释/说明/"
|
||||
"为什么/是什么/有没有/帮我看/概述/总结一下』这类是**对内容的提问** → 一律 qa(用文字答),不要误判成重做报告。\n"
|
||||
"2. 含『节点/连线/这一层/这个目的/行为体/流程图…的 改/删/加/换』 → modify_flow(前提是已有流程图)。\n"
|
||||
"3. 明确说『报告』要重写/重做才是 regenerate_report;只是问报告内容是 qa。\n"
|
||||
"4. **入库类意图只有在下文上下文标注「可入库=是」时才允许返回 ingest**;若标注不可入库,用户提入库就归 qa"
|
||||
"(用文字说明当前不可入库/无适配技能)。\n"
|
||||
"示例:\n"
|
||||
'「再帮我生成一次报告」→ {"intent":"regenerate_report","reason":"明确要求重新生成报告"}\n'
|
||||
'「把目的1的名称改短点」→ {"intent":"modify_flow","reason":"要求修改流程图某节点"}\n'
|
||||
'「流程图重新画一张」→ {"intent":"regenerate_flow","reason":"要求重画整张流程图"}\n'
|
||||
'「OTA 平台为什么是直接行为体」→ {"intent":"qa","reason":"纯提问不改产物"}\n'
|
||||
'「改一下」→ {"intent":"unclear","reason":"没说改报告还是流程图"}'
|
||||
'「帮我介绍一下这报告中的内容」→ {"intent":"qa","reason":"要求介绍报告内容,属提问答疑"}\n'
|
||||
'「这方案为什么这么定」→ {"intent":"qa","reason":"询问方案理由"}\n'
|
||||
'「帮我入库」(可入库=是) → {"intent":"ingest","reason":"明确要求入库且当前可入库"}\n'
|
||||
'「再帮我生成一次报告」→ {"intent":"regenerate_report","reason":"明确要求重做报告"}\n'
|
||||
'「把目的1的名称改短点」→ {"intent":"modify_flow","reason":"修改流程图某节点"}\n'
|
||||
'「流程图重新画一张」→ {"intent":"regenerate_flow","reason":"重画整张流程图"}\n'
|
||||
'「改一下」→ {"intent":"unclear","reason":"没说改什么"}'
|
||||
)
|
||||
|
||||
|
||||
@@ -729,7 +745,11 @@ async def classify_flow_intent(request: Request, payload: FlowIntentRequest = Bo
|
||||
msg = (payload.message or "").strip()
|
||||
if not msg:
|
||||
return {"intent": "unclear", "reason": "空消息"}
|
||||
ctx = f"当前上下文:{'已生成报告' if payload.has_report else '尚无报告'};{'已生成流程图' if payload.has_flow else '尚无流程图'}。"
|
||||
ctx = (
|
||||
f"当前上下文:{'已生成报告' if payload.has_report else '尚无报告'};"
|
||||
f"{'已生成流程图' if payload.has_flow else '尚无流程图'};"
|
||||
f"可入库={'是' if payload.can_ingest else '否'}。"
|
||||
)
|
||||
try:
|
||||
model = create_chat_model(name=_resolve_model(payload.model), thinking_enabled=False)
|
||||
res = await model.ainvoke(
|
||||
@@ -740,6 +760,9 @@ async def classify_flow_intent(request: Request, payload: FlowIntentRequest = Bo
|
||||
intent = _as_str(obj.get("intent")).lower()
|
||||
if intent not in _INTENTS:
|
||||
intent = "unclear"
|
||||
# 不可入库时模型若误判 ingest → 降级为 qa(用文字说明当前不可入库),不要触发入库。
|
||||
if intent == "ingest" and not payload.can_ingest:
|
||||
intent = "qa"
|
||||
return {"intent": intent, "reason": _as_str(obj.get("reason"))}
|
||||
except Exception: # noqa: BLE001 — 分类失败不致命,回退澄清让用户自己定
|
||||
logger.warning("flow intent classify failed", exc_info=True)
|
||||
|
||||
Reference in new issue
Block a user