tools.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. """Stage tools that persist exclusively to ``find_agent_v2_*`` tables."""
  2. from __future__ import annotations
  3. import json
  4. from collections.abc import Callable, Iterable
  5. from typing import Any
  6. from find_agent_v2.providers import (
  7. fetch_details,
  8. fetch_portraits,
  9. search_internal,
  10. search_tikhub,
  11. )
  12. from find_agent_v2.service import get_find_agent_v2_service
  13. from supply_agent.tools import tool
  14. from supply_agent.tools.registry import ToolRegistry
  15. ToolFn = Callable[..., Any]
  16. @tool
  17. async def search_videos_v2(run_id: str, round_index: int, searches: list[dict[str, Any]]) -> str:
  18. """批量搜索并仅写入 find_agent_v2_search/candidate;searches 最多 6 项。"""
  19. if not searches or len(searches) > 6:
  20. return json.dumps({"error": "searches 必须为 1~6 项"}, ensure_ascii=False)
  21. service = get_find_agent_v2_service()
  22. outputs: list[dict[str, Any]] = []
  23. for raw_task in searches:
  24. keyword = str(raw_task.get("keyword") or "").strip()
  25. reason = str(raw_task.get("query_reason") or "").strip()
  26. provider = str(raw_task.get("provider") or "internal_keyword")
  27. if not keyword or not reason:
  28. outputs.append({"error": "keyword/query_reason 不能为空"})
  29. continue
  30. max_pages = max(1, min(int(raw_task.get("max_pages") or 1), 2))
  31. cursor: str | int = raw_task.get("cursor") or 0
  32. provider_search_id = str(raw_task.get("search_id") or "")
  33. backtrace = str(raw_task.get("backtrace") or "")
  34. for page_no in range(1, max_pages + 1):
  35. common = {
  36. "keyword": keyword,
  37. "content_type": str(raw_task.get("content_type") or "视频"),
  38. "sort_type": str(raw_task.get("sort_type") or "综合排序"),
  39. "publish_time": str(raw_task.get("publish_time") or "不限"),
  40. "min_duration_seconds": int(raw_task.get("min_duration_seconds") or 30),
  41. }
  42. if provider == "tikhub":
  43. payload = await search_tikhub(
  44. **common,
  45. cursor=int(cursor or 0),
  46. filter_duration=str(raw_task.get("filter_duration") or "不限"),
  47. search_id=provider_search_id,
  48. backtrace=backtrace,
  49. )
  50. else:
  51. provider = "internal_keyword"
  52. payload = await search_internal(**common, cursor=str(cursor or "0"))
  53. saved = service.save_search(
  54. run_id=run_id,
  55. round_index=int(round_index),
  56. keyword=keyword,
  57. query_reason=reason,
  58. source_type=str(raw_task.get("source_type") or "mixed"),
  59. provider=provider,
  60. cursor=str(cursor),
  61. page_no=page_no,
  62. payload=payload,
  63. )
  64. outputs.append({
  65. "keyword": keyword,
  66. "provider": provider,
  67. "page_no": page_no,
  68. "error": payload.get("error"),
  69. "has_more": bool(payload.get("has_more")),
  70. "next_cursor": payload.get("next_cursor"),
  71. **saved,
  72. })
  73. if payload.get("error") or not payload.get("has_more"):
  74. break
  75. cursor = payload.get("next_cursor") or cursor
  76. provider_search_id = str(payload.get("search_id") or provider_search_id)
  77. backtrace = str(payload.get("backtrace") or backtrace)
  78. return json.dumps({"run_id": run_id, "searches": outputs}, ensure_ascii=False)
  79. @tool
  80. async def fetch_candidate_details_v2(run_id: str, candidate_ids: list[int]) -> str:
  81. """批量获取候选详情并仅写入 find_agent_v2_candidate/evidence;最多 8 项。"""
  82. service = get_find_agent_v2_service()
  83. candidates = service.candidate_inputs(run_id, candidate_ids[:8])
  84. payload = await fetch_details([item["aweme_id"] for item in candidates])
  85. service.save_details(run_id, list(payload.get("details") or []), list(payload.get("errors") or []))
  86. return json.dumps({
  87. "run_id": run_id,
  88. "success_count": int(payload.get("success_count") or len(payload.get("details") or [])),
  89. "failed_count": int(payload.get("failed_count") or len(payload.get("errors") or [])),
  90. "errors": payload.get("errors") or [],
  91. }, ensure_ascii=False)
  92. @tool
  93. async def fetch_candidate_portraits_v2(run_id: str, candidate_ids: list[int]) -> str:
  94. """批量获取候选双侧年龄画像并仅写入 find_agent_v2_candidate/evidence。"""
  95. service = get_find_agent_v2_service()
  96. candidates = service.candidate_inputs(run_id, candidate_ids[:8])
  97. payload = await fetch_portraits([{
  98. "aweme_id": item["aweme_id"],
  99. "author_sec_uid": item.get("author_sec_uid"),
  100. } for item in candidates])
  101. results = list(payload.get("results") or [])
  102. service.save_portraits(run_id, results)
  103. return json.dumps({"run_id": run_id, "count": len(results), "results": results}, ensure_ascii=False)
  104. @tool
  105. def evaluate_candidates_v2(run_id: str, items: list[dict[str, Any]]) -> str:
  106. """写入 R/E/S/V 与 primary/rejected;程序会对 primary 强制执行 P0 门禁。"""
  107. updated = get_find_agent_v2_service().evaluate(run_id, items)
  108. return json.dumps({"run_id": run_id, "updated": updated}, ensure_ascii=False)
  109. @tool
  110. def query_find_agent_v2_state(run_id: str, limit: int = 100) -> str:
  111. """查询完全隔离的 find_agent_v2 运行、搜索与候选状态。"""
  112. state = get_find_agent_v2_service().get_full_state(run_id, limit=limit)
  113. return json.dumps(state, ensure_ascii=False, default=str)
  114. @tool
  115. def query_pending_candidates_v2(run_id: str, limit: int = 100) -> str:
  116. """仅查询当前 run 尚未分池的 pending_evaluation 候选。"""
  117. state = get_find_agent_v2_service().get_full_state(
  118. run_id, limit=limit, pending_only=True,
  119. )
  120. return json.dumps(state, ensure_ascii=False, default=str)
  121. SEARCH_TOOLS: tuple[ToolFn, ...] = (search_videos_v2, query_find_agent_v2_state)
  122. EVIDENCE_TOOLS: tuple[ToolFn, ...] = (
  123. fetch_candidate_details_v2,
  124. fetch_candidate_portraits_v2,
  125. query_pending_candidates_v2,
  126. )
  127. EVALUATION_TOOLS: tuple[ToolFn, ...] = (
  128. evaluate_candidates_v2,
  129. query_pending_candidates_v2,
  130. )
  131. REPORT_TOOLS: tuple[ToolFn, ...] = (query_find_agent_v2_state,)
  132. def build_tool_registry(functions: Iterable[ToolFn]) -> ToolRegistry:
  133. return ToolRegistry().from_decorated(*tuple(functions))