206 lines
7.2 KiB
Python
206 lines
7.2 KiB
Python
"""Build bounded, traceable seat input from MAF roundtable context.
|
|
|
|
Converts the shared roundtable state (task, previous turns, fact ledger,
|
|
stage instructions) into a single human message for a seat's DeerFlow thread.
|
|
|
|
Design constraints (MAF-02-06):
|
|
- Bounded: total output STRICTLY never exceeds ``max_context_chars``.
|
|
- Trimmable: each section is independently truncatable.
|
|
- Traceable: includes turn references so the seat can cite prior work.
|
|
- Never injects credentials, tokens, or raw binary content.
|
|
- All content is sanitized before inclusion.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from app.maf_roundtable.sanitize import sanitize
|
|
|
|
_DEFAULT_MAX_CONTEXT_CHARS = 12000
|
|
_DEFAULT_MAX_TURN_CHARS = 2000
|
|
_DEFAULT_MAX_FACT_CHARS = 3000
|
|
|
|
|
|
def build_seat_input(
|
|
*,
|
|
task_text: str,
|
|
stage_instruction: str,
|
|
role_description: str,
|
|
prior_turns: list[dict[str, Any]],
|
|
fact_ledger: dict[str, Any] | None = None,
|
|
max_context_chars: int = _DEFAULT_MAX_CONTEXT_CHARS,
|
|
max_turn_chars: int = _DEFAULT_MAX_TURN_CHARS,
|
|
max_fact_chars: int = _DEFAULT_MAX_FACT_CHARS,
|
|
) -> str:
|
|
"""Assemble the human message for a seat's DeerFlow run.
|
|
|
|
The total output length is STRICTLY <= max_context_chars. Truncation
|
|
happens per-section first, then a final hard cap ensures no overflow.
|
|
"""
|
|
sections: list[str] = []
|
|
|
|
sections.append(f"【任务】\n{task_text}")
|
|
|
|
if role_description:
|
|
sections.append(f"【你的角色】\n{role_description}")
|
|
|
|
if stage_instruction:
|
|
sections.append(f"【本阶段要求】\n{stage_instruction}")
|
|
|
|
if prior_turns:
|
|
turns_text = _format_prior_turns(prior_turns, max_turn_chars)
|
|
if turns_text:
|
|
sections.append(f"【已有讨论】\n{turns_text}")
|
|
|
|
if fact_ledger:
|
|
sanitized_ledger = sanitize(fact_ledger)
|
|
if isinstance(sanitized_ledger, dict):
|
|
facts_text = _format_fact_ledger(sanitized_ledger, max_fact_chars)
|
|
if facts_text:
|
|
sections.append(f"【共享事实账本】\n{facts_text}")
|
|
|
|
result = "\n\n".join(sections)
|
|
|
|
if len(result) > max_context_chars:
|
|
result = result[:max_context_chars]
|
|
|
|
return result
|
|
|
|
|
|
def build_manager_synthesis_input(
|
|
*,
|
|
task_text: str,
|
|
seat_results: list[dict[str, Any]],
|
|
fact_ledger: dict[str, Any] | None = None,
|
|
max_context_chars: int = _DEFAULT_MAX_CONTEXT_CHARS,
|
|
) -> str:
|
|
"""Build the manager's final synthesis prompt."""
|
|
sections: list[str] = []
|
|
sections.append(f"【原始任务】\n{task_text}")
|
|
sections.append("【各席位分析结果】")
|
|
|
|
for r in seat_results:
|
|
agent_id = r.get("agent_id", "unknown")
|
|
content = str(r.get("content", ""))
|
|
if len(content) > 3000:
|
|
content = content[:3000] + "..."
|
|
sections.append(f"--- {agent_id} ---\n{content}")
|
|
|
|
if fact_ledger:
|
|
sanitized_ledger = sanitize(fact_ledger)
|
|
if isinstance(sanitized_ledger, dict):
|
|
facts_text = _format_fact_ledger(sanitized_ledger, _DEFAULT_MAX_FACT_CHARS)
|
|
if facts_text:
|
|
sections.append(f"【共享事实账本】\n{facts_text}")
|
|
|
|
sections.append(
|
|
"【综合要求】\n"
|
|
"请基于以上各席位分析和事实账本,生成最终结论。要求:\n"
|
|
"1. 明确标注共识点和分歧点\n"
|
|
"2. 区分「建议动作」和「已实际执行的动作」\n"
|
|
"3. 引用具体证据来源\n"
|
|
"4. 列出失败或状态不确定的动作"
|
|
)
|
|
|
|
result = "\n\n".join(sections)
|
|
if len(result) > max_context_chars:
|
|
result = result[:max_context_chars]
|
|
return result
|
|
|
|
|
|
# ── internal helpers ──────────────────────────────────────────────────────────
|
|
|
|
|
|
def _format_prior_turns(turns: list[dict[str, Any]], max_per_turn: int) -> str:
|
|
lines: list[str] = []
|
|
for t in turns:
|
|
agent_id = t.get("agent_id", "unknown")
|
|
turn_idx = t.get("turn_index", "?")
|
|
summary = str(t.get("summary", ""))
|
|
if len(summary) > max_per_turn:
|
|
summary = summary[:max_per_turn] + "..."
|
|
lines.append(f"[轮次{turn_idx}] {agent_id}: {summary}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _format_fact_ledger(ledger: dict[str, Any], max_chars: int) -> str:
|
|
parts: list[str] = []
|
|
|
|
candidates = ledger.get("candidate_facts")
|
|
if candidates and isinstance(candidates, list):
|
|
# Phase-3: candidate facts are UNVERIFIED seat claims — label them
|
|
# explicitly so downstream seats / the finalizer never treat them as
|
|
# established conclusions.
|
|
parts.append("未经核验的席位主张(不能作为结论):")
|
|
for c in candidates[:20]:
|
|
if isinstance(c, dict):
|
|
parts.append(f" - {c.get('text', c)}")
|
|
else:
|
|
parts.append(f" - {c}")
|
|
|
|
facts = ledger.get("confirmed_facts")
|
|
if facts and isinstance(facts, list):
|
|
parts.append("已确认事实(经交叉审查核验):")
|
|
for f in facts[:20]:
|
|
if isinstance(f, dict):
|
|
parts.append(f" - {f.get('text', f)}")
|
|
else:
|
|
parts.append(f" - {f}")
|
|
|
|
evidence = ledger.get("evidence")
|
|
if evidence and isinstance(evidence, list):
|
|
parts.append("证据引用:")
|
|
for e in evidence[:10]:
|
|
if isinstance(e, dict):
|
|
parts.append(f" - {e.get('ref') or e.get('text') or e}")
|
|
else:
|
|
parts.append(f" - {e}")
|
|
|
|
tool_results = ledger.get("tool_results")
|
|
if tool_results and isinstance(tool_results, list):
|
|
parts.append("工具执行结果:")
|
|
for tr in tool_results[:10]:
|
|
if isinstance(tr, dict):
|
|
parts.append(f" - {tr.get('tool_name', '?')}: {tr.get('status', '?')}")
|
|
else:
|
|
parts.append(f" - {tr}")
|
|
|
|
proposed = ledger.get("proposed_actions")
|
|
if proposed and isinstance(proposed, list):
|
|
parts.append("建议动作(未执行):")
|
|
for p in proposed[:8]:
|
|
parts.append(f" - {p}")
|
|
|
|
executed = ledger.get("executed_actions")
|
|
if executed and isinstance(executed, list):
|
|
parts.append("已执行外部写入:")
|
|
for e in executed[:8]:
|
|
if isinstance(e, dict):
|
|
parts.append(f" - {e.get('tool_name', '?')} ({e.get('action_id', '')})")
|
|
else:
|
|
parts.append(f" - {e}")
|
|
|
|
conflicts = ledger.get("conflicts")
|
|
if conflicts and isinstance(conflicts, list):
|
|
parts.append("未解决冲突:")
|
|
for c in conflicts[:5]:
|
|
parts.append(f" - {c}")
|
|
|
|
pending = ledger.get("pending_actions")
|
|
if pending and isinstance(pending, list):
|
|
parts.append("等待确认动作:")
|
|
for p in pending[:5]:
|
|
parts.append(f" - {p}")
|
|
|
|
failed = ledger.get("failed_or_unknown_actions")
|
|
if failed and isinstance(failed, list):
|
|
parts.append("失败或状态不确定的动作:")
|
|
for f in failed[:5]:
|
|
parts.append(f" - {f}")
|
|
|
|
result = "\n".join(parts)
|
|
if len(result) > max_chars:
|
|
result = result[:max_chars]
|
|
return result
|