Files

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