verify_find_agent_live.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. """Run a real find_agent acceptance case and clean its database records by default."""
  2. from __future__ import annotations
  3. import argparse
  4. import asyncio
  5. import json
  6. from typing import Any
  7. from sqlalchemy import delete, func, select
  8. from agents.find_agent import create_find_agent
  9. from agents.find_agent.agent import _validate_report_buckets
  10. from supply_agent.config import Settings
  11. from supply_agent.types import AgentEventType
  12. from supply_infra.db.models.video_discovery import (
  13. VideoDiscoveryCandidate,
  14. VideoDiscoveryRun,
  15. VideoDiscoverySearch,
  16. )
  17. from supply_infra.db.session import get_session
  18. def _parse_json(raw: str) -> dict[str, Any]:
  19. try:
  20. value = json.loads(raw)
  21. except (TypeError, json.JSONDecodeError):
  22. return {}
  23. return value if isinstance(value, dict) else {}
  24. def _cleanup_run(run_id: str) -> int:
  25. with get_session() as session:
  26. session.execute(
  27. delete(VideoDiscoveryCandidate).where(
  28. VideoDiscoveryCandidate.run_id == run_id
  29. )
  30. )
  31. session.execute(
  32. delete(VideoDiscoverySearch).where(
  33. VideoDiscoverySearch.run_id == run_id
  34. )
  35. )
  36. session.execute(
  37. delete(VideoDiscoveryRun).where(VideoDiscoveryRun.run_id == run_id)
  38. )
  39. with get_session() as session:
  40. remaining = session.scalar(
  41. select(func.count(VideoDiscoveryRun.id)).where(
  42. VideoDiscoveryRun.run_id == run_id
  43. )
  44. )
  45. return int(remaining or 0)
  46. async def _run(args: argparse.Namespace) -> None:
  47. agent = create_find_agent(settings=Settings.from_env(LOG_ENABLED=False))
  48. relevant_points = [{"point": point} for point in args.relevant_point]
  49. prompt = (
  50. "这是一次真实验收测试,请完整执行并严格遵守完成条件。\n"
  51. f"demand_word:{args.demand_word}\n"
  52. f"seed_video_title:{args.seed_video_title}\n"
  53. f"relevant_points:{json.dumps(relevant_points, ensure_ascii=False)}\n"
  54. "请寻找老年人或临近退休人群可能喜欢观看和分享的视频。"
  55. )
  56. tool_calls: list[str] = []
  57. tool_payloads: list[dict[str, Any]] = []
  58. run_ids: list[str] = []
  59. domain_errors: list[dict[str, Any]] = []
  60. audit_results: list[dict[str, Any]] = []
  61. states: list[dict[str, Any]] = []
  62. final_content = ""
  63. done_data: dict[str, Any] = {}
  64. try:
  65. async for event in agent.astream(prompt):
  66. if event.type == AgentEventType.TOOL_CALL:
  67. name = str(event.data.get("name") or "")
  68. call_args = _parse_json(str(event.data.get("arguments") or "{}"))
  69. tool_calls.append(name)
  70. tool_payloads.append({"name": name, "args": call_args})
  71. print(f"TOOL_CALL {len(tool_calls)} {name}", flush=True)
  72. elif event.type == AgentEventType.TOOL_RESULT:
  73. name = str(event.data.get("name") or "")
  74. data = _parse_json(str(event.data.get("content") or ""))
  75. if name == "create_video_discovery_run" and data.get("run_id"):
  76. run_ids.append(str(data["run_id"]))
  77. if name in {
  78. "audit_video_discovery_process",
  79. "audit_video_discovery_run",
  80. } and not data.get("error"):
  81. audit_results.append(data)
  82. if name == "query_video_discovery_state" and not data.get("error"):
  83. states.append(data)
  84. if data.get("error"):
  85. domain_errors.append(
  86. {
  87. "tool": name,
  88. "budget_exhausted": bool(data.get("budget_exhausted")),
  89. "error": str(data["error"])[:300],
  90. }
  91. )
  92. status = "DOMAIN_ERROR" if data.get("error") else "OK"
  93. if event.data.get("is_error") and not data.get("error"):
  94. status = "ERROR"
  95. extra = ""
  96. if data.get("error"):
  97. extra = " error=" + json.dumps(
  98. str(data["error"])[:240],
  99. ensure_ascii=False,
  100. )
  101. if name in {
  102. "audit_video_discovery_process",
  103. "audit_video_discovery_run",
  104. } and not data.get("error"):
  105. audit_flag = data.get("can_finish")
  106. extra = f" can_finish={audit_flag}"
  107. if audit_flag is False:
  108. extra += " violations=" + json.dumps(
  109. data.get("critical_violations"),
  110. ensure_ascii=False,
  111. )
  112. print(f"TOOL_RESULT {name} {status}{extra}", flush=True)
  113. elif event.type == AgentEventType.MESSAGE:
  114. final_content = str(event.data.get("content") or "")
  115. elif event.type == AgentEventType.DONE:
  116. done_data = event.data
  117. if not final_content:
  118. final_content = str(event.data.get("content") or "")
  119. unique_keywords: list[str] = []
  120. recorded_sources: list[Any] = []
  121. pagination_calls = 0
  122. for entry in tool_payloads:
  123. name = entry["name"]
  124. call_args = entry["args"]
  125. if name in {"douyin_search", "douyin_search_tikhub"}:
  126. keyword = str(call_args.get("keyword") or "").strip()
  127. if keyword and keyword not in unique_keywords:
  128. unique_keywords.append(keyword)
  129. if (
  130. str(call_args.get("cursor") or "0") not in {"", "0"}
  131. or call_args.get("search_id")
  132. ):
  133. pagination_calls += 1
  134. if name == "record_video_search_page":
  135. recorded_sources.append(call_args.get("source_type"))
  136. last_state = states[-1] if states else {}
  137. final_bucket_error = (
  138. _validate_report_buckets(final_content, last_state)
  139. if final_content and last_state
  140. else "缺少最终文本或最终状态"
  141. )
  142. report = {
  143. "model": agent.model,
  144. "iterations": done_data.get("iterations"),
  145. "tool_calls": len(tool_calls),
  146. "tool_names": tool_calls,
  147. "unique_search_keywords": unique_keywords,
  148. "pagination_calls": pagination_calls,
  149. "recorded_source_types": recorded_sources,
  150. "qwen_calls": tool_calls.count("qwen_video_analyze"),
  151. "audit_calls": len(audit_results),
  152. "last_audit_can_finish": (
  153. audit_results[-1].get("can_finish") if audit_results else None
  154. ),
  155. "last_audit_violations": (
  156. audit_results[-1].get("critical_violations")
  157. if audit_results
  158. else None
  159. ),
  160. "domain_errors": domain_errors,
  161. "state_search_count": len(last_state.get("searches", [])),
  162. "state_candidate_count": len(last_state.get("candidates", [])),
  163. "state_run": last_state.get("run"),
  164. "final_content_chars": len(final_content),
  165. "final_has_primary_section": "主推荐" in final_content,
  166. "final_has_backup_section": "补充推荐" in final_content,
  167. "final_bucket_consistent": final_bucket_error is None,
  168. "final_bucket_error": final_bucket_error,
  169. "max_iterations_failure": final_content.startswith(
  170. "Max iterations reached"
  171. ),
  172. "run_ids": run_ids,
  173. }
  174. print("AGENT_REPORT " + json.dumps(report, ensure_ascii=False), flush=True)
  175. print("FINAL_CONTENT_BEGIN", flush=True)
  176. print(final_content, flush=True)
  177. print("FINAL_CONTENT_END", flush=True)
  178. finally:
  179. if not args.keep_data:
  180. for run_id in set(run_ids):
  181. remaining = _cleanup_run(run_id)
  182. print(f"CLEANUP {run_id} remaining={remaining}", flush=True)
  183. def _arguments() -> argparse.Namespace:
  184. parser = argparse.ArgumentParser()
  185. parser.add_argument(
  186. "--demand-word",
  187. default="个人养老金税收优惠",
  188. )
  189. parser.add_argument(
  190. "--seed-video-title",
  191. default="个人养老金制度全面实施,退休前这样缴纳可以享受税收优惠",
  192. )
  193. parser.add_argument(
  194. "--relevant-point",
  195. action="append",
  196. default=[
  197. "个人养老金缴费如何抵扣个税",
  198. "适合临近退休人群转发给家人了解",
  199. ],
  200. )
  201. parser.add_argument(
  202. "--keep-data",
  203. action="store_true",
  204. help="保留本次运行的数据库记录;默认清理。",
  205. )
  206. return parser.parse_args()
  207. if __name__ == "__main__":
  208. asyncio.run(_run(_arguments()))