diff --git a/frontend-web/src/roundtable-planning/api/flow-extract.ts b/frontend-web/src/roundtable-planning/api/flow-extract.ts index 44c735de..89b36dac 100644 --- a/frontend-web/src/roundtable-planning/api/flow-extract.ts +++ b/frontend-web/src/roundtable-planning/api/flow-extract.ts @@ -82,6 +82,8 @@ export interface FlowExtractRequest { chainLevels?: string[]; /** 业务链总控编排提示(写明层级时优先用它)。 */ coordinatorPrompt?: string | null; + /** 深链接业务码(rwfx→6BF 等)。非空 → 后端按业务链抽取规范强制注入完整层级。 */ + businessCode?: string | null; /** 推理模型名(沿用第三步所选)。 */ model?: string; /** Phase 2 问答修改:当前流程图 + 指令 → 按指令改图(其余不变)。 */ @@ -109,6 +111,7 @@ export async function streamFlowExtract(req: FlowExtractRequest): Promise ({ sender: s.sender, content: s.content })), chain_levels: req.chainLevels ?? [], coordinator_prompt: req.coordinatorPrompt ?? null, + business_code: req.businessCode ?? null, model: req.model, current_graph: req.currentGraph ?? null, instruction: req.instruction ?? null, diff --git a/frontend-web/src/roundtable-planning/api/multi-agent.ts b/frontend-web/src/roundtable-planning/api/multi-agent.ts index 02c7fd99..a14c7ab1 100644 --- a/frontend-web/src/roundtable-planning/api/multi-agent.ts +++ b/frontend-web/src/roundtable-planning/api/multi-agent.ts @@ -240,6 +240,11 @@ export interface StreamMultiAgentRequest { * 与该席位 agent 自身 config.yaml 的同名开关取 OR。省略 = false(关)。 */ seatSkillDirective?: boolean; + /** + * 深链接业务码(rwfx→6BF 等)。**仅方案总结报告(summary)单例消费**:非空 → 后端按业务链抽取规范 + * 强制注入「完整性核对」指令,让总结核对产出相对业务链完不完整。普通会商省略 = 不注入。 + */ + businessCode?: string | null; skillStopNames?: string[]; /** * 仅 leader 用。置 true 表示这是「派活后让总控判断继续/综合」的轮次:后端会把各已交付 @@ -493,6 +498,8 @@ export async function streamMultiAgent( subagent_enabled: req.subagentEnabled, // 仅 special 用;会商技能识别强化(沿用所选业务链条开关)。省略 → 后端按 false(关)处理。 seat_skill_directive: req.seatSkillDirective ?? false, + // 仅 summary 单例用;深链接业务码 → 后端注入业务链完整性核对。省略/空 → 不注入。 + business_code: req.businessCode ?? null, skill_stop_names: req.skillStopNames ?? [], // 仅 leader 用;综合轮让后端注入各席位完整交付正文。省略 → 后端按 false 处理。 synthesis_mode: req.synthesisMode ?? false, diff --git a/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts b/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts index 8045b638..5bf1790a 100644 --- a/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts +++ b/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts @@ -108,6 +108,8 @@ function toStartBody(p: StartJobPayload) { ), // 研讨前并行取数开关(来源链条 gatherFirst):true = 后台研讨前先并行取数再研讨。缺省 false。 gather_first: p.gatherFirst ?? false, + // 深链接业务码:后台总结报告据此做业务链完整性核对(写实业务如 rwfx 才生效)。空 → 不注入。 + business_code: p.businessCode ?? null, ...(seat ? { thinking_enabled: seat.thinking_enabled, diff --git a/frontend-web/src/roundtable-planning/hooks/useStep3Summary.ts b/frontend-web/src/roundtable-planning/hooks/useStep3Summary.ts index 1a55216d..d14c88dd 100644 --- a/frontend-web/src/roundtable-planning/hooks/useStep3Summary.ts +++ b/frontend-web/src/roundtable-planning/hooks/useStep3Summary.ts @@ -321,8 +321,10 @@ function buildSummaryPrompt(opts: { "【动笔前必做:意图对齐与缺口核对】各席位可能由能力较弱的模型产出,未必扣住用户意图、未必充分。" + "你是这份总结的质量兜底,不能只把各席位交付简单拼接。请先把上面的「任务意图」(目标/约束/关键假设)拆成" + "用户核心诉求清单(编排提示对最终产出的要求也并入),逐条核对各席位交付+共识是否覆盖、够不够、有无跑题或矛盾;" + - "再据此组织并按护栏补强:① 能从已有原料重组/推理补上的→直接补全讲透;② 属于你自己的推断/建议→显式标注「(综合推断)」;" + - "③ 缺真实信息、靠现有原料补不了的→绝不凭空编,用一行『> ⚠️ 待补充:<具体缺什么>。可在对话里让我补全』标出,交给用户。" + + "再据此组织:① 能从已有原料重组/推理补上的→直接补全讲透;② 缺真实信息补不了的→绝不凭空编。" + + "**写进 .md 报告文件的必须是一份完整、干净、可直接使用、无需用户修改的报告——不要在报告文件里写「待补充/不足/⚠️」之类提示或占位**。" + + "**但在对话回复正文、报告之后,必须追加一段简短「核对说明」**(只在对话里、**不写进 .md 文件**):逐条说明产出是否覆盖了用户意图、哪些充分/哪些不足/有哪些可补充(可在对话里让我补全);" + + "**即便都已覆盖、无明显缺口,也要明确写一句「已核对:各诉求均已覆盖,无明显缺口」,这段核对说明任何时候都不要省略。**" + "最终产出要明显比各席位交付的简单拼接更扣用户意图。", ); if (opts.flowMode) { @@ -1041,6 +1043,8 @@ export function useStep3Summary( dAgentThreadId: { [agentName]: threadId }, displayName: "方案总结报告", newMessage: prompt, + // 深链接业务码:后端为 summary 单例注入「业务链完整性核对」(核对产出相对业务链完不完整)。 + businessCode: ingestBusinessCodeRef.current, model: selectedModel || undefined, files: files && files.length > 0 ? files : undefined, signal: aborter.signal, @@ -1136,6 +1140,8 @@ export function useStep3Summary( seatDeliveries, chainLevels, coordinatorPrompt, + // 深链接业务码:后端按业务链抽取规范强制注入完整层级(根治"无论什么业务都按目的→行为体抽")。 + businessCode: ingestBusinessCodeRef.current, model: selectedModel || undefined, currentGraph: opts?.currentGraph ?? null, instruction: opts?.instruction, diff --git a/frontend-web/src/roundtable-planning/lib/job-types.ts b/frontend-web/src/roundtable-planning/lib/job-types.ts index 9d5ee618..922edd66 100644 --- a/frontend-web/src/roundtable-planning/lib/job-types.ts +++ b/frontend-web/src/roundtable-planning/lib/job-types.ts @@ -147,6 +147,11 @@ export interface StartJobPayload { * 在 `toStartBody` 里下发为后端 `gather_first`。缺省 false。 */ gatherFirst?: boolean; + /** + * 深链接业务码(rwfx→6BF 等)。透传给后台总结报告的「业务链完整性核对」(写实业务如 rwfx 才生效)。 + * 普通会商省略。`toStartBody` 里下发为后端 `business_code`。 + */ + businessCode?: string | null; } /** 终态集合——SSE/轮询判定作业是否结束。 */ diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index 4813c6ce..7c83f6c5 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -1546,6 +1546,7 @@ export default function RoundtablePlanningPage() { seatSkillDirectives, // 每席位技能识别强化沿用所选业务链条里各席位的开关 seatModes, // 每席位推理深度覆盖沿用所选业务链条里各席位的配置 gatherFirst, // 研讨前并行取数沿用所选业务链条的开关 + businessCode: prefillRef.current?.businessCode ?? undefined, // 深链接业务码 → 后台总结业务链完整性核对 // 后台作业引擎支持 dag(后端 _run_dag:stage 串行 / stage 内并行)—— // 直接把当前编排模式透传,业务链的分层 / 并行 / 阶段目标 / 总控提示词全部保留。 orchestrationMode, @@ -1628,6 +1629,7 @@ export default function RoundtablePlanningPage() { seatSkillDirectives: seatSkillDirectivesSnap, seatModes: seatModesSnap, gatherFirst: gatherFirstSnap, + businessCode: prefillRef.current?.businessCode ?? undefined, orchestrationMode: mode, chain: chainSnap, orchestrationPlan: planSnap, @@ -1697,6 +1699,7 @@ export default function RoundtablePlanningPage() { seatSkillDirectives, seatModes, gatherFirst, + businessCode: prefillRef.current?.businessCode ?? undefined, orchestrationMode, chain: chainMeta, orchestrationPlan: orchestrationMode === "dag" ? orchestrationPlan : null, diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 30b9d866..01c702c6 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -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. | | **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 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 `` 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 再按其流程取信息/执行") to the **seat's per-turn task (human turn)** — far stronger pull on weak models than the system prompt. Wired into **both** seat paths: foreground `multi_agent._special_run` (only `role in {seat, seat_parallel}`; Step3 singletons report/summary/dashboard/ingest excluded) + background `InProcessRoundtableGateway.run_seat`. No-op (returns task unchanged) when the seat has no configured skills. **2026-06-20: default-off + two opt-in switches (OR semantics)** — was global-on, now injects only when **either** the seat agent's own switch (`AgentConfig.seat_skill_directive`, edited in the agent edit dialog `agent-card.tsx` + `agent-detail-panel.tsx`) **or** the **per-seat** switch on that agent **within the selected business chain** is on. The per-seat chain switch lives **in the chain's `seats` JSON** (`ChainSeatSchema.seat_skill_directive`, no DB column — rides the existing JSON like the per-seat `model` override), edited via a small ✨ icon → dropdown (Switch + description) on each agent card in `BusinessChainEditorPage`. It is threaded **per-seat** to each seat run: foreground `multi_agent` per-call field `seat_skill_directive` (orchestration looks it up per agentId from a `{agentId: bool}` session map) → `_special_run` → `chain_enabled`; background as the job field `seat_skill_directives: dict[str,bool]` (StartJobRequest → StartParams → deps factory → `InProcessRoundtableGateway._seat_skill_directives`) → `run_seat` looks up `agent_id` → `chain_enabled`. `ROUNDTABLE_SEAT_SKILL_DIRECTIVE` is only a **master kill-switch** (default allow; set `0/false` to hard-disable regardless of the switches — never force-enables). Tests: `tests/test_roundtable_seat_skills.py`, `tests/test_agents_backend.py`. **2026-06-20 Step2 提速三件套**:(1) **每席位推理深度覆盖** —— 业务链条 `seats` JSON 里逐席位可配 `seat_mode`(flash/thinking/pro/ultra,覆盖链条级 `seatMode`;编辑器席位卡右侧 ⏱Gauge 图标下拉,null=跟随链条默认)。前台经 `useStep2Orchestration` 三处 dispatch 的 `seatModeToRequestFields(seatModes[id] ?? selectedMode)` 透传;后台经 job `seat_modes: dict[str,str]` → `StartParams` → `InProcessRoundtableGateway._seat_modes`,`run_seat` 用 `_SEAT_MODE_PARAMS`(与前端 `seatModeToRunParams` 逐字对齐)解析覆盖作业级 seat_*(未命中→跟随作业级→policy 默认)。无 DB 列(ride `seats` JSON,但 `ChainSeatSchema.seat_mode` 须显式声明否则 Pydantic 剥掉)。Tests: `tests/test_roundtable_seat_modes.py`。(2) **recommend 批内并行** —— 后端 `engine._run_recommend` 由 for-await 串行改 `asyncio.gather`(对齐前端 2026-06-13 已并行;总控一轮派多席位=判定相互独立→并发;批内用派活前 deliveries 快照、批后统一并入,语义不变)。(3) **研讨前并行『取数』两段式**(业务链条 `gather_first` 列,逐条 opt-in,migration `20260620_02`,editor「研讨前并行取数」开关)—— `gather_first=True` 时 `run_orchestration` 在分发前先 `engine._run_gather_phase`(全席位 `asyncio.gather` 跑 `_GATHER_TASK`「只取数不研讨」),产出 `gathered: dict` 经 `_seat_message(gathered=...)` 作为「已收集资料」块注入 recommend/chain/dag 各研讨轮席位任务(与「前序交付」块分开),把最慢的技能延迟前置并行、给(尤其 chain 串行的)研讨段提速;研讨阶段不限制再调技能。前台对称实现:`useStep2Orchestration.runGatherPhase` 在 `runActiveOrchestration` 分发前跑一次(`gatherPhaseDoneRef` 闸门,resetInitGate 复位),产出存 `gatheredDeliveriesRef` 经 `peerDeliveries`/初始 `priorDeliveries` 注入研讨席位。链条级 `gatherFirst`(camel)↔`gather_first`(snake) 经 `api/chains.ts` 映射;job 经 `StartJobRequest.gather_first`→`StartParams`→`run_orchestration(gather_first=…)`。Tests: `tests/test_roundtable_gather_phase.py`。(4) **削减重复广播**(前端 only,多智能体 foreground 路径)—— `suppress_pre_broadcast`(仅 leader:抑制「总控本轮输入→各席位 thread」预广播 + 「总控给X派活」→兄弟席位的 post 广播,二者一个 flag)此前只在 DAG 派活轮开;现扩到 recommend(`safetyCounter > 1`,即第 2 轮起的续轮/综合)+ chain 逐席位(`i > 0`,第 2 个席位起)+ chain 收口轮(恒 true)。**保留每种模式的首轮预广播**(它为席位打底任务背景),只 suppress 重复的续轮广播(N 次读写巨大席位 thread 的纯浪费 + 弱模型模仿派活);席位的前序交付另经「席位交付广播」`broadcastSeatDeliveryToOthers` 送达,不受影响。后台引擎路径无此广播(`_seat_message` 直接拼上下文),故 #4 仅前端。 | +| **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 `` 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 再按其流程取信息/执行") to the **seat's per-turn task (human turn)** — far stronger pull on weak models than the system prompt. Wired into **both** seat paths: foreground `multi_agent._special_run` (only `role in {seat, seat_parallel}`; Step3 singletons report/summary/dashboard/ingest excluded) + background `InProcessRoundtableGateway.run_seat`. No-op (returns task unchanged) when the seat has no configured skills. **2026-06-20: default-off + two opt-in switches (OR semantics)** — was global-on, now injects only when **either** the seat agent's own switch (`AgentConfig.seat_skill_directive`, edited in the agent edit dialog `agent-card.tsx` + `agent-detail-panel.tsx`) **or** the **per-seat** switch on that agent **within the selected business chain** is on. The per-seat chain switch lives **in the chain's `seats` JSON** (`ChainSeatSchema.seat_skill_directive`, no DB column — rides the existing JSON like the per-seat `model` override), edited via a small ✨ icon → dropdown (Switch + description) on each agent card in `BusinessChainEditorPage`. It is threaded **per-seat** to each seat run: foreground `multi_agent` per-call field `seat_skill_directive` (orchestration looks it up per agentId from a `{agentId: bool}` session map) → `_special_run` → `chain_enabled`; background as the job field `seat_skill_directives: dict[str,bool]` (StartJobRequest → StartParams → deps factory → `InProcessRoundtableGateway._seat_skill_directives`) → `run_seat` looks up `agent_id` → `chain_enabled`. `ROUNDTABLE_SEAT_SKILL_DIRECTIVE` is only a **master kill-switch** (default allow; set `0/false` to hard-disable regardless of the switches — never force-enables). Tests: `tests/test_roundtable_seat_skills.py`, `tests/test_agents_backend.py`. **2026-06-20 Step2 提速三件套**:(1) **每席位推理深度覆盖** —— 业务链条 `seats` JSON 里逐席位可配 `seat_mode`(flash/thinking/pro/ultra,覆盖链条级 `seatMode`;编辑器席位卡右侧 ⏱Gauge 图标下拉,null=跟随链条默认)。前台经 `useStep2Orchestration` 三处 dispatch 的 `seatModeToRequestFields(seatModes[id] ?? selectedMode)` 透传;后台经 job `seat_modes: dict[str,str]` → `StartParams` → `InProcessRoundtableGateway._seat_modes`,`run_seat` 用 `_SEAT_MODE_PARAMS`(与前端 `seatModeToRunParams` 逐字对齐)解析覆盖作业级 seat_*(未命中→跟随作业级→policy 默认)。无 DB 列(ride `seats` JSON,但 `ChainSeatSchema.seat_mode` 须显式声明否则 Pydantic 剥掉)。Tests: `tests/test_roundtable_seat_modes.py`。(2) **recommend 批内并行** —— 后端 `engine._run_recommend` 由 for-await 串行改 `asyncio.gather`(对齐前端 2026-06-13 已并行;总控一轮派多席位=判定相互独立→并发;批内用派活前 deliveries 快照、批后统一并入,语义不变)。(3) **研讨前并行『取数』两段式**(业务链条 `gather_first` 列,逐条 opt-in,migration `20260620_02`,editor「研讨前并行取数」开关)—— `gather_first=True` 时 `run_orchestration` 在分发前先 `engine._run_gather_phase`(全席位 `asyncio.gather` 跑 `_GATHER_TASK`「只取数不研讨」),产出 `gathered: dict` 经 `_seat_message(gathered=...)` 作为「已收集资料」块注入 recommend/chain/dag 各研讨轮席位任务(与「前序交付」块分开),把最慢的技能延迟前置并行、给(尤其 chain 串行的)研讨段提速;研讨阶段不限制再调技能。前台对称实现:`useStep2Orchestration.runGatherPhase` 在 `runActiveOrchestration` 分发前跑一次(`gatherPhaseDoneRef` 闸门,resetInitGate 复位),产出存 `gatheredDeliveriesRef` 经 `peerDeliveries`/初始 `priorDeliveries` 注入研讨席位。链条级 `gatherFirst`(camel)↔`gather_first`(snake) 经 `api/chains.ts` 映射;job 经 `StartJobRequest.gather_first`→`StartParams`→`run_orchestration(gather_first=…)`。Tests: `tests/test_roundtable_gather_phase.py`。(4) **削减重复广播**(前端 only,多智能体 foreground 路径)—— `suppress_pre_broadcast`(仅 leader:抑制「总控本轮输入→各席位 thread」预广播 + 「总控给X派活」→兄弟席位的 post 广播,二者一个 flag)此前只在 DAG 派活轮开;现扩到 recommend(`safetyCounter > 1`,即第 2 轮起的续轮/综合)+ chain 逐席位(`i > 0`,第 2 个席位起)+ chain 收口轮(恒 true)。**保留每种模式的首轮预广播**(它为席位打底任务背景),只 suppress 重复的续轮广播(N 次读写巨大席位 thread 的纯浪费 + 弱模型模仿派活);席位的前序交付另经「席位交付广播」`broadcastSeatDeliveryToOthers` 送达,不受影响。后台引擎路径无此广播(`_seat_message` 直接拼上下文),故 #4 仅前端。**2026-06-20 深链接业务链抽取规范**:深链接会商(taskId+goPath,businessCode 如 rwfx→6BF)下,结构化抽取(flow-json)与方案总结报告都按所选业务链**完整层级**强制校正。结构化抽取实为 `roundtable-structure` 智能体(`flow_extract.py` 的 `_system_prompt()=load_agent_soul(STRUCTURE_AGENT_ID)`,抽取→校验两段式 `model.astream`);其 SOUL 全局一份,旧实现把某业务层级写死进 SOUL→所有业务都按「目的→行为体」抽(bug)。修法:**单份领域知识、两角色用法**的 `deerflow.agents.roundtable_orchestrator.extraction_spec`(放 harness 层,app+harness 都可 import;`build_structure_directive`/`build_summary_directive` 按 businessCode 取;rwfx=6BF 写实「任务→目的→行为体→预期行为→驱动因素→叙事,预期行为↔驱动因素 1:1、余 1:N、label 不含步骤/阶段名、优先报告回退席位」,其余业务通用规范=强约束完整性+命名+取数且明令不套固定模板;business_code 空→""零副作用)。注入:(1) 结构化抽取 `flow_extract._hierarchy_hint`+`_validate_messages` 用 `build_structure_directive` 权威覆盖默认层级(前端 `streamFlowExtract` 传 `business_code`,源 `ingestBusinessCode`);(2) 方案总结报告 `build_summary_directive`——前台 `multi_agent._special_run`(role==summary 且 business_code) 追加、后台 `engine.build_summary_prompt(business_code=…)`(StartParams/StartJobRequest 透传)。**仅深链接生效**。种子 `roundtable-structure/SOUL.md` 层级顺序改 spec-driven、连接示例去 rwfx 写死(存量 live SOUL 不被 seed 覆盖,但注入规范权威可纠正)。Tests: `tests/test_roundtable_extraction_spec.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_` ? 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/`. 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"`. | diff --git a/offline-backend-20260512/backend/app/gateway/roundtable_job_executor.py b/offline-backend-20260512/backend/app/gateway/roundtable_job_executor.py index f0d83a43..7f3b11cd 100644 --- a/offline-backend-20260512/backend/app/gateway/roundtable_job_executor.py +++ b/offline-backend-20260512/backend/app/gateway/roundtable_job_executor.py @@ -200,6 +200,9 @@ class StartParams: # 是否在研讨前先跑一轮**全席位并行『取数』**(业务链条逐条 opt-in):各席位并行只调技能取数、 # 不研讨,产出作为共享资料注入后续研讨轮。透传给 run_orchestration 的 gather_first。默认 False。 gather_first: bool = False + # 深链接业务码(rwfx→6BF 等)。透传给 run_orchestration → build_summary_prompt:写实业务额外注入 + # 「业务链完整性核对」,让后台总结报告核对产出相对业务链完不完整。普通会商为空 → 不注入。 + business_code: str | None = None # 网关工厂:给定一次启动参数,返回一个 RoundtableGateway 实例。 @@ -288,6 +291,7 @@ class RoundtableJobExecutor: enable_report=params.enable_report, plan=params.orchestration_plan, gather_first=params.gather_first, + business_code=params.business_code, ) # 终态:把最终对话 + step3 + jobStatus 落进同一个 run(含 furthest_step=3)。 await self._write_run_to_draft( diff --git a/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/SOUL.md b/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/SOUL.md index 40ec5ca6..426d2b7c 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/SOUL.md +++ b/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/SOUL.md @@ -44,7 +44,7 @@ {"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` 标对层级; (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 填全、填对; ⑤ **务必把每个层级的每一条都输出成节点、不要漏层、不要只输出开头一两个节点就停**(典型规模约 13–16 个节点); ⑥ **只画**这些层级实体节点;**不要**把「综合分析 / 核心洞察 / 执行路线 / 建议 / 结论」以及「阶段①②③④」等章节名 / 阶段名 / 汇总段落画成节点;节点 `label` 也**绝不能**是「阶段1 / 阶段2 / 第N阶段」这类阶段编号。 -## 三、层级顺序 +## 三、层级顺序(重要:不要写死任何一套层级) -- 若数据来源里给了「业务链总控编排提示」并写明了层级链(如 任务 → 目的 → 行为体 → 预期行为 → 驱动因素 → 叙事),**优先按它的层级**组织; -- 否则按给出的「默认层级」;都没有时,以「任务」为根、按报告 / 交付的逻辑结构合理分层。 +**每个业务链的层级各不相同**,绝不要默认套用某一套固定层级(如「目的→行为体」)。本轮该用哪套层级,按下面的优先级,**完全以本轮给出的信息为准**: + +1. 若本轮用户消息里给了「业务链抽取规范」(写明完整层级链、层间一对一/一对多关系等)——**这是最高优先级,严格逐层照它抽全、不得缺层、不得停在中间层**; +2. 否则若给了「业务链总控编排提示」并写明了层级链——按它的层级; +3. 都没有时,以「任务」为根、按报告 / 交付里**真实出现的层级**逐层展开。 + +> 示例(仅示意「层级链」这个概念,**不是固定模板、不要照搬**):某业务可能是 任务 → 目的 → 行为体 → 预期行为 → 驱动因素 → 叙事;另一业务可能完全不同。实际层级一律以上面第 1/2/3 条给出的为准。 ## 四、修改 / 校验时 diff --git a/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-summary/SOUL.md b/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-summary/SOUL.md index cda907f5..e72394c0 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-summary/SOUL.md +++ b/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-summary/SOUL.md @@ -29,11 +29,12 @@ 2. **逐条核对覆盖**:对每条诉求,检查各席位交付 + 总控共识**是否覆盖、覆盖得够不够、有没有跑题或自相矛盾**。 3. **据此组织报告**,并按下面的「缺口处理护栏」补强——让最终报告明显**比"各席位交付的简单拼接"更扣用户意图**:意图里要的、席位没说清的,你补强或标缺口;意图里没要的、席位跑题的,不要喧宾夺主。 -### 缺口处理护栏(避免弱模型把幻觉带进报告) -- **能从已有原料重组 / 推理补上的** → 直接补全、讲透,正常写进报告。这是你最该做的增值:把零散、跑题、含糊的席位产物,按用户意图重新梳理成扣题的方案。 -- **超出席位原料、属于你自己的合理推断 / 建议** → 可以写,但**显式标注「(综合推断)」**,让用户能区分哪些有席位依据、哪些是你的补充判断。 -- **缺真实信息、靠现有原料补不了的**(需要最新数据 / 外部事实,而席位没给)→ **绝不凭空编**。在报告对应位置用一行醒目标注:`> ⚠️ 待补充:<具体缺什么>。如需补全,可在下方对话里告诉我(例如「补一下 X 的最新情况」),我会据此完善。` 把球交给用户——你本身不联网、不调研,真补由用户在对话里发起。 -- 始终:**宁可标注缺口,也不要假装完整**;绝不编造与原料矛盾、或无依据的"事实"。 +### 缺口处理护栏(报告文件要完整干净、把不足说在对话里) +**核心原则:写进 `.md` 报告文件的,必须是一份完整、干净、可直接使用、无需用户再修改的报告——报告文件里不要出现「待补充 / 不足 / ⚠️ / 尚需完善」之类的提示或占位。** 着急的用户拿到文件就能用。 +- **能从已有原料重组 / 推理补上的** → 直接补全、讲透,正常写进报告。这是你最该做的增值:把零散、跑题、含糊的席位产物,按用户意图重新梳理成扣题、完整的方案。 +- **超出席位原料、属于你自己的合理推断 / 建议** → 可以写进报告,让报告完整连贯;不必在报告里逐处打标签(保持可读),绝不编造与原料矛盾的内容。 +- **缺真实信息、靠现有原料补不了的**(需要最新数据 / 外部事实,而席位没给)→ **绝不凭空编**,但也**不要在报告文件里写「待补充」之类的警告**。报告文件就按现有信息给出尽量完整的版本;这些「不足 / 尚可补充之处」**只在对话回复正文里、报告之后单独说明**(见下「B」)。 +- 始终:报告文件 = 一份完整可用的报告(无缺口提示);不足只在对话文字里点出;绝不编造无依据的"事实"。 ## 产出要求(最重要) @@ -53,9 +54,10 @@ - **严禁**写 Python / shell / 任何脚本去"生成"文件,也**严禁**用 bash——你必须自己把 Markdown 文本直接写进 `write_file`; - `write_file` **支持很长的内容,不会截断**——请放心一次性把整篇报告写进去。 -### B. 在对话回复里给出报告正文 +### B. 在对话回复里给出报告正文(不足只写在这里) - 写完文件后,**把这份报告的完整正文也输出到对话回复里**(直接给用户读),不要只回一句「已写入文件」。 -- 用户既能在对话里直接读到报告,也能在右侧预览 / 下载这个 `.md` 文件——两边内容应当一致。 +- 报告正文部分与 `.md` 文件**应当一致**(都是那份完整干净的报告)。 +- **在对话回复里、报告正文之后,必须追加一段简短「核对说明」**(这段**只在对话里**、**绝不写进 `.md` 报告文件**):对照用户意图(及业务链各层级,如有)**逐条说明产出是否覆盖、哪些充分 / 哪些不足、有哪些可补充完善之处**,并提示可在对话里让你补全(如「---\n**核对说明**:① 已覆盖:…;② 资料不足 / 尚可补充:…(可直接告诉我补全)」)。**即便核对下来各诉求 / 层级都已覆盖、无明显缺口,也要明确写一句「已核对:各诉求 / 层级均已覆盖,无明显缺口」——这段核对说明任何时候都不要省略。** 报告 `.md` 文件本身始终是一份完整可用、无需修改、不含任何缺口提示的版本。 ## 报告结构(叙事型,文字为主线) **下面是默认结构——仅当「业务链总控编排提示」没有指定输出结构时使用。** 用户指定了结构(章节、表格、清单、固定模板等)就完全按用户的来;两者只部分重叠时,以用户要求为骨架、默认章节做补充。按下面的章节顺序组织,缺料的章节可省略或合并: @@ -136,7 +138,7 @@ - ❌ 在报告任何位置添加「AI 自动生成」「AI generated」之类的标签、徽章或水印 - ❌ 编造与原料明显矛盾的内容 - ❌ 只把各席位交付简单拼接、不做「意图对齐与缺口核对」(席位可能弱,你要兜底扣住用户意图) -- ❌ 缺真实信息时凭空编造来"填满"报告(应按护栏标注「> ⚠️ 待补充:…」交给用户在对话里补) -- ❌ 自己的推断不加「(综合推断)」标注,让用户误以为有席位依据 +- ❌ 缺真实信息时凭空编造来"填满"报告(按现有信息给出完整版即可;不足只在对话正文点出,不写进 .md 文件) +- ❌ 在 `.md` 报告文件里写「待补充 / 不足 / ⚠️ / 尚需完善」等缺口提示或占位(报告文件要完整干净、无需修改;不足只写在对话正文里) - ❌ 忽视「业务链总控编排提示」的口径要求(它优先级最高) - ❌ 编排提示已指定输出结构 / 重点席位,却仍按默认章节平均用力(结构要完全跟用户走,重点席位要明显加权) diff --git a/offline-backend-20260512/backend/app/gateway/routers/flow_extract.py b/offline-backend-20260512/backend/app/gateway/routers/flow_extract.py index e448ea66..36e6ff53 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/flow_extract.py +++ b/offline-backend-20260512/backend/app/gateway/routers/flow_extract.py @@ -31,6 +31,8 @@ from fastapi.responses import StreamingResponse from langchain_core.messages import HumanMessage, SystemMessage from pydantic import BaseModel, Field +from deerflow.agents.roundtable_orchestrator.extraction_spec import build_structure_directive + from app.gateway.routers._roundtable_seed import ( STRUCTURE_AGENT_ID, ensure_roundtable_functional_agents, @@ -59,6 +61,9 @@ class FlowExtractRequest(BaseModel): # 层级骨架:优先 coordinator_prompt 里写明的层级,否则用 chain_levels。 chain_levels: list[str] = Field(default_factory=list) coordinator_prompt: str | None = Field(default=None) + # 深链接业务码(rwfx→6BF 等)。非空 → 按该业务的「业务链抽取规范」**强制**注入完整层级 + # (覆盖默认/示例层级假设),根治"无论什么业务都按目的→行为体抽"。普通会商为空 → 不注入。 + business_code: str | None = Field(default=None) model: str | None = Field(default=None) # Phase 2 问答修改:带上当前图 + 用户指令 → 进 edit 模式(按指令改图,其余不变)。 current_graph: dict[str, Any] | None = Field(default=None) @@ -438,6 +443,17 @@ def _material(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(): return "层级顺序优先采用下面「业务链总控编排提示」里写明的层级链(如它写的 任务→目的→行为体→…):\n" + req.coordinator_prompt.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]: + # 深链接业务规范:校验阶段也带上完整层级,确保按规范补齐缺层(不只按抽取里出现的层级)。 + spec = build_structure_directive(req.business_code) + spec_block = (spec + "\n\n") if spec else "" user = ( - _material(req) + spec_block + + _material(req) + "\n\n===== 已抽取的 flow-json(待校验修复)=====\n" + extracted + "\n===== 待校验结束 =====\n\n请对照数据来源,**逐个节点检查上面这份 flow-json 是不是缺字段**、并修复它:" diff --git a/offline-backend-20260512/backend/app/gateway/routers/multi_agent.py b/offline-backend-20260512/backend/app/gateway/routers/multi_agent.py index 888c01a3..7457d9af 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/multi_agent.py +++ b/offline-backend-20260512/backend/app/gateway/routers/multi_agent.py @@ -64,6 +64,8 @@ from langgraph.checkpoint.base import empty_checkpoint from app.gateway.roundtable_diag import record_foreground_diag 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.routers._roundtable_seed import ( COORDINATOR_AGENT_ID, @@ -1738,6 +1740,7 @@ async def _special_run( reasoning_effort: str | None = None, subagent_enabled: bool = False, seat_skill_directive: bool = False, + business_code: str = "", ) -> AsyncIterator[str | dict[str, Any]]: """子智能体(special)单轮:执行总控下发的 task 文本,交付 content。 @@ -1796,6 +1799,12 @@ async def _special_run( seat_message = new_message if role in ("seat", "seat_parallel"): 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( agent_name=agent_name, model_name=model_name, @@ -1963,6 +1972,9 @@ async def run_multi_agent_stream(request: Request, payload: dict = Body(...)) -> # 业务链条级「技能识别强化」开关(仅 special 席位消费):前端读所选业务链条的同名配置 # 后逐席位透传。与该席位 agent 自身的 config.yaml 开关取 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: raise HTTPException( @@ -2014,6 +2026,7 @@ async def run_multi_agent_stream(request: Request, payload: dict = Body(...)) -> reasoning_effort=seat_reasoning_effort, subagent_enabled=seat_subagent_enabled, seat_skill_directive=seat_skill_directive, + business_code=business_code, ) async for chunk in gen: if isinstance(chunk, dict): diff --git a/offline-backend-20260512/backend/app/gateway/routers/roundtable_jobs.py b/offline-backend-20260512/backend/app/gateway/routers/roundtable_jobs.py index b06c59e2..5116d0b8 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/roundtable_jobs.py +++ b/offline-backend-20260512/backend/app/gateway/routers/roundtable_jobs.py @@ -87,6 +87,8 @@ class StartJobRequest(BaseModel): seat_modes: dict[str, str] = Field(default_factory=dict) # 是否研讨前先并行『取数』(业务链条逐条 opt-in;前端读所选链条的开关后下发)。默认 False。 gather_first: bool = False + # 深链接业务码(rwfx→6BF 等)。透传到后台总结报告的「业务链完整性核对」。普通会商为空。 + business_code: str | None = None 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_modes=dict(body.seat_modes or {}), gather_first=bool(body.gather_first), + business_code=(body.business_code or None), ) ) return _to_response(row) diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/engine.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/engine.py index b28d054b..fe36c3b0 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/engine.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/engine.py @@ -23,6 +23,7 @@ import logging from dataclasses import dataclass, field from datetime import UTC, datetime +from .extraction_spec import build_summary_directive from .progress import JobProgressWriter from .types import ( CHAIN_CLOSEOUT_PROMPT, @@ -81,6 +82,7 @@ async def run_orchestration( enable_report: bool = True, plan: dict | None = None, gather_first: bool = False, + business_code: str | None = None, ) -> OrchestrationResult: """运行一次完整编排(循环 → 共识 → 报告绘制)。 @@ -162,6 +164,7 @@ async def run_orchestration( consensus_text=outcome.last_leader_content, deliveries=outcome.deliveries, coordinator_prompt=(dag_plan.coordinator_prompt if dag_plan is not None else None), + business_code=business_code, ) await progress.done() return OrchestrationResult( @@ -597,6 +600,7 @@ async def _run_step3( consensus_text: str, deliveries: dict[str, str], coordinator_prompt: str | None = None, + business_code: str | None = None, ) -> dict | None: """Step3 双产物:「方案总结报告」(roundtable-summary 写 md)与「结果绘制」 (roundtable-report 画 HTML)**并行执行**(两段无依赖,2026-06-13 由串行改并行, @@ -617,7 +621,8 @@ async def _run_step3( await progress.summary_writing() summary_thread_id = await gateway.create_summary_thread() 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_thread_id=summary_thread_id, prompt=summary_prompt, model=model @@ -772,6 +777,7 @@ def build_summary_prompt( consensus_text: str, *, coordinator_prompt: str | None = None, + business_code: str | None = None, ) -> str: """拼装 Step3「方案总结报告」prompt —— 对齐前端 ``useStep3Summary.buildSummaryPrompt`` (非流程图分支)+ ``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( "请严格遵循你的职责设定:纯 Markdown 文本(不画 HTML / 图表)、全程简体中文。" "报告结构:默认用叙事型结构(一句话结论 → 背景目标 → 各席位观点 → 综合分析 → 最终共识方案 → 执行路线);" diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/extraction_spec.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/extraction_spec.py new file mode 100644 index 00000000..200e48d7 --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/extraction_spec.py @@ -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 文件,不要跳层。" + ) diff --git a/offline-backend-20260512/backend/tests/test_roundtable_extraction_spec.py b/offline-backend-20260512/backend/tests/test_roundtable_extraction_spec.py new file mode 100644 index 00000000..6557e56a --- /dev/null +++ b/offline-backend-20260512/backend/tests/test_roundtable_extraction_spec.py @@ -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 diff --git a/offline-backend-20260512/backend/tests/test_roundtable_summary_prompt.py b/offline-backend-20260512/backend/tests/test_roundtable_summary_prompt.py index b2f92741..871eafab 100644 --- a/offline-backend-20260512/backend/tests/test_roundtable_summary_prompt.py +++ b/offline-backend-20260512/backend/tests/test_roundtable_summary_prompt.py @@ -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 "不写进 .md" in out + assert "核对说明" in out + assert "不要省略" in out def test_summary_prompt_alignment_directive_present_even_without_coordinator_prompt():