|
|
@@ -9,12 +9,29 @@ from typing import Any, Protocol
|
|
|
|
|
|
from langgraph.graph import END, START, StateGraph
|
|
|
|
|
|
-from find_agent_v2.context import build_node_slots, render_node_context
|
|
|
+from find_agent_v2.context import assignment_slots, render_assignment
|
|
|
from find_agent_v2.gates import evaluate_candidate_gate
|
|
|
from find_agent_v2.observability import InputSlot, NullObserver, graph_spec_for
|
|
|
-from find_agent_v2.prompts import EVALUATOR_PROMPT, EVIDENCE_PROMPT, SEARCH_PROMPT, SUPERVISOR_PROMPT
|
|
|
+from find_agent_v2.prompts import (
|
|
|
+ EVALUATOR_PROMPT,
|
|
|
+ EVIDENCE_PROMPT,
|
|
|
+ PLANNER_PROMPT,
|
|
|
+ SEARCH_PROMPT,
|
|
|
+ SUPERVISOR_PROMPT,
|
|
|
+)
|
|
|
from find_agent_v2.service import FindAgentV2Service
|
|
|
-from find_agent_v2.state import FindAgentGraphState, FindAgentState, NodeRun
|
|
|
+from find_agent_v2.state import (
|
|
|
+ EvidenceAssignment,
|
|
|
+ EvaluationAssignment,
|
|
|
+ FindAgentGraphState,
|
|
|
+ FindAgentState,
|
|
|
+ NodeRun,
|
|
|
+ PlannerAssignment,
|
|
|
+ PlanningDecision,
|
|
|
+ SearchAssignment,
|
|
|
+ SupervisorAssignment,
|
|
|
+ SupervisorDecision,
|
|
|
+)
|
|
|
from find_agent_v2.tools import (
|
|
|
EVALUATION_TOOLS,
|
|
|
EVIDENCE_TOOLS,
|
|
|
@@ -45,8 +62,13 @@ class FindAgentRoundGraph:
|
|
|
"""Supervisor-directed graph with deterministic policy approval."""
|
|
|
|
|
|
def __init__(
|
|
|
- self, *, service: FindAgentV2Service, runner: NodeRunner, observer=None,
|
|
|
- max_actions: int = 16, max_search_actions: int = 3,
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ service: FindAgentV2Service,
|
|
|
+ runner: NodeRunner,
|
|
|
+ observer=None,
|
|
|
+ max_actions: int = 16,
|
|
|
+ max_search_actions: int = 3,
|
|
|
) -> None:
|
|
|
self.service = service
|
|
|
self.runner = runner
|
|
|
@@ -58,55 +80,84 @@ class FindAgentRoundGraph:
|
|
|
|
|
|
def _full_state(self, state: FindAgentGraphState, *, pending_only: bool = False):
|
|
|
return self.service.get_full_state(
|
|
|
- state["run_id"], pending_only=pending_only,
|
|
|
- )
|
|
|
-
|
|
|
- def _context(self, state: FindAgentGraphState, *, pending_only: bool = False) -> str:
|
|
|
- return render_node_context(
|
|
|
- user_input=state["user_input"],
|
|
|
- full_state=self._full_state(state, pending_only=pending_only),
|
|
|
- round_index=state["round_index"],
|
|
|
- plan=state.get("plan", ""),
|
|
|
- )
|
|
|
-
|
|
|
- def _slots(
|
|
|
- self, state: FindAgentGraphState, *, pending_only: bool = False,
|
|
|
- ) -> tuple[InputSlot, ...]:
|
|
|
- return build_node_slots(
|
|
|
- user_input=state["user_input"],
|
|
|
- full_state=self._full_state(state, pending_only=pending_only),
|
|
|
- round_index=state["round_index"],
|
|
|
- plan=state.get("plan", ""),
|
|
|
+ state["run_id"],
|
|
|
+ pending_only=pending_only,
|
|
|
)
|
|
|
|
|
|
- def _shard_state(
|
|
|
- self, state: FindAgentGraphState, items: list[dict[str, Any]],
|
|
|
- ) -> dict[str, Any]:
|
|
|
- full_state = self._full_state(state, pending_only=True)
|
|
|
- return {**full_state, "candidates": items}
|
|
|
-
|
|
|
- def _shard_context(
|
|
|
- self, state: FindAgentGraphState, items: list[dict[str, Any]],
|
|
|
- ) -> str:
|
|
|
- return render_node_context(
|
|
|
- user_input=state["user_input"],
|
|
|
- full_state=self._shard_state(state, items),
|
|
|
- round_index=state["round_index"],
|
|
|
- plan=state.get("plan", ""),
|
|
|
- )
|
|
|
+ @staticmethod
|
|
|
+ def _assignment_input(assignment, *, source: str) -> tuple[str, tuple[InputSlot, ...]]:
|
|
|
+ return render_assignment(assignment), assignment_slots(assignment, source=source)
|
|
|
|
|
|
- def _shard_slots(
|
|
|
- self, state: FindAgentGraphState, items: list[dict[str, Any]],
|
|
|
- ) -> tuple[InputSlot, ...]:
|
|
|
- return build_node_slots(
|
|
|
- user_input=state["user_input"],
|
|
|
- full_state=self._shard_state(state, items),
|
|
|
- round_index=state["round_index"],
|
|
|
- plan=state.get("plan", ""),
|
|
|
- )
|
|
|
+ def _supervisor_state(self, state: FindAgentGraphState) -> dict[str, Any]:
|
|
|
+ """Compact progress projection; routing never needs full candidate payloads."""
|
|
|
+ full_state = self._full_state(state)
|
|
|
+ run = full_state.get("run") or {}
|
|
|
+ candidates = list(full_state.get("candidates") or [])
|
|
|
+ pending_candidates = [
|
|
|
+ item
|
|
|
+ for item in candidates
|
|
|
+ if item.get("decision_bucket") == "pending_evaluation"
|
|
|
+ ]
|
|
|
+ return {
|
|
|
+ "run": {
|
|
|
+ key: run.get(key)
|
|
|
+ for key in (
|
|
|
+ "status", "outcome_status", "current_round", "search_count",
|
|
|
+ "candidate_count", "valid_primary_count",
|
|
|
+ )
|
|
|
+ },
|
|
|
+ "searches": full_state.get("searches") or [],
|
|
|
+ "candidate_progress": {
|
|
|
+ "pending_count": sum(
|
|
|
+ 1 for _item in pending_candidates
|
|
|
+ ),
|
|
|
+ "primary_count": sum(
|
|
|
+ item.get("decision_bucket") == "primary" for item in candidates
|
|
|
+ ),
|
|
|
+ "rejected_count": sum(
|
|
|
+ item.get("decision_bucket") == "rejected" for item in candidates
|
|
|
+ ),
|
|
|
+ "detail_pending_count": sum(
|
|
|
+ item.get("detail_status") == "pending"
|
|
|
+ for item in pending_candidates
|
|
|
+ ),
|
|
|
+ "detail_success_count": sum(
|
|
|
+ item.get("detail_status") == "success"
|
|
|
+ for item in pending_candidates
|
|
|
+ ),
|
|
|
+ "detail_failed_count": sum(
|
|
|
+ item.get("detail_status") == "failed"
|
|
|
+ for item in pending_candidates
|
|
|
+ ),
|
|
|
+ "portrait_pending_count": sum(
|
|
|
+ item.get("portrait_status") == "pending"
|
|
|
+ for item in pending_candidates
|
|
|
+ ),
|
|
|
+ "portrait_success_count": sum(
|
|
|
+ item.get("portrait_status") == "success"
|
|
|
+ for item in pending_candidates
|
|
|
+ ),
|
|
|
+ "portrait_failed_count": sum(
|
|
|
+ item.get("portrait_status") == "failed"
|
|
|
+ for item in pending_candidates
|
|
|
+ ),
|
|
|
+ "evidence_completed_count": sum(
|
|
|
+ item.get("detail_status") != "pending"
|
|
|
+ and item.get("portrait_status") != "pending"
|
|
|
+ for item in pending_candidates
|
|
|
+ ),
|
|
|
+ "evidence_success_count": sum(
|
|
|
+ item.get("detail_status") == "success"
|
|
|
+ and item.get("portrait_status") == "success"
|
|
|
+ for item in pending_candidates
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ }
|
|
|
|
|
|
def _video_understanding_ids(
|
|
|
- self, state: FindAgentGraphState, items: list[dict[str, Any]],
|
|
|
+ self,
|
|
|
+ state: FindAgentGraphState,
|
|
|
+ items: list[dict[str, Any]],
|
|
|
) -> list[int]:
|
|
|
"""Only candidates passing every deterministic hard gate may use video understanding."""
|
|
|
run = self._full_state(state).get("run") or {}
|
|
|
@@ -116,8 +167,7 @@ class FindAgentRoundGraph:
|
|
|
gate = evaluate_candidate_gate(item, rules)
|
|
|
checks = list(gate.get("checks") or [])
|
|
|
hard_gate_passed = gate.get("status") == "pass" and all(
|
|
|
- check.get("status") == "pass" and not check.get("compensated")
|
|
|
- for check in checks
|
|
|
+ check.get("status") == "pass" and not check.get("compensated") for check in checks
|
|
|
)
|
|
|
if str(item.get("video_url") or "").strip() and hard_gate_passed:
|
|
|
selected.append(int(item["candidate_id"]))
|
|
|
@@ -132,7 +182,7 @@ class FindAgentRoundGraph:
|
|
|
else:
|
|
|
start, end = text.find("{"), text.rfind("}")
|
|
|
if start >= 0 and end > start:
|
|
|
- text = text[start:end + 1]
|
|
|
+ text = text[start : end + 1]
|
|
|
try:
|
|
|
value = json.loads(text)
|
|
|
return value if isinstance(value, dict) else {}
|
|
|
@@ -140,7 +190,9 @@ class FindAgentRoundGraph:
|
|
|
return {}
|
|
|
|
|
|
def _approve_action(
|
|
|
- self, state: FindAgentGraphState, proposal: dict[str, Any],
|
|
|
+ self,
|
|
|
+ state: FindAgentGraphState,
|
|
|
+ proposal: dict[str, Any],
|
|
|
) -> tuple[str, str, int, str]:
|
|
|
"""Turn an LLM proposal into a safe, executable transition."""
|
|
|
pending = self._full_state(state, pending_only=True).get("candidates") or []
|
|
|
@@ -185,38 +237,135 @@ class FindAgentRoundGraph:
|
|
|
return "finish", reason or "没有待处理候选,结束本轮", worker_count, scope
|
|
|
|
|
|
async def _supervisor(self, state: FindAgentGraphState) -> dict[str, Any]:
|
|
|
- run = await self.runner.run_node(
|
|
|
- node="supervisor",
|
|
|
- round_index=state["round_index"],
|
|
|
- system_prompt=SUPERVISOR_PROMPT,
|
|
|
- user_content=self._context(state),
|
|
|
- tools=(),
|
|
|
- max_iterations=2,
|
|
|
- slots=self._slots(state),
|
|
|
- allow_delegation=False,
|
|
|
- )
|
|
|
- proposal = self._parse_supervisor(run.content)
|
|
|
- action, reason, workers, scope = self._approve_action(state, proposal)
|
|
|
- proposed_plan = proposal.get("plan")
|
|
|
- plan = (
|
|
|
- json.dumps(proposed_plan, ensure_ascii=False)
|
|
|
- if isinstance(proposed_plan, dict)
|
|
|
- else state.get("plan", "")
|
|
|
- )
|
|
|
- decision = {
|
|
|
- "step": int(state.get("supervisor_step") or 0) + 1,
|
|
|
- "proposed_action": proposal.get("next_action"),
|
|
|
- "approved_action": action,
|
|
|
- "reason": reason,
|
|
|
- "worker_count": workers,
|
|
|
- "evidence_scope": scope,
|
|
|
- }
|
|
|
+ execution_plan = state.get("execution_plan")
|
|
|
+ observed_decision: dict[str, Any] = {}
|
|
|
+ if execution_plan is None:
|
|
|
+ assignment = PlannerAssignment(
|
|
|
+ run_id=state["run_id"],
|
|
|
+ round_index=state["round_index"],
|
|
|
+ raw_demand=state["user_input"],
|
|
|
+ )
|
|
|
+ user_content, slots = self._assignment_input(
|
|
|
+ assignment,
|
|
|
+ source="find_agent_v2_run.input_json",
|
|
|
+ )
|
|
|
+ structured_runner = getattr(self.runner, "run_planning", None)
|
|
|
+ if callable(structured_runner):
|
|
|
+ run, planning = await structured_runner(
|
|
|
+ round_index=state["round_index"],
|
|
|
+ system_prompt=PLANNER_PROMPT,
|
|
|
+ user_content=user_content,
|
|
|
+ slots=slots,
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ run = await self.runner.run_node(
|
|
|
+ node="supervisor",
|
|
|
+ round_index=state["round_index"],
|
|
|
+ system_prompt=PLANNER_PROMPT,
|
|
|
+ user_content=user_content,
|
|
|
+ tools=(),
|
|
|
+ max_iterations=2,
|
|
|
+ slots=slots,
|
|
|
+ allow_delegation=False,
|
|
|
+ )
|
|
|
+ planning = PlanningDecision.model_validate(self._parse_supervisor(run.content))
|
|
|
+ execution_plan = planning.execution_plan
|
|
|
+ proposal = planning.model_dump(mode="json", exclude={"execution_plan"})
|
|
|
+ else:
|
|
|
+ assignment = SupervisorAssignment(
|
|
|
+ run_id=state["run_id"],
|
|
|
+ round_index=state["round_index"],
|
|
|
+ execution_plan=execution_plan,
|
|
|
+ execution_state=self._supervisor_state(state),
|
|
|
+ )
|
|
|
+ user_content, slots = self._assignment_input(
|
|
|
+ assignment,
|
|
|
+ source="ExecutionPlan + FindAgentV2Service.get_full_state",
|
|
|
+ )
|
|
|
+ structured_runner = getattr(self.runner, "run_supervision", None)
|
|
|
+ def approve_for_observation(supervision: SupervisorDecision) -> dict[str, Any]:
|
|
|
+ proposal_payload = supervision.model_dump(mode="json")
|
|
|
+ approved, approved_reason, approved_workers, approved_scope = (
|
|
|
+ self._approve_action(state, proposal_payload)
|
|
|
+ )
|
|
|
+ decision_payload = {
|
|
|
+ "step": int(state.get("supervisor_step") or 0) + 1,
|
|
|
+ "proposed_action": proposal_payload.get("next_action"),
|
|
|
+ "approved_action": approved,
|
|
|
+ "overridden": proposal_payload.get("next_action") != approved,
|
|
|
+ "reason": approved_reason,
|
|
|
+ "worker_count": approved_workers,
|
|
|
+ "evidence_scope": approved_scope,
|
|
|
+ }
|
|
|
+ observed_decision.update(decision_payload)
|
|
|
+ return {"Supervisor决策校验": decision_payload}
|
|
|
+
|
|
|
+ if callable(structured_runner):
|
|
|
+ run, supervision = await structured_runner(
|
|
|
+ round_index=state["round_index"],
|
|
|
+ system_prompt=SUPERVISOR_PROMPT,
|
|
|
+ user_content=user_content,
|
|
|
+ slots=slots,
|
|
|
+ output_enricher=approve_for_observation,
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ run = await self.runner.run_node(
|
|
|
+ node="supervisor",
|
|
|
+ round_index=state["round_index"],
|
|
|
+ system_prompt=SUPERVISOR_PROMPT,
|
|
|
+ user_content=user_content,
|
|
|
+ tools=(),
|
|
|
+ max_iterations=2,
|
|
|
+ slots=slots,
|
|
|
+ allow_delegation=False,
|
|
|
+ )
|
|
|
+ supervision = SupervisorDecision.model_validate(
|
|
|
+ self._parse_supervisor(run.content),
|
|
|
+ )
|
|
|
+ proposal = supervision.model_dump(mode="json")
|
|
|
+ if supervision.additional_search_tasks:
|
|
|
+ known = {item.task_id for item in execution_plan.search_tasks}
|
|
|
+ merged = list(execution_plan.search_tasks)
|
|
|
+ for item in supervision.additional_search_tasks:
|
|
|
+ if item.task_id not in known and len(merged) < 24:
|
|
|
+ merged.append(item)
|
|
|
+ known.add(item.task_id)
|
|
|
+ if len(merged) != len(execution_plan.search_tasks):
|
|
|
+ execution_plan = type(execution_plan).model_validate(
|
|
|
+ {
|
|
|
+ **execution_plan.model_dump(),
|
|
|
+ "search_tasks": merged,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ if observed_decision:
|
|
|
+ decision = observed_decision
|
|
|
+ action = str(decision["approved_action"])
|
|
|
+ reason = str(decision["reason"])
|
|
|
+ workers = int(decision["worker_count"])
|
|
|
+ scope = str(decision["evidence_scope"])
|
|
|
+ else:
|
|
|
+ action, reason, workers, scope = self._approve_action(state, proposal)
|
|
|
+ decision = {
|
|
|
+ "step": int(state.get("supervisor_step") or 0) + 1,
|
|
|
+ "proposed_action": proposal.get("next_action"),
|
|
|
+ "approved_action": action,
|
|
|
+ "overridden": proposal.get("next_action") != action,
|
|
|
+ "reason": reason,
|
|
|
+ "worker_count": workers,
|
|
|
+ "evidence_scope": scope,
|
|
|
+ }
|
|
|
self.service.update_round(
|
|
|
- state["run_id"], state["round_index"], phase="planning", plan=plan,
|
|
|
+ state["run_id"],
|
|
|
+ state["round_index"],
|
|
|
+ phase="planning",
|
|
|
+ plan=execution_plan.model_dump_json(exclude_none=True),
|
|
|
)
|
|
|
return {
|
|
|
- "phase": "planning", "plan": plan, "approved_action": action,
|
|
|
- "supervisor_step": decision["step"], "worker_count": workers,
|
|
|
+ "phase": "planning",
|
|
|
+ "execution_plan": execution_plan,
|
|
|
+ "approved_action": action,
|
|
|
+ "supervisor_step": decision["step"],
|
|
|
+ "worker_count": workers,
|
|
|
"evidence_scope": scope,
|
|
|
"decision_history": [*state.get("decision_history", []), decision],
|
|
|
"node_runs": [*state.get("node_runs", []), run],
|
|
|
@@ -224,14 +373,49 @@ class FindAgentRoundGraph:
|
|
|
|
|
|
async def _search(self, state: FindAgentGraphState) -> dict[str, Any]:
|
|
|
self.service.update_round(state["run_id"], state["round_index"], phase="searching")
|
|
|
+ execution_plan = state.get("execution_plan")
|
|
|
+ if execution_plan is None:
|
|
|
+ raise RuntimeError("Search 缺少已校验的 ExecutionPlan")
|
|
|
+ existing = {
|
|
|
+ (
|
|
|
+ str(item.get("keyword") or ""),
|
|
|
+ str(item.get("provider") or ""),
|
|
|
+ str(item.get("query_reason") or ""),
|
|
|
+ )
|
|
|
+ for item in self._full_state(state).get("searches") or []
|
|
|
+ }
|
|
|
+ tasks = [
|
|
|
+ item
|
|
|
+ for item in execution_plan.search_tasks
|
|
|
+ if (item.keyword, item.provider, item.query_reason) not in existing
|
|
|
+ ][:6]
|
|
|
+ assignment = SearchAssignment(
|
|
|
+ run_id=state["run_id"],
|
|
|
+ round_index=state["round_index"],
|
|
|
+ tasks=tasks,
|
|
|
+ )
|
|
|
+ if not tasks:
|
|
|
+ return {
|
|
|
+ "phase": "searching",
|
|
|
+ "search_actions": int(state.get("search_actions") or 0) + 1,
|
|
|
+ "action_count": int(state.get("action_count") or 0) + 1,
|
|
|
+ "node_runs": [
|
|
|
+ *state.get("node_runs", []),
|
|
|
+ NodeRun("search", state["round_index"], "没有未执行的搜索任务", 0, 0),
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ user_content, slots = self._assignment_input(
|
|
|
+ assignment,
|
|
|
+ source="ExecutionPlan.search_tasks",
|
|
|
+ )
|
|
|
run = await self.runner.run_node(
|
|
|
node="search",
|
|
|
round_index=state["round_index"],
|
|
|
system_prompt=SEARCH_PROMPT,
|
|
|
- user_content=self._context(state),
|
|
|
+ user_content=user_content,
|
|
|
tools=SEARCH_TOOLS,
|
|
|
max_iterations=10,
|
|
|
- slots=self._slots(state),
|
|
|
+ slots=slots,
|
|
|
)
|
|
|
return {
|
|
|
"phase": "searching",
|
|
|
@@ -251,19 +435,31 @@ class FindAgentRoundGraph:
|
|
|
elif scope == "portrait":
|
|
|
detail_items = []
|
|
|
jobs = [
|
|
|
- ("detail", detail_items[index:index + 8])
|
|
|
- for index in range(0, len(detail_items), 8)
|
|
|
+ ("detail", detail_items[index : index + 8]) for index in range(0, len(detail_items), 8)
|
|
|
] + [
|
|
|
- ("portrait", portrait_items[index:index + 8])
|
|
|
+ ("portrait", portrait_items[index : index + 8])
|
|
|
for index in range(0, len(portrait_items), 8)
|
|
|
]
|
|
|
|
|
|
semaphore = asyncio.Semaphore(max(1, min(8, int(state.get("worker_count") or 4))))
|
|
|
|
|
|
async def run_shard(
|
|
|
- index: int, evidence_type: str, items: list[dict[str, Any]],
|
|
|
+ index: int,
|
|
|
+ evidence_type: str,
|
|
|
+ items: list[dict[str, Any]],
|
|
|
) -> NodeRun:
|
|
|
candidate_ids = [int(item["candidate_id"]) for item in items]
|
|
|
+ assignment = EvidenceAssignment(
|
|
|
+ run_id=state["run_id"],
|
|
|
+ round_index=state["round_index"],
|
|
|
+ candidate_ids=candidate_ids,
|
|
|
+ evidence_type=evidence_type,
|
|
|
+ candidates=items,
|
|
|
+ )
|
|
|
+ user_content, slots = self._assignment_input(
|
|
|
+ assignment,
|
|
|
+ source="host evidence shard",
|
|
|
+ )
|
|
|
selected = (
|
|
|
(EVIDENCE_TOOLS[0], EVIDENCE_TOOLS[2])
|
|
|
if evidence_type == "detail"
|
|
|
@@ -272,26 +468,30 @@ class FindAgentRoundGraph:
|
|
|
async with semaphore:
|
|
|
return await self.runner.run_node(
|
|
|
node="evidence",
|
|
|
- round_index=state["round_index"],
|
|
|
- system_prompt=EVIDENCE_PROMPT,
|
|
|
- user_content=(
|
|
|
- f"你只负责当前 {evidence_type} 分片 candidate_ids={candidate_ids}。"
|
|
|
- f"必须为这些候选补齐 {evidence_type},不得访问其他候选。\n\n"
|
|
|
- + self._shard_context(state, items)
|
|
|
- ),
|
|
|
- tools=bound_candidate_tools(
|
|
|
- selected, run_id=state["run_id"], candidate_ids=candidate_ids,
|
|
|
- ),
|
|
|
- max_iterations=12,
|
|
|
- slots=self._shard_slots(state, items),
|
|
|
- branch_key=f"{evidence_type}-shard-{index}",
|
|
|
- allow_delegation=False,
|
|
|
+ round_index=state["round_index"],
|
|
|
+ system_prompt=EVIDENCE_PROMPT,
|
|
|
+ user_content=user_content,
|
|
|
+ tools=bound_candidate_tools(
|
|
|
+ selected,
|
|
|
+ run_id=state["run_id"],
|
|
|
+ candidate_ids=candidate_ids,
|
|
|
+ ),
|
|
|
+ max_iterations=12,
|
|
|
+ slots=slots,
|
|
|
+ branch_key=f"{evidence_type}-shard-{index}",
|
|
|
+ allow_delegation=False,
|
|
|
)
|
|
|
|
|
|
- runs = await asyncio.gather(*(
|
|
|
- run_shard(index, evidence_type, items)
|
|
|
- for index, (evidence_type, items) in enumerate(jobs, start=1)
|
|
|
- )) if jobs else []
|
|
|
+ runs = (
|
|
|
+ await asyncio.gather(
|
|
|
+ *(
|
|
|
+ run_shard(index, evidence_type, items)
|
|
|
+ for index, (evidence_type, items) in enumerate(jobs, start=1)
|
|
|
+ )
|
|
|
+ )
|
|
|
+ if jobs
|
|
|
+ else []
|
|
|
+ )
|
|
|
return {
|
|
|
"phase": "evidence",
|
|
|
"action_count": int(state.get("action_count") or 0) + 1,
|
|
|
@@ -314,25 +514,38 @@ class FindAgentRoundGraph:
|
|
|
else:
|
|
|
gate_failures.append((int(item["candidate_id"]), gate))
|
|
|
self.service.reject_failed_gates(state["run_id"], gate_failures)
|
|
|
- shards = [eligible[index:index + 8] for index in range(0, len(eligible), 8)]
|
|
|
+ shards = [eligible[index : index + 8] for index in range(0, len(eligible), 8)]
|
|
|
semaphore = asyncio.Semaphore(max(1, min(8, int(state.get("worker_count") or 4))))
|
|
|
|
|
|
async def run_shard(index: int, items: list[dict[str, Any]]) -> NodeRun:
|
|
|
candidate_ids = [int(item["candidate_id"]) for item in items]
|
|
|
video_understanding_ids = self._video_understanding_ids(state, items)
|
|
|
+ execution_plan = state.get("execution_plan")
|
|
|
+ if execution_plan is None:
|
|
|
+ raise RuntimeError("Evaluator 缺少已校验的 ExecutionPlan")
|
|
|
+ assignment = EvaluationAssignment(
|
|
|
+ run_id=state["run_id"],
|
|
|
+ round_index=state["round_index"],
|
|
|
+ candidate_ids=candidate_ids,
|
|
|
+ evaluation_brief=execution_plan.evaluation_brief,
|
|
|
+ quality_gate_rules=rules,
|
|
|
+ current_datetime=str(rules.get("current_datetime") or ""),
|
|
|
+ timezone=str(rules.get("timezone") or ""),
|
|
|
+ video_understanding_candidate_ids=video_understanding_ids,
|
|
|
+ candidates=items,
|
|
|
+ )
|
|
|
+ user_content, slots = self._assignment_input(
|
|
|
+ assignment,
|
|
|
+ source="ExecutionPlan.evaluation_brief + host candidate shard",
|
|
|
+ )
|
|
|
video_tools = (
|
|
|
bound_candidate_tools(
|
|
|
(EVALUATION_TOOLS[0],),
|
|
|
run_id=state["run_id"],
|
|
|
candidate_ids=video_understanding_ids,
|
|
|
)
|
|
|
- if video_understanding_ids else ()
|
|
|
- )
|
|
|
- evaluation_instruction = (
|
|
|
- f"你只负责当前分片 candidate_ids={candidate_ids}。"
|
|
|
- f"这些候选均已通过硬门禁;有播放地址、允许按需视频理解的 "
|
|
|
- f"candidate_ids={video_understanding_ids}。"
|
|
|
- "视频理解不是硬性要求,可根据已有证据决定是否调用;只允许对这个列表中的候选调用。"
|
|
|
+ if video_understanding_ids
|
|
|
+ else ()
|
|
|
)
|
|
|
async with semaphore:
|
|
|
structured_runner = getattr(self.runner, "run_evaluation", None)
|
|
|
@@ -340,17 +553,14 @@ class FindAgentRoundGraph:
|
|
|
run, proposed = await structured_runner(
|
|
|
round_index=state["round_index"],
|
|
|
system_prompt=EVALUATOR_PROMPT,
|
|
|
- user_content=(
|
|
|
- evaluation_instruction
|
|
|
- + "必须为分片内每个 candidate_id 各输出一次结构化评估。\n\n"
|
|
|
- + self._shard_context(state, items)
|
|
|
- ),
|
|
|
- slots=self._shard_slots(state, items),
|
|
|
+ user_content=user_content,
|
|
|
+ slots=slots,
|
|
|
branch_key=f"step-{state.get('supervisor_step', 0)}-shard-{index}",
|
|
|
tools=video_tools,
|
|
|
)
|
|
|
normalized = normalize_evaluation_items(
|
|
|
- proposed, allowed_candidates=items,
|
|
|
+ proposed,
|
|
|
+ allowed_candidates=items,
|
|
|
)
|
|
|
updated = self.service.evaluate(state["run_id"], normalized)
|
|
|
updated_ids = {int(item["candidate_id"]) for item in updated}
|
|
|
@@ -364,11 +574,7 @@ class FindAgentRoundGraph:
|
|
|
node="evaluator",
|
|
|
round_index=state["round_index"],
|
|
|
system_prompt=EVALUATOR_PROMPT,
|
|
|
- user_content=(
|
|
|
- evaluation_instruction
|
|
|
- + "必须把这些候选全部分池,不得评估其他候选。\n\n"
|
|
|
- + self._shard_context(state, items)
|
|
|
- ),
|
|
|
+ user_content=user_content,
|
|
|
tools=(
|
|
|
*video_tools,
|
|
|
*bound_candidate_tools(
|
|
|
@@ -378,14 +584,18 @@ class FindAgentRoundGraph:
|
|
|
),
|
|
|
),
|
|
|
max_iterations=12,
|
|
|
- slots=self._shard_slots(state, items),
|
|
|
+ slots=slots,
|
|
|
branch_key=f"step-{state.get('supervisor_step', 0)}-shard-{index}",
|
|
|
allow_delegation=False,
|
|
|
)
|
|
|
|
|
|
- runs = await asyncio.gather(*(
|
|
|
- run_shard(index, items) for index, items in enumerate(shards, start=1)
|
|
|
- )) if shards else []
|
|
|
+ runs = (
|
|
|
+ await asyncio.gather(
|
|
|
+ *(run_shard(index, items) for index, items in enumerate(shards, start=1))
|
|
|
+ )
|
|
|
+ if shards
|
|
|
+ else []
|
|
|
+ )
|
|
|
node_runs.extend(runs)
|
|
|
self.service.recount_valid_primary(state["run_id"])
|
|
|
after = self.service.snapshot(state["run_id"])
|
|
|
@@ -393,11 +603,11 @@ class FindAgentRoundGraph:
|
|
|
stagnant = stagnant + 1 if after.pending_count >= before.pending_count else 0
|
|
|
if stagnant >= 3:
|
|
|
raise RuntimeError(
|
|
|
- "评估节点连续 3 次未消费 pending_evaluation 候选:"
|
|
|
- f"remaining={after.pending_count}"
|
|
|
+ f"评估节点连续 3 次未消费 pending_evaluation 候选:remaining={after.pending_count}"
|
|
|
)
|
|
|
return {
|
|
|
- "phase": "evaluating", "node_runs": node_runs,
|
|
|
+ "phase": "evaluating",
|
|
|
+ "node_runs": node_runs,
|
|
|
"action_count": int(state.get("action_count") or 0) + 1,
|
|
|
"evaluator_stagnation": stagnant,
|
|
|
}
|
|
|
@@ -413,10 +623,16 @@ class FindAgentRoundGraph:
|
|
|
builder.add_node("evidence", self._evidence)
|
|
|
builder.add_node("evaluator", self._evaluator)
|
|
|
builder.add_edge(START, "supervisor")
|
|
|
- builder.add_conditional_edges("supervisor", self._route, {
|
|
|
- "search": "search", "evidence": "evidence",
|
|
|
- "evaluator": "evaluator", "finish": END,
|
|
|
- })
|
|
|
+ builder.add_conditional_edges(
|
|
|
+ "supervisor",
|
|
|
+ self._route,
|
|
|
+ {
|
|
|
+ "search": "search",
|
|
|
+ "evidence": "evidence",
|
|
|
+ "evaluator": "evaluator",
|
|
|
+ "finish": END,
|
|
|
+ },
|
|
|
+ )
|
|
|
builder.add_edge("search", "supervisor")
|
|
|
builder.add_edge("evidence", "supervisor")
|
|
|
builder.add_edge("evaluator", "supervisor")
|
|
|
@@ -427,7 +643,7 @@ class FindAgentRoundGraph:
|
|
|
"run_id": state.run_id,
|
|
|
"user_input": state.user_input,
|
|
|
"round_index": state.round_index,
|
|
|
- "plan": state.plan,
|
|
|
+ "execution_plan": state.execution_plan,
|
|
|
"phase": state.phase,
|
|
|
"node_runs": [],
|
|
|
"snapshot": state.snapshot,
|
|
|
@@ -440,12 +656,14 @@ class FindAgentRoundGraph:
|
|
|
"evaluator_stagnation": 0,
|
|
|
}
|
|
|
with self.observer.round(
|
|
|
- round_index=state.round_index, spec=self.obagent_spec,
|
|
|
+ round_index=state.round_index,
|
|
|
+ spec=self.obagent_spec,
|
|
|
) as round_observation:
|
|
|
output = await self.app.ainvoke(
|
|
|
- graph_state, config={"recursion_limit": max(64, self.max_actions * 4)},
|
|
|
+ graph_state,
|
|
|
+ config={"recursion_limit": max(64, self.max_actions * 4)},
|
|
|
)
|
|
|
- state.plan = str(output.get("plan") or "")
|
|
|
+ state.execution_plan = output.get("execution_plan")
|
|
|
state.node_runs.extend(output.get("node_runs") or [])
|
|
|
state.snapshot = self.service.snapshot(state.run_id)
|
|
|
state.phase = "done"
|
|
|
@@ -456,9 +674,16 @@ class FindAgentRoundGraph:
|
|
|
status="done",
|
|
|
snapshot=state.snapshot,
|
|
|
)
|
|
|
- round_observation.set_output({
|
|
|
- "状态快照": state.snapshot.__dict__,
|
|
|
- "本轮计划": state.plan,
|
|
|
- "Supervisor决策轨迹": output.get("decision_history") or [],
|
|
|
- }, ok=True)
|
|
|
+ round_observation.set_output(
|
|
|
+ {
|
|
|
+ "状态快照": state.snapshot.__dict__,
|
|
|
+ "本轮计划": (
|
|
|
+ state.execution_plan.model_dump(mode="json")
|
|
|
+ if state.execution_plan is not None
|
|
|
+ else {}
|
|
|
+ ),
|
|
|
+ "Supervisor决策轨迹": output.get("decision_history") or [],
|
|
|
+ },
|
|
|
+ ok=True,
|
|
|
+ )
|
|
|
return state
|