| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- """Thin workflow state.
- Search pages, candidates, evidence and final buckets stay in the existing database.
- This state only contains orchestration pointers and audit summaries.
- """
- from __future__ import annotations
- from dataclasses import dataclass, field
- from typing import Any, Literal, TypedDict
- from pydantic import BaseModel, Field, model_validator
- from supply_agent.types import AgentResult
- Phase = Literal["planning", "searching", "evidence", "evaluating", "done"]
- SupervisorAction = Literal["search", "evidence", "evaluator", "finish"]
- EndKind = Literal["goal_met", "partial", "no_match", "failed", "stopped"]
- class DemandBrief(BaseModel):
- """Planner-owned compact interpretation of the immutable raw demand."""
- core_intent: str = Field(min_length=1)
- target_audience: list[str] = Field(default_factory=list)
- forwarding_audience: list[str] = Field(default_factory=list)
- key_scenarios: list[str] = Field(default_factory=list)
- reference_signals: list[str] = Field(default_factory=list)
- relevance_criteria: list[str] = Field(min_length=1)
- exclusion_criteria: list[str] = Field(default_factory=list)
- temporal_requirements: list[str] = Field(default_factory=list)
- class SearchTask(BaseModel):
- """One independently executable search task produced by planning."""
- task_id: str = Field(min_length=1)
- keyword: str = Field(min_length=1)
- query_reason: str = Field(min_length=1)
- source_type: Literal["demand", "seed", "point", "mixed"] = "mixed"
- provider: Literal["internal_keyword", "tikhub"] = "internal_keyword"
- max_pages: int = Field(default=1, ge=1, le=2)
- coverage_targets: list[str] = Field(default_factory=list)
- class EvaluationBrief(BaseModel):
- """Demand semantics required by evaluators, without the original request."""
- relevance_criteria: list[str] = Field(min_length=1)
- audience_criteria: list[str] = Field(default_factory=list)
- temporal_requirements: list[str] = Field(default_factory=list)
- exclusion_criteria: list[str] = Field(default_factory=list)
- class ExecutionPlan(BaseModel):
- """Validated contract passed from the planner to later stages."""
- schema_version: Literal["1.0"] = "1.0"
- demand_brief: DemandBrief
- search_tasks: list[SearchTask] = Field(min_length=1, max_length=24)
- evaluation_brief: EvaluationBrief
- @model_validator(mode="after")
- def validate_unique_task_ids(self):
- task_ids = [item.task_id for item in self.search_tasks]
- if len(task_ids) != len(set(task_ids)):
- raise ValueError("search_tasks.task_id 不能重复")
- return self
- class SupervisorDecision(BaseModel):
- """One routing decision; subsequent decisions may add search tasks."""
- next_action: SupervisorAction
- reason: str = ""
- worker_count: int = Field(default=4, ge=1, le=8)
- evidence_scope: Literal["detail", "portrait", "both"] = "both"
- additional_search_tasks: list[SearchTask] = Field(default_factory=list, max_length=6)
- class PlanningDecision(SupervisorDecision):
- execution_plan: ExecutionPlan
- class PlannerAssignment(BaseModel):
- run_id: str
- round_index: int
- raw_demand: str
- class SupervisorAssignment(BaseModel):
- run_id: str
- round_index: int
- execution_plan: dict[str, Any]
- execution_state: dict[str, Any]
- class SearchAssignment(BaseModel):
- run_id: str
- round_index: int
- tasks: list[SearchTask]
- class EvidenceAssignment(BaseModel):
- run_id: str
- round_index: int
- candidate_ids: list[int]
- evidence_type: Literal["detail", "portrait"]
- candidates: list[dict[str, Any]]
- class EvaluationAssignment(BaseModel):
- run_id: str
- round_index: int
- candidate_ids: list[int]
- evaluation_brief: EvaluationBrief
- quality_gate_rules: dict[str, Any]
- current_datetime: str = ""
- timezone: str = ""
- video_understanding_candidate_ids: list[int] = Field(default_factory=list)
- candidates: list[dict[str, Any]]
- class ReportAssignment(BaseModel):
- run_id: str
- final_state: dict[str, Any]
- @dataclass(frozen=True)
- class DiscoverySnapshot:
- """Small deterministic projection of the database state."""
- status: str
- search_count: int
- candidate_count: int
- pending_count: int
- primary_count: int
- valid_primary_count: int
- rejected_count: int
- outcome_status: str = ""
- @dataclass(frozen=True)
- class NodeRun:
- """One node's observable result."""
- node: str
- round_index: int
- content: str
- iterations: int
- tool_calls_made: int
- @classmethod
- def from_agent_result(
- cls,
- node: str,
- round_index: int,
- result: AgentResult,
- ) -> "NodeRun":
- return cls(
- node=node,
- round_index=round_index,
- content=result.content or "",
- iterations=int(result.iterations or 0),
- tool_calls_made=int(result.tool_calls_made or 0),
- )
- @dataclass
- class FindAgentState:
- """Control state passed through the single-round graph."""
- run_id: str
- user_input: str
- round_index: int = 0
- phase: Phase = "planning"
- execution_plan: ExecutionPlan | None = None
- stop: bool = False
- stop_reason: str = ""
- previous_snapshot: DiscoverySnapshot | None = None
- snapshot: DiscoverySnapshot | None = None
- node_runs: list[NodeRun] = field(default_factory=list)
- failures: list[dict[str, Any]] = field(default_factory=list)
- class FindAgentGraphState(TypedDict, total=False):
- """Serializable state used by the real one-round LangGraph."""
- run_id: str
- user_input: str
- round_index: int
- execution_plan: ExecutionPlan | None
- phase: Phase
- node_runs: list[NodeRun]
- snapshot: DiscoverySnapshot | None
- supervisor_step: int
- approved_action: SupervisorAction
- action_count: int
- search_actions: int
- worker_count: int
- evidence_scope: str
- decision_history: list[dict[str, Any]]
- evaluator_stagnation: int
- @dataclass(frozen=True)
- class FindAgentResult:
- """Workflow result with technical and business status kept separate."""
- run_id: str
- status: EndKind
- succeeded: bool
- business_outcome: str
- valid_primary_count: int
- rounds: int
- final_output: str
- node_runs: tuple[NodeRun, ...]
- stop_reason: str = ""
- @property
- def iterations(self) -> int:
- return sum(item.iterations for item in self.node_runs)
- @property
- def tool_calls_made(self) -> int:
- return sum(item.tool_calls_made for item in self.node_runs)
|