state.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. """Thin workflow state.
  2. Search pages, candidates, evidence and final buckets stay in the existing database.
  3. This state only contains orchestration pointers and audit summaries.
  4. """
  5. from __future__ import annotations
  6. from dataclasses import dataclass, field
  7. from typing import Any, Literal, TypedDict
  8. from supply_agent.types import AgentResult
  9. Phase = Literal["planning", "searching", "evidence", "evaluating", "done"]
  10. SupervisorAction = Literal["search", "evidence", "evaluator", "finish"]
  11. EndKind = Literal["goal_met", "partial", "no_match", "failed", "stopped"]
  12. @dataclass(frozen=True)
  13. class DiscoverySnapshot:
  14. """Small deterministic projection of the database state."""
  15. status: str
  16. search_count: int
  17. candidate_count: int
  18. pending_count: int
  19. primary_count: int
  20. valid_primary_count: int
  21. rejected_count: int
  22. outcome_status: str = ""
  23. @dataclass(frozen=True)
  24. class NodeRun:
  25. """One node's observable result."""
  26. node: str
  27. round_index: int
  28. content: str
  29. iterations: int
  30. tool_calls_made: int
  31. @classmethod
  32. def from_agent_result(
  33. cls,
  34. node: str,
  35. round_index: int,
  36. result: AgentResult,
  37. ) -> "NodeRun":
  38. return cls(
  39. node=node,
  40. round_index=round_index,
  41. content=result.content or "",
  42. iterations=int(result.iterations or 0),
  43. tool_calls_made=int(result.tool_calls_made or 0),
  44. )
  45. @dataclass
  46. class FindAgentState:
  47. """Control state passed through the single-round graph."""
  48. run_id: str
  49. user_input: str
  50. round_index: int = 0
  51. phase: Phase = "planning"
  52. plan: str = ""
  53. stop: bool = False
  54. stop_reason: str = ""
  55. previous_snapshot: DiscoverySnapshot | None = None
  56. snapshot: DiscoverySnapshot | None = None
  57. node_runs: list[NodeRun] = field(default_factory=list)
  58. failures: list[dict[str, Any]] = field(default_factory=list)
  59. class FindAgentGraphState(TypedDict, total=False):
  60. """Serializable state used by the real one-round LangGraph."""
  61. run_id: str
  62. user_input: str
  63. round_index: int
  64. plan: str
  65. phase: Phase
  66. node_runs: list[NodeRun]
  67. snapshot: DiscoverySnapshot | None
  68. supervisor_step: int
  69. approved_action: SupervisorAction
  70. action_count: int
  71. search_actions: int
  72. worker_count: int
  73. evidence_scope: str
  74. decision_history: list[dict[str, Any]]
  75. evaluator_stagnation: int
  76. @dataclass(frozen=True)
  77. class FindAgentResult:
  78. """Workflow result with technical and business status kept separate."""
  79. run_id: str
  80. status: EndKind
  81. succeeded: bool
  82. business_outcome: str
  83. valid_primary_count: int
  84. rounds: int
  85. final_output: str
  86. node_runs: tuple[NodeRun, ...]
  87. stop_reason: str = ""
  88. @property
  89. def iterations(self) -> int:
  90. return sum(item.iterations for item in self.node_runs)
  91. @property
  92. def tool_calls_made(self) -> int:
  93. return sum(item.tool_calls_made for item in self.node_runs)