find_agent_v2.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. """Admin-facing read model and execution service for find_agent_v2."""
  2. from __future__ import annotations
  3. import json
  4. from typing import Any
  5. from sqlalchemy import func, or_, select
  6. from find_agent_v2.demand_context import prepare_latest_v2_demand_run_by_name
  7. from find_agent_v2.models import (
  8. FindAgentV2Candidate,
  9. FindAgentV2Evidence,
  10. FindAgentV2Round,
  11. FindAgentV2Run,
  12. FindAgentV2Search,
  13. )
  14. from find_agent_v2.runner import run_prepared_find_agent_v2
  15. from find_agent_v2.service import get_find_agent_v2_service
  16. from supply_infra.db.session import get_session
  17. def _json(raw: str | None, default: Any) -> Any:
  18. try:
  19. return json.loads(raw) if raw else default
  20. except (TypeError, ValueError):
  21. return default
  22. def _time(value: Any) -> str | None:
  23. return value.isoformat() if value is not None else None
  24. def _number(value: Any) -> float | None:
  25. return float(value) if value is not None else None
  26. def _run(row: FindAgentV2Run) -> dict[str, Any]:
  27. return {
  28. "id": int(row.id),
  29. "run_id": row.run_id,
  30. "demand_grade_id": row.demand_grade_id,
  31. "demand_word": row.demand_word,
  32. "status": row.status,
  33. "outcome_status": row.outcome_status,
  34. "current_round": row.current_round,
  35. "search_count": row.search_count,
  36. "candidate_count": row.candidate_count,
  37. "valid_primary_count": row.valid_primary_count,
  38. "input_tokens": row.input_tokens,
  39. "output_tokens": row.output_tokens,
  40. "total_tokens": row.total_tokens,
  41. "cost_usd": _number(row.cost_usd) or 0,
  42. "intent_summary": row.intent_summary,
  43. "stop_reason": row.stop_reason,
  44. "obagent_run_uid": row.obagent_run_uid,
  45. "triggered_by_user_id": row.triggered_by_user_id,
  46. "triggered_by_username": row.triggered_by_username,
  47. "trigger_source": row.trigger_source,
  48. "create_time": _time(row.create_time),
  49. "update_time": _time(row.update_time),
  50. }
  51. def list_runs(
  52. *, status: str | None = None, keyword: str | None = None,
  53. limit: int = 30, offset: int = 0,
  54. ) -> dict[str, Any]:
  55. with get_session() as session:
  56. conditions = []
  57. if status:
  58. conditions.append(FindAgentV2Run.status == status)
  59. normalized = str(keyword or "").strip().lower()
  60. if normalized:
  61. pattern = f"%{normalized}%"
  62. conditions.append(or_(
  63. func.lower(FindAgentV2Run.demand_word).like(pattern),
  64. func.lower(FindAgentV2Run.run_id).like(pattern),
  65. func.lower(func.coalesce(FindAgentV2Run.triggered_by_username, "")).like(pattern),
  66. ))
  67. total = int(session.scalar(
  68. select(func.count()).select_from(FindAgentV2Run).where(*conditions)
  69. ) or 0)
  70. rows = list(session.scalars(
  71. select(FindAgentV2Run).where(*conditions)
  72. .order_by(FindAgentV2Run.create_time.desc(), FindAgentV2Run.id.desc())
  73. .limit(limit).offset(offset)
  74. ))
  75. return {"items": [_run(row) for row in rows], "total": total, "limit": limit, "offset": offset}
  76. def get_run_detail(run_id: str) -> dict[str, Any] | None:
  77. with get_session() as session:
  78. run = session.scalar(select(FindAgentV2Run).where(FindAgentV2Run.run_id == run_id))
  79. if run is None:
  80. return None
  81. rounds = list(session.scalars(
  82. select(FindAgentV2Round).where(FindAgentV2Round.run_id == run_id)
  83. .order_by(FindAgentV2Round.round_index, FindAgentV2Round.id)
  84. ))
  85. searches = list(session.scalars(
  86. select(FindAgentV2Search).where(FindAgentV2Search.run_id == run_id)
  87. .order_by(FindAgentV2Search.round_index, FindAgentV2Search.id)
  88. ))
  89. candidates = list(session.scalars(
  90. select(FindAgentV2Candidate).where(FindAgentV2Candidate.run_id == run_id)
  91. .order_by(FindAgentV2Candidate.id)
  92. ))
  93. evidence = list(session.scalars(
  94. select(FindAgentV2Evidence).where(FindAgentV2Evidence.run_id == run_id)
  95. .order_by(FindAgentV2Evidence.id)
  96. ))
  97. round_items = [{
  98. "id": int(row.id), "round_index": row.round_index, "phase": row.phase,
  99. "status": row.status, "plan": _json(row.plan_json, {}),
  100. "start_snapshot": _json(row.start_snapshot_json, {}),
  101. "end_snapshot": _json(row.end_snapshot_json, {}),
  102. "error_message": row.error_message, "create_time": _time(row.create_time),
  103. "update_time": _time(row.update_time),
  104. } for row in rounds]
  105. search_items = [{
  106. "id": int(row.id), "round_index": row.round_index, "keyword": row.keyword,
  107. "query_reason": row.query_reason, "source_type": row.source_type,
  108. "provider": row.provider, "cursor": row.cursor, "page_no": row.page_no,
  109. "has_more": bool(row.has_more), "next_cursor": row.next_cursor,
  110. "provider_state": _json(row.provider_state_json, {}),
  111. "result_count": row.result_count, "status": row.status,
  112. "error_message": row.error_message,
  113. "raw_response": _json(row.raw_response_json, {}),
  114. "create_time": _time(row.create_time),
  115. } for row in searches]
  116. evidence_by_candidate: dict[int, list[dict[str, Any]]] = {}
  117. evidence_items = []
  118. for row in evidence:
  119. item = {
  120. "id": int(row.id), "candidate_id": row.candidate_id,
  121. "evidence_type": row.evidence_type, "provider": row.provider,
  122. "status": row.status, "raw": _json(row.raw_json, {}),
  123. "normalized": _json(row.normalized_json, {}),
  124. "error_message": row.error_message, "create_time": _time(row.create_time),
  125. }
  126. evidence_items.append(item)
  127. if row.candidate_id is not None:
  128. evidence_by_candidate.setdefault(int(row.candidate_id), []).append(item)
  129. candidate_items = [{
  130. "id": int(row.id), "first_search_id": row.first_search_id,
  131. "aweme_id": row.aweme_id, "title": row.title, "content_link": row.content_link,
  132. "author_name": row.author_name, "author_sec_uid": row.author_sec_uid,
  133. "source_keywords": _json(row.source_keywords_json, []), "tags": _json(row.tags_json, []),
  134. "publish_at": _time(row.publish_at), "duration_seconds": _number(row.duration_seconds),
  135. "play_count": row.play_count, "like_count": row.like_count,
  136. "comment_count": row.comment_count, "collect_count": row.collect_count,
  137. "share_count": row.share_count, "detail": _json(row.detail_json, {}),
  138. "portrait": _json(row.portrait_json, {}), "detail_status": row.detail_status,
  139. "portrait_status": row.portrait_status,
  140. "content_50_plus_ratio": _number(row.content_50_plus_ratio),
  141. "account_50_plus_ratio": _number(row.account_50_plus_ratio),
  142. "relevance_score": _number(row.relevance_score), "elder_score": _number(row.elder_score),
  143. "share_score": _number(row.share_score), "value_score": _number(row.value_score),
  144. "gate_status": row.gate_status, "gate_result": _json(row.gate_result_json, {}),
  145. "decision_bucket": row.decision_bucket, "decision_reason": row.decision_reason,
  146. "reject_reason_code": row.reject_reason_code,
  147. "evidence": evidence_by_candidate.get(int(row.id), []),
  148. "create_time": _time(row.create_time), "update_time": _time(row.update_time),
  149. } for row in candidates]
  150. candidates_by_aweme = {item["aweme_id"]: item for item in candidate_items}
  151. for search in search_items:
  152. raw_results = search["raw_response"].get("search_results", [])
  153. result_ids = [
  154. str(item.get("aweme_id") or "")
  155. for item in raw_results
  156. if isinstance(item, dict) and item.get("aweme_id")
  157. ]
  158. search["result_aweme_ids"] = result_ids
  159. search["matched_candidates"] = [
  160. candidates_by_aweme[aweme_id]
  161. for aweme_id in result_ids
  162. if aweme_id in candidates_by_aweme
  163. ]
  164. timeline = []
  165. for row in round_items:
  166. timeline.append({"type": "round", "time": row["create_time"], "data": row})
  167. for row in search_items:
  168. timeline.append({"type": "search", "time": row["create_time"], "data": row})
  169. for row in evidence_items:
  170. timeline.append({"type": "evidence", "time": row["create_time"], "data": row})
  171. timeline.sort(key=lambda item: str(item.get("time") or ""))
  172. return {
  173. "run": {**_run(run), "input": _json(run.input_json, {}),
  174. "rule_config": _json(run.rule_config_json, {})},
  175. "rounds": round_items, "searches": search_items, "candidates": candidate_items,
  176. "evidence": evidence_items, "timeline": timeline,
  177. }
  178. def prepare_test_run(demand_word: str, current_user: dict[str, Any]) -> dict[str, Any]:
  179. prepared = prepare_latest_v2_demand_run_by_name(
  180. demand_word,
  181. triggered_by_user_id=int(current_user["id"]),
  182. triggered_by_username=str(current_user["username"]),
  183. trigger_source="web_admin",
  184. )
  185. return prepared.summary()
  186. def execute_test_run(run_id: str) -> None:
  187. """Background task entry point. Always persist bootstrap failures."""
  188. try:
  189. run_prepared_find_agent_v2(run_id)
  190. except Exception as exc:
  191. get_find_agent_v2_service().fail_run(run_id, f"{type(exc).__name__}: {exc}")