agent.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. """Outer round orchestrator for the isolated find agent."""
  2. from __future__ import annotations
  3. import json
  4. from find_agent_v2.context import build_node_slots
  5. from find_agent_v2.graph import FindAgentRoundGraph, NodeRunner
  6. from find_agent_v2.observability import ObagentObserver
  7. from find_agent_v2.prompts import REPORT_PROMPT
  8. from find_agent_v2.runtime import FindAgentNodeHost, normalize_models
  9. from find_agent_v2.service import FindAgentV2Service, get_find_agent_v2_service
  10. from find_agent_v2.state import FindAgentResult, FindAgentState
  11. from find_agent_v2.tools import REPORT_TOOLS
  12. from supply_agent.config import Settings
  13. class FindAgentV2:
  14. """Python outer loop + guarded Supervisor graph + node-local ReAct."""
  15. def __init__(
  16. self,
  17. *,
  18. service: FindAgentV2Service | None = None,
  19. node_runner: NodeRunner | None = None,
  20. settings: Settings | None = None,
  21. models_by_role: dict[str, str] | None = None,
  22. max_rounds: int = 2,
  23. target_primary_count: int = 5,
  24. max_actions_per_round: int = 16,
  25. max_search_actions_per_round: int = 3,
  26. observer: ObagentObserver | None = None,
  27. ) -> None:
  28. self.service = service or get_find_agent_v2_service()
  29. self.observer = observer or ObagentObserver()
  30. self.node_runner = node_runner or FindAgentNodeHost(
  31. settings=settings,
  32. models_by_role=models_by_role,
  33. observer=self.observer,
  34. )
  35. self.models_by_role = dict(models_by_role or {})
  36. self.max_rounds = max(1, int(max_rounds))
  37. self.target_primary_count = max(1, int(target_primary_count))
  38. self.max_actions_per_round = max(4, int(max_actions_per_round))
  39. self.max_search_actions_per_round = max(1, int(max_search_actions_per_round))
  40. async def arun(
  41. self, *, run_id: str, user_input: str, resume: bool = False,
  42. ) -> FindAgentResult:
  43. run = self.service.require_run(run_id)
  44. if resume and str(run.get("status") or "") != "running":
  45. run = self.service.prepare_resume(run_id)
  46. if str(run.get("status") or "") != "running":
  47. raise ValueError(f"run_id={run_id} 当前状态不可执行: {run.get('status')}")
  48. state = FindAgentState(run_id=run_id, user_input=user_input)
  49. graph = FindAgentRoundGraph(
  50. service=self.service, runner=self.node_runner, observer=self.observer,
  51. max_actions=self.max_actions_per_round,
  52. max_search_actions=self.max_search_actions_per_round,
  53. target_primary_count=self.target_primary_count,
  54. )
  55. reset_usage = getattr(self.node_runner, "reset_usage", None)
  56. if callable(reset_usage):
  57. reset_usage()
  58. default_model = self.models_by_role.get("supervisor", "google/gemini-3-flash-preview")
  59. with self.observer.run(
  60. run_id=run_id,
  61. demand_word=str(run.get("demand_word") or ""),
  62. model=default_model,
  63. models_by_role=self.models_by_role,
  64. ) as observation_run:
  65. self.service.set_obagent_run_uid(
  66. run_id, getattr(observation_run, "run_uid", None),
  67. )
  68. result = await self._arun_inner(
  69. state=state, graph=graph, start_round=int(run.get("current_round") or 0) + 1,
  70. )
  71. usage = getattr(self.node_runner, "usage", None)
  72. if isinstance(usage, dict):
  73. self.service.add_usage(run_id, usage)
  74. observation_run.finish(final_output=result.final_output)
  75. return result
  76. async def _arun_inner(
  77. self, *, state: FindAgentState, graph: FindAgentRoundGraph, start_round: int = 1,
  78. ) -> FindAgentResult:
  79. run_id = state.run_id
  80. end_reason = ""
  81. failed = False
  82. try:
  83. end_round = start_round + self.max_rounds
  84. for round_index in range(start_round, end_round):
  85. state.round_index = round_index
  86. state.previous_snapshot = self.service.snapshot(run_id)
  87. self.service.begin_round(run_id, round_index, state.previous_snapshot)
  88. await graph.invoke(state)
  89. current = state.snapshot or self.service.snapshot(run_id)
  90. if current.valid_primary_count >= self.target_primary_count:
  91. end_reason = f"已获得 {current.valid_primary_count} 条有效 primary"
  92. break
  93. if current.pending_count:
  94. failed = True
  95. end_reason = f"第 {round_index} 轮结束仍有 {current.pending_count} 条待评估候选"
  96. break
  97. if current.candidate_count <= state.previous_snapshot.candidate_count:
  98. end_reason = "本轮未发现新增候选,搜索前沿已无信息增益"
  99. break
  100. if round_index == end_round - 1:
  101. end_reason = f"达到最大业务轮数 {self.max_rounds}"
  102. except Exception as exc:
  103. failed = True
  104. end_reason = f"{type(exc).__name__}: {exc}"
  105. state.failures.append({"round": state.round_index, "error": end_reason})
  106. if state.round_index:
  107. try:
  108. self.service.update_round(
  109. run_id, state.round_index, status="failed", error=end_reason,
  110. )
  111. except Exception:
  112. pass
  113. final_run = self.service.finalize(
  114. run_id,
  115. failed=failed,
  116. reason=end_reason,
  117. target_primary_count=self.target_primary_count,
  118. )
  119. final_output = (
  120. f"find_agent_v2 {final_run['outcome_status']},"
  121. f"有效 primary={final_run['valid_primary_count']}。"
  122. )
  123. if not failed:
  124. try:
  125. report = await self.node_runner.run_node(
  126. node="report",
  127. round_index=state.round_index,
  128. system_prompt=REPORT_PROMPT,
  129. user_content=json.dumps(
  130. self.service.get_full_state(run_id),
  131. ensure_ascii=False,
  132. default=str,
  133. ),
  134. tools=REPORT_TOOLS,
  135. max_iterations=4,
  136. slots=build_node_slots(
  137. user_input=state.user_input,
  138. full_state=self.service.get_full_state(run_id),
  139. round_index=state.round_index,
  140. plan=state.plan,
  141. ),
  142. )
  143. state.node_runs.append(report)
  144. final_output = report.content or final_output
  145. except Exception as exc:
  146. state.failures.append({"round": state.round_index, "node": "report", "error": str(exc)})
  147. outcome = str(final_run.get("outcome_status") or "failed")
  148. return FindAgentResult(
  149. run_id=run_id,
  150. status=outcome if outcome in {"goal_met", "partial", "no_match", "failed"} else "failed", # type: ignore[arg-type]
  151. succeeded=not failed and final_run.get("status") == "finished",
  152. business_outcome=outcome,
  153. valid_primary_count=int(final_run.get("valid_primary_count") or 0),
  154. rounds=state.round_index,
  155. final_output=final_output,
  156. node_runs=tuple(state.node_runs),
  157. stop_reason=end_reason,
  158. )
  159. def create_find_agent_v2(
  160. settings: Settings | None = None,
  161. *,
  162. model: str | None = None,
  163. planning_model: str | None = None,
  164. search_model: str | None = None,
  165. evidence_model: str | None = None,
  166. evaluation_model: str | None = None,
  167. report_model: str | None = None,
  168. max_rounds: int = 2,
  169. max_actions_per_round: int = 16,
  170. max_search_actions_per_round: int = 3,
  171. ) -> FindAgentV2:
  172. return FindAgentV2(
  173. settings=settings,
  174. models_by_role=normalize_models(
  175. model=model,
  176. planning=planning_model,
  177. search=search_model,
  178. evidence=evidence_model,
  179. evaluation=evaluation_model,
  180. report=report_model,
  181. ),
  182. max_rounds=max_rounds,
  183. max_actions_per_round=max_actions_per_round,
  184. max_search_actions_per_round=max_search_actions_per_round,
  185. )