feat(roundtable): 第三步「数据入库」由结构化输出智能体自行调用技能
- 入库改为让 roundtable-structure 走完整 agent 运行时(ingest run policy) 自行挑选/读取/调用入库技能并传入 taskId,不再前端代调演示 - 新增 /api/flow-extract/ingest-eligibility(智能体判断当前业务场景是否有 合适入库技能,决定入库按钮是否显示)+ /api/multi-agent/structure/init - 入库前判断是否已生成流程图:无则发起「是否先生成流程图」用户协助 - 「知识库入库」更名为「数据入库」 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a85d2480b4
commit
03c155f8a7
10 files changed
+507
-80
No files matched your search
@@ -23,6 +23,7 @@ import type {
|
||||
} from "@/roundtable-planning/api/drafts";
|
||||
import {
|
||||
buildArtifactDownloadUrl,
|
||||
initStructureThread,
|
||||
initSummaryThread,
|
||||
listSessionArtifacts,
|
||||
streamMultiAgent,
|
||||
@@ -53,6 +54,8 @@ import { nowTime } from "@/roundtable-planning/lib/roundtable-constants";
|
||||
const SUMMARY_OUTPUT_PATH = "/mnt/user-data/outputs/方案总结报告.md";
|
||||
/** 内置单例 id —— 恢复草稿后续问时无 init 返回值,用它兜底拼 dAgentThreadId。 */
|
||||
const SUMMARY_AGENT_NAME = "roundtable-summary";
|
||||
/** 结构化抽取智能体 id —— 入库轮由它(走 ingest run policy)真正调用入库技能。 */
|
||||
const STRUCTURE_AGENT_NAME = "roundtable-structure";
|
||||
|
||||
export type SummaryPhase = "idle" | "creating" | "streaming" | "done" | "error";
|
||||
|
||||
@@ -89,6 +92,15 @@ interface UseStep3SummaryOptions {
|
||||
* 技能(技能必需信息)。非深链(无 taskId)时入库功能整体不出现。
|
||||
*/
|
||||
taskId?: string | null;
|
||||
/**
|
||||
* 入库适用性判定选中的技能名(来自 `/ingest-eligibility`)。入库时作为提示传给结构化
|
||||
* 输出智能体,引导它优先用这个技能(它仍会自行读 SKILL.md 判断);缺省时让它自行挑选。
|
||||
*/
|
||||
ingestSkill?: string | null;
|
||||
/** 业务码(如 6BF/rwfx-A)——入库时给智能体做场景线索,帮它挑对入库技能。 */
|
||||
ingestBusinessCode?: string | null;
|
||||
/** 业务链条名称——入库场景线索之一。 */
|
||||
ingestChainName?: string | null;
|
||||
}
|
||||
|
||||
/** 第三步入库状态机。 */
|
||||
@@ -150,15 +162,17 @@ export interface UseStep3SummaryReturn {
|
||||
resolveIngestChoice: (choice: "now" | "later" | "never") => void;
|
||||
/** 入库协助选项点选(开始入库/补充内容后再入库/只是提问→转交普通对话)。 */
|
||||
resolveIngestClarification: (choice: "start_ingest" | "add_data" | "other") => void;
|
||||
/** 手动入库(按钮 / 直接触发):在总结线程跑入库技能并显示步骤条。 */
|
||||
/**
|
||||
* 入库时尚无流程图 → 挂起的「是否先生成流程图」协助(非空 → 组件展示协助选项)。
|
||||
* 数据入库以结构化流程图为核心内容,没有流程图时先问用户要不要生成。
|
||||
*/
|
||||
pendingFlowForIngest: boolean;
|
||||
/** 「无流程图」协助点选:生成流程图(生成后再走入库询问)/ 暂不入库。 */
|
||||
resolveFlowForIngest: (choice: "generate" | "cancel") => void;
|
||||
/** 手动入库(按钮 / 直接触发):让结构化输出智能体走完整运行时自行调用入库技能。 */
|
||||
runIngest: (opts?: { note?: string }) => void;
|
||||
}
|
||||
|
||||
/** 演示用:步骤条之间的小停顿,让「调用入库技能」的过程看得见。 */
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/** 把研讨原料拼成一段(首轮 prompt 和「恢复后无线程续问」都用得到)。 */
|
||||
function buildMaterials(opts: {
|
||||
intentReady: ReportIntent | null;
|
||||
@@ -417,6 +431,26 @@ function summaryPhaseLabel(phase: StreamPhase, toolName?: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** 入库轮的工具阶段 → 中文步骤标签(让前端步骤条体现「读技能说明 → 执行入库」真实过程)。 */
|
||||
function ingestPhaseLabel(phase: StreamPhase, toolName?: string): string {
|
||||
switch (phase) {
|
||||
case "connecting":
|
||||
return "调度入库技能";
|
||||
case "connected":
|
||||
return "已连接,等待响应";
|
||||
case "tool_calling":
|
||||
if (toolName === "read_file") return "读取入库技能说明(SKILL.md)";
|
||||
if (toolName === "bash" || toolName === "write_file") return "调用入库技能执行入库";
|
||||
return toolName ? `调用工具:${toolName}` : "调用入库技能";
|
||||
case "tool_result":
|
||||
return "处理入库结果";
|
||||
case "streaming":
|
||||
return "汇报入库结果";
|
||||
default:
|
||||
return "入库中…";
|
||||
}
|
||||
}
|
||||
|
||||
export function useStep3Summary(
|
||||
options: UseStep3SummaryOptions,
|
||||
): UseStep3SummaryReturn {
|
||||
@@ -432,6 +466,9 @@ export function useStep3Summary(
|
||||
onSummarySaved,
|
||||
ingestAvailable,
|
||||
taskId: ingestTaskId,
|
||||
ingestSkill,
|
||||
ingestBusinessCode,
|
||||
ingestChainName,
|
||||
} = options;
|
||||
|
||||
const onSummarySavedRef = useRef(onSummarySaved);
|
||||
@@ -442,6 +479,8 @@ export function useStep3Summary(
|
||||
// ── 入库状态 ──
|
||||
const [ingestStatus, setIngestStatus] = useState<IngestStatus>("idle");
|
||||
const [pendingIngestClarification, setPendingIngestClarification] = useState<string | null>(null);
|
||||
// 入库时尚无流程图 → 挂起「是否先生成流程图」协助(数据入库以结构化流程图为核心内容)。
|
||||
const [pendingFlowForIngest, setPendingFlowForIngest] = useState<boolean>(false);
|
||||
// 镜像供 render 外的回调(runFlowExtract 完成、ask 拦截)同步读到最新值。
|
||||
const ingestStatusRef = useRef<IngestStatus>("idle");
|
||||
useEffect(() => {
|
||||
@@ -458,8 +497,17 @@ export function useStep3Summary(
|
||||
useEffect(() => {
|
||||
ingestTaskIdRef.current = ingestTaskId ?? null;
|
||||
}, [ingestTaskId]);
|
||||
// 入库演示运行序号:新一轮入库 / 中断会自增,旧循环据此自停(防重复入库)。
|
||||
// 入库运行序号:新一轮入库 / 中断会自增,旧循环据此自停(防重复入库)。
|
||||
const ingestRunRef = useRef(0);
|
||||
// 入库场景线索(适用性判定选中的技能 + 业务码/链名)——镜像供 runIngest 回调读最新值。
|
||||
const ingestSkillRef = useRef<string | null>(ingestSkill ?? null);
|
||||
const ingestBusinessCodeRef = useRef<string | null>(ingestBusinessCode ?? null);
|
||||
const ingestChainNameRef = useRef<string | null>(ingestChainName ?? null);
|
||||
useEffect(() => {
|
||||
ingestSkillRef.current = ingestSkill ?? null;
|
||||
ingestBusinessCodeRef.current = ingestBusinessCode ?? null;
|
||||
ingestChainNameRef.current = ingestChainName ?? null;
|
||||
}, [ingestSkill, ingestBusinessCode, ingestChainName]);
|
||||
|
||||
const {
|
||||
setArtifacts,
|
||||
@@ -1247,60 +1295,118 @@ export function useStep3Summary(
|
||||
}, [selectedModel]);
|
||||
|
||||
/**
|
||||
* 入库(示例 · 演示用):在**总结线程**上出一条「知识库入库」气泡,按
|
||||
* 调度 → 调用入库技能 →(补充内容)→ 提交 → 完成 的步骤条演示调用过程,与第二步同款。
|
||||
* 真实入库技能后续替换这里(改为真的跑技能 / 调入库接口)。
|
||||
* 入库(真执行):让**结构化输出智能体**(`roundtable-structure`,走 ingest run policy
|
||||
* 的完整 agent 运行时)**自己挑选合适的入库技能、读 SKILL.md 并调用**完成入库——
|
||||
* 而不是前端代它调技能。出一条「数据入库」气泡,实时显示它读技能说明 / 执行入库的
|
||||
* 真实工具步骤与最终汇报。
|
||||
*
|
||||
* 入库内容**以最后修改后的为准**:当前报告 md + 最新流程图(画布手动改也同步进来),
|
||||
* 并把 **taskId**(深链外部任务 id,入库技能必需信息)随消息一起传给智能体。
|
||||
*/
|
||||
const runIngest = useCallback(
|
||||
async (opts?: { note?: string }) => {
|
||||
// 未挂载入库技能(或非深链模式)→ 不执行任何入库操作。
|
||||
// 未挂载/不适用入库技能(或非深链模式)→ 不执行任何入库操作。
|
||||
if (!ingestAvailableRef.current) return;
|
||||
setPendingIngestClarification(null);
|
||||
// 数据入库以**结构化流程图**为核心内容。意图理解后先判断是否已生成流程图:
|
||||
// 没有 → 不入库,挂起「是否先生成流程图」协助;有 → 正常调用智能体入库。
|
||||
if (!flowGraphRef.current) {
|
||||
setPendingFlowForIngest(true);
|
||||
return;
|
||||
}
|
||||
setPendingFlowForIngest(false);
|
||||
setIngestStatus("running");
|
||||
const myRun = ++ingestRunRef.current;
|
||||
const aborter = new AbortController();
|
||||
const bubbleId = appendStreamingDialogue({
|
||||
sender: "知识库入库",
|
||||
sender: "数据入库",
|
||||
avatarType: "blue",
|
||||
agentThreadId: liveThreadRef.current ?? undefined,
|
||||
});
|
||||
const title = intentReady?.objective?.trim() || "会商方案";
|
||||
// 入库数据**以最后修改后的为准**:读 flowGraphRef.current(画布手动改图也会同步进来)
|
||||
// 与当前报告 md。taskId 是入库技能必需信息,必须随入库一起传给技能。
|
||||
const flow = flowGraphRef.current;
|
||||
const md = currentMdRef.current;
|
||||
const taskId = ingestTaskIdRef.current;
|
||||
const submitArgs: Record<string, unknown> = {
|
||||
title,
|
||||
taskId, // 技能必需:深链外部任务 id
|
||||
hasReport: !!md.trim(),
|
||||
flowNodes: flow?.nodes?.length ?? 0,
|
||||
flowEdges: flow?.edges?.length ?? 0,
|
||||
...(opts?.note ? { note: opts.note } : {}),
|
||||
};
|
||||
const steps: Array<{ label: string; key: string; args?: Record<string, unknown> }> = [
|
||||
{ label: "调度入库技能", key: "tool_calling:ingest_dispatch", args: { taskId } },
|
||||
{ label: "调用「知识库入库」技能", key: "tool_calling:ingest_skill", args: { skill: "knowledge-base-ingest", taskId } },
|
||||
...(opts?.note
|
||||
? [{ label: "并入补充内容", key: "tool_calling:ingest_append", args: { note: opts.note } }]
|
||||
: []),
|
||||
{ label: "提交方案数据入库(以最新流程图为准)", key: "tool_calling:ingest_submit", args: submitArgs },
|
||||
const skillHint = ingestSkillRef.current;
|
||||
const businessCode = ingestBusinessCodeRef.current;
|
||||
const chainName = ingestChainNameRef.current;
|
||||
const flowJson = flow ? serializeFlowGraph(flow) : "";
|
||||
|
||||
// 入库指令:把场景线索 + taskId + 报告 + 结构化流程图全量给智能体,让它按 SOUL 的
|
||||
// 「五、入库」流程自行挑技能、读 SKILL.md、按契约调用(taskId 必传)。
|
||||
const promptParts: string[] = [
|
||||
"【任务】本轮请执行「入库」:把本次会商成果沉淀进知识库。请按你的设定(SOUL「五、入库」)",
|
||||
"自行挑选**适合当前业务场景**的入库技能(技能名可能含「六步法」等场景线索),用 read_file 读它的",
|
||||
"SKILL.md,再严格按其契约调用完成入库。**taskId 是入库技能的必需信息,务必随入库一起传入。**",
|
||||
"",
|
||||
"【业务场景】",
|
||||
`- 方案标题:${title}`,
|
||||
];
|
||||
for (const s of steps) {
|
||||
if (ingestRunRef.current !== myRun) return; // 被新一轮入库 / 中断取代
|
||||
setDialogueConfidence(bubbleId, s.label);
|
||||
appendStepIfNew(bubbleId, s.label, s.key, s.args);
|
||||
await sleep(700);
|
||||
}
|
||||
if (ingestRunRef.current !== myRun) return;
|
||||
finalizeDialogue(
|
||||
bubbleId,
|
||||
`✅ 已将「${title}」入库到知识库(示例技能 · 演示用,后续接入真实入库技能)。`,
|
||||
"入库完成",
|
||||
if (businessCode) promptParts.push(`- 业务码:${businessCode}`);
|
||||
if (chainName) promptParts.push(`- 业务链名称:${chainName}`);
|
||||
if (skillHint) promptParts.push(`- 适用性判定建议优先使用的入库技能:${skillHint}(仍请你读其 SKILL.md 确认)`);
|
||||
promptParts.push(
|
||||
`- taskId(必传给入库技能):${taskId ?? "(未提供)"}`,
|
||||
"",
|
||||
"【入库内容(以下为最终、用户最后修改后的成果,入库以此为准)】",
|
||||
"—— 方案总结报告(Markdown)——",
|
||||
md.trim() || "(无报告正文)",
|
||||
"",
|
||||
"—— 结构化流程图(flow-json,含节点/连线/层级)——",
|
||||
flowJson || "(无结构化流程图)",
|
||||
);
|
||||
setIngestStatus("done");
|
||||
persistSummary(); // 把入库气泡 + ingestStatus 落进草稿
|
||||
if (opts?.note?.trim()) {
|
||||
promptParts.push("", "【用户补充内容(请一并纳入入库)】", opts.note.trim());
|
||||
}
|
||||
const prompt = promptParts.join("\n");
|
||||
|
||||
try {
|
||||
const init = await initStructureThread({ signal: aborter.signal });
|
||||
if (ingestRunRef.current !== myRun || aborter.signal.aborted) return;
|
||||
const threadId = init.threadId;
|
||||
const agentName = init.agentName;
|
||||
setDialogues((prev) =>
|
||||
prev.map((d) => (d.id === bubbleId ? { ...d, agentThreadId: threadId } : d)),
|
||||
);
|
||||
const res = await streamMultiAgent({
|
||||
agentType: "special",
|
||||
agentName,
|
||||
dAgentThreadId: { [agentName]: threadId },
|
||||
displayName: "数据入库",
|
||||
newMessage: prompt,
|
||||
model: selectedModel || undefined,
|
||||
signal: aborter.signal,
|
||||
onTextDelta: (text) => {
|
||||
if (ingestRunRef.current === myRun) appendToDialogue(bubbleId, text);
|
||||
},
|
||||
onPhaseChange: (p, toolName) => {
|
||||
if (ingestRunRef.current !== myRun) return;
|
||||
const label = ingestPhaseLabel(p, toolName);
|
||||
setDialogueConfidence(bubbleId, label);
|
||||
appendStepIfNew(bubbleId, label, `${p}:${toolName ?? ""}`);
|
||||
},
|
||||
});
|
||||
if (ingestRunRef.current !== myRun || aborter.signal.aborted) return;
|
||||
finalizeDialogue(bubbleId, res.content || `✅ 已完成「${title}」数据入库。`, "入库完成");
|
||||
setIngestStatus("done");
|
||||
persistSummary(); // 把入库气泡 + ingestStatus 落进草稿
|
||||
} catch (err) {
|
||||
if (ingestRunRef.current !== myRun || aborter.signal.aborted) return;
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
finalizeDialogue(bubbleId, `入库失败:${msg}`, "入库失败");
|
||||
setIngestStatus("asking"); // 失败回到可重试(按钮/询问仍在)
|
||||
}
|
||||
},
|
||||
[appendStreamingDialogue, appendStepIfNew, setDialogueConfidence, finalizeDialogue, intentReady, persistSummary],
|
||||
[
|
||||
appendStreamingDialogue,
|
||||
appendToDialogue,
|
||||
appendStepIfNew,
|
||||
setDialogueConfidence,
|
||||
finalizeDialogue,
|
||||
setDialogues,
|
||||
selectedModel,
|
||||
intentReady,
|
||||
persistSummary,
|
||||
],
|
||||
);
|
||||
|
||||
/** 流程图后入库询问三选一。 */
|
||||
@@ -1319,6 +1425,26 @@ export function useStep3Summary(
|
||||
[runIngest, persistSummary],
|
||||
);
|
||||
|
||||
/**
|
||||
* 「入库时无流程图」协助点选:
|
||||
* - generate → 关闭协助、生成流程图(生成完成后会自动弹出「是否入库」询问,再由用户入库);
|
||||
* - cancel → 关闭协助、置「稍后入库」(手动入库按钮仍在,可生成流程图后再入库)。
|
||||
*/
|
||||
const resolveFlowForIngest = useCallback(
|
||||
(choice: "generate" | "cancel") => {
|
||||
setPendingFlowForIngest(false);
|
||||
if (choice === "generate") {
|
||||
// 重置为 idle —— 流程图生成完成后 runFlowExtract 在 idle 时会自动询问入库。
|
||||
setIngestStatus("idle");
|
||||
regenerateFlow();
|
||||
} else {
|
||||
setIngestStatus("later");
|
||||
persistSummary();
|
||||
}
|
||||
},
|
||||
[regenerateFlow, persistSummary],
|
||||
);
|
||||
|
||||
const ask = useCallback(
|
||||
async (message: string, files?: File[], opts?: { forcedIntent?: FlowIntent; skipIngest?: boolean }) => {
|
||||
const text = message.trim();
|
||||
@@ -1706,6 +1832,8 @@ export function useStep3Summary(
|
||||
pendingIngestClarification,
|
||||
resolveIngestChoice,
|
||||
resolveIngestClarification,
|
||||
pendingFlowForIngest,
|
||||
resolveFlowForIngest,
|
||||
runIngest: (opts?: { note?: string }) => void runIngest(opts),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user