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

185 lines
7.2 KiB
Python
Raw 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.

"""Validate and persist requirement revisions. Never starts planning or a run."""
from __future__ import annotations
from typing import Any
from uuid import uuid4
from app.report_collaboration.contracts.clarifications import ClarificationRecord
from app.report_collaboration.contracts.requirements import ReportRequirementSnapshot
from app.report_collaboration.requirement.defaults import COORDINATOR_NAME, DEFAULT_TITLE
from app.report_collaboration.requirement.intent_router import PrePlanDecision, route_pre_plan_turn
from deerflow.config.app_config import get_app_config
from deerflow.persistence.report_collaboration import ReportCollaborationStore
from deerflow.persistence.report_collaboration.codec import iso_now
def _state_from_session(record: dict[str, Any]) -> dict[str, Any]:
raw = record.get("requirement_json")
if isinstance(raw, dict) and ("snapshot" in raw or "clarifications" in raw):
return {
"snapshot": raw.get("snapshot"),
"clarifications": list(raw.get("clarifications") or []),
"assumptions": list(raw.get("assumptions") or []),
"clarification_round": int(raw.get("clarification_round") or 0),
"ready_to_plan": bool(raw.get("ready_to_plan")),
}
if isinstance(raw, dict) and raw.get("topic"):
return {
"snapshot": raw,
"clarifications": [],
"assumptions": [],
"clarification_round": 0,
"ready_to_plan": False,
}
return {"snapshot": None, "clarifications": [], "assumptions": [], "clarification_round": 0, "ready_to_plan": False}
def _dump_state(
*,
snapshot: ReportRequirementSnapshot,
clarifications: list[dict[str, Any]],
assumptions: list[str],
clarification_round: int,
ready_to_plan: bool,
) -> dict[str, Any]:
return {
"snapshot": snapshot.model_dump(),
"clarifications": clarifications,
"assumptions": assumptions,
"clarification_round": clarification_round,
"ready_to_plan": ready_to_plan,
}
def format_clarification_message(questions: list[str], *, allow_skip: bool = True) -> str:
numbered = "\n".join(f"{index}. {question}" for index, question in enumerate(questions, start=1))
suffix = "\n\n也可以直接回复「直接开始」,我会按默认假设记录需求。澄清完成不会自动生成方案。" if allow_skip else ""
return f"收到您的报告需求。为了给出最合适的协作方案,请先确认:\n\n{numbered}{suffix}"
def format_ready_message(decision: PrePlanDecision) -> str:
req = decision.requirement
angles = "、".join(req.required_angles) if req.required_angles else "(未指定)"
lines = [
f"已写入需求版本 v{req.revision}。",
f"主题:{req.topic}",
f"读者:{req.audience or '未指定'}",
f"角度:{angles}",
]
if decision.assumptions:
lines.append("默认假设:" + ";".join(decision.assumptions) + "。")
lines.append("澄清完成不等于已经生成候选方案,需要显式发起规划后才会出现方案卡。")
return "\n".join(lines)
def _title_from_topic(topic: str, current: str | None) -> str | None:
if current and current != DEFAULT_TITLE:
return None
cleaned = (topic or "").strip()
if not cleaned or cleaned == "未指定主题":
return None
return cleaned[:40]
class RequirementResolver:
"""Writes requirement revisions and clarification records. Does not enqueue plans."""
def __init__(self, store: ReportCollaborationStore) -> None:
self._store = store
async def handle_user_message(
self,
session_id: str,
*,
user_message: dict[str, Any],
idempotency_key: str,
max_clarification_rounds: int | None = None,
) -> PrePlanDecision | None:
record = await self._store.get_session_record(session_id)
if record is None:
return None
status = str(record.get("status") or "empty")
if status not in {"empty", "clarifying"}:
return None
messages = await self._store.list_messages(session_id)
if any((item.get("metadata") or {}).get("preplan_for") == user_message["message_id"] for item in messages):
return None
state = _state_from_session(record)
cfg_rounds = max_clarification_rounds
if cfg_rounds is None:
cfg_rounds = int(get_app_config().report_collaboration.max_clarification_rounds)
decision = route_pre_plan_turn(
str(user_message.get("content") or ""),
previous_snapshot=state["snapshot"],
previous_revision=int(record.get("requirement_revision") or 0),
clarification_round=int(state["clarification_round"] or 0),
max_clarification_rounds=cfg_rounds,
)
await self._apply(session_id, record, state, decision, user_message=user_message, idempotency_key=idempotency_key)
return decision
async def _apply(
self,
session_id: str,
record: dict[str, Any],
state: dict[str, Any],
decision: PrePlanDecision,
*,
user_message: dict[str, Any],
idempotency_key: str,
) -> None:
clarifications = [dict(item) for item in state["clarifications"]]
round_count = int(state["clarification_round"] or 0)
if decision.action == "clarify":
round_count += 1
for item in clarifications:
if not item.get("resolved"):
item["resolved"] = True
clarification = ClarificationRecord(
id=f"clr_{uuid4().hex[:12]}",
questions=list(decision.questions),
impact=list(decision.impact),
resolved=False,
created_at=iso_now(),
)
clarifications.append(clarification.model_dump())
content = format_clarification_message(decision.questions)
metadata = {
"preplan_for": user_message["message_id"],
"clarification": clarification.model_dump(),
}
ready = False
else:
for item in clarifications:
item["resolved"] = True
content = format_ready_message(decision)
metadata = {"preplan_for": user_message["message_id"], "ready_to_plan": True}
ready = True
payload = _dump_state(
snapshot=decision.requirement,
clarifications=clarifications,
assumptions=list(decision.assumptions),
clarification_round=round_count,
ready_to_plan=ready,
)
await self._store.save_requirement_state(
session_id,
requirement_json=payload,
requirement_revision=decision.requirement.revision,
status="clarifying",
title=_title_from_topic(decision.requirement.topic, record.get("title")),
)
await self._store.create_message(
session_id,
role="ai",
content=content,
idempotency_key=f"{idempotency_key}:preplan",
name=COORDINATOR_NAME,
metadata=metadata,
completed=True,
)