deerflow-code/offline-backend-20260512/backend/app/report_collaboration/planning/controller.py
2026-09-07 18:24:55 +08:00

314 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""PlanController: 2–3 strategy RoleDemand graphs. Models never emit executable graphs."""
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field
from app.report_collaboration.contracts.plans import RoleDemand
from app.report_collaboration.contracts.requirements import ReportRequirementSnapshot
from app.report_collaboration.requirement.defaults import DEFAULT_ANGLES
STRATEGIES: tuple[str, ...] = ("balanced_review", "parallel_depth", "focused_fast")
class RoleSlot(BaseModel):
"""Controller-local slot. ``demand`` is what a model would emit; ``slot_id`` is server-assigned."""
model_config = ConfigDict(extra="forbid")
slot_id: str
demand: RoleDemand
depends_on: list[str] = Field(default_factory=list)
class StrategyRoleSet(BaseModel):
model_config = ConfigDict(extra="forbid")
strategy: str
title: str
summary: str
rationale: str
estimated_duration_seconds: int
estimated_cost_level: str
recommended: bool = False
slots: list[RoleSlot] = Field(default_factory=list)
quality_gate_ids: list[str] = Field(default_factory=list)
def resolve_angles(requirement: ReportRequirementSnapshot, count: int) -> list[str]:
excluded = {item.strip() for item in requirement.excluded_angles if item and item.strip()}
source = [item for item in requirement.required_angles if item.strip() and item.strip() not in excluded]
if not source:
source = [item for item in DEFAULT_ANGLES if item not in excluded]
if not source:
source = ["综合趋势"]
if count <= 1:
return source[:1]
picked = source[:count]
return picked if picked else source[:1]
def _research_demand(angle: str) -> RoleDemand:
return RoleDemand(
role_key="researcher",
display_name="研究员",
angle=angle,
mission=f"检索并整理「{angle}」的事实、来源、日期和摘录,覆盖冲突与缺口",
depends_on_roles=[],
output_artifact_type="EvidenceBundle",
suggested_tools=["web_search", "web_fetch", "knowledge_search"],
)
class PlanController:
"""Deterministic strategy templates. A later planner model may only fill RoleDemand."""
def propose(self, requirement: ReportRequirementSnapshot, *, max_parallel_tasks: int, max_team_members: int) -> list[StrategyRoleSet]:
topic = requirement.topic
focused_angles = resolve_angles(requirement, 1)
balanced_n = min(2, max(1, max_parallel_tasks))
parallel_n = min(3, max(1, max_parallel_tasks), max(1, max_team_members - 5))
balanced_angles = resolve_angles(requirement, balanced_n)
parallel_angles = resolve_angles(requirement, parallel_n)
return [
self._balanced(topic, balanced_angles),
self._parallel(topic, parallel_angles),
self._focused(topic, focused_angles),
]
def _balanced(self, topic: str, angles: list[str]) -> StrategyRoleSet:
slots: list[RoleSlot] = []
research_ids: list[str] = []
for index, angle in enumerate(angles):
slot_id = f"research-{index}"
research_ids.append(slot_id)
slots.append(
RoleSlot(
slot_id=slot_id,
demand=_research_demand(angle),
depends_on=[],
)
)
slots.append(
RoleSlot(
slot_id="verify",
demand=RoleDemand(
role_key="verifier",
display_name="核验员",
mission="交叉核验关键数字口径、时效与冲突来源",
depends_on_roles=["researcher"],
output_artifact_type="VerificationResult",
),
depends_on=list(research_ids),
)
)
slots.append(
RoleSlot(
slot_id="analyze",
demand=RoleDemand(
role_key="analyst",
display_name="分析员",
mission=f"综合各角度证据,形成关于「{topic}」的判断、假设和局限",
depends_on_roles=["verifier"],
output_artifact_type="AngleAnalysis",
),
depends_on=["verify"],
)
)
slots.append(
RoleSlot(
slot_id="write",
demand=RoleDemand(
role_key="writer",
display_name="撰写员",
mission="按模板撰写带引用的完整报告",
depends_on_roles=["analyst"],
output_artifact_type="ReportVersion",
),
depends_on=["analyze"],
)
)
slots.append(
RoleSlot(
slot_id="review",
demand=RoleDemand(
role_key="reviewer",
display_name="审稿员",
mission="独立审稿:结论与证据一致、章节完整",
depends_on_roles=["writer"],
output_artifact_type="ReviewDecision",
),
depends_on=["write"],
)
)
return StrategyRoleSet(
strategy="balanced_review",
title="均衡协作 · 研究核验写作审稿全链路",
summary=f"{len(angles)} 条研究线 + 交叉核验 + 分析 + 写作 + 独立审稿,质量与成本均衡。",
rationale="在覆盖主要角度的同时控制团队规模,适合大多数正式报告。",
estimated_duration_seconds=1500,
estimated_cost_level="medium",
recommended=True,
slots=slots,
quality_gate_ids=["cite_required", "independent_review"],
)
def _parallel(self, topic: str, angles: list[str]) -> StrategyRoleSet:
slots: list[RoleSlot] = []
research_ids: list[str] = []
for index, angle in enumerate(angles):
slot_id = f"research-{index}"
research_ids.append(slot_id)
slots.append(RoleSlot(slot_id=slot_id, demand=_research_demand(angle), depends_on=[]))
mid = max(1, len(research_ids) // 2)
first, second = research_ids[:mid], research_ids[mid:] or research_ids[:1]
slots.append(
RoleSlot(
slot_id="verify-a",
demand=RoleDemand(
role_key="verifier",
display_name="核验员",
angle="主证据链",
mission="核验前半研究线的口径、时效与冲突",
depends_on_roles=["researcher"],
output_artifact_type="VerificationResult",
),
depends_on=list(first),
)
)
slots.append(
RoleSlot(
slot_id="verify-b",
demand=RoleDemand(
role_key="verifier",
display_name="核验员",
angle="交叉证据链",
mission="二次核验其余研究线与高风险结论",
depends_on_roles=["researcher"],
output_artifact_type="VerificationResult",
),
depends_on=list(second),
)
)
slots.append(
RoleSlot(
slot_id="analyze",
demand=RoleDemand(
role_key="analyst",
display_name="分析员",
mission=f"分角度深入分析「{topic}」,并给出 12–24 个月展望",
depends_on_roles=["verifier"],
output_artifact_type="AngleAnalysis",
),
depends_on=["verify-a", "verify-b"],
)
)
slots.append(
RoleSlot(
slot_id="write",
demand=RoleDemand(
role_key="writer",
display_name="撰写员",
mission="撰写完整报告",
depends_on_roles=["analyst"],
output_artifact_type="ReportVersion",
),
depends_on=["analyze"],
)
)
slots.append(
RoleSlot(
slot_id="review",
demand=RoleDemand(
role_key="reviewer",
display_name="审稿员",
mission="高风险场景独立审稿",
depends_on_roles=["writer"],
output_artifact_type="ReviewDecision",
),
depends_on=["write"],
)
)
return StrategyRoleSet(
strategy="parallel_depth",
title="深度并行 · 多角度双重核验",
summary=f"{len(angles)} 条并行研究 + 双重核验 + 分析 + 写作 + 审稿,覆盖更广、成本更高。",
rationale="面向高风险决策:角度覆盖与证据强度优先,接受更高成本与时长。",
estimated_duration_seconds=2400,
estimated_cost_level="high",
slots=slots,
quality_gate_ids=["cite_required", "dual_verification", "independent_review", "coverage_matrix"],
)
def _focused(self, topic: str, angles: list[str]) -> StrategyRoleSet:
angle = angles[0]
slots = [
RoleSlot(
slot_id="research-0",
demand=RoleDemand(
role_key="researcher",
display_name="研究员",
angle=angle,
mission=f"按指定角度「{angle}」检索证据,范围收窄",
depends_on_roles=[],
output_artifact_type="EvidenceBundle",
suggested_tools=["web_search", "web_fetch", "knowledge_search"],
),
depends_on=[],
),
RoleSlot(
slot_id="verify",
demand=RoleDemand(
role_key="verifier",
display_name="核验员",
mission="核验关键事实与数字来源",
depends_on_roles=["researcher"],
output_artifact_type="VerificationResult",
),
depends_on=["research-0"],
),
RoleSlot(
slot_id="analyze",
demand=RoleDemand(
role_key="analyst",
display_name="分析员",
mission=f"形成关于「{topic}」的聚焦结论",
depends_on_roles=["verifier"],
output_artifact_type="AngleAnalysis",
),
depends_on=["verify"],
),
RoleSlot(
slot_id="write",
demand=RoleDemand(
role_key="writer",
display_name="撰写员",
mission="快速产出带引用的报告",
depends_on_roles=["analyst"],
output_artifact_type="ReportVersion",
),
depends_on=["analyze"],
),
RoleSlot(
slot_id="review",
demand=RoleDemand(
role_key="reviewer",
display_name="审稿员",
mission="快速审稿,拦截高危问题",
depends_on_roles=["writer"],
output_artifact_type="ReviewDecision",
),
depends_on=["write"],
),
]
return StrategyRoleSet(
strategy="focused_fast",
title="快速聚焦 · 窄范围短链路",
summary="单研究线 + 核验 + 分析 + 写作 + 审稿,保留引用核验与审稿,最快出稿。",
rationale="适合时间紧、角度已知、需要可信结论而非全面覆盖的场景。",
estimated_duration_seconds=800,
estimated_cost_level="low",
slots=slots,
quality_gate_ids=["cite_required", "independent_review"],
)