feat(roundtable): 深链接业务链抽取规范 + 总结报告文件干净/对话强制核对

深链接会商(taskId+goPath,businessCode)下按所选业务链完整层级强制校正
结构化抽取(flow-json) 与 方案总结报告:

- 新增 harness 层 extraction_spec(app+harness 共用,单份领域知识两角色用法):
  build_structure_directive / build_summary_directive 按 businessCode 取
  · rwfx(6BF):线性 任务→目的→行为体→预期行为→驱动因素→叙事;预期行为↔驱动因素 1:1、余 1:N
  · xdfx(7BF):分叉汇聚 DAG,根=行动;行动→受众目标/关键传播节点(1:N)→脆弱点(1:N)→策略(多对一)
  · 其余业务通用规范(强约束完整性+命名+取数、不套固定模板);business_code 空→零副作用
- 结构化抽取:flow_extract._hierarchy_hint + _validate_messages 用 structure_directive
  权威覆盖默认层级(根治"无论什么业务都按目的→行为体抽/停在行为体");前端 streamFlowExtract
  传 business_code(源 ingestBusinessCode)
- 方案总结报告:前台 multi_agent._special_run(summary) + 后台 build_summary_prompt 注入
  business 完整性核对(StartJobRequest/StartParams 透传)
- 报告产物策略:.md 报告文件保持完整、干净、无需修改(不写 待补充/不足/⚠️);
  「核对说明」(意图对齐+业务链完整性+不足/可补充)改为只在对话回复正文、且每次必出
- 种子 roundtable-structure SOUL 层级顺序改 spec-driven、去 rwfx 写死示例

Tests: test_roundtable_extraction_spec / test_roundtable_summary_prompt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-20 18:13:03 +08:00
1 parent 3ab674e438
commit b1b1ff98f5
17 files changed
+378 -25

No files matched your search

