test_pattern_recall_archive.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. from __future__ import annotations
  2. from content_agent.business_modules.content_discovery.pattern_recall import recall_decision
  3. from content_agent.integrations.runtime_files import LocalRuntimeFileStore
  4. from tests.gemini_helpers import FakeGeminiVideoClient, fake_gemini_fail, fake_gemini_pool, fake_gemini_review
  5. def test_pattern_recall_marks_all_judged_videos_for_oss_archive(tmp_path):
  6. run_id = "run_001"
  7. policy_run_id = "policy_run_001"
  8. runtime = LocalRuntimeFileStore(tmp_path / "runtime")
  9. runtime.prepare_run(run_id)
  10. items = [
  11. _item(run_id, policy_run_id, "content_pool", "d_001"),
  12. _item(run_id, policy_run_id, "content_review", "d_002"),
  13. _item(run_id, policy_run_id, "content_failed", "d_003"),
  14. ]
  15. media = [_media(run_id, policy_run_id, item["platform_content_id"]) for item in items]
  16. bundles = [_bundle(item) for item in items]
  17. pool_result = fake_gemini_pool()
  18. pool_result["timing_metrics"] = {
  19. "video_fetch": {"download_duration_ms": 100, "ffmpeg_duration_ms": 200},
  20. "gemini_request": {"total_duration_ms": 300},
  21. }
  22. gemini = FakeGeminiVideoClient(
  23. result_by_content_id={
  24. "content_pool": pool_result,
  25. "content_review": fake_gemini_review(),
  26. "content_failed": fake_gemini_fail("gemini_client_timeout"),
  27. }
  28. )
  29. result = recall_decision.run(
  30. run_id,
  31. policy_run_id,
  32. items,
  33. media,
  34. bundles,
  35. {"ext_data": {"evidence_pack": {"seed_terms": ["养生"]}}},
  36. runtime,
  37. gemini,
  38. )
  39. assert {
  40. row["platform_content_id"]: row["content_media_status"]
  41. for row in result["content_media_records"]
  42. } == {
  43. "content_pool": "oss_upload_pending",
  44. "content_review": "oss_upload_pending",
  45. "content_failed": "oss_upload_pending",
  46. }
  47. for row in result["content_media_records"]:
  48. assert row["raw_payload"]["oss_archive_status"] == "pending"
  49. media_by_id = {row["platform_content_id"]: row for row in result["content_media_records"]}
  50. assert media_by_id["content_pool"]["raw_payload"]["gemini_timing_metrics"]["video_fetch"] == {
  51. "download_duration_ms": 100,
  52. "ffmpeg_duration_ms": 200,
  53. }
  54. evidence_by_id = {row["platform_content_id"]: row for row in result["pattern_recall_evidence"]}
  55. assert evidence_by_id["content_pool"]["evidence_summary"]["timing_metrics"]["gemini_request"] == {
  56. "total_duration_ms": 300,
  57. }
  58. def test_pattern_recall_submits_oss_archive_before_gemini_judgment(tmp_path):
  59. run_id = "run_001"
  60. policy_run_id = "policy_run_001"
  61. runtime = LocalRuntimeFileStore(tmp_path / "runtime")
  62. runtime.prepare_run(run_id)
  63. item = _item(run_id, policy_run_id, "content_pool", "d_001")
  64. media = _media(run_id, policy_run_id, item["platform_content_id"])
  65. bundle = _bundle(item)
  66. dispatcher = RecordingArchiveDispatcher()
  67. gemini = ArchiveAwareGeminiVideoClient(dispatcher)
  68. recall_decision.run(
  69. run_id,
  70. policy_run_id,
  71. [item],
  72. [media],
  73. [bundle],
  74. {"ext_data": {"evidence_pack": {"seed_terms": ["养生"]}}},
  75. runtime,
  76. gemini,
  77. archive_dispatcher=dispatcher,
  78. )
  79. assert dispatcher.submitted_content_ids == ["content_pool"]
  80. assert gemini.calls
  81. def test_pattern_recall_waits_for_oss_archive_before_gemini_judgment(tmp_path):
  82. run_id = "run_001"
  83. policy_run_id = "policy_run_001"
  84. runtime = LocalRuntimeFileStore(tmp_path / "runtime")
  85. runtime.prepare_run(run_id)
  86. item = _item(run_id, policy_run_id, "content_pool", "d_001")
  87. media = _media(run_id, policy_run_id, item["platform_content_id"])
  88. bundle = _bundle(item)
  89. dispatcher = SynchronousArchiveDispatcher()
  90. gemini = OssAwareGeminiVideoClient()
  91. result = recall_decision.run(
  92. run_id,
  93. policy_run_id,
  94. [item],
  95. [media],
  96. [bundle],
  97. {"ext_data": {"evidence_pack": {"seed_terms": ["养生"]}}},
  98. runtime,
  99. gemini,
  100. archive_dispatcher=dispatcher,
  101. )
  102. assert dispatcher.archived_content_ids == ["content_pool"]
  103. assert gemini.calls[0]["media"]["oss_url"] == "https://res.example/content_pool.mp4"
  104. [row] = result["content_media_records"]
  105. assert row["content_media_status"] == "oss_uploaded"
  106. assert row["oss_url"] == "https://res.example/content_pool.mp4"
  107. assert row["raw_payload"]["gemini_timing_metrics"]["video_fetch"]["gemini_video_source"] == "oss_url"
  108. def test_pattern_recall_falls_back_to_play_url_when_oss_archive_is_pending(tmp_path):
  109. run_id = "run_001"
  110. policy_run_id = "policy_run_001"
  111. runtime = LocalRuntimeFileStore(tmp_path / "runtime")
  112. runtime.prepare_run(run_id)
  113. item = _item(run_id, policy_run_id, "content_pool", "d_001")
  114. media = _media(run_id, policy_run_id, item["platform_content_id"])
  115. bundle = _bundle(item)
  116. dispatcher = PendingArchiveDispatcher()
  117. gemini = PlayUrlAwareGeminiVideoClient()
  118. result = recall_decision.run(
  119. run_id,
  120. policy_run_id,
  121. [item],
  122. [media],
  123. [bundle],
  124. {"ext_data": {"evidence_pack": {"seed_terms": ["养生"]}}},
  125. runtime,
  126. gemini,
  127. archive_dispatcher=dispatcher,
  128. )
  129. assert gemini.calls[0]["media"]["oss_url"] is None
  130. assert gemini.calls[0]["media"]["play_url"] == "https://source.example/content_pool.mp4"
  131. [row] = result["content_media_records"]
  132. assert row["content_media_status"] == "oss_upload_pending"
  133. assert row["raw_payload"]["oss_archive_last_error"] == "oss_upload_http_error"
  134. assert row["raw_payload"]["gemini_timing_metrics"]["video_fetch"]["gemini_video_source"] == "play_url"
  135. class RecordingArchiveDispatcher:
  136. def __init__(self) -> None:
  137. self.submitted_content_ids: list[str] = []
  138. def submit_records(self, records: list[dict]) -> None:
  139. self.submitted_content_ids.extend(row["platform_content_id"] for row in records)
  140. class SynchronousArchiveDispatcher:
  141. def __init__(self) -> None:
  142. self.archived_content_ids: list[str] = []
  143. def archive_records(self, records: list[dict]) -> list[dict]:
  144. archived = []
  145. for row in records:
  146. content_id = row["platform_content_id"]
  147. self.archived_content_ids.append(content_id)
  148. raw_payload = {
  149. **(row.get("raw_payload") or {}),
  150. "content_media_status": "oss_uploaded",
  151. "oss_url": f"https://res.example/{content_id}.mp4",
  152. "oss_archive_status": "uploaded",
  153. "oss_object_key": f"crawler/video/{content_id}.mp4",
  154. }
  155. archived.append(
  156. {
  157. **row,
  158. "content_media_status": "oss_uploaded",
  159. "oss_url": f"https://res.example/{content_id}.mp4",
  160. "raw_payload": raw_payload,
  161. }
  162. )
  163. return archived
  164. class PendingArchiveDispatcher:
  165. def archive_records(self, records: list[dict]) -> list[dict]:
  166. archived = []
  167. for row in records:
  168. raw_payload = {
  169. **(row.get("raw_payload") or {}),
  170. "content_media_status": "oss_upload_pending",
  171. "oss_archive_status": "pending",
  172. "oss_archive_last_error": "oss_upload_http_error",
  173. "oss_archive_last_exception_type": "HTTPStatusError",
  174. }
  175. archived.append(
  176. {
  177. **row,
  178. "content_media_status": "oss_upload_pending",
  179. "oss_url": None,
  180. "raw_payload": raw_payload,
  181. }
  182. )
  183. return archived
  184. class ArchiveAwareGeminiVideoClient(FakeGeminiVideoClient):
  185. def __init__(self, dispatcher: RecordingArchiveDispatcher) -> None:
  186. super().__init__(default_result=fake_gemini_pool())
  187. self.dispatcher = dispatcher
  188. def analyze(self, content: dict, media: dict, source_context: dict) -> dict:
  189. assert self.dispatcher.submitted_content_ids == [content["platform_content_id"]]
  190. return super().analyze(content, media, source_context)
  191. class PlayUrlAwareGeminiVideoClient(FakeGeminiVideoClient):
  192. def __init__(self) -> None:
  193. result = fake_gemini_pool()
  194. result["timing_metrics"] = {
  195. "video_fetch": {"gemini_video_source": "play_url"},
  196. }
  197. super().__init__(default_result=result)
  198. def analyze(self, content: dict, media: dict, source_context: dict) -> dict:
  199. assert media.get("oss_url") is None
  200. assert media["play_url"].startswith("https://source.example/")
  201. return super().analyze(content, media, source_context)
  202. class OssAwareGeminiVideoClient(FakeGeminiVideoClient):
  203. def __init__(self) -> None:
  204. result = fake_gemini_pool()
  205. result["timing_metrics"] = {
  206. "video_fetch": {"gemini_video_source": "oss_url"},
  207. }
  208. super().__init__(default_result=result)
  209. def analyze(self, content: dict, media: dict, source_context: dict) -> dict:
  210. assert media["oss_url"] == f"https://res.example/{content['platform_content_id']}.mp4"
  211. assert media["content_media_status"] == "oss_uploaded"
  212. return super().analyze(content, media, source_context)
  213. def _item(run_id: str, policy_run_id: str, content_id: str, discovery_id: str) -> dict:
  214. return {
  215. "record_schema_version": "runtime_record.v1",
  216. "run_id": run_id,
  217. "policy_run_id": policy_run_id,
  218. "content_discovery_id": discovery_id,
  219. "search_query_id": "q_001",
  220. "platform": "douyin",
  221. "platform_content_id": content_id,
  222. "platform_content_format": "video",
  223. "description": "stub",
  224. "platform_author_id": "author_001",
  225. "author_display_name": "author",
  226. "statistics": {"digg_count": 100, "comment_count": 10, "share_count": 5},
  227. "tags": [],
  228. "discovery_start_source": "pattern_itemset",
  229. "previous_discovery_step": "query_direct",
  230. "raw_payload": {"platform_content_id": content_id},
  231. }
  232. def _media(run_id: str, policy_run_id: str, content_id: str) -> dict:
  233. return {
  234. "record_schema_version": "runtime_record.v1",
  235. "run_id": run_id,
  236. "policy_run_id": policy_run_id,
  237. "platform": "douyin",
  238. "platform_content_id": content_id,
  239. "content_media_status": "metadata_only",
  240. "content_metadata_source": "douyin_keyword_search",
  241. "play_url": f"https://source.example/{content_id}.mp4",
  242. "local_path": None,
  243. "oss_url": None,
  244. "raw_payload": {"platform_content_id": content_id},
  245. }
  246. def _bundle(item: dict) -> dict:
  247. return {
  248. "source_evidence": {"discovered_platform_content_id": item["platform_content_id"]},
  249. "content": {
  250. "decision_target_type": "content",
  251. "decision_target_id": item["platform_content_id"],
  252. "content_discovery_id": item["content_discovery_id"],
  253. "search_query_id": item["search_query_id"],
  254. "platform": item["platform"],
  255. "platform_content_id": item["platform_content_id"],
  256. "platform_content_format": "video",
  257. "description": item["description"],
  258. },
  259. "pattern_match_result": {},
  260. "content_engagement_metrics": {},
  261. "run_context": {
  262. "decision_input_snapshot_id": f"evidence_bundle:{item['platform_content_id']}",
  263. },
  264. }