|
|
@@ -3,9 +3,11 @@
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import json
|
|
|
+from datetime import datetime, timedelta
|
|
|
from typing import Any
|
|
|
+from zoneinfo import ZoneInfo
|
|
|
|
|
|
-from sqlalchemy import func, or_, select
|
|
|
+from sqlalchemy import func, or_, select, update
|
|
|
|
|
|
from find_agent_v2.demand_context import prepare_latest_v2_demand_run_by_name
|
|
|
from find_agent_v2.models import (
|
|
|
@@ -16,10 +18,33 @@ from find_agent_v2.models import (
|
|
|
FindAgentV2Search,
|
|
|
)
|
|
|
from find_agent_v2.runner import run_prepared_find_agent_v2
|
|
|
-from find_agent_v2.service import get_find_agent_v2_service
|
|
|
+from find_agent_v2.service import (
|
|
|
+ RUN_TIMEOUT_MINUTES,
|
|
|
+ RUN_TIMEOUT_REASON,
|
|
|
+ get_find_agent_v2_service,
|
|
|
+)
|
|
|
from supply_infra.db.session import get_session
|
|
|
|
|
|
|
|
|
+def _expire_overdue_runs(session, *, now: datetime | None = None) -> int:
|
|
|
+ current = now or datetime.now(ZoneInfo("Asia/Shanghai")).replace(tzinfo=None)
|
|
|
+ cutoff = current - timedelta(minutes=RUN_TIMEOUT_MINUTES)
|
|
|
+ result = session.execute(
|
|
|
+ update(FindAgentV2Run)
|
|
|
+ .where(
|
|
|
+ FindAgentV2Run.status == "running",
|
|
|
+ FindAgentV2Run.create_time < cutoff,
|
|
|
+ )
|
|
|
+ .values(
|
|
|
+ status="failed",
|
|
|
+ outcome_status="failed",
|
|
|
+ stop_reason=RUN_TIMEOUT_REASON,
|
|
|
+ update_time=current,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return int(result.rowcount or 0)
|
|
|
+
|
|
|
+
|
|
|
def _json(raw: str | None, default: Any) -> Any:
|
|
|
try:
|
|
|
return json.loads(raw) if raw else default
|
|
|
@@ -62,11 +87,69 @@ def _run(row: FindAgentV2Run) -> dict[str, Any]:
|
|
|
}
|
|
|
|
|
|
|
|
|
+def _evidence_item(row: FindAgentV2Evidence) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "id": int(row.id), "candidate_id": row.candidate_id,
|
|
|
+ "evidence_type": row.evidence_type, "provider": row.provider,
|
|
|
+ "status": row.status, "raw": _json(row.raw_json, {}),
|
|
|
+ "normalized": _json(row.normalized_json, {}),
|
|
|
+ "error_message": row.error_message, "create_time": _time(row.create_time),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _candidate_item(
|
|
|
+ row: FindAgentV2Candidate, evidence: list[dict[str, Any]] | None = None,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "id": int(row.id), "first_search_id": row.first_search_id,
|
|
|
+ "aweme_id": row.aweme_id, "title": row.title, "content_link": row.content_link,
|
|
|
+ "author_name": row.author_name, "author_sec_uid": row.author_sec_uid,
|
|
|
+ "source_keywords": _json(row.source_keywords_json, []), "tags": _json(row.tags_json, []),
|
|
|
+ "publish_at": _time(row.publish_at), "duration_seconds": _number(row.duration_seconds),
|
|
|
+ "play_count": row.play_count, "like_count": row.like_count,
|
|
|
+ "comment_count": row.comment_count, "collect_count": row.collect_count,
|
|
|
+ "share_count": row.share_count, "detail": _json(row.detail_json, {}),
|
|
|
+ "portrait": _json(row.portrait_json, {}), "detail_status": row.detail_status,
|
|
|
+ "portrait_status": row.portrait_status,
|
|
|
+ "content_50_plus_ratio": _number(row.content_50_plus_ratio),
|
|
|
+ "account_50_plus_ratio": _number(row.account_50_plus_ratio),
|
|
|
+ "relevance_score": _number(row.relevance_score), "elder_score": _number(row.elder_score),
|
|
|
+ "share_score": _number(row.share_score), "value_score": _number(row.value_score),
|
|
|
+ "gate_status": row.gate_status, "gate_result": _json(row.gate_result_json, {}),
|
|
|
+ "decision_bucket": row.decision_bucket, "decision_reason": row.decision_reason,
|
|
|
+ "reject_reason_code": row.reject_reason_code, "evidence": evidence or [],
|
|
|
+ "create_time": _time(row.create_time), "update_time": _time(row.update_time),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _candidate_rows_with_evidence(session, query) -> list[dict[str, Any]]:
|
|
|
+ rows = list(session.scalars(query))
|
|
|
+ ids = [int(row.id) for row in rows]
|
|
|
+ evidence_rows = list(session.scalars(
|
|
|
+ select(FindAgentV2Evidence).where(FindAgentV2Evidence.candidate_id.in_(ids))
|
|
|
+ .order_by(FindAgentV2Evidence.id)
|
|
|
+ )) if ids else []
|
|
|
+ evidence_by_candidate: dict[int, list[dict[str, Any]]] = {}
|
|
|
+ for row in evidence_rows:
|
|
|
+ if row.candidate_id is not None:
|
|
|
+ evidence_by_candidate.setdefault(int(row.candidate_id), []).append(
|
|
|
+ _evidence_item(row)
|
|
|
+ )
|
|
|
+ return [
|
|
|
+ _candidate_item(row, evidence_by_candidate.get(int(row.id), []))
|
|
|
+ for row in rows
|
|
|
+ ]
|
|
|
+
|
|
|
+
|
|
|
def list_runs(
|
|
|
*, status: str | None = None, keyword: str | None = None,
|
|
|
- limit: int = 30, offset: int = 0,
|
|
|
+ page: int = 1, page_size: int = 30,
|
|
|
) -> dict[str, Any]:
|
|
|
+ normalized_page = max(1, int(page))
|
|
|
+ normalized_page_size = min(100, max(1, int(page_size)))
|
|
|
+ offset = (normalized_page - 1) * normalized_page_size
|
|
|
with get_session() as session:
|
|
|
+ _expire_overdue_runs(session)
|
|
|
conditions = []
|
|
|
if status:
|
|
|
conditions.append(FindAgentV2Run.status == status)
|
|
|
@@ -84,13 +167,23 @@ def list_runs(
|
|
|
rows = list(session.scalars(
|
|
|
select(FindAgentV2Run).where(*conditions)
|
|
|
.order_by(FindAgentV2Run.create_time.desc(), FindAgentV2Run.id.desc())
|
|
|
- .limit(limit).offset(offset)
|
|
|
+ .limit(normalized_page_size).offset(offset)
|
|
|
))
|
|
|
- return {"items": [_run(row) for row in rows], "total": total, "limit": limit, "offset": offset}
|
|
|
+ total_pages = max(1, (total + normalized_page_size - 1) // normalized_page_size)
|
|
|
+ return {
|
|
|
+ "items": [_run(row) for row in rows],
|
|
|
+ "total": total,
|
|
|
+ "page": normalized_page,
|
|
|
+ "page_size": normalized_page_size,
|
|
|
+ "total_pages": total_pages,
|
|
|
+ "has_previous": normalized_page > 1,
|
|
|
+ "has_next": normalized_page < total_pages,
|
|
|
+ }
|
|
|
|
|
|
|
|
|
def get_run_detail(run_id: str) -> dict[str, Any] | None:
|
|
|
with get_session() as session:
|
|
|
+ _expire_overdue_runs(session)
|
|
|
run = session.scalar(select(FindAgentV2Run).where(FindAgentV2Run.run_id == run_id))
|
|
|
if run is None:
|
|
|
return None
|
|
|
@@ -102,14 +195,9 @@ def get_run_detail(run_id: str) -> dict[str, Any] | None:
|
|
|
select(FindAgentV2Search).where(FindAgentV2Search.run_id == run_id)
|
|
|
.order_by(FindAgentV2Search.round_index, FindAgentV2Search.id)
|
|
|
))
|
|
|
- candidates = list(session.scalars(
|
|
|
- select(FindAgentV2Candidate).where(FindAgentV2Candidate.run_id == run_id)
|
|
|
- .order_by(FindAgentV2Candidate.id)
|
|
|
- ))
|
|
|
- evidence = list(session.scalars(
|
|
|
- select(FindAgentV2Evidence).where(FindAgentV2Evidence.run_id == run_id)
|
|
|
- .order_by(FindAgentV2Evidence.id)
|
|
|
- ))
|
|
|
+ candidate_summaries = list(session.execute(select(
|
|
|
+ FindAgentV2Candidate.aweme_id, FindAgentV2Candidate.decision_bucket,
|
|
|
+ ).where(FindAgentV2Candidate.run_id == run_id)))
|
|
|
round_items = [{
|
|
|
"id": int(row.id), "round_index": row.round_index, "phase": row.phase,
|
|
|
"status": row.status, "plan": _json(row.plan_json, {}),
|
|
|
@@ -123,76 +211,73 @@ def get_run_detail(run_id: str) -> dict[str, Any] | None:
|
|
|
"query_reason": row.query_reason, "source_type": row.source_type,
|
|
|
"provider": row.provider, "cursor": row.cursor, "page_no": row.page_no,
|
|
|
"has_more": bool(row.has_more), "next_cursor": row.next_cursor,
|
|
|
- "provider_state": _json(row.provider_state_json, {}),
|
|
|
"result_count": row.result_count, "status": row.status,
|
|
|
"error_message": row.error_message,
|
|
|
- "raw_response": _json(row.raw_response_json, {}),
|
|
|
"create_time": _time(row.create_time),
|
|
|
} for row in searches]
|
|
|
- evidence_by_candidate: dict[int, list[dict[str, Any]]] = {}
|
|
|
- evidence_items = []
|
|
|
- for row in evidence:
|
|
|
- item = {
|
|
|
- "id": int(row.id), "candidate_id": row.candidate_id,
|
|
|
- "evidence_type": row.evidence_type, "provider": row.provider,
|
|
|
- "status": row.status, "raw": _json(row.raw_json, {}),
|
|
|
- "normalized": _json(row.normalized_json, {}),
|
|
|
- "error_message": row.error_message, "create_time": _time(row.create_time),
|
|
|
- }
|
|
|
- evidence_items.append(item)
|
|
|
- if row.candidate_id is not None:
|
|
|
- evidence_by_candidate.setdefault(int(row.candidate_id), []).append(item)
|
|
|
- candidate_items = [{
|
|
|
- "id": int(row.id), "first_search_id": row.first_search_id,
|
|
|
- "aweme_id": row.aweme_id, "title": row.title, "content_link": row.content_link,
|
|
|
- "author_name": row.author_name, "author_sec_uid": row.author_sec_uid,
|
|
|
- "source_keywords": _json(row.source_keywords_json, []), "tags": _json(row.tags_json, []),
|
|
|
- "publish_at": _time(row.publish_at), "duration_seconds": _number(row.duration_seconds),
|
|
|
- "play_count": row.play_count, "like_count": row.like_count,
|
|
|
- "comment_count": row.comment_count, "collect_count": row.collect_count,
|
|
|
- "share_count": row.share_count, "detail": _json(row.detail_json, {}),
|
|
|
- "portrait": _json(row.portrait_json, {}), "detail_status": row.detail_status,
|
|
|
- "portrait_status": row.portrait_status,
|
|
|
- "content_50_plus_ratio": _number(row.content_50_plus_ratio),
|
|
|
- "account_50_plus_ratio": _number(row.account_50_plus_ratio),
|
|
|
- "relevance_score": _number(row.relevance_score), "elder_score": _number(row.elder_score),
|
|
|
- "share_score": _number(row.share_score), "value_score": _number(row.value_score),
|
|
|
- "gate_status": row.gate_status, "gate_result": _json(row.gate_result_json, {}),
|
|
|
- "decision_bucket": row.decision_bucket, "decision_reason": row.decision_reason,
|
|
|
- "reject_reason_code": row.reject_reason_code,
|
|
|
- "evidence": evidence_by_candidate.get(int(row.id), []),
|
|
|
- "create_time": _time(row.create_time), "update_time": _time(row.update_time),
|
|
|
- } for row in candidates]
|
|
|
- candidates_by_aweme = {item["aweme_id"]: item for item in candidate_items}
|
|
|
+ bucket_by_aweme = {
|
|
|
+ str(aweme_id): str(bucket) for aweme_id, bucket in candidate_summaries
|
|
|
+ }
|
|
|
+ search_rows_by_id = {int(row.id): row for row in searches}
|
|
|
for search in search_items:
|
|
|
- raw_results = search["raw_response"].get("search_results", [])
|
|
|
- result_ids = [
|
|
|
- str(item.get("aweme_id") or "")
|
|
|
+ row = search_rows_by_id[search["id"]]
|
|
|
+ raw_results = _json(row.raw_response_json, {}).get("search_results", [])
|
|
|
+ result_ids = list(dict.fromkeys(
|
|
|
+ str(item.get("aweme_id"))
|
|
|
for item in raw_results
|
|
|
if isinstance(item, dict) and item.get("aweme_id")
|
|
|
- ]
|
|
|
- search["result_aweme_ids"] = result_ids
|
|
|
- search["matched_candidates"] = [
|
|
|
- candidates_by_aweme[aweme_id]
|
|
|
- for aweme_id in result_ids
|
|
|
- if aweme_id in candidates_by_aweme
|
|
|
- ]
|
|
|
+ ))
|
|
|
+ buckets = [bucket_by_aweme.get(item, "pending_evaluation") for item in result_ids]
|
|
|
+ search["video_count"] = len(result_ids)
|
|
|
+ search["primary_count"] = sum(item == "primary" for item in buckets)
|
|
|
+ search["rejected_count"] = sum(item == "rejected" for item in buckets)
|
|
|
+ search["pending_count"] = (
|
|
|
+ search["video_count"] - search["primary_count"] - search["rejected_count"]
|
|
|
+ )
|
|
|
timeline = []
|
|
|
for row in round_items:
|
|
|
timeline.append({"type": "round", "time": row["create_time"], "data": row})
|
|
|
for row in search_items:
|
|
|
timeline.append({"type": "search", "time": row["create_time"], "data": row})
|
|
|
- for row in evidence_items:
|
|
|
- timeline.append({"type": "evidence", "time": row["create_time"], "data": row})
|
|
|
timeline.sort(key=lambda item: str(item.get("time") or ""))
|
|
|
return {
|
|
|
"run": {**_run(run), "input": _json(run.input_json, {}),
|
|
|
"rule_config": _json(run.rule_config_json, {})},
|
|
|
- "rounds": round_items, "searches": search_items, "candidates": candidate_items,
|
|
|
- "evidence": evidence_items, "timeline": timeline,
|
|
|
+ "rounds": round_items, "searches": search_items, "timeline": timeline,
|
|
|
}
|
|
|
|
|
|
|
|
|
+def get_run_candidates(run_id: str) -> list[dict[str, Any]] | None:
|
|
|
+ with get_session() as session:
|
|
|
+ if session.scalar(select(FindAgentV2Run.id).where(FindAgentV2Run.run_id == run_id)) is None:
|
|
|
+ return None
|
|
|
+ return _candidate_rows_with_evidence(session, select(FindAgentV2Candidate).where(
|
|
|
+ FindAgentV2Candidate.run_id == run_id,
|
|
|
+ ).order_by(FindAgentV2Candidate.id))
|
|
|
+
|
|
|
+
|
|
|
+def get_search_candidates(run_id: str, search_id: int) -> list[dict[str, Any]] | None:
|
|
|
+ with get_session() as session:
|
|
|
+ search = session.scalar(select(FindAgentV2Search).where(
|
|
|
+ FindAgentV2Search.run_id == run_id, FindAgentV2Search.id == int(search_id),
|
|
|
+ ))
|
|
|
+ if search is None:
|
|
|
+ return None
|
|
|
+ raw_results = _json(search.raw_response_json, {}).get("search_results", [])
|
|
|
+ aweme_ids = list(dict.fromkeys(
|
|
|
+ str(item.get("aweme_id")) for item in raw_results
|
|
|
+ if isinstance(item, dict) and item.get("aweme_id")
|
|
|
+ ))
|
|
|
+ if not aweme_ids:
|
|
|
+ return []
|
|
|
+ items = _candidate_rows_with_evidence(session, select(FindAgentV2Candidate).where(
|
|
|
+ FindAgentV2Candidate.run_id == run_id,
|
|
|
+ FindAgentV2Candidate.aweme_id.in_(aweme_ids),
|
|
|
+ ))
|
|
|
+ by_aweme = {item["aweme_id"]: item for item in items}
|
|
|
+ return [by_aweme[item] for item in aweme_ids if item in by_aweme]
|
|
|
+
|
|
|
+
|
|
|
def prepare_test_run(demand_word: str, current_user: dict[str, Any]) -> dict[str, Any]:
|
|
|
prepared = prepare_latest_v2_demand_run_by_name(
|
|
|
demand_word,
|