|
|
@@ -1,21 +1,19 @@
|
|
|
+"""内容判定(V3-M2C):Gemini 直读视频,产出判定字段写进 pattern_match_result。
|
|
|
+
|
|
|
+替换原 decode 异步解构 + 分类树匹配。每条内容调一次 gemini_video_client.analyze,
|
|
|
+把 4 个判定字段(fit_senior_50plus / fit_confidence / relevance_score / reason)写进
|
|
|
+discovered item 的 pattern_match_result,并镜像 fit_senior_50plus 进 content_audience_profile。
|
|
|
+pattern_recall / category_or_element_binding 设为 "matched" 桥接键,仅为 M2→M3 过渡
|
|
|
+(让未重写的旧 hard_gate 不误拒);M3 删除旧门槛后移除这两个桥接键。
|
|
|
+"""
|
|
|
+
|
|
|
from __future__ import annotations
|
|
|
|
|
|
-import time
|
|
|
from datetime import datetime, timezone
|
|
|
-from typing import Any, Callable
|
|
|
+from typing import Any
|
|
|
|
|
|
-from content_agent.business_modules.content_discovery.pattern_recall.category_match import (
|
|
|
- match_decode_terms,
|
|
|
-)
|
|
|
-from content_agent.business_modules.content_discovery.pattern_recall.decode import (
|
|
|
- _extract_data_content,
|
|
|
- decode_content,
|
|
|
- extract_decode_elements,
|
|
|
- normalize_decode_status,
|
|
|
-)
|
|
|
from content_agent.constants import RUNTIME_RECORD_SCHEMA_VERSION
|
|
|
-from content_agent.integrations.decode_api import redact_sensitive_payload
|
|
|
-from content_agent.interfaces import CategoryMatchClient, DecodeClient, RuntimeFileStore
|
|
|
+from content_agent.interfaces import GeminiVideoClient, RuntimeFileStore
|
|
|
|
|
|
|
|
|
def run(
|
|
|
@@ -25,81 +23,29 @@ def run(
|
|
|
content_media_records: list[dict[str, Any]],
|
|
|
evidence_bundles: list[dict[str, Any]],
|
|
|
source_context: dict[str, Any],
|
|
|
- pattern_seed_pack: dict[str, Any],
|
|
|
runtime: RuntimeFileStore,
|
|
|
- decode_client: DecodeClient,
|
|
|
- category_match_client: CategoryMatchClient,
|
|
|
- max_wait_seconds: float = 1200.0,
|
|
|
- poll_interval_seconds: float = 5.0,
|
|
|
- now_fn: Callable[[], float] | None = None,
|
|
|
- sleep_fn: Callable[[float], None] | None = None,
|
|
|
+ gemini_video_client: GeminiVideoClient,
|
|
|
start_index: int = 1,
|
|
|
- event_sink: Callable[[dict[str, Any]], None] | None = None,
|
|
|
) -> dict[str, Any]:
|
|
|
created_at = datetime.now(timezone.utc).isoformat()
|
|
|
+ media_by_content_id = {
|
|
|
+ row["platform_content_id"]: row for row in content_media_records
|
|
|
+ }
|
|
|
evidence_rows: list[dict[str, Any]] = []
|
|
|
updated_items: list[dict[str, Any]] = []
|
|
|
updated_bundles: list[dict[str, Any]] = []
|
|
|
-
|
|
|
- media_by_content_id = {
|
|
|
- row["platform_content_id"]: row
|
|
|
- for row in content_media_records
|
|
|
- }
|
|
|
- decisions: list[dict[str, Any]] = []
|
|
|
- for index, item in enumerate(discovered_content_items, start=start_index):
|
|
|
+ for offset, item in enumerate(discovered_content_items):
|
|
|
media = media_by_content_id.get(item["platform_content_id"], {})
|
|
|
- decisions.append(
|
|
|
- _recall_one(
|
|
|
- index=index,
|
|
|
- content=item,
|
|
|
- media=media,
|
|
|
- source_context=source_context,
|
|
|
- pattern_seed_pack=pattern_seed_pack,
|
|
|
- decode_client=decode_client,
|
|
|
- category_match_client=category_match_client,
|
|
|
- max_wait_seconds=max_wait_seconds,
|
|
|
- poll_interval_seconds=poll_interval_seconds,
|
|
|
- now_fn=now_fn,
|
|
|
- sleep_fn=sleep_fn,
|
|
|
- event_sink=event_sink,
|
|
|
- )
|
|
|
- )
|
|
|
-
|
|
|
- # M6B 最小补跑(06 已拍板): 主循环结束后对仍 pending/running 的 decode 同步补跑一轮,
|
|
|
- # 每条只再调一次 get_decode_result,不重新 submit、不新增等待循环、不起后台线程。
|
|
|
- for offset, decision in enumerate(decisions):
|
|
|
- decode_result = decision["decode_result"]
|
|
|
- if decode_result.get("decode_status") not in {"pending", "running"}:
|
|
|
- continue
|
|
|
- if not decode_result.get("decode_task_id"):
|
|
|
- continue
|
|
|
- decisions[offset] = _rerun_pending_decode(
|
|
|
- content=discovered_content_items[offset],
|
|
|
- decision=decision,
|
|
|
- decode_client=decode_client,
|
|
|
- category_match_client=category_match_client,
|
|
|
- source_context=source_context,
|
|
|
- pattern_seed_pack=pattern_seed_pack,
|
|
|
- now_fn=now_fn,
|
|
|
- event_sink=event_sink,
|
|
|
+ recall_evidence_id = f"recall_{start_index + offset:03d}"
|
|
|
+ judgment = gemini_video_client.analyze(item, media, source_context)
|
|
|
+ pattern_match_result = _build_pattern_match_result(judgment, recall_evidence_id)
|
|
|
+ updated_items.append(_update_discovered_item(item, pattern_match_result))
|
|
|
+ updated_bundles.append(
|
|
|
+ _update_evidence_bundle(evidence_bundles[offset], pattern_match_result)
|
|
|
)
|
|
|
-
|
|
|
- for offset, item in enumerate(discovered_content_items):
|
|
|
- bundle = evidence_bundles[offset]
|
|
|
- decision = decisions[offset]
|
|
|
- pattern_match_result = decision["pattern_match_result"]
|
|
|
- evidence_row = _build_evidence_row(
|
|
|
- run_id=run_id,
|
|
|
- policy_run_id=policy_run_id,
|
|
|
- content=item,
|
|
|
- decision=decision,
|
|
|
- created_at=created_at,
|
|
|
+ evidence_rows.append(
|
|
|
+ _build_evidence_row(run_id, policy_run_id, item, recall_evidence_id, judgment, created_at)
|
|
|
)
|
|
|
- updated_item = _update_discovered_item(item, pattern_match_result)
|
|
|
- updated_bundle = _update_evidence_bundle(bundle, pattern_match_result)
|
|
|
- evidence_rows.append(evidence_row)
|
|
|
- updated_items.append(updated_item)
|
|
|
- updated_bundles.append(updated_bundle)
|
|
|
|
|
|
runtime.append_jsonl(run_id, "pattern_recall_evidence.jsonl", evidence_rows)
|
|
|
runtime.append_jsonl(run_id, "discovered_content_items.jsonl", updated_items)
|
|
|
@@ -110,268 +56,17 @@ def run(
|
|
|
}
|
|
|
|
|
|
|
|
|
-def _recall_one(
|
|
|
- *,
|
|
|
- index: int,
|
|
|
- content: dict[str, Any],
|
|
|
- media: dict[str, Any],
|
|
|
- source_context: dict[str, Any],
|
|
|
- pattern_seed_pack: dict[str, Any],
|
|
|
- decode_client: DecodeClient,
|
|
|
- category_match_client: CategoryMatchClient,
|
|
|
- max_wait_seconds: float,
|
|
|
- poll_interval_seconds: float,
|
|
|
- now_fn: Callable[[], float] | None,
|
|
|
- sleep_fn: Callable[[float], None] | None,
|
|
|
- event_sink: Callable[[dict[str, Any]], None] | None = None,
|
|
|
-) -> dict[str, Any]:
|
|
|
- recall_evidence_id = f"recall_{index:03d}"
|
|
|
- decode_result = decode_content(
|
|
|
- content=content,
|
|
|
- media=media,
|
|
|
- source_context=source_context,
|
|
|
- decode_client=decode_client,
|
|
|
- max_wait_seconds=max_wait_seconds,
|
|
|
- poll_interval_seconds=poll_interval_seconds,
|
|
|
- now_fn=now_fn,
|
|
|
- sleep_fn=sleep_fn,
|
|
|
- event_sink=event_sink,
|
|
|
- )
|
|
|
- decode_status = decode_result["decode_status"]
|
|
|
- if decode_status in {"pending", "running"}:
|
|
|
- return _decision(
|
|
|
- recall_evidence_id=recall_evidence_id,
|
|
|
- decode_result=decode_result,
|
|
|
- recall_status="pending",
|
|
|
- pattern_recall="pattern_recall_pending",
|
|
|
- category_or_element_binding="pattern_recall_pending",
|
|
|
- match_result={},
|
|
|
- )
|
|
|
- if decode_status == "failed":
|
|
|
- is_client_failure = decode_result.get("failure_reason") == "decode_client_error"
|
|
|
- return _decision(
|
|
|
- recall_evidence_id=recall_evidence_id,
|
|
|
- decode_result=decode_result,
|
|
|
- recall_status="failed" if is_client_failure else "rejected",
|
|
|
- pattern_recall="pattern_recall_failed" if is_client_failure else "pattern_recall_rejected",
|
|
|
- category_or_element_binding=(
|
|
|
- "pattern_recall_failed" if is_client_failure else "pattern_recall_rejected"
|
|
|
- ),
|
|
|
- match_result={},
|
|
|
- )
|
|
|
-
|
|
|
- return _match_and_decide(
|
|
|
- recall_evidence_id=recall_evidence_id,
|
|
|
- decode_result=decode_result,
|
|
|
- category_match_client=category_match_client,
|
|
|
- source_context=source_context,
|
|
|
- pattern_seed_pack=pattern_seed_pack,
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-def _match_and_decide(
|
|
|
- *,
|
|
|
- recall_evidence_id: str,
|
|
|
- decode_result: dict[str, Any],
|
|
|
- category_match_client: CategoryMatchClient,
|
|
|
- source_context: dict[str, Any],
|
|
|
- pattern_seed_pack: dict[str, Any],
|
|
|
-) -> dict[str, Any]:
|
|
|
- try:
|
|
|
- match_result = match_decode_terms(
|
|
|
- decode_elements=decode_result.get("decode_elements") or {},
|
|
|
- category_match_client=category_match_client,
|
|
|
- )
|
|
|
- except Exception as exc:
|
|
|
- match_result = _match_client_failure(exc)
|
|
|
- return _decision(
|
|
|
- recall_evidence_id=recall_evidence_id,
|
|
|
- decode_result=decode_result,
|
|
|
- recall_status="failed",
|
|
|
- pattern_recall="pattern_recall_failed",
|
|
|
- category_or_element_binding="pattern_recall_failed",
|
|
|
- match_result=match_result,
|
|
|
- )
|
|
|
- matched_terms = match_result.get("matched_terms") or []
|
|
|
- matched_paths = match_result.get("matched_category_paths") or []
|
|
|
- if not matched_terms or not matched_paths:
|
|
|
- return _decision(
|
|
|
- recall_evidence_id=recall_evidence_id,
|
|
|
- decode_result=decode_result,
|
|
|
- recall_status="no_match",
|
|
|
- pattern_recall="pattern_recall_no_match",
|
|
|
- category_or_element_binding="pattern_recall_no_match",
|
|
|
- match_result=match_result,
|
|
|
- )
|
|
|
- if not _can_explain_pattern(matched_terms, matched_paths, source_context, pattern_seed_pack):
|
|
|
- return _decision(
|
|
|
- recall_evidence_id=recall_evidence_id,
|
|
|
- decode_result=decode_result,
|
|
|
- recall_status="no_match",
|
|
|
- pattern_recall="pattern_recall_no_match",
|
|
|
- category_or_element_binding="pattern_recall_no_match",
|
|
|
- match_result=match_result,
|
|
|
- )
|
|
|
- return _decision(
|
|
|
- recall_evidence_id=recall_evidence_id,
|
|
|
- decode_result=decode_result,
|
|
|
- recall_status="matched",
|
|
|
- pattern_recall="matched",
|
|
|
- category_or_element_binding="tree_walk_match",
|
|
|
- match_result=match_result,
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-def _rerun_pending_decode(
|
|
|
- *,
|
|
|
- content: dict[str, Any],
|
|
|
- decision: dict[str, Any],
|
|
|
- decode_client: DecodeClient,
|
|
|
- category_match_client: CategoryMatchClient,
|
|
|
- source_context: dict[str, Any],
|
|
|
- pattern_seed_pack: dict[str, Any],
|
|
|
- now_fn: Callable[[], float] | None,
|
|
|
- event_sink: Callable[[dict[str, Any]], None] | None,
|
|
|
-) -> dict[str, Any]:
|
|
|
- old_decode_result = decision["decode_result"]
|
|
|
- decode_task_id = str(old_decode_result["decode_task_id"])
|
|
|
- attempt = int(old_decode_result.get("decode_poll_attempts") or 1) + 1
|
|
|
- now = now_fn or time.monotonic
|
|
|
- started_monotonic = now()
|
|
|
-
|
|
|
- def _emit(event_type: str, decode_status: str, failure_reason: str | None = None) -> None:
|
|
|
- if event_sink is None:
|
|
|
- return
|
|
|
- try:
|
|
|
- event_sink(
|
|
|
- {
|
|
|
- "event_type": event_type,
|
|
|
- "platform_content_id": str(content.get("platform_content_id") or ""),
|
|
|
- "decode_task_id": decode_task_id,
|
|
|
- "decode_status": decode_status,
|
|
|
- "attempt": attempt,
|
|
|
- "wait_seconds": 0.0,
|
|
|
- "elapsed_ms": int((now() - started_monotonic) * 1000),
|
|
|
- "failure_reason": failure_reason,
|
|
|
- }
|
|
|
- )
|
|
|
- except Exception:
|
|
|
- pass
|
|
|
-
|
|
|
- try:
|
|
|
- current = decode_client.get_decode_result(decode_task_id)
|
|
|
- except Exception:
|
|
|
- _emit("decode_timeout", str(old_decode_result.get("decode_status")), "decode_timeout_20m")
|
|
|
- return decision
|
|
|
- status = normalize_decode_status(current)
|
|
|
- data_content = _extract_data_content(current)
|
|
|
- decode_elements = extract_decode_elements(data_content)
|
|
|
- # 只有真正可用的成功结果才更新 evidence;其余一律保持原 pending 决策,不改语义。
|
|
|
- if status != "success" or not isinstance(data_content, dict) or not decode_elements.get("strong_terms"):
|
|
|
- _emit("decode_timeout", status, "decode_timeout_20m")
|
|
|
- return decision
|
|
|
- _emit("decode_succeeded", "success")
|
|
|
- decode_result = {
|
|
|
- "decode_status": "success",
|
|
|
- "decode_task_id": decode_task_id,
|
|
|
- "failure_reason": None,
|
|
|
- "decode_elements": decode_elements,
|
|
|
- "raw_request": current.get("request") or old_decode_result.get("raw_request"),
|
|
|
- "raw_response": current.get("response") or old_decode_result.get("raw_response"),
|
|
|
- "decode_poll_attempts": attempt,
|
|
|
- }
|
|
|
- return _match_and_decide(
|
|
|
- recall_evidence_id=decision["recall_evidence_id"],
|
|
|
- decode_result=decode_result,
|
|
|
- category_match_client=category_match_client,
|
|
|
- source_context=source_context,
|
|
|
- pattern_seed_pack=pattern_seed_pack,
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-def _decision(
|
|
|
- *,
|
|
|
- recall_evidence_id: str,
|
|
|
- decode_result: dict[str, Any],
|
|
|
- recall_status: str,
|
|
|
- pattern_recall: str,
|
|
|
- category_or_element_binding: str,
|
|
|
- match_result: dict[str, Any],
|
|
|
-) -> dict[str, Any]:
|
|
|
- matched_paths = match_result.get("matched_category_paths") or []
|
|
|
- matched_terms = match_result.get("matched_terms") or []
|
|
|
- primary_path = matched_paths[0] if matched_paths else None
|
|
|
- pattern_match_result = {
|
|
|
- "pattern_recall": pattern_recall,
|
|
|
- "category_or_element_binding": category_or_element_binding,
|
|
|
- "decode_status": decode_result.get("decode_status"),
|
|
|
- "match_status": "matched" if recall_status == "matched" else recall_status,
|
|
|
- "recall_status": recall_status,
|
|
|
- "matched_terms": matched_terms,
|
|
|
- "matched_category_paths": matched_paths,
|
|
|
- "primary_matched_category_path": primary_path,
|
|
|
- "decode_elements": decode_result.get("decode_elements") or {},
|
|
|
- "pattern_recall_evidence_id": recall_evidence_id,
|
|
|
- }
|
|
|
- return {
|
|
|
- "recall_evidence_id": recall_evidence_id,
|
|
|
- "decode_result": decode_result,
|
|
|
- "match_result": match_result,
|
|
|
- "pattern_match_result": pattern_match_result,
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-def _build_evidence_row(
|
|
|
- *,
|
|
|
- run_id: str,
|
|
|
- policy_run_id: str,
|
|
|
- content: dict[str, Any],
|
|
|
- decision: dict[str, Any],
|
|
|
- created_at: str,
|
|
|
-) -> dict[str, Any]:
|
|
|
- decode_result = decision["decode_result"]
|
|
|
- match_result = decision["match_result"]
|
|
|
- pattern_match = decision["pattern_match_result"]
|
|
|
- raw_payload = redact_sensitive_payload(
|
|
|
- {
|
|
|
- "run_id": run_id,
|
|
|
- "policy_run_id": policy_run_id,
|
|
|
- "recall_evidence_id": decision["recall_evidence_id"],
|
|
|
- "platform": content.get("platform"),
|
|
|
- "primary_matched_category_path": pattern_match.get("primary_matched_category_path"),
|
|
|
- "pending_reason": decode_result.get("pending_reason"),
|
|
|
- "failure_reason": decode_result.get("failure_reason") or match_result.get("failure_reason"),
|
|
|
- "decode_request": decode_result.get("raw_request"),
|
|
|
- "decode_response": decode_result.get("raw_response"),
|
|
|
- "match_paths_request": match_result.get("request"),
|
|
|
- "match_paths_response": match_result.get("response"),
|
|
|
- }
|
|
|
- )
|
|
|
+def _build_pattern_match_result(judgment: dict[str, Any], recall_evidence_id: str) -> dict[str, Any]:
|
|
|
return {
|
|
|
- "record_schema_version": RUNTIME_RECORD_SCHEMA_VERSION,
|
|
|
- "run_id": run_id,
|
|
|
- "policy_run_id": policy_run_id,
|
|
|
- "recall_evidence_id": decision["recall_evidence_id"],
|
|
|
- "content_discovery_id": content.get("content_discovery_id"),
|
|
|
- "platform": content.get("platform"),
|
|
|
- "platform_content_id": content.get("platform_content_id"),
|
|
|
- "decode_status": pattern_match.get("decode_status"),
|
|
|
- "decode_task_id": decode_result.get("decode_task_id"),
|
|
|
- "recall_status": pattern_match.get("recall_status"),
|
|
|
- "matched_terms": pattern_match.get("matched_terms"),
|
|
|
- "matched_category_paths": pattern_match.get("matched_category_paths"),
|
|
|
- "decode_elements": pattern_match.get("decode_elements"),
|
|
|
- "match_paths_request": match_result.get("request"),
|
|
|
- "match_paths_response": match_result.get("response"),
|
|
|
- "evidence_summary": {
|
|
|
- "pattern_recall": pattern_match.get("pattern_recall"),
|
|
|
- "category_or_element_binding": pattern_match.get("category_or_element_binding"),
|
|
|
- "primary_matched_category_path": pattern_match.get("primary_matched_category_path"),
|
|
|
- "pending_reason": decode_result.get("pending_reason"),
|
|
|
- "failure_reason": decode_result.get("failure_reason") or match_result.get("failure_reason"),
|
|
|
- },
|
|
|
- "raw_payload": raw_payload,
|
|
|
- "created_at": created_at,
|
|
|
+ "fit_senior_50plus": bool(judgment.get("fit_senior_50plus", False)),
|
|
|
+ "fit_confidence": float(judgment.get("fit_confidence") or 0.0),
|
|
|
+ "relevance_score": float(judgment.get("relevance_score") or 0.0),
|
|
|
+ "reason": str(judgment.get("reason") or ""),
|
|
|
+ "judge_status": str(judgment.get("status") or "ok"),
|
|
|
+ # M2→M3 桥接键:让未重写的旧 hard_gate(not_in ["matched"])不误拒。M3 删旧门槛后移除。
|
|
|
+ "pattern_recall": "matched",
|
|
|
+ "category_or_element_binding": "matched",
|
|
|
+ "pattern_recall_evidence_id": recall_evidence_id,
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -379,7 +74,13 @@ def _update_discovered_item(
|
|
|
item: dict[str, Any],
|
|
|
pattern_match_result: dict[str, Any],
|
|
|
) -> dict[str, Any]:
|
|
|
- updated = {**item, "pattern_match_result": pattern_match_result}
|
|
|
+ updated = {
|
|
|
+ **item,
|
|
|
+ "pattern_match_result": pattern_match_result,
|
|
|
+ "content_audience_profile": {
|
|
|
+ "fit_senior_50plus": pattern_match_result["fit_senior_50plus"]
|
|
|
+ },
|
|
|
+ }
|
|
|
raw_payload = dict(updated.get("raw_payload") or {})
|
|
|
raw_payload["pattern_match_result"] = pattern_match_result
|
|
|
updated["raw_payload"] = raw_payload
|
|
|
@@ -394,49 +95,37 @@ def _update_evidence_bundle(
|
|
|
return {**bundle, "pattern_match_result": {**existing, **pattern_match_result}}
|
|
|
|
|
|
|
|
|
-def _can_explain_pattern(
|
|
|
- matched_terms: list[str],
|
|
|
- matched_paths: list[str],
|
|
|
- source_context: dict[str, Any],
|
|
|
- pattern_seed_pack: dict[str, Any],
|
|
|
-) -> bool:
|
|
|
- evidence_pack = source_context.get("ext_data", {}).get("evidence_pack", {})
|
|
|
- seed_terms = set(pattern_seed_pack.get("seed_terms") or evidence_pack.get("seed_terms") or [])
|
|
|
- if seed_terms.intersection(matched_terms):
|
|
|
- return True
|
|
|
- binding_paths = [
|
|
|
- binding.get("category_path")
|
|
|
- for binding in [
|
|
|
- *(pattern_seed_pack.get("category_bindings") or []),
|
|
|
- *(evidence_pack.get("category_bindings") or []),
|
|
|
- *(pattern_seed_pack.get("itemsets") or []),
|
|
|
- *(pattern_seed_pack.get("itemset_items") or []),
|
|
|
- *(evidence_pack.get("itemset_items") or []),
|
|
|
- ]
|
|
|
- if isinstance(binding, dict) and binding.get("category_path")
|
|
|
- ]
|
|
|
- for matched_path in matched_paths:
|
|
|
- for binding_path in binding_paths:
|
|
|
- if matched_path in binding_path or binding_path in matched_path:
|
|
|
- return True
|
|
|
- if _path_leaf(matched_path) == _path_leaf(binding_path):
|
|
|
- return True
|
|
|
- return False
|
|
|
-
|
|
|
-
|
|
|
-def _match_client_failure(exc: Exception) -> dict[str, Any]:
|
|
|
+def _build_evidence_row(
|
|
|
+ run_id: str,
|
|
|
+ policy_run_id: str,
|
|
|
+ content: dict[str, Any],
|
|
|
+ recall_evidence_id: str,
|
|
|
+ judgment: dict[str, Any],
|
|
|
+ created_at: str,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ summary = {
|
|
|
+ "pattern_recall": "matched",
|
|
|
+ "fit_senior_50plus": bool(judgment.get("fit_senior_50plus", False)),
|
|
|
+ "fit_confidence": float(judgment.get("fit_confidence") or 0.0),
|
|
|
+ "relevance_score": float(judgment.get("relevance_score") or 0.0),
|
|
|
+ "reason": str(judgment.get("reason") or ""),
|
|
|
+ "judge_status": str(judgment.get("status") or "ok"),
|
|
|
+ }
|
|
|
return {
|
|
|
- "request": {},
|
|
|
- "response": {
|
|
|
- "operation": "match_paths",
|
|
|
- "error_type": type(exc).__name__,
|
|
|
+ "record_schema_version": RUNTIME_RECORD_SCHEMA_VERSION,
|
|
|
+ "run_id": run_id,
|
|
|
+ "policy_run_id": policy_run_id,
|
|
|
+ "recall_evidence_id": recall_evidence_id,
|
|
|
+ "content_discovery_id": content.get("content_discovery_id"),
|
|
|
+ "platform": content.get("platform"),
|
|
|
+ "platform_content_id": content.get("platform_content_id"),
|
|
|
+ "recall_status": "judged",
|
|
|
+ "evidence_summary": summary,
|
|
|
+ "raw_payload": {
|
|
|
+ "run_id": run_id,
|
|
|
+ "policy_run_id": policy_run_id,
|
|
|
+ "recall_evidence_id": recall_evidence_id,
|
|
|
+ **summary,
|
|
|
},
|
|
|
- "matched_terms": [],
|
|
|
- "matched_category_paths": [],
|
|
|
- "path_matches": [],
|
|
|
- "failure_reason": "category_match_client_error",
|
|
|
+ "created_at": created_at,
|
|
|
}
|
|
|
-
|
|
|
-
|
|
|
-def _path_leaf(path: str) -> str:
|
|
|
- return str(path).rstrip("/").split("/")[-1]
|