| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576 |
- from __future__ import annotations
- import threading
- import time
- from content_agent.business_modules.content_discovery.pattern_recall import recall_decision
- from content_agent.business_modules.content_discovery.pattern_recall import media_pipeline
- from content_agent.integrations.runtime_files import LocalRuntimeFileStore
- from tests.gemini_helpers import FakeGeminiVideoClient, fake_gemini_fail, fake_gemini_pool, fake_gemini_review
- def test_pattern_recall_marks_all_judged_videos_for_oss_archive(tmp_path):
- run_id = "run_001"
- policy_run_id = "policy_run_001"
- runtime = LocalRuntimeFileStore(tmp_path / "runtime")
- runtime.prepare_run(run_id)
- items = [
- _item(run_id, policy_run_id, "content_pool", "d_001"),
- _item(run_id, policy_run_id, "content_review", "d_002"),
- _item(run_id, policy_run_id, "content_failed", "d_003"),
- ]
- media = [_media(run_id, policy_run_id, item["platform_content_id"]) for item in items]
- bundles = [_bundle(item) for item in items]
- pool_result = fake_gemini_pool()
- pool_result["timing_metrics"] = {
- "video_fetch": {"download_duration_ms": 100, "ffmpeg_duration_ms": 200},
- "gemini_request": {"total_duration_ms": 300},
- }
- gemini = FakeGeminiVideoClient(
- result_by_content_id={
- "content_pool": pool_result,
- "content_review": fake_gemini_review(),
- "content_failed": fake_gemini_fail("gemini_client_timeout"),
- }
- )
- result = recall_decision.run(
- run_id,
- policy_run_id,
- items,
- media,
- bundles,
- {"ext_data": {"evidence_pack": {"seed_terms": ["养生"]}}},
- runtime,
- gemini,
- )
- assert {
- row["platform_content_id"]: row["content_media_status"]
- for row in result["content_media_records"]
- } == {
- "content_pool": "oss_upload_pending",
- "content_review": "oss_upload_pending",
- "content_failed": "oss_upload_pending",
- }
- for row in result["content_media_records"]:
- assert row["raw_payload"]["oss_archive_status"] == "pending"
- media_by_id = {row["platform_content_id"]: row for row in result["content_media_records"]}
- assert media_by_id["content_pool"]["raw_payload"]["gemini_timing_metrics"]["video_fetch"] == {
- "download_duration_ms": 100,
- "ffmpeg_duration_ms": 200,
- }
- evidence_by_id = {row["platform_content_id"]: row for row in result["pattern_recall_evidence"]}
- assert evidence_by_id["content_pool"]["evidence_summary"]["timing_metrics"]["gemini_request"] == {
- "total_duration_ms": 300,
- }
- def test_pattern_recall_submits_oss_archive_before_gemini_judgment(tmp_path):
- run_id = "run_001"
- policy_run_id = "policy_run_001"
- runtime = LocalRuntimeFileStore(tmp_path / "runtime")
- runtime.prepare_run(run_id)
- item = _item(run_id, policy_run_id, "content_pool", "d_001")
- media = _media(run_id, policy_run_id, item["platform_content_id"])
- bundle = _bundle(item)
- dispatcher = RecordingArchiveDispatcher()
- gemini = ArchiveAwareGeminiVideoClient(dispatcher)
- recall_decision.run(
- run_id,
- policy_run_id,
- [item],
- [media],
- [bundle],
- {"ext_data": {"evidence_pack": {"seed_terms": ["养生"]}}},
- runtime,
- gemini,
- archive_dispatcher=dispatcher,
- )
- assert dispatcher.submitted_content_ids == ["content_pool"]
- assert gemini.calls
- def test_pattern_recall_waits_for_oss_archive_before_gemini_judgment(tmp_path):
- run_id = "run_001"
- policy_run_id = "policy_run_001"
- runtime = LocalRuntimeFileStore(tmp_path / "runtime")
- runtime.prepare_run(run_id)
- item = _item(run_id, policy_run_id, "content_pool", "d_001")
- media = _media(run_id, policy_run_id, item["platform_content_id"])
- bundle = _bundle(item)
- dispatcher = SynchronousArchiveDispatcher()
- gemini = OssAwareGeminiVideoClient()
- result = recall_decision.run(
- run_id,
- policy_run_id,
- [item],
- [media],
- [bundle],
- {"ext_data": {"evidence_pack": {"seed_terms": ["养生"]}}},
- runtime,
- gemini,
- archive_dispatcher=dispatcher,
- )
- assert dispatcher.archived_content_ids == ["content_pool"]
- assert gemini.calls[0]["media"]["oss_url"] == "https://res.example/content_pool.mp4"
- [row] = result["content_media_records"]
- assert row["content_media_status"] == "oss_uploaded"
- assert row["oss_url"] == "https://res.example/content_pool.mp4"
- assert row["raw_payload"]["gemini_timing_metrics"]["video_fetch"]["gemini_video_source"] == "oss_url"
- def test_pattern_recall_pipelines_qwen_after_each_oss_completion(tmp_path, monkeypatch):
- monkeypatch.setattr(recall_decision, "_resolve_max_workers", lambda: 4)
- run_id = "run_001"
- policy_run_id = "policy_run_001"
- runtime = LocalRuntimeFileStore(tmp_path / "runtime")
- runtime.prepare_run(run_id)
- items = [
- _item(run_id, policy_run_id, "slow", "d_001"),
- _item(run_id, policy_run_id, "fast", "d_002"),
- ]
- media = [_media(run_id, policy_run_id, item["platform_content_id"]) for item in items]
- bundles = [_bundle(item) for item in items]
- events: list[str] = []
- lock = threading.Lock()
- dispatcher = DelayedArchiveDispatcher(events, lock)
- gemini = EventGeminiVideoClient(events, lock)
- recall_decision.run(
- run_id,
- policy_run_id,
- items,
- media,
- bundles,
- {"ext_data": {"evidence_pack": {"seed_terms": ["养生"]}}},
- runtime,
- gemini,
- archive_dispatcher=dispatcher,
- )
- assert events.index("qwen:fast") < events.index("oss_end:slow")
- tasks = runtime.read_media_pipeline_tasks(run_id)
- assert {task["task_type"] for task in tasks} == {"oss_upload", "qwen_judgment"}
- assert len(tasks) == 4
- def test_qwen_retry_wait_does_not_occupy_worker(tmp_path, monkeypatch):
- monkeypatch.setattr(recall_decision, "_resolve_max_workers", lambda: 1)
- monkeypatch.setattr(media_pipeline, "_base_qwen_retry_delay", lambda judgment, attempt_count: 0.03)
- run_id = "run_001"
- policy_run_id = "policy_run_001"
- runtime = LocalRuntimeFileStore(tmp_path / "runtime")
- runtime.prepare_run(run_id)
- items = [
- _item(run_id, policy_run_id, "retry", "d_001"),
- _item(run_id, policy_run_id, "other", "d_002"),
- ]
- media = [_media(run_id, policy_run_id, item["platform_content_id"]) for item in items]
- bundles = [_bundle(item) for item in items]
- events: list[str] = []
- dispatcher = SynchronousArchiveDispatcher()
- qwen = RetryOnceQwenClient(events)
- result = recall_decision.run(
- run_id,
- policy_run_id,
- items,
- media,
- bundles,
- {"ext_data": {"evidence_pack": {"seed_terms": ["养生"]}}},
- runtime,
- qwen,
- archive_dispatcher=dispatcher,
- )
- assert events[:3] == ["qwen:retry:1", "qwen:other:1", "qwen:retry:2"]
- evidence = {row["platform_content_id"]: row for row in result["pattern_recall_evidence"]}
- assert evidence["retry"]["recall_status"] == "judged"
- tasks = [
- task
- for task in runtime.read_media_pipeline_tasks(run_id)
- if task["task_type"] == "qwen_judgment"
- ]
- retry_task = next(task for task in tasks if task["platform_content_id"] == "retry")
- assert retry_task["attempt_count"] == 2
- assert retry_task["next_retry_at"] is None
- assert retry_task["input_payload"]["qwen_retry_policy_version"] == media_pipeline.QWEN_RETRY_POLICY_VERSION
- def test_qwen_rate_limit_lowers_dynamic_worker_limit():
- now = time.monotonic()
- assert media_pipeline._dynamic_qwen_limit(5, now + 30, now) == 2
- assert media_pipeline._dynamic_qwen_limit(5, now - 1, now) == 5
- def test_pattern_recall_skips_qwen_when_oss_archive_is_pending(tmp_path):
- run_id = "run_001"
- policy_run_id = "policy_run_001"
- runtime = LocalRuntimeFileStore(tmp_path / "runtime")
- runtime.prepare_run(run_id)
- item = _item(run_id, policy_run_id, "content_pool", "d_001")
- media = _media(run_id, policy_run_id, item["platform_content_id"])
- bundle = _bundle(item)
- dispatcher = PendingArchiveDispatcher()
- gemini = PlayUrlAwareGeminiVideoClient()
- result = recall_decision.run(
- run_id,
- policy_run_id,
- [item],
- [media],
- [bundle],
- {"ext_data": {"evidence_pack": {"seed_terms": ["养生"]}}},
- runtime,
- gemini,
- archive_dispatcher=dispatcher,
- )
- assert gemini.calls == []
- [row] = result["content_media_records"]
- assert row["content_media_status"] == "oss_upload_pending"
- assert row["raw_payload"]["oss_archive_last_error"] == "oss_upload_http_error"
- [evidence] = result["pattern_recall_evidence"]
- assert evidence["recall_status"] == "failed"
- assert evidence["evidence_summary"]["failure_type"] == "oss_upload_http_error"
- def test_pattern_recall_enqueues_qwen_after_oss_fallback_candidate_succeeds(tmp_path):
- run_id = "run_001"
- policy_run_id = "policy_run_001"
- runtime = LocalRuntimeFileStore(tmp_path / "runtime")
- runtime.prepare_run(run_id)
- item = _item(run_id, policy_run_id, "content_pool", "d_001")
- media = _media(run_id, policy_run_id, item["platform_content_id"])
- media["raw_payload"]["video_url_candidates"] = [
- {
- "url": "https://source.example/content_pool.mp4",
- "host": "source.example",
- "path": "$.search.video_url_list[0].video_url",
- "source": "search",
- "candidate_index": 0,
- },
- {
- "url": "https://backup.example/content_pool.mp4",
- "host": "backup.example",
- "path": "$.detail.video_url_list[0].video_url",
- "source": "detail",
- "candidate_index": 1,
- },
- ]
- bundle = _bundle(item)
- dispatcher = FallbackUploadDispatcher()
- gemini = OssAwareGeminiVideoClient()
- result = recall_decision.run(
- run_id,
- policy_run_id,
- [item],
- [media],
- [bundle],
- {"ext_data": {"evidence_pack": {"seed_terms": ["养生"]}}},
- runtime,
- gemini,
- archive_dispatcher=dispatcher,
- )
- assert dispatcher.calls == [
- "https://source.example/content_pool.mp4",
- "https://backup.example/content_pool.mp4",
- ]
- assert gemini.calls
- [row] = result["content_media_records"]
- assert row["content_media_status"] == "oss_uploaded"
- assert row["raw_payload"]["oss_archive_selected_video_url_host"] == "backup.example"
- assert row["raw_payload"]["oss_url_attempts"][0]["failure_type"] == "oss_upload_http_error"
- tasks = runtime.read_media_pipeline_tasks(run_id)
- assert {task["task_type"] for task in tasks} == {"oss_upload", "qwen_judgment"}
- class RecordingArchiveDispatcher:
- def __init__(self) -> None:
- self.submitted_content_ids: list[str] = []
- def archive_records(self, records: list[dict]) -> list[dict]:
- self.submitted_content_ids.extend(row["platform_content_id"] for row in records)
- archived = []
- for row in records:
- content_id = row["platform_content_id"]
- raw_payload = {
- **(row.get("raw_payload") or {}),
- "content_media_status": "oss_uploaded",
- "oss_url": f"https://res.example/{content_id}.mp4",
- "oss_archive_status": "uploaded",
- }
- archived.append(
- {
- **row,
- "content_media_status": "oss_uploaded",
- "oss_url": f"https://res.example/{content_id}.mp4",
- "raw_payload": raw_payload,
- }
- )
- return archived
- class SynchronousArchiveDispatcher:
- def __init__(self) -> None:
- self.archived_content_ids: list[str] = []
- def archive_records(self, records: list[dict]) -> list[dict]:
- archived = []
- for row in records:
- content_id = row["platform_content_id"]
- self.archived_content_ids.append(content_id)
- raw_payload = {
- **(row.get("raw_payload") or {}),
- "content_media_status": "oss_uploaded",
- "oss_url": f"https://res.example/{content_id}.mp4",
- "oss_archive_status": "uploaded",
- "oss_object_key": f"crawler/video/{content_id}.mp4",
- }
- archived.append(
- {
- **row,
- "content_media_status": "oss_uploaded",
- "oss_url": f"https://res.example/{content_id}.mp4",
- "raw_payload": raw_payload,
- }
- )
- return archived
- class PendingArchiveDispatcher:
- def archive_records(self, records: list[dict]) -> list[dict]:
- archived = []
- for row in records:
- raw_payload = {
- **(row.get("raw_payload") or {}),
- "content_media_status": "oss_upload_pending",
- "oss_archive_status": "pending",
- "oss_archive_last_error": "oss_upload_http_error",
- "oss_archive_last_exception_type": "HTTPStatusError",
- }
- archived.append(
- {
- **row,
- "content_media_status": "oss_upload_pending",
- "oss_url": None,
- "raw_payload": raw_payload,
- }
- )
- return archived
- class FallbackUploadDispatcher:
- def __init__(self) -> None:
- self.calls: list[str] = []
- def upload_fn(self, src_url: str, **kwargs) -> dict:
- self.calls.append(src_url)
- if src_url.startswith("https://source.example/"):
- return {
- "status": "failed",
- "failure_type": "oss_upload_http_error",
- "exception_type": "ReadTimeout",
- "response_absent": True,
- }
- return {
- "status": "ok",
- "oss_url": "https://res.example/content_pool.mp4",
- "oss_object_key": "crawler/video/content_pool.mp4",
- }
- class DelayedArchiveDispatcher:
- def __init__(self, events: list[str], lock: threading.Lock) -> None:
- self.events = events
- self.lock = lock
- def archive_records(self, records: list[dict]) -> list[dict]:
- archived = []
- for row in records:
- content_id = row["platform_content_id"]
- with self.lock:
- self.events.append(f"oss_start:{content_id}")
- if content_id == "slow":
- time.sleep(0.08)
- else:
- time.sleep(0.01)
- with self.lock:
- self.events.append(f"oss_end:{content_id}")
- raw_payload = {
- **(row.get("raw_payload") or {}),
- "content_media_status": "oss_uploaded",
- "oss_url": f"https://res.example/{content_id}.mp4",
- "oss_archive_status": "uploaded",
- }
- archived.append(
- {
- **row,
- "content_media_status": "oss_uploaded",
- "oss_url": f"https://res.example/{content_id}.mp4",
- "raw_payload": raw_payload,
- }
- )
- return archived
- class ArchiveAwareGeminiVideoClient(FakeGeminiVideoClient):
- def __init__(self, dispatcher: RecordingArchiveDispatcher) -> None:
- super().__init__(default_result=fake_gemini_pool())
- self.dispatcher = dispatcher
- def analyze(self, content: dict, media: dict, source_context: dict) -> dict:
- assert self.dispatcher.submitted_content_ids == [content["platform_content_id"]]
- return super().analyze(content, media, source_context)
- class PlayUrlAwareGeminiVideoClient(FakeGeminiVideoClient):
- def __init__(self) -> None:
- result = fake_gemini_pool()
- result["timing_metrics"] = {
- "video_fetch": {"gemini_video_source": "play_url"},
- }
- super().__init__(default_result=result)
- def analyze(self, content: dict, media: dict, source_context: dict) -> dict:
- assert media.get("oss_url") is None
- assert media["play_url"].startswith("https://source.example/")
- return super().analyze(content, media, source_context)
- class OssAwareGeminiVideoClient(FakeGeminiVideoClient):
- def __init__(self) -> None:
- result = fake_gemini_pool()
- result["timing_metrics"] = {
- "video_fetch": {"gemini_video_source": "oss_url"},
- }
- super().__init__(default_result=result)
- def analyze(self, content: dict, media: dict, source_context: dict) -> dict:
- assert media["oss_url"] == f"https://res.example/{content['platform_content_id']}.mp4"
- assert media["content_media_status"] == "oss_uploaded"
- return super().analyze(content, media, source_context)
- class EventGeminiVideoClient(OssAwareGeminiVideoClient):
- def __init__(self, events: list[str], lock: threading.Lock) -> None:
- super().__init__()
- self.events = events
- self.lock = lock
- def analyze(self, content: dict, media: dict, source_context: dict) -> dict:
- with self.lock:
- self.events.append(f"qwen:{content['platform_content_id']}")
- return super().analyze(content, media, source_context)
- class RetryOnceQwenClient:
- def __init__(self, events: list[str]) -> None:
- self.events = events
- self.attempts_by_content_id: dict[str, int] = {}
- def analyze_once(
- self,
- content: dict,
- media: dict,
- source_context: dict,
- *,
- attempt_number: int = 1,
- previous_attempts: list[dict] | None = None,
- request_started_at=None,
- ) -> dict:
- content_id = content["platform_content_id"]
- self.attempts_by_content_id[content_id] = self.attempts_by_content_id.get(content_id, 0) + 1
- attempt = self.attempts_by_content_id[content_id]
- self.events.append(f"qwen:{content_id}:{attempt}")
- attempts = [
- *(previous_attempts or []),
- {
- "attempt": attempt_number,
- "status": "failed" if content_id == "retry" and attempt == 1 else "ok",
- "failure_type": "qwen_http_error" if content_id == "retry" and attempt == 1 else None,
- "http_status_code": 429 if content_id == "retry" and attempt == 1 else 200,
- },
- ]
- if content_id == "retry" and attempt == 1:
- result = fake_gemini_fail("qwen_http_error")
- return {
- **result,
- "http_status_code": 429,
- "qwen_retryable": True,
- "qwen_retry_after_seconds": 0.03,
- "timing_metrics": {"qwen_request": {"attempts": attempts}},
- }
- result = fake_gemini_pool()
- return {
- **result,
- "timing_metrics": {"qwen_request": {"attempts": attempts}},
- }
- def analyze(self, content: dict, media: dict, source_context: dict) -> dict:
- return self.analyze_once(content, media, source_context)
- def _item(run_id: str, policy_run_id: str, content_id: str, discovery_id: str) -> dict:
- return {
- "record_schema_version": "runtime_record.v1",
- "run_id": run_id,
- "policy_run_id": policy_run_id,
- "content_discovery_id": discovery_id,
- "search_query_id": "q_001",
- "platform": "douyin",
- "platform_content_id": content_id,
- "platform_content_format": "video",
- "description": "stub",
- "platform_author_id": "author_001",
- "author_display_name": "author",
- "statistics": {"digg_count": 100, "comment_count": 10, "share_count": 5},
- "tags": [],
- "discovery_start_source": "pattern_itemset",
- "previous_discovery_step": "query_direct",
- "raw_payload": {"platform_content_id": content_id},
- }
- def _media(run_id: str, policy_run_id: str, content_id: str) -> dict:
- return {
- "record_schema_version": "runtime_record.v1",
- "run_id": run_id,
- "policy_run_id": policy_run_id,
- "platform": "douyin",
- "platform_content_id": content_id,
- "content_media_status": "metadata_only",
- "content_metadata_source": "douyin_keyword_search",
- "play_url": f"https://source.example/{content_id}.mp4",
- "local_path": None,
- "oss_url": None,
- "raw_payload": {"platform_content_id": content_id},
- }
- def _bundle(item: dict) -> dict:
- return {
- "source_evidence": {"discovered_platform_content_id": item["platform_content_id"]},
- "content": {
- "decision_target_type": "content",
- "decision_target_id": item["platform_content_id"],
- "content_discovery_id": item["content_discovery_id"],
- "search_query_id": item["search_query_id"],
- "platform": item["platform"],
- "platform_content_id": item["platform_content_id"],
- "platform_content_format": "video",
- "description": item["description"],
- },
- "pattern_match_result": {},
- "content_engagement_metrics": {},
- "run_context": {
- "decision_input_snapshot_id": f"evidence_bundle:{item['platform_content_id']}",
- },
- }
|