@@ -82,6 +82,8 @@ export interface FlowExtractRequest {
chainLevels?: string[]; chainLevels?: string[];
/** 业务链总控编排提示(写明层级时优先用它)。 */ /** 业务链总控编排提示(写明层级时优先用它)。 */
coordinatorPrompt?: string | null; coordinatorPrompt?: string | null;
/** 深链接业务码(rwfx→6BF 等)。非空 → 后端按业务链抽取规范强制注入完整层级。 */
businessCode?: string | null;
/** 推理模型名(沿用第三步所选)。 */ /** 推理模型名(沿用第三步所选)。 */
model?: string; model?: string;
/** Phase 2 问答修改:当前流程图 + 指令 → 按指令改图(其余不变)。 */ /** Phase 2 问答修改:当前流程图 + 指令 → 按指令改图(其余不变)。 */
@@ -109,6 +111,7 @@ export async function streamFlowExtract(req: FlowExtractRequest): Promise<FlowGr
seat_deliveries: (req.seatDeliveries ?? []).map((s) => ({ sender: s.sender, content: s.content })), seat_deliveries: (req.seatDeliveries ?? []).map((s) => ({ sender: s.sender, content: s.content })),
chain_levels: req.chainLevels ?? [], chain_levels: req.chainLevels ?? [],
coordinator_prompt: req.coordinatorPrompt ?? null, coordinator_prompt: req.coordinatorPrompt ?? null,
business_code: req.businessCode ?? null,
model: req.model, model: req.model,
current_graph: req.currentGraph ?? null, current_graph: req.currentGraph ?? null,
instruction: req.instruction ?? null, instruction: req.instruction ?? null,
@@ -240,6 +240,11 @@ export interface StreamMultiAgentRequest {
* 与该席位 agent 自身 config.yaml 的同名开关取 OR。省略 = false(关)。 * 与该席位 agent 自身 config.yaml 的同名开关取 OR。省略 = false(关)。
*/ */
seatSkillDirective?: boolean; seatSkillDirective?: boolean;
/**
* 深链接业务码(rwfx→6BF 等)。**仅方案总结报告(summary)单例消费**:非空 → 后端按业务链抽取规范
* 强制注入「完整性核对」指令,让总结核对产出相对业务链完不完整。普通会商省略 = 不注入。
*/
businessCode?: string | null;
skillStopNames?: string[]; skillStopNames?: string[];
/** /**
* 仅 leader 用。置 true 表示这是「派活后让总控判断继续/综合」的轮次:后端会把各已交付 * 仅 leader 用。置 true 表示这是「派活后让总控判断继续/综合」的轮次:后端会把各已交付
@@ -493,6 +498,8 @@ export async function streamMultiAgent(
subagent_enabled: req.subagentEnabled, subagent_enabled: req.subagentEnabled,
// 仅 special 用;会商技能识别强化(沿用所选业务链条开关)。省略 → 后端按 false(关)处理。 // 仅 special 用;会商技能识别强化(沿用所选业务链条开关)。省略 → 后端按 false(关)处理。
seat_skill_directive: req.seatSkillDirective ?? false, seat_skill_directive: req.seatSkillDirective ?? false,
// 仅 summary 单例用;深链接业务码 → 后端注入业务链完整性核对。省略/空 → 不注入。
business_code: req.businessCode ?? null,
skill_stop_names: req.skillStopNames ?? [], skill_stop_names: req.skillStopNames ?? [],
// 仅 leader 用;综合轮让后端注入各席位完整交付正文。省略 → 后端按 false 处理。 // 仅 leader 用;综合轮让后端注入各席位完整交付正文。省略 → 后端按 false 处理。
synthesis_mode: req.synthesisMode ?? false, synthesis_mode: req.synthesisMode ?? false,
@@ -108,6 +108,8 @@ function toStartBody(p: StartJobPayload) {
), ),
// 研讨前并行取数开关(来源链条 gatherFirst):true = 后台研讨前先并行取数再研讨。缺省 false。 // 研讨前并行取数开关(来源链条 gatherFirst):true = 后台研讨前先并行取数再研讨。缺省 false。
gather_first: p.gatherFirst ?? false, gather_first: p.gatherFirst ?? false,
// 深链接业务码:后台总结报告据此做业务链完整性核对(写实业务如 rwfx 才生效)。空 → 不注入。
business_code: p.businessCode ?? null,
...(seat ...(seat
? { ? {
thinking_enabled: seat.thinking_enabled, thinking_enabled: seat.thinking_enabled,
@@ -321,8 +321,10 @@ function buildSummaryPrompt(opts: {
"【动笔前必做:意图对齐与缺口核对】各席位可能由能力较弱的模型产出,未必扣住用户意图、未必充分。" + "【动笔前必做:意图对齐与缺口核对】各席位可能由能力较弱的模型产出,未必扣住用户意图、未必充分。" +
"你是这份总结的质量兜底,不能只把各席位交付简单拼接。请先把上面的「任务意图」(目标/约束/关键假设)拆成" + "你是这份总结的质量兜底,不能只把各席位交付简单拼接。请先把上面的「任务意图」(目标/约束/关键假设)拆成" +
"用户核心诉求清单(编排提示对最终产出的要求也并入),逐条核对各席位交付+共识是否覆盖、够不够、有无跑题或矛盾;" + "用户核心诉求清单(编排提示对最终产出的要求也并入),逐条核对各席位交付+共识是否覆盖、够不够、有无跑题或矛盾;" +
"再据此组织并按护栏补强:① 能从已有原料重组/推理补上的→直接补全讲透;② 属于你自己的推断/建议→显式标注「(综合推断)」;" + "再据此组织:① 能从已有原料重组/推理补上的→直接补全讲透;② 缺真实信息补不了的→绝不凭空编。" +
"③ 缺真实信息、靠现有原料补不了的→绝不凭空编,用一行『> ⚠️ 待补充:<具体缺什么>。可在对话里让我补全』标出,交给用户。" + "**写进 .md 报告文件的必须是一份完整、干净、可直接使用、无需用户修改的报告——不要在报告文件里写「待补充/不足/⚠️」之类提示或占位**。" +
"**但在对话回复正文、报告之后,必须追加一段简短「核对说明」**(只在对话里、**不写进 .md 文件**):逐条说明产出是否覆盖了用户意图、哪些充分/哪些不足/有哪些可补充(可在对话里让我补全);" +
"**即便都已覆盖、无明显缺口,也要明确写一句「已核对:各诉求均已覆盖,无明显缺口」,这段核对说明任何时候都不要省略。**" +
"最终产出要明显比各席位交付的简单拼接更扣用户意图。", "最终产出要明显比各席位交付的简单拼接更扣用户意图。",
); );
if (opts.flowMode) { if (opts.flowMode) {
@@ -1041,6 +1043,8 @@ export function useStep3Summary(
dAgentThreadId: { [agentName]: threadId }, dAgentThreadId: { [agentName]: threadId },
displayName: "方案总结报告", displayName: "方案总结报告",
newMessage: prompt, newMessage: prompt,
// 深链接业务码:后端为 summary 单例注入「业务链完整性核对」(核对产出相对业务链完不完整)。
businessCode: ingestBusinessCodeRef.current,
model: selectedModel || undefined, model: selectedModel || undefined,
files: files && files.length > 0 ? files : undefined, files: files && files.length > 0 ? files : undefined,
signal: aborter.signal, signal: aborter.signal,
@@ -1136,6 +1140,8 @@ export function useStep3Summary(
seatDeliveries, seatDeliveries,
chainLevels, chainLevels,
coordinatorPrompt, coordinatorPrompt,
// 深链接业务码:后端按业务链抽取规范强制注入完整层级(根治"无论什么业务都按目的→行为体抽")。
businessCode: ingestBusinessCodeRef.current,
model: selectedModel || undefined, model: selectedModel || undefined,
currentGraph: opts?.currentGraph ?? null, currentGraph: opts?.currentGraph ?? null,
instruction: opts?.instruction, instruction: opts?.instruction,
@@ -147,6 +147,11 @@ export interface StartJobPayload {
* 在 `toStartBody` 里下发为后端 `gather_first`。缺省 false。 * 在 `toStartBody` 里下发为后端 `gather_first`。缺省 false。
*/ */
gatherFirst?: boolean; gatherFirst?: boolean;
/**
* 深链接业务码(rwfx→6BF 等)。透传给后台总结报告的「业务链完整性核对」(写实业务如 rwfx 才生效)。
* 普通会商省略。`toStartBody` 里下发为后端 `business_code`。
*/
businessCode?: string | null;
} }
/** 终态集合——SSE/轮询判定作业是否结束。 */ /** 终态集合——SSE/轮询判定作业是否结束。 */
@@ -1546,6 +1546,7 @@ export default function RoundtablePlanningPage() {
seatSkillDirectives, // 每席位技能识别强化沿用所选业务链条里各席位的开关 seatSkillDirectives, // 每席位技能识别强化沿用所选业务链条里各席位的开关
seatModes, // 每席位推理深度覆盖沿用所选业务链条里各席位的配置 seatModes, // 每席位推理深度覆盖沿用所选业务链条里各席位的配置
gatherFirst, // 研讨前并行取数沿用所选业务链条的开关 gatherFirst, // 研讨前并行取数沿用所选业务链条的开关
businessCode: prefillRef.current?.businessCode ?? undefined, // 深链接业务码 → 后台总结业务链完整性核对
// 后台作业引擎支持 dag(后端 _run_dag:stage 串行 / stage 内并行)—— // 后台作业引擎支持 dag(后端 _run_dag:stage 串行 / stage 内并行)——
// 直接把当前编排模式透传,业务链的分层 / 并行 / 阶段目标 / 总控提示词全部保留。 // 直接把当前编排模式透传,业务链的分层 / 并行 / 阶段目标 / 总控提示词全部保留。
orchestrationMode, orchestrationMode,
@@ -1628,6 +1629,7 @@ export default function RoundtablePlanningPage() {
seatSkillDirectives: seatSkillDirectivesSnap, seatSkillDirectives: seatSkillDirectivesSnap,
seatModes: seatModesSnap, seatModes: seatModesSnap,
gatherFirst: gatherFirstSnap, gatherFirst: gatherFirstSnap,
businessCode: prefillRef.current?.businessCode ?? undefined,
orchestrationMode: mode, orchestrationMode: mode,
chain: chainSnap, chain: chainSnap,
orchestrationPlan: planSnap, orchestrationPlan: planSnap,
@@ -1697,6 +1699,7 @@ export default function RoundtablePlanningPage() {
seatSkillDirectives, seatSkillDirectives,
seatModes, seatModes,
gatherFirst, gatherFirst,
businessCode: prefillRef.current?.businessCode ?? undefined,
orchestrationMode, orchestrationMode,
chain: chainMeta, chain: chainMeta,
orchestrationPlan: orchestrationMode === "dag" ? orchestrationPlan : null, orchestrationPlan: orchestrationMode === "dag" ? orchestrationPlan : null,
+1 -1
View File
@@ -247,7 +247,7 @@ FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWA
| **Public Shares** (`/api/public/shares`) | **Unauthenticated** (the `/api/public/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES`). `GET /{code}` - read-only conversation snapshot (title + serialized messages + artifacts) for a `view`-mode share; messages are read straight from the latest checkpoint. `GET /{code}/files/{path}` - serves a file referenced by that conversation (images, Markdown, text, PDF, ?) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (`text/html`, `application/xhtml+xml`, `image/svg+xml`) is forced to download (`_public_share_disposition`); everything else is served inline. Missing/revoked/non-`view` codes all return 404. | | **Public Shares** (`/api/public/shares`) | **Unauthenticated** (the `/api/public/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES`). `GET /{code}` - read-only conversation snapshot (title + serialized messages + artifacts) for a `view`-mode share; messages are read straight from the latest checkpoint. `GET /{code}/files/{path}` - serves a file referenced by that conversation (images, Markdown, text, PDF, ?) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (`text/html`, `application/xhtml+xml`, `image/svg+xml`) is forced to download (`_public_share_disposition`); everything else is served inline. Missing/revoked/non-`view` codes all return 404. |
| **Roundtable Drafts** (`/api/roundtable-drafts`) | **Strictly per-user** persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete (all owner-scoped). A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). taskId 深链会商记录 are **not** here — they live in the separate `/api/roundtable-task-drafts` store (see below) so personal history never mixes with task records. Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. Tests: `tests/test_roundtable_task_drafts.py`. | | **Roundtable Drafts** (`/api/roundtable-drafts`) | **Strictly per-user** persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete (all owner-scoped). A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). taskId 深链会商记录 are **not** here — they live in the separate `/api/roundtable-task-drafts` store (see below) so personal history never mixes with task records. Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. Tests: `tests/test_roundtable_task_drafts.py`. |
| **Roundtable Task Drafts** (`/api/roundtable-task-drafts`) | **Task-scoped, unpartitioned** store for taskId-deeplink roundtable 聊天记录 (无界嵌入抽屉). Keyed **only by `task_id`** (one task → many records), **never partitioned by user** — any authenticated caller may read/write/delete a task's records (`created_by` is audit-only). Physically separate from `roundtable_drafts` so the two never mix. `GET /by-task/{task_id}` - all drafts for a task, newest-first (history dropdown), enriched with the latest background-job status per draft via `RoundtableJobRepository.status_map_by_drafts`; `GET /by-task/{task_id}/latest` - newest draft (回显最新一条); `POST /` - create (`task_id` required); `GET/PUT/DELETE /{id}` - full read / partial update / delete (all user-agnostic). Backed by `deerflow.persistence.roundtable_task_drafts` (table `roundtable_task_drafts`, auto-created by `create_all`; migration `20260618_01`); store wired in `deps.py` as `app.state.roundtable_task_draft_store`. Existing `task_id`-bearing `roundtable_drafts` rows are migrated over by `scripts/migrate_roundtable_task_drafts.py`. Tests: `tests/test_roundtable_task_drafts.py`, `tests/test_roundtable_drafts_by_task.py`. | | **Roundtable Task Drafts** (`/api/roundtable-task-drafts`) | **Task-scoped, unpartitioned** store for taskId-deeplink roundtable 聊天记录 (无界嵌入抽屉). Keyed **only by `task_id`** (one task → many records), **never partitioned by user** — any authenticated caller may read/write/delete a task's records (`created_by` is audit-only). Physically separate from `roundtable_drafts` so the two never mix. `GET /by-task/{task_id}` - all drafts for a task, newest-first (history dropdown), enriched with the latest background-job status per draft via `RoundtableJobRepository.status_map_by_drafts`; `GET /by-task/{task_id}/latest` - newest draft (回显最新一条); `POST /` - create (`task_id` required); `GET/PUT/DELETE /{id}` - full read / partial update / delete (all user-agnostic). Backed by `deerflow.persistence.roundtable_task_drafts` (table `roundtable_task_drafts`, auto-created by `create_all`; migration `20260618_01`); store wired in `deps.py` as `app.state.roundtable_task_draft_store`. Existing `task_id`-bearing `roundtable_drafts` rows are migrated over by `scripts/migrate_roundtable_task_drafts.py`. Tests: `tests/test_roundtable_task_drafts.py`, `tests/test_roundtable_drafts_by_task.py`. |
| **Roundtable Diagnostics** (`/api/roundtable-diagnostics`) | 会商全流程诊断日志 (报错 + 关键事件). One row per event in `roundtable_diagnostics`: `{scope (background = 后台挂起 / foreground = 前台服务端观测 / client = 前端上报), stage (step1_intent/step2_init/step2_leader/step2_seat/step2_recommend/step2_orchestration/step3_summary/step3_report/step3_dashboard/step3_structure/upstream/job), level (info/warning/error), event, message, detail (JSON), job_id/draft_id/task_id/user_id/agent_id/agent_name/cycle, created_at}`. **Reads are admin-only**: `GET /` - filter (scope/stage/level/event/job_id/draft_id/task_id/user_id/q/since/until; `scope` accepts a comma-separated set so 前端「前台调用」= `foreground,client`) + paginate (newest-first), each item enriched with `userName` (user_id → 邮箱 `@`-prefix via `get_local_provider().get_user`, best-effort batched cache); `GET /facets` - distinct values for filter dropdowns; `POST /admin/cleanup` - retention purge. **`POST /report` is NOT admin-gated** — any authenticated user's frontend posts the errors *the user actually hit* (HTTP 502 before any SSE frame / network drop / SSE error frame) so they're collected too; stored as `scope="client"`. Backed by `deerflow.persistence.roundtable_diagnostics` (table `roundtable_diagnostics`, auto-created by `create_all`; migration `20260619_01`); store wired in `deps.py` as `app.state.roundtable_diagnostic_store` + a process-level default store (`register_default_store`) so the harness-side engine/gateway can write across the app/harness boundary. **Writers** (all best-effort — diagnostics never break a run): background path uses `DiagnosticsRecorder` (bound to job ctx) threaded through `RoundtableJobExecutor` → `JobProgressWriter.diag(...)` (engine transitions/step3 failures/empty-delivery) + `InProcessRoundtableGateway` (per-turn timeout/report·summary·dashboard truncation+missing); foreground path uses `app/gateway/roundtable_diag.py::record_foreground_diag` in `multi_agent.py`/`intent.py`/`recommend.py` error frames. **Bug-fix shipped alongside**: `InProcessRoundtableGateway._run_agent_turn` guards each agent run with an **inactivity (idle) timeout** (`_await_run_with_idle_guard`, `ROUNDTABLE_SEAT_IDLE_TIMEOUT_SECONDS` default 1200s = 20min, tuned high for unstable intranet models) — **not** a total-duration cap, so a slow-but-alive run that keeps streaming tokens runs as long as it needs; only a run that emits **zero StreamBridge events for `idle_timeout` seconds straight** (a true freeze: 内网模型连接池挂死 / sqlite 锁在挂载卷上挂死) is cancelled + raises (顶层把 job 标 error,而非永久冻结) + records a `seat_turn_timeout` diagnostic. Activity is observed via `app.state.stream_bridge` (the worker publishes every chunk there). Optional absolute cap `ROUNDTABLE_SEAT_MAX_TOTAL_SECONDS` (default 0 = off). Frontend: admin page `pages/RoundtableDiagnosticsPage.tsx` (route `/page/strategy/admin/roundtable-diagnostics`), opened from the **会商页右上角设置按钮** (admin only — `handleSettingsClick` in `roundtable-planning/pages/RoundtablePlanningPage.tsx`); admin read client `strategy-components/api/roundtable-diagnostics.ts`. **Client error reporter** `roundtable-planning/api/diagnostics-report.ts` (`reportRoundtableError` fire-and-forget + `keepalive`, `setRoundtableErrorContext` for draft/task attribution set by the page, `stageForRoundtableAgent`) is called at every client-observable failure: `multi-agent.ts` (`streamMultiAgent` — covers Step2 leader/seat AND Step3 report/summary/dashboard/structure — reports HTTP error + SSE `{status:"error"}` frame + worker `event:error` frame [模型调用没成功] + fetch-reject [网络不可达] + mid-stream drop [连接重置/超时], deduped via a `reported` flag; `initMultiAgent` HTTP/error-frame/incomplete/mid-stream), `intent.ts` (Step1 HTTP+error-frame), `recommend.ts` (Step2 recommend HTTP+error-frame), `roundtable-jobs.ts` (job api/stream). The admin page's first filter is a **后台挂起 / 前台调用** toggle (frontend = `foreground,client`); the stage dropdown options adapt to it (background → step2+step3+job; frontend → step1+step2+step3) and each row shows the reporting user's name + 👤. Tests: `tests/test_roundtable_diagnostics.py`, `tests/test_roundtable_inprocess_timeout.py`. **Seat skill-recognition (弱模型补偿)**: a seat = one custom agent run whose configured skills (`agent config.yaml → skills`) are injected **only** via the system prompt's `<skill_system>` block (model must `read_file` the SKILL.md itself) — weak models (deepseek-chat 等) ignore it and never use their skills. `app/gateway/roundtable_seat_skills.py::append_seat_skill_directive(agent_id, task, *, chain_enabled=False)` appends an explicit Chinese directive (each configured-and-enabled skill's name + description + SKILL.md container path + "先 read_file 再按其流程取信息/执行") toLine truncated | **Roundtable Diagnostics** (`/api/roundtable-diagnostics`) | 会商全流程诊断日志 (报错 + 关键事件). One row per event in `roundtable_diagnostics`: `{scope (background = 后台挂起 / foreground = 前台服务端观测 / client = 前端上报), stage (step1_intent/step2_init/step2_leader/step2_seat/step2_recommend/step2_orchestration/step3_summary/step3_report/step3_dashboard/step3_structure/upstream/job), level (info/warning/error), event, message, detail (JSON), job_id/draft_id/task_id/user_id/agent_id/agent_name/cycle, created_at}`. **Reads are admin-only**: `GET /` - filter (scope/stage/level/event/job_id/draft_id/task_id/user_id/q/since/until; `scope` accepts a comma-separated set so 前端「前台调用」= `foreground,client`) + paginate (newest-first), each item enriched with `userName` (user_id → 邮箱 `@`-prefix via `get_local_provider().get_user`, best-effort batched cache); `GET /facets` - distinct values for filter dropdowns; `POST /admin/cleanup` - retention purge. **`POST /report` is NOT admin-gated** — any authenticated user's frontend posts the errors *the user actually hit* (HTTP 502 before any SSE frame / network drop / SSE error frame) so they're collected too; stored as `scope="client"`. Backed by `deerflow.persistence.roundtable_diagnostics` (table `roundtable_diagnostics`, auto-created by `create_all`; migration `20260619_01`); store wired in `deps.py` as `app.state.roundtable_diagnostic_store` + a process-level default store (`register_default_store`) so the harness-side engine/gateway can write across the app/harness boundary. **Writers** (all best-effort — diagnostics never break a run): background path uses `DiagnosticsRecorder` (bound to job ctx) threaded through `RoundtableJobExecutor` → `JobProgressWriter.diag(...)` (engine transitions/step3 failures/empty-delivery) + `InProcessRoundtableGateway` (per-turn timeout/report·summary·dashboard truncation+missing); foreground path uses `app/gateway/roundtable_diag.py::record_foreground_diag` in `multi_agent.py`/`intent.py`/`recommend.py` error frames. **Bug-fix shipped alongside**: `InProcessRoundtableGateway._run_agent_turn` guards each agent run with an **inactivity (idle) timeout** (`_await_run_with_idle_guard`, `ROUNDTABLE_SEAT_IDLE_TIMEOUT_SECONDS` default 1200s = 20min, tuned high for unstable intranet models) — **not** a total-duration cap, so a slow-but-alive run that keeps streaming tokens runs as long as it needs; only a run that emits **zero StreamBridge events for `idle_timeout` seconds straight** (a true freeze: 内网模型连接池挂死 / sqlite 锁在挂载卷上挂死) is cancelled + raises (顶层把 job 标 error,而非永久冻结) + records a `seat_turn_timeout` diagnostic. Activity is observed via `app.state.stream_bridge` (the worker publishes every chunk there). Optional absolute cap `ROUNDTABLE_SEAT_MAX_TOTAL_SECONDS` (default 0 = off). Frontend: admin page `pages/RoundtableDiagnosticsPage.tsx` (route `/page/strategy/admin/roundtable-diagnostics`), opened from the **会商页右上角设置按钮** (admin only — `handleSettingsClick` in `roundtable-planning/pages/RoundtablePlanningPage.tsx`); admin read client `strategy-components/api/roundtable-diagnostics.ts`. **Client error reporter** `roundtable-planning/api/diagnostics-report.ts` (`reportRoundtableError` fire-and-forget + `keepalive`, `setRoundtableErrorContext` for draft/task attribution set by the page, `stageForRoundtableAgent`) is called at every client-observable failure: `multi-agent.ts` (`streamMultiAgent` — covers Step2 leader/seat AND Step3 report/summary/dashboard/structure — reports HTTP error + SSE `{status:"error"}` frame + worker `event:error` frame [模型调用没成功] + fetch-reject [网络不可达] + mid-stream drop [连接重置/超时], deduped via a `reported` flag; `initMultiAgent` HTTP/error-frame/incomplete/mid-stream), `intent.ts` (Step1 HTTP+error-frame), `recommend.ts` (Step2 recommend HTTP+error-frame), `roundtable-jobs.ts` (job api/stream). The admin page's first filter is a **后台挂起 / 前台调用** toggle (frontend = `foreground,client`); the stage dropdown options adapt to it (background → step2+step3+job; frontend → step1+step2+step3) and each row shows the reporting user's name + 👤. Tests: `tests/test_roundtable_diagnostics.py`, `tests/test_roundtable_inprocess_timeout.py`. **Seat skill-recognition (弱模型补偿)**: a seat = one custom agent run whose configured skills (`agent config.yaml → skills`) are injected **only** via the system prompt's `<skill_system>` block (model must `read_file` the SKILL.md itself) — weak models (deepseek-chat 等) ignore it and never use their skills. `app/gateway/roundtable_seat_skills.py::append_seat_skill_directive(agent_id, task, *, chain_enabled=False)` appends an explicit Chinese directive (each configured-and-enabled skill's name + description + SKILL.md container path + "先 read_file 再按其流程取信息/执行") toLine truncated
| **Roundtable Draft Shares** (`/api/roundtable-draft-shares`) | Per-draft sharing for the roundtable-planning page, mirroring Thread Shares but the shared resource is a `roundtable_drafts` row. Two modes (`roundtable_draft_shares.mode`). **`import`**: `POST /` `{draft_id, mode}` - create (or reuse) a revocable code; `GET /{code}/preview` + `POST /{code}/import` - copy that draft (step1/2/3) into the caller's account under a deterministic per-(code,recipient) id (`imp_<sha1>` ? re-import is a no-op). **`view`**: a public, read-only result page ? see Public Roundtable Shares. Both: `GET /` - list my codes; `DELETE /{code}` - revoke. Backed by `deerflow.persistence.roundtable_draft_shares` (table `roundtable_draft_shares`, migration `20260606_01`); store wired in `deps.py` as `app.state.roundtable_draft_share_store`. Tests: `tests/test_roundtable_draft_shares.py`. | | **Roundtable Draft Shares** (`/api/roundtable-draft-shares`) | Per-draft sharing for the roundtable-planning page, mirroring Thread Shares but the shared resource is a `roundtable_drafts` row. Two modes (`roundtable_draft_shares.mode`). **`import`**: `POST /` `{draft_id, mode}` - create (or reuse) a revocable code; `GET /{code}/preview` + `POST /{code}/import` - copy that draft (step1/2/3) into the caller's account under a deterministic per-(code,recipient) id (`imp_<sha1>` ? re-import is a no-op). **`view`**: a public, read-only result page ? see Public Roundtable Shares. Both: `GET /` - list my codes; `DELETE /{code}` - revoke. Backed by `deerflow.persistence.roundtable_draft_shares` (table `roundtable_draft_shares`, migration `20260606_01`); store wired in `deps.py` as `app.state.roundtable_draft_share_store`. Tests: `tests/test_roundtable_draft_shares.py`. |
| **Public Roundtable Shares** (`/api/public/roundtable-shares`) | **Unauthenticated** (`/api/public/` whitelisted). `GET /{code}` - read-only snapshot for a `view`-mode draft share: returns the Step 3??????report HTML (`step3.html`, a self-contained document) + title/summary/`has_report`. The frontend renders it in a sandboxed `iframe` (no `allow-scripts`) at `#/roundtable-share/<code>`. Missing/revoked/non-`view` codes all return 404. | | **Public Roundtable Shares** (`/api/public/roundtable-shares`) | **Unauthenticated** (`/api/public/` whitelisted). `GET /{code}` - read-only snapshot for a `view`-mode draft share: returns the Step 3??????report HTML (`step3.html`, a self-contained document) + title/summary/`has_report`. The frontend renders it in a sandboxed `iframe` (no `allow-scripts`) at `#/roundtable-share/<code>`. Missing/revoked/non-`view` codes all return 404. |
| **Scheduled Tasks** (`/api/scheduled-tasks`) | Per-user cron-like agent tasks. `GET/POST /` - list/create owned tasks; `PATCH/DELETE /{id}` - update/delete (owner); `POST /{id}/{pause,resume,trigger}`; `GET /{id}/runs`, `GET /runs/{rid}`, run-file preview/edit/download; `GET /runs/{rid}/messages` - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via `_load_run`); `GET /runs/{rid}/html` - generated HTML document for an `html_page` run; `DELETE /runs/{rid}` - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under `/{id}/{publish,unpublish,subscribe}` and `GET /published`. **Admin (cross-user):** `GET /admin/all` - every task; `PATCH/DELETE /admin/{id}` - edit/delete any task; `POST /admin/{id}/trigger`; `GET /admin/{id}/runs`. Admin routes require `system_role == "admin"`. | | **Scheduled Tasks** (`/api/scheduled-tasks`) | Per-user cron-like agent tasks. `GET/POST /` - list/create owned tasks; `PATCH/DELETE /{id}` - update/delete (owner); `POST /{id}/{pause,resume,trigger}`; `GET /{id}/runs`, `GET /runs/{rid}`, run-file preview/edit/download; `GET /runs/{rid}/messages` - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via `_load_run`); `GET /runs/{rid}/html` - generated HTML document for an `html_page` run; `DELETE /runs/{rid}` - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under `/{id}/{publish,unpublish,subscribe}` and `GET /published`. **Admin (cross-user):** `GET /admin/all` - every task; `PATCH/DELETE /admin/{id}` - edit/delete any task; `POST /admin/{id}/trigger`; `GET /admin/{id}/runs`. Admin routes require `system_role == "admin"`. |
@@ -200,6 +200,9 @@ class StartParams:
# 是否在研讨前先跑一轮**全席位并行『取数』**(业务链条逐条 opt-in):各席位并行只调技能取数、 # 是否在研讨前先跑一轮**全席位并行『取数』**(业务链条逐条 opt-in):各席位并行只调技能取数、
# 不研讨,产出作为共享资料注入后续研讨轮。透传给 run_orchestration 的 gather_first。默认 False。 # 不研讨,产出作为共享资料注入后续研讨轮。透传给 run_orchestration 的 gather_first。默认 False。
gather_first: bool = False gather_first: bool = False
# 深链接业务码(rwfx→6BF 等)。透传给 run_orchestration → build_summary_prompt:写实业务额外注入
# 「业务链完整性核对」,让后台总结报告核对产出相对业务链完不完整。普通会商为空 → 不注入。
business_code: str | None = None
# 网关工厂:给定一次启动参数,返回一个 RoundtableGateway 实例。 # 网关工厂:给定一次启动参数,返回一个 RoundtableGateway 实例。
@@ -288,6 +291,7 @@ class RoundtableJobExecutor:
enable_report=params.enable_report, enable_report=params.enable_report,
plan=params.orchestration_plan, plan=params.orchestration_plan,
gather_first=params.gather_first, gather_first=params.gather_first,
business_code=params.business_code,
) )
# 终态:把最终对话 + step3 + jobStatus 落进同一个 run(含 furthest_step=3)。 # 终态:把最终对话 + step3 + jobStatus 落进同一个 run(含 furthest_step=3)。
await self._write_run_to_draft( await self._write_run_to_draft(
@@ -44,7 +44,7 @@
{"id":"n2","label":"精准追踪管理公众情绪曲线","type":"task","group":"目的","detail":"…"} ← 少了 "from" {"id":"n2","label":"精准追踪管理公众情绪曲线","type":"task","group":"目的","detail":"…"} ← 少了 "from"
``` ```
**硬性要求**:除根节点(任务,n1)外,**每个节点都必须有 `from`**(连到它在上一层级里对应的那个节点);目的i 连 任务,行为体i 连 目的i,预期行为i 连 行为体i,叙事i 连 预期行为i……一组一条自上而下的链,不要让任何节点悬空。 **硬性要求**:除根节点(任务,n1)外,**每个节点都必须有 `from`**——连到它在上一层级里**与之对应**的那个节点(第 k 层第 i 条 → 第 k−1 层对应的那条),一组一条自上而下的链,不要让任何节点悬空。(具体有哪些层级、层间是一对一还是一对多,以本轮给出的「业务链抽取规范」为准。)
## 二、画法(严格遵守) ## 二、画法(严格遵守)
@@ -57,17 +57,22 @@
④ **连线**:你要做对三件事 —— ④ **连线**:你要做对三件事 ——
(a) 每个节点的 `group` 标对层级; (a) 每个节点的 `group` 标对层级;
(b) **各层内的条目按"相同的对应顺序"排列**——报告里相互对应的一组(目的1 ↔ 它对应的行为体 ↔ 对应的预期行为 ↔ 对应的叙事)在各自层里**都排同一位次**(第 1 组都排第 1 位、第 2 组都排第 2 位…); (b) **各层内的条目按"相同的对应顺序"排列**——报告里相互对应的一组(目的1 ↔ 它对应的行为体 ↔ 对应的预期行为 ↔ 对应的叙事)在各自层里**都排同一位次**(第 1 组都排第 1 位、第 2 组都排第 2 位…);
(c) **每个非根节点都填上 `from`**,连到它在上一层级里对应的那个节点(目的i 连 任务、行为体i 连 目的i、预期行为i 连 行为体i、叙事i 连 预期行为i……一组一条自上而下的链)——**最常见的错误就是漏填 from 导致节点平铺,务必每个都填**。 (c) **每个非根节点都填上 `from`**,连到它在上一层级里**与之对应**的那个节点(第 k 层第 i 条 → 第 k−1 层对应的那条,一组一条自上而下的链)——**最常见的错误就是漏填 from 导致节点平铺,务必每个都填**。
系统还会按 group 层级关系**校正 / 补全连线**作为兜底,但你必须先把 from 填全、填对; 系统还会按 group 层级关系**校正 / 补全连线**作为兜底,但你必须先把 from 填全、填对;
⑤ **务必把每个层级的每一条都输出成节点、不要漏层、不要只输出开头一两个节点就停**(典型规模约 13–16 个节点); ⑤ **务必把每个层级的每一条都输出成节点、不要漏层、不要只输出开头一两个节点就停**(典型规模约 13–16 个节点);
⑥ **只画**这些层级实体节点;**不要**把「综合分析 / 核心洞察 / 执行路线 / 建议 / 结论」以及「阶段①②③④」等章节名 / 阶段名 / 汇总段落画成节点;节点 `label` 也**绝不能**是「阶段1 / 阶段2 / 第N阶段」这类阶段编号。 ⑥ **只画**这些层级实体节点;**不要**把「综合分析 / 核心洞察 / 执行路线 / 建议 / 结论」以及「阶段①②③④」等章节名 / 阶段名 / 汇总段落画成节点;节点 `label` 也**绝不能**是「阶段1 / 阶段2 / 第N阶段」这类阶段编号。
## 三、层级顺序 ## 三、层级顺序(重要:不要写死任何一套层级)
- 若数据来源里给了「业务链总控编排提示」并写明了层级链(如 任务 → 目的 → 行为体 → 预期行为 → 驱动因素 → 叙事),**优先按它的层级**组织; **每个业务链的层级各不相同**,绝不要默认套用某一套固定层级(如「目的→行为体」)。本轮该用哪套层级,按下面的优先级,**完全以本轮给出的信息为准**:
- 否则按给出的「默认层级」;都没有时,以「任务」为根、按报告 / 交付的逻辑结构合理分层。
1. 若本轮用户消息里给了「业务链抽取规范」(写明完整层级链、层间一对一/一对多关系等)——**这是最高优先级,严格逐层照它抽全、不得缺层、不得停在中间层**;
2. 否则若给了「业务链总控编排提示」并写明了层级链——按它的层级;
3. 都没有时,以「任务」为根、按报告 / 交付里**真实出现的层级**逐层展开。
> 示例(仅示意「层级链」这个概念,**不是固定模板、不要照搬**):某业务可能是 任务 → 目的 → 行为体 → 预期行为 → 驱动因素 → 叙事;另一业务可能完全不同。实际层级一律以上面第 1/2/3 条给出的为准。
## 四、修改 / 校验时 ## 四、修改 / 校验时
@@ -29,11 +29,12 @@
2. **逐条核对覆盖**:对每条诉求,检查各席位交付 + 总控共识**是否覆盖、覆盖得够不够、有没有跑题或自相矛盾**。 2. **逐条核对覆盖**:对每条诉求,检查各席位交付 + 总控共识**是否覆盖、覆盖得够不够、有没有跑题或自相矛盾**。
3. **据此组织报告**,并按下面的「缺口处理护栏」补强——让最终报告明显**比"各席位交付的简单拼接"更扣用户意图**:意图里要的、席位没说清的,你补强或标缺口;意图里没要的、席位跑题的,不要喧宾夺主。 3. **据此组织报告**,并按下面的「缺口处理护栏」补强——让最终报告明显**比"各席位交付的简单拼接"更扣用户意图**:意图里要的、席位没说清的,你补强或标缺口;意图里没要的、席位跑题的,不要喧宾夺主。
### 缺口处理护栏(避免弱模型把幻觉带进报告) ### 缺口处理护栏(报告文件要完整干净、把不足说在对话里)
- **能从已有原料重组 / 推理补上的** → 直接补全、讲透,正常写进报告。这是你最该做的增值:把零散、跑题、含糊的席位产物,按用户意图重新梳理成扣题的方案。 **核心原则:写进 `.md` 报告文件的,必须是一份完整、干净、可直接使用、无需用户再修改的报告——报告文件里不要出现「待补充 / 不足 / ⚠️ / 尚需完善」之类的提示或占位。** 着急的用户拿到文件就能用。
- **超出席位原料、属于你自己的合理推断 / 建议** → 可以写,但**显式标注「(综合推断)」**,让用户能区分哪些有席位依据、哪些是你的补充判断。 - **能从已有原料重组 / 推理补上的** → 直接补全、讲透,正常写进报告。这是你最该做的增值:把零散、跑题、含糊的席位产物,按用户意图重新梳理成扣题、完整的方案。
- **缺真实信息、靠现有原料补不了的**(需要最新数据 / 外部事实,而席位没给)→ **绝不凭空编**。在报告对应位置用一行醒目标注:`> ⚠️ 待补充:<具体缺什么>。如需补全,可在下方对话里告诉我(例如「补一下 X 的最新情况」),我会据此完善。` 把球交给用户——你本身不联网、不调研,真补由用户在对话里发起。 - **超出席位原料、属于你自己的合理推断 / 建议** → 可以写进报告,让报告完整连贯;不必在报告里逐处打标签(保持可读),绝不编造与原料矛盾的内容。
- 始终:**宁可标注缺口,也不要假装完整**;绝不编造与原料矛盾、或无依据的"事实"。 - **缺真实信息、靠现有原料补不了的**(需要最新数据 / 外部事实,而席位没给)→ **绝不凭空编**,但也**不要在报告文件里写「待补充」之类的警告**。报告文件就按现有信息给出尽量完整的版本;这些「不足 / 尚可补充之处」**只在对话回复正文里、报告之后单独说明**(见下「B」)。
- 始终:报告文件 = 一份完整可用的报告(无缺口提示);不足只在对话文字里点出;绝不编造无依据的"事实"。
## 产出要求(最重要) ## 产出要求(最重要)
@@ -53,9 +54,10 @@
- **严禁**写 Python / shell / 任何脚本去"生成"文件,也**严禁**用 bash——你必须自己把 Markdown 文本直接写进 `write_file`; - **严禁**写 Python / shell / 任何脚本去"生成"文件,也**严禁**用 bash——你必须自己把 Markdown 文本直接写进 `write_file`;
- `write_file` **支持很长的内容,不会截断**——请放心一次性把整篇报告写进去。 - `write_file` **支持很长的内容,不会截断**——请放心一次性把整篇报告写进去。
### B. 在对话回复里给出报告正文 ### B. 在对话回复里给出报告正文(不足只写在这里)
- 写完文件后,**把这份报告的完整正文也输出到对话回复里**(直接给用户读),不要只回一句「已写入文件」。 - 写完文件后,**把这份报告的完整正文也输出到对话回复里**(直接给用户读),不要只回一句「已写入文件」。
- 用户既能在对话里直接读到报告,也能在右侧预览 / 下载这个 `.md` 文件——两边内容应当一致。 - 报告正文部分与 `.md` 文件**应当一致**(都是那份完整干净的报告)。
- **在对话回复里、报告正文之后,必须追加一段简短「核对说明」**(这段**只在对话里**、**绝不写进 `.md` 报告文件**):对照用户意图(及业务链各层级,如有)**逐条说明产出是否覆盖、哪些充分 / 哪些不足、有哪些可补充完善之处**,并提示可在对话里让你补全(如「---\n**核对说明**:① 已覆盖:…;② 资料不足 / 尚可补充:…(可直接告诉我补全)」)。**即便核对下来各诉求 / 层级都已覆盖、无明显缺口,也要明确写一句「已核对:各诉求 / 层级均已覆盖,无明显缺口」——这段核对说明任何时候都不要省略。** 报告 `.md` 文件本身始终是一份完整可用、无需修改、不含任何缺口提示的版本。
## 报告结构(叙事型,文字为主线) ## 报告结构(叙事型,文字为主线)
**下面是默认结构——仅当「业务链总控编排提示」没有指定输出结构时使用。** 用户指定了结构(章节、表格、清单、固定模板等)就完全按用户的来;两者只部分重叠时,以用户要求为骨架、默认章节做补充。按下面的章节顺序组织,缺料的章节可省略或合并: **下面是默认结构——仅当「业务链总控编排提示」没有指定输出结构时使用。** 用户指定了结构(章节、表格、清单、固定模板等)就完全按用户的来;两者只部分重叠时,以用户要求为骨架、默认章节做补充。按下面的章节顺序组织,缺料的章节可省略或合并:
@@ -136,7 +138,7 @@
- ❌ 在报告任何位置添加「AI 自动生成」「AI generated」之类的标签、徽章或水印 - ❌ 在报告任何位置添加「AI 自动生成」「AI generated」之类的标签、徽章或水印
- ❌ 编造与原料明显矛盾的内容 - ❌ 编造与原料明显矛盾的内容
- ❌ 只把各席位交付简单拼接、不做「意图对齐与缺口核对」(席位可能弱,你要兜底扣住用户意图) - ❌ 只把各席位交付简单拼接、不做「意图对齐与缺口核对」(席位可能弱,你要兜底扣住用户意图)
- ❌ 缺真实信息时凭空编造来"填满"报告(应按护栏标注「> ⚠️ 待补充:…」交给用户在对话里补) - ❌ 缺真实信息时凭空编造来"填满"报告(按现有信息给出完整版即可;不足只在对话正文点出,不写进 .md 文件)
- ❌ 自己的推断不加「(综合推断)」标注,让用户误以为有席位依据 - ❌ 在 `.md` 报告文件里写「待补充 / 不足 / ⚠️ / 尚需完善」等缺口提示或占位(报告文件要完整干净、无需修改;不足只写在对话正文里)
- ❌ 忽视「业务链总控编排提示」的口径要求(它优先级最高) - ❌ 忽视「业务链总控编排提示」的口径要求(它优先级最高)
- ❌ 编排提示已指定输出结构 / 重点席位,却仍按默认章节平均用力(结构要完全跟用户走,重点席位要明显加权) - ❌ 编排提示已指定输出结构 / 重点席位,却仍按默认章节平均用力(结构要完全跟用户走,重点席位要明显加权)
@@ -31,6 +31,8 @@ from fastapi.responses import StreamingResponse
from langchain_core.messages import HumanMessage, SystemMessage from langchain_core.messages import HumanMessage, SystemMessage
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from deerflow.agents.roundtable_orchestrator.extraction_spec import build_structure_directive
from app.gateway.routers._roundtable_seed import ( from app.gateway.routers._roundtable_seed import (
STRUCTURE_AGENT_ID, STRUCTURE_AGENT_ID,
ensure_roundtable_functional_agents, ensure_roundtable_functional_agents,
@@ -59,6 +61,9 @@ class FlowExtractRequest(BaseModel):
# 层级骨架:优先 coordinator_prompt 里写明的层级,否则用 chain_levels。 # 层级骨架:优先 coordinator_prompt 里写明的层级,否则用 chain_levels。
chain_levels: list[str] = Field(default_factory=list) chain_levels: list[str] = Field(default_factory=list)
coordinator_prompt: str | None = Field(default=None) coordinator_prompt: str | None = Field(default=None)
# 深链接业务码(rwfx→6BF 等)。非空 → 按该业务的「业务链抽取规范」**强制**注入完整层级
# (覆盖默认/示例层级假设),根治"无论什么业务都按目的→行为体抽"。普通会商为空 → 不注入。
business_code: str | None = Field(default=None)
model: str | None = Field(default=None) model: str | None = Field(default=None)
# Phase 2 问答修改:带上当前图 + 用户指令 → 进 edit 模式(按指令改图,其余不变)。 # Phase 2 问答修改:带上当前图 + 用户指令 → 进 edit 模式(按指令改图,其余不变)。
current_graph: dict[str, Any] | None = Field(default=None) current_graph: dict[str, Any] | None = Field(default=None)
@@ -438,6 +443,17 @@ def _material(req: FlowExtractRequest) -> str:
def _hierarchy_hint(req: FlowExtractRequest) -> str: def _hierarchy_hint(req: FlowExtractRequest) -> str:
# 深链接业务规范**最高优先**:按 business_code 取写实/通用规范,覆盖任何默认层级假设。
spec = build_structure_directive(req.business_code)
if spec:
cp = (req.coordinator_prompt or "").strip()
if cp:
spec += (
"\n(若「业务链总控编排提示」对层级另有更细要求,可在**不违背上述完整层级**的前提下采纳:\n"
+ cp
+ ")"
)
return spec
if (req.coordinator_prompt or "").strip(): if (req.coordinator_prompt or "").strip():
return "层级顺序优先采用下面「业务链总控编排提示」里写明的层级链(如它写的 任务→目的→行为体→…):\n" + req.coordinator_prompt.strip() return "层级顺序优先采用下面「业务链总控编排提示」里写明的层级链(如它写的 任务→目的→行为体→…):\n" + req.coordinator_prompt.strip()
levels = [s.strip() for s in req.chain_levels if s.strip()] levels = [s.strip() for s in req.chain_levels if s.strip()]
@@ -504,8 +520,12 @@ def _edit_messages(req: FlowExtractRequest) -> list[Any]:
def _validate_messages(req: FlowExtractRequest, extracted: str) -> list[Any]: def _validate_messages(req: FlowExtractRequest, extracted: str) -> list[Any]:
# 深链接业务规范:校验阶段也带上完整层级,确保按规范补齐缺层(不只按抽取里出现的层级)。
spec = build_structure_directive(req.business_code)
spec_block = (spec + "\n\n") if spec else ""
user = ( user = (
_material(req) spec_block
+ _material(req)
+ "\n\n===== 已抽取的 flow-json(待校验修复)=====\n" + "\n\n===== 已抽取的 flow-json(待校验修复)=====\n"
+ extracted + extracted
+ "\n===== 待校验结束 =====\n\n请对照数据来源,**逐个节点检查上面这份 flow-json 是不是缺字段**、并修复它:" + "\n===== 待校验结束 =====\n\n请对照数据来源,**逐个节点检查上面这份 flow-json 是不是缺字段**、并修复它:"
@@ -64,6 +64,8 @@ from langgraph.checkpoint.base import empty_checkpoint
from app.gateway.roundtable_diag import record_foreground_diag from app.gateway.roundtable_diag import record_foreground_diag
from app.gateway.roundtable_run_policy import merge_skill_stop_names, roundtable_run_policy from app.gateway.roundtable_run_policy import merge_skill_stop_names, roundtable_run_policy
from deerflow.agents.roundtable_orchestrator.extraction_spec import build_summary_directive
from app.gateway.roundtable_seat_skills import append_seat_skill_directive from app.gateway.roundtable_seat_skills import append_seat_skill_directive
from app.gateway.routers._roundtable_seed import ( from app.gateway.routers._roundtable_seed import (
COORDINATOR_AGENT_ID, COORDINATOR_AGENT_ID,
@@ -1738,6 +1740,7 @@ async def _special_run(
reasoning_effort: str | None = None, reasoning_effort: str | None = None,
subagent_enabled: bool = False, subagent_enabled: bool = False,
seat_skill_directive: bool = False, seat_skill_directive: bool = False,
business_code: str = "",
) -> AsyncIterator[str | dict[str, Any]]: ) -> AsyncIterator[str | dict[str, Any]]:
"""子智能体(special)单轮:执行总控下发的 task 文本,交付 content。 """子智能体(special)单轮:执行总控下发的 task 文本,交付 content。
@@ -1796,6 +1799,12 @@ async def _special_run(
seat_message = new_message seat_message = new_message
if role in ("seat", "seat_parallel"): if role in ("seat", "seat_parallel"):
seat_message = append_seat_skill_directive(agent_name, new_message, chain_enabled=seat_skill_directive) seat_message = append_seat_skill_directive(agent_name, new_message, chain_enabled=seat_skill_directive)
# 深链接业务规范(仅 summary 单例):把该业务的「业务链完整性核对」指令拼进本轮任务,强制总结报告
# 核对产出相对业务链完不完整。business_code 为空(普通会商)/ 非写实业务 → directive 为 "",原样不变。
elif role == "summary" and business_code:
directive = build_summary_directive(business_code)
if directive:
seat_message = f"{new_message}\n\n---\n\n{directive}"
body = _run_payload( body = _run_payload(
agent_name=agent_name, agent_name=agent_name,
model_name=model_name, model_name=model_name,
@@ -1963,6 +1972,9 @@ async def run_multi_agent_stream(request: Request, payload: dict = Body(...)) ->
# 业务链条级「技能识别强化」开关(仅 special 席位消费):前端读所选业务链条的同名配置 # 业务链条级「技能识别强化」开关(仅 special 席位消费):前端读所选业务链条的同名配置
# 后逐席位透传。与该席位 agent 自身的 config.yaml 开关取 OR;缺省 False(默认关)。 # 后逐席位透传。与该席位 agent 自身的 config.yaml 开关取 OR;缺省 False(默认关)。
seat_skill_directive: bool = bool(payload.get("seat_skill_directive") or False) seat_skill_directive: bool = bool(payload.get("seat_skill_directive") or False)
# 深链接业务码(rwfx→6BF 等)。仅 summary 单例消费:非空 → 按业务链抽取规范强制注入「完整性核对」
# 指令,让总结报告核对产出相对业务链完不完整。普通会商为空 → 不注入。
business_code: str = str(payload.get("business_code") or "")
if not agent_name or agent_name not in agent_threads: if not agent_name or agent_name not in agent_threads:
raise HTTPException( raise HTTPException(
@@ -2014,6 +2026,7 @@ async def run_multi_agent_stream(request: Request, payload: dict = Body(...)) ->
reasoning_effort=seat_reasoning_effort, reasoning_effort=seat_reasoning_effort,
subagent_enabled=seat_subagent_enabled, subagent_enabled=seat_subagent_enabled,
seat_skill_directive=seat_skill_directive, seat_skill_directive=seat_skill_directive,
business_code=business_code,
) )
async for chunk in gen: async for chunk in gen:
if isinstance(chunk, dict): if isinstance(chunk, dict):
@@ -87,6 +87,8 @@ class StartJobRequest(BaseModel):
seat_modes: dict[str, str] = Field(default_factory=dict) seat_modes: dict[str, str] = Field(default_factory=dict)
# 是否研讨前先并行『取数』(业务链条逐条 opt-in;前端读所选链条的开关后下发)。默认 False。 # 是否研讨前先并行『取数』(业务链条逐条 opt-in;前端读所选链条的开关后下发)。默认 False。
gather_first: bool = False gather_first: bool = False
# 深链接业务码(rwfx→6BF 等)。透传到后台总结报告的「业务链完整性核对」。普通会商为空。
business_code: str | None = None
class ResumeJobRequest(BaseModel): class ResumeJobRequest(BaseModel):
@@ -248,6 +250,7 @@ async def start_job(request: Request, body: StartJobRequest) -> RoundtableJobRes
seat_skill_directives=dict(body.seat_skill_directives or {}), seat_skill_directives=dict(body.seat_skill_directives or {}),
seat_modes=dict(body.seat_modes or {}), seat_modes=dict(body.seat_modes or {}),
gather_first=bool(body.gather_first), gather_first=bool(body.gather_first),
business_code=(body.business_code or None),
) )
) )
return _to_response(row) return _to_response(row)
@@ -23,6 +23,7 @@ import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import UTC, datetime from datetime import UTC, datetime
from .extraction_spec import build_summary_directive
from .progress import JobProgressWriter from .progress import JobProgressWriter
from .types import ( from .types import (
CHAIN_CLOSEOUT_PROMPT, CHAIN_CLOSEOUT_PROMPT,
@@ -81,6 +82,7 @@ async def run_orchestration(
enable_report: bool = True, enable_report: bool = True,
plan: dict | None = None, plan: dict | None = None,
gather_first: bool = False, gather_first: bool = False,
business_code: str | None = None,
) -> OrchestrationResult: ) -> OrchestrationResult:
"""运行一次完整编排(循环 → 共识 → 报告绘制)。 """运行一次完整编排(循环 → 共识 → 报告绘制)。
@@ -162,6 +164,7 @@ async def run_orchestration(
consensus_text=outcome.last_leader_content, consensus_text=outcome.last_leader_content,
deliveries=outcome.deliveries, deliveries=outcome.deliveries,
coordinator_prompt=(dag_plan.coordinator_prompt if dag_plan is not None else None), coordinator_prompt=(dag_plan.coordinator_prompt if dag_plan is not None else None),
business_code=business_code,
) )
await progress.done() await progress.done()
return OrchestrationResult( return OrchestrationResult(
@@ -597,6 +600,7 @@ async def _run_step3(
consensus_text: str, consensus_text: str,
deliveries: dict[str, str], deliveries: dict[str, str],
coordinator_prompt: str | None = None, coordinator_prompt: str | None = None,
business_code: str | None = None,
) -> dict | None: ) -> dict | None:
"""Step3 双产物:「方案总结报告」(roundtable-summary 写 md)与「结果绘制」 """Step3 双产物:「方案总结报告」(roundtable-summary 写 md)与「结果绘制」
(roundtable-report 画 HTML)**并行执行**(两段无依赖,2026-06-13 由串行改并行, (roundtable-report 画 HTML)**并行执行**(两段无依赖,2026-06-13 由串行改并行,
@@ -617,7 +621,8 @@ async def _run_step3(
await progress.summary_writing() await progress.summary_writing()
summary_thread_id = await gateway.create_summary_thread() summary_thread_id = await gateway.create_summary_thread()
summary_prompt = build_summary_prompt( summary_prompt = build_summary_prompt(
intent_text, agents, deliveries, consensus_text, coordinator_prompt=coordinator_prompt intent_text, agents, deliveries, consensus_text,
coordinator_prompt=coordinator_prompt, business_code=business_code,
) )
summary: SummaryResult = await gateway.run_summary( summary: SummaryResult = await gateway.run_summary(
summary_thread_id=summary_thread_id, prompt=summary_prompt, model=model summary_thread_id=summary_thread_id, prompt=summary_prompt, model=model
@@ -772,6 +777,7 @@ def build_summary_prompt(
consensus_text: str, consensus_text: str,
*, *,
coordinator_prompt: str | None = None, coordinator_prompt: str | None = None,
business_code: str | None = None,
) -> str: ) -> str:
"""拼装 Step3「方案总结报告」prompt —— 对齐前端 ``useStep3Summary.buildSummaryPrompt`` """拼装 Step3「方案总结报告」prompt —— 对齐前端 ``useStep3Summary.buildSummaryPrompt``
(非流程图分支)+ ``buildMaterials`` 的原料结构,使后台产出与手动调用一致。""" (非流程图分支)+ ``buildMaterials`` 的原料结构,使后台产出与手动调用一致。"""
@@ -812,11 +818,16 @@ def build_summary_prompt(
"【动笔前必做:意图对齐与缺口核对】各席位可能由能力较弱的模型产出,未必扣住用户意图、未必充分。" "【动笔前必做:意图对齐与缺口核对】各席位可能由能力较弱的模型产出,未必扣住用户意图、未必充分。"
"你是这份报告的质量兜底,不能只把各席位交付简单拼接。请先把上面的「任务意图」(目标/约束/关键假设)" "你是这份报告的质量兜底,不能只把各席位交付简单拼接。请先把上面的「任务意图」(目标/约束/关键假设)"
"拆成用户的核心诉求清单(编排提示对最终产出的要求也并入),逐条核对各席位交付+共识是否覆盖、够不够、" "拆成用户的核心诉求清单(编排提示对最终产出的要求也并入),逐条核对各席位交付+共识是否覆盖、够不够、"
"有无跑题或矛盾;再据此组织报告并按护栏补强:① 能从已有原料重组/推理补上的→直接补全讲透;" "有无跑题或矛盾;再据此组织:① 能从已有原料重组/推理补上的→直接补全讲透;② 缺真实信息补不了的→**绝不凭空编**。"
"② 属于你自己的推断/建议→写,但显式标注「(综合推断)」;③ 缺真实信息、靠现有原料补不了的→**绝不凭空编**," "**写进 .md 报告文件的必须是一份完整、干净、可直接使用、无需用户修改的报告——不要在报告文件里写「待补充/不足/⚠️」之类提示或占位**。"
"用一行 `> ⚠️ 待补充:<具体缺什么>。可在对话里让我补全` 标出,把球交给用户。" "**但在对话回复正文、报告之后,必须追加一段简短「核对说明」**(只在对话里、**不写进 .md 文件**):逐条说明产出是否覆盖了用户意图、哪些充分 / 哪些不足 / 有哪些可补充(可在对话里让我补全);"
"**即便核对下来都已覆盖、无明显缺口,也要明确写一句「已核对:各诉求均已覆盖,无明显缺口」,这段核对说明任何时候都不要省略。**"
"最终报告要明显比「各席位交付的简单拼接」更扣用户意图。" "最终报告要明显比「各席位交付的简单拼接」更扣用户意图。"
) )
# 深链接业务规范:写实业务(如 rwfx)额外注入「业务链完整性核对」,强制核对产出覆盖了每一层级。
biz_directive = build_summary_directive(business_code)
if biz_directive:
lines.append(biz_directive)
lines.append( lines.append(
"请严格遵循你的职责设定:纯 Markdown 文本(不画 HTML / 图表)、全程简体中文。" "请严格遵循你的职责设定:纯 Markdown 文本(不画 HTML / 图表)、全程简体中文。"
"报告结构:默认用叙事型结构(一句话结论 → 背景目标 → 各席位观点 → 综合分析 → 最终共识方案 → 执行路线);" "报告结构:默认用叙事型结构(一句话结论 → 背景目标 → 各席位观点 → 综合分析 → 最终共识方案 → 执行路线);"
@@ -0,0 +1,160 @@
"""按 businessCode 的「业务链抽取规范」——深链接会商模式下强制注入 summary / 结构化抽取。
背景:结构化抽取(flow_extract,roundtable-structure 智能体)和方案总结报告(roundtable-summary)
的层级该怎么抽/怎么核对,是**每个业务链各不相同**的领域知识;但智能体 SOUL 只有一份、全局共用,
写死某一套层级就会对所有业务套同一套(典型 bug:无论什么业务都按「目的→行为体」抽、停在行为体)。
解法:把每个业务的**完整层级 + 层间关系 + 节点命名 + 取数优先**做成这里的一份规范,**单份领域知识、
两个角色用法**:
- ``build_structure_directive``:给结构化抽取——按完整层级抽全、层间 1:1/1:N、节点命名、取数优先;
- ``build_summary_directive``:给方案总结报告——核对产出相对业务链完不完整。
**仅深链接模式(business_code 非空)注入**;普通手动会商 business_code 为空 → 返回 ""(零副作用)。
rwfx(6BF) 先写实,其余业务(3Q/7BF/8BF)暂用通用规范(仍强约束完整性 / 命名 / 取数,等真实层级再补)。
放在 harness 层(``deerflow.*``):app 层(flow_extract / multi_agent)与 harness 层(engine
后台 build_summary_prompt)都要用它,故置于此(app 可 import harness,harness 不可 import app)。
"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class ChainSpec:
"""一个业务链的抽取规范。"""
code: str # 规范的规范化业务码(如 6BF)
label: str # 人类可读业务名(如 rwfx)
levels: list[str] # 自上而下的层级名(线性链=逐层;分叉/汇聚的复杂链=各类节点自上而下的粗略顺序)
relations_note: str # 层间关系说明(1:1 / 1:N / 多对一 / 分叉汇聚,复杂结构全写在这里)
completeness_note: str = "" # 「完整性」要求行;空 → 用通用「以上 N 层都要出节点」
extra_note: str = "" # 该业务额外补充(可空)
# 节点命名 / 取数优先:各业务通用,单独抽出避免重复。
_NAMING_NOTE = (
"节点 label 必须用该层级下的**具体实体 / 概念短名**(≤12 字),"
"**绝不能含「步骤 / 阶段 / 环节 / 第N步 / 阶段①②③」等流程或编号词**,也不要拿层级名本身当 label。"
)
_SOURCE_NOTE = "**优先**从《方案总结报告》抽取;没有报告时再用各席位交付内容。逐条抽取、勿改写、勿精简理由。"
# ── rwfx(6BF):六步法完整链(线性) ─────────────────────────────────────────
_RWFX = ChainSpec(
code="6BF",
label="rwfx",
levels=["任务", "目的", "行为体", "预期行为", "驱动因素", "叙事"],
relations_note=(
"层间关系:**「预期行为」与「驱动因素」一一对应**(两层数量必须相等、一个预期行为恰对一个驱动因素,"
"按相同位次配对);其余相邻层级(任务→目的、目的→行为体、行为体→预期行为、驱动因素→叙事)均为"
"**一对多**(上层一条可带下层多条)。"
),
completeness_note=(
"以上 6 个层级都要出节点;一一对应的层级(预期行为↔驱动因素)两层节点数必须相等并按位次配对。"
),
)
# ── xdfx(7BF):分叉汇聚 DAG(非线性,根为「行动」) ──────────────────────────
_XDFX = ChainSpec(
code="7BF",
label="xdfx",
levels=["行动", "受众目标 / 关键传播节点", "脆弱点", "策略"],
relations_note=(
"这是一张**分叉汇聚**的图(不是单链),层间关系如下:\n"
"① **根节点 = 用户传入的「行动」**(不是「任务」);\n"
"② 「行动」下分**两类并列分支**——「受众目标」与「关键传播节点」(行动→受众目标 一对多、"
"行动→关键传播节点 一对多);\n"
"③ 「受众目标」和「关键传播节点」**各自都挂多个「脆弱点」**(受众目标→脆弱点 一对多、"
"关键传播节点→脆弱点 一对多);\n"
"④ 「策略」由「脆弱点」**汇聚而来、是多对一**:一个策略可对应「某个受众目标的全部或部分脆弱点」"
"「多个受众目标各自的全部或部分脆弱点」(关键传播节点侧同理)——即多个脆弱点(**可跨多个受众目标"
"或多个关键传播节点**)汇聚到一个策略;策略节点的 from 要连到它实际汇聚的那些脆弱点(可能多个、可跨分支)。"
),
completeness_note=(
"行动 / 受众目标 / 关键传播节点 / 脆弱点 / 策略 这几类节点都要出全;每个脆弱点都要 from 连到它所属的"
"那个受众目标或关键传播节点;策略按上面的多对一关系,from 连到它汇聚的全部脆弱点。"
),
)
# 规范化业务码 → 规范。同时收录人类可读别名(rwfx/xdfx)便于前端任意传。
_SPECS: dict[str, ChainSpec] = {
"6BF": _RWFX,
"RWFX": _RWFX,
"7BF": _XDFX,
"XDFX": _XDFX,
}
def _normalize(code: str | None) -> str:
"""规范化业务码:大写、去首尾空白、去掉多智能体 ``-A`` 后缀(rwfx-A → RWFX)。"""
c = (code or "").strip().upper()
if c.endswith("-A"):
c = c[:-2]
return c
def get_chain_spec(business_code: str | None) -> ChainSpec | None:
"""取某业务的写实规范;无 business_code 或未写实 → None。"""
norm = _normalize(business_code)
if not norm:
return None
return _SPECS.get(norm)
def build_structure_directive(business_code: str | None) -> str:
"""结构化抽取(flow-json)用的强制层级规范。
- 无 business_code(非深链接 / 普通会商)→ ""(不注入,保持现状);
- 有 business_code 但未写实(3Q/7BF/8BF…)→ 通用规范:强约束完整性 + 命名 + 取数,且明确**不要套用任何
固定的「目的→行为体」式模板**(counter 掉 SOUL 里可能残留的写死层级);
- 写实业务(rwfx)→ 完整层级 + 层间关系 + 命名 + 取数 + 完整性硬约束。
"""
norm = _normalize(business_code)
if not norm:
return ""
spec = _SPECS.get(norm)
if spec is None:
return (
"【本业务链抽取规范(强制遵守,优先级高于系统提示里的任何默认/示例层级)】\n"
"- 本业务**没有固定层级模板**:请严格按《方案总结报告》/ 各席位交付里**真实出现的层级**,"
"自「任务」为根逐层完整展开,每层每一条都出成节点,不要只抽开头一两层就停;\n"
"- **不要套用「目的→行为体」之类的固定示例层级**——层级名一律取自本次报告 / 交付里的真实层级;\n"
f"- {_NAMING_NOTE}\n- {_SOURCE_NOTE}"
)
arrows = " → ".join(spec.levels)
completeness = spec.completeness_note or f"以上 {len(spec.levels)} 个层级都要出节点,不得缺层。"
lines = [
f"【{spec.label} 业务链抽取规范(强制遵守,优先级高于系统提示里的任何默认/示例层级)】",
f"- **完整层级**(自上而下,必须逐层抽全、不得缺层、不得停在中间层):{arrows};",
f"- {spec.relations_note}",
f"- {_NAMING_NOTE}",
f"- {_SOURCE_NOTE}",
f"- **完整性**:{completeness}",
]
if spec.extra_note:
lines.append(f"- {spec.extra_note}")
return "\n".join(lines)
def build_summary_directive(business_code: str | None) -> str:
"""方案总结报告用的「业务链完整性核对」规范。
无 business_code 或未写实 → ""(普通会商 / 未知业务不强加完整性核对)。写实业务 → 注入完整层级,
要求 summary 核对产出是否覆盖了每一层级、缺层按「待补充」护栏标出。
"""
spec = get_chain_spec(business_code)
if spec is None:
return ""
arrows = " → ".join(spec.levels)
return (
f"【{spec.label} 业务链完整性核对(强制)】本次为 {spec.label} 业务场景,其完整业务链层级为:{arrows}。\n"
f"{spec.relations_note}\n"
"撰写报告时请**额外核对各席位交付 + 共识是否完整覆盖了上述每一个层级**:"
"报告文件本身按现有信息给出尽量完整的版本(**不要在 .md 文件里写「待补充」之类提示**);"
"并在**对话回复正文、报告之后的「核对说明」里逐层说明各层级是否覆盖、缺了哪层 / 哪层条目不足**"
"(即便各层级都已覆盖,也要写一句「各层级均已覆盖」),可在对话里让我补全;这段核对只在对话里、不写进 .md 文件,不要跳层。"
)
@@ -0,0 +1,107 @@
"""按 businessCode 的「业务链抽取规范」+ 其在 flow_extract / summary 的注入测试。
深链接会商模式下:结构化抽取(flow_extract)按业务完整层级抽全(根治"无论什么业务都按目的→行为体抽")、
方案总结报告按业务链完整性核对。规范单份、两角色用法;仅 business_code 非空时注入。
"""
from __future__ import annotations
from deerflow.agents.roundtable_orchestrator.extraction_spec import (
build_structure_directive,
build_summary_directive,
get_chain_spec,
)
# ── 规范本身 ──────────────────────────────────────────────────────────────────
def test_rwfx_structure_directive_has_full_six_levels_and_relations():
out = build_structure_directive("6BF")
for level in ("任务", "目的", "行为体", "预期行为", "驱动因素", "叙事"):
assert level in out
assert "一一对应" in out # 预期行为↔驱动因素 1:1
assert "一对多" in out
# 不含步骤/阶段名 的命名约束
assert "步骤" in out and "阶段" in out
assert "优先" in out and "方案总结报告" in out # 取数优先
def test_xdfx_structure_directive_is_branching_dag_with_many_to_one():
out = build_structure_directive("7BF")
for node in ("行动", "受众目标", "关键传播节点", "脆弱点", "策略"):
assert node in out, node
assert "分叉汇聚" in out
assert "多对一" in out
# 根是「行动」而非「任务」
assert "根节点 = 用户传入的「行动」" in out
# 7BF / xdfx / xdfx-A 都命中同一规范
for code in ("7BF", "xdfx", "XDFX-A"):
assert get_chain_spec(code) is not None and get_chain_spec(code).label == "xdfx", code
def test_business_code_normalization_handles_suffix_and_case():
# rwfx-A / 小写 / 含 -A 后缀都应命中 rwfx 规范
for code in ("rwfx", "RWFX", "rwfx-A", "6bf", "6BF"):
spec = get_chain_spec(code)
assert spec is not None and spec.label == "rwfx", code
def test_unknown_business_gets_generic_directive_not_rwfx_template():
out = build_structure_directive("3Q")
assert out # 非空(深链接业务仍注入通用规范)
assert "不要套用" in out # 明确不要套固定模板
# 通用规范不应写死 rwfx 的「预期行为/驱动因素」层级
assert "预期行为" not in out
def test_no_business_code_returns_empty():
assert build_structure_directive("") == ""
assert build_structure_directive(None) == ""
assert build_summary_directive("") == ""
assert build_summary_directive(None) == ""
def test_rwfx_summary_directive_checks_completeness():
out = build_summary_directive("6BF")
assert "完整性核对" in out
assert "叙事" in out and "驱动因素" in out
assert "待补充" in out # 缺层走护栏标注
# ── flow_extract 层级提示注入(结构化抽取) ───────────────────────────────────
def test_flow_extract_hierarchy_hint_uses_spec_when_business_code():
from app.gateway.routers.flow_extract import FlowExtractRequest, _hierarchy_hint
req = FlowExtractRequest(report_md="x", business_code="6BF")
hint = _hierarchy_hint(req)
# 业务规范权威注入:含完整层级 + 覆盖默认假设的措辞
assert "预期行为" in hint and "驱动因素" in hint and "叙事" in hint
assert "强制遵守" in hint
# 无 business_code → 回退原行为(不含业务规范块)
req2 = FlowExtractRequest(report_md="x", chain_levels=["目的", "行为体"])
hint2 = _hierarchy_hint(req2)
assert "强制遵守" not in hint2
assert "目的" in hint2 # 仍用 chain_levels
# ── build_summary_prompt 业务完整性注入(方案总结报告) ────────────────────────
def test_build_summary_prompt_injects_business_completeness():
from deerflow.agents.roundtable_orchestrator.engine import build_summary_prompt
from deerflow.agents.roundtable_orchestrator.types import SeatRef
agents = [SeatRef("seat-a", "情报")]
out = build_summary_prompt(
"目标X", agents, {"seat-a": "交付"}, "共识", business_code="6BF"
)
assert "完整性核对" in out
assert "预期行为" in out and "驱动因素" in out
# 无 business_code → 不注入业务完整性块(但意图对齐块仍在)
out2 = build_summary_prompt("目标X", agents, {"seat-a": "交付"}, "共识")
assert "完整性核对" not in out2
assert "意图对齐与缺口核对" in out2
@@ -31,10 +31,12 @@ def test_summary_prompt_carries_intent_and_alignment_directive():
# 意图对齐 + 缺口核对指令 # 意图对齐 + 缺口核对指令
assert "意图对齐与缺口核对" in out assert "意图对齐与缺口核对" in out
assert "核心诉求清单" in out assert "核心诉求清单" in out
# 防幻觉护栏三条 # 防幻觉 + 报告文件完整干净(不足只在对话正文、不写进 .md)+ 对话必须有「核对说明」
assert "综合推断" in out
assert "待补充" in out
assert "绝不凭空编" in out assert "绝不凭空编" in out
assert "无需用户修改" in out
assert "不写进 .md" in out
assert "核对说明" in out
assert "不要省略" in out
def test_summary_prompt_alignment_directive_present_even_without_coordinator_prompt(): def test_summary_prompt_alignment_directive_present_even_without_coordinator_prompt():