| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- """Authenticated-user records for the find-agent video discovery workflow."""
- from __future__ import annotations
- import json
- from typing import Any
- from sqlalchemy import func, or_, select
- from sqlalchemy.orm import load_only
- from supply_infra.db.models.video_discovery import (
- VideoDiscoveryCandidate,
- VideoDiscoveryRun,
- VideoDiscoverySearch,
- )
- from supply_infra.db.session import get_session
- _MIN_VISIBLE_BIZ_DT = "20260730"
- def _json_value(raw: str | None) -> Any:
- if not raw:
- return None
- try:
- return json.loads(raw)
- except (TypeError, ValueError):
- return raw
- def _timestamp(value: Any) -> str | None:
- return value.isoformat() if value is not None else None
- def _number(value: Any) -> float | None:
- return float(value) if value is not None else None
- def _serialize_run(row: VideoDiscoveryRun, counts: dict[str, int]) -> dict[str, Any]:
- return {
- "id": int(row.id),
- "run_id": row.run_id,
- "biz_dt": row.biz_dt,
- "demand_grade_id": (
- int(row.demand_grade_id) if row.demand_grade_id is not None else None
- ),
- "demand_word": row.demand_word,
- "seed_video_id": row.seed_video_id,
- "seed_video_title": row.seed_video_title,
- "relevant_points": _json_value(row.relevant_points_json),
- "intent_summary": row.intent_summary,
- "status": row.status,
- "search_count": counts.get("search", int(row.search_count or 0)),
- "candidate_count": counts.get("candidate", 0),
- "primary_count": counts.get("primary", int(row.primary_count or 0)),
- "rejected_count": counts.get("rejected", 0),
- "pending_count": counts.get("pending_evaluation", 0),
- "stop_reason": row.stop_reason,
- "rule_version": row.rule_version,
- "rule_config": _json_value(row.rule_config_json),
- "create_time": _timestamp(row.create_time),
- "update_time": _timestamp(row.update_time),
- }
- def _serialize_search_candidate(row: VideoDiscoveryCandidate) -> dict[str, Any]:
- return {
- "id": int(row.id),
- "aweme_id": row.aweme_id,
- "title": row.title,
- "content_link": row.content_link,
- "author_name": row.author_name,
- "decision_bucket": row.decision_bucket,
- "publish_at": _timestamp(row.publish_at),
- "duration_seconds": _number(row.duration_seconds),
- "play_count": int(row.play_count) if row.play_count is not None else None,
- "like_count": int(row.like_count) if row.like_count is not None else None,
- "comment_count": (
- int(row.comment_count) if row.comment_count is not None else None
- ),
- "collect_count": (
- int(row.collect_count) if row.collect_count is not None else None
- ),
- "share_count": int(row.share_count) if row.share_count is not None else None,
- "relevance_score": _number(row.relevance_score),
- "elder_score": _number(row.elder_score),
- "share_score": _number(row.share_score),
- "value_score": _number(row.value_score),
- "content_50_plus_ratio": _number(row.content_50_plus_ratio),
- "content_portrait_status": row.content_portrait_status,
- "account_50_plus_ratio": _number(row.account_50_plus_ratio),
- "account_portrait_status": row.account_portrait_status,
- "portrait_conflict": bool(row.portrait_conflict),
- "temporal_status": row.temporal_status,
- "gate_status": row.gate_status,
- "reject_reason_code": row.reject_reason_code,
- }
- def _serialize_search(
- row: VideoDiscoverySearch,
- candidates: list[VideoDiscoveryCandidate],
- ) -> dict[str, Any]:
- return {
- "id": int(row.id),
- "run_id": row.run_id,
- "search_key": row.search_key,
- "keyword": row.keyword,
- "query_reason": row.query_reason,
- "source_type": row.source_type,
- "source_value": row.source_value,
- "parent_search_id": (
- int(row.parent_search_id) if row.parent_search_id is not None else None
- ),
- "provider": row.provider,
- "provider_state": _json_value(row.provider_state_json),
- "content_type": row.content_type,
- "sort_type": row.sort_type,
- "publish_time": row.publish_time,
- "cursor": row.cursor,
- "page_no": int(row.page_no),
- "results_count": int(row.results_count or 0),
- "new_candidate_count": int(row.new_candidate_count or 0),
- "has_more": bool(row.has_more),
- "next_cursor": row.next_cursor,
- "result_ids": _json_value(row.result_ids_json),
- "candidates": [_serialize_search_candidate(candidate) for candidate in candidates],
- "status": row.status,
- "error_message": row.error_message,
- "create_time": _timestamp(row.create_time),
- "update_time": _timestamp(row.update_time),
- }
- def _serialize_candidate(row: VideoDiscoveryCandidate) -> dict[str, Any]:
- return {
- "id": int(row.id),
- "run_id": row.run_id,
- "search_id": int(row.search_id) if row.search_id is not None else None,
- "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_value(row.source_keywords_json),
- "source_search_ids": _json_value(row.source_search_ids_json),
- "tags": _json_value(row.tags_json),
- "publish_at": _timestamp(row.publish_at),
- "duration_seconds": _number(row.duration_seconds),
- "play_count": int(row.play_count) if row.play_count is not None else None,
- "like_count": int(row.like_count) if row.like_count is not None else None,
- "comment_count": (
- int(row.comment_count) if row.comment_count is not None else None
- ),
- "collect_count": (
- int(row.collect_count) if row.collect_count is not None else None
- ),
- "share_count": int(row.share_count) if row.share_count is not None else None,
- "content_age_evidence": _json_value(row.content_age_evidence_json),
- "account_age_evidence": _json_value(row.account_age_evidence_json),
- "age_normalization": _json_value(row.age_normalization_json),
- "content_50_plus_ratio": _number(row.content_50_plus_ratio),
- "content_50_plus_tgi": _number(row.content_50_plus_tgi),
- "content_portrait_status": row.content_portrait_status,
- "account_50_plus_ratio": _number(row.account_50_plus_ratio),
- "account_50_plus_tgi": _number(row.account_50_plus_tgi),
- "account_portrait_status": row.account_portrait_status,
- "portrait_conflict": bool(row.portrait_conflict),
- "temporal_type": row.temporal_type,
- "temporal_status": row.temporal_status,
- "temporal_evidence": _json_value(row.temporal_evidence_json),
- "gate_status": row.gate_status,
- "gate_results": _json_value(row.gate_results_json),
- "reject_reason_code": row.reject_reason_code,
- "rule_version": row.rule_version,
- "relevance_score": _number(row.relevance_score),
- "elder_score": _number(row.elder_score),
- "share_score": _number(row.share_score),
- "value_score": _number(row.value_score),
- "decision_reason": row.decision_reason,
- "decision_bucket": row.decision_bucket,
- "aigc_crawler_plan_id": row.aigc_crawler_plan_id,
- "aigc_produce_plan_id": row.aigc_produce_plan_id,
- "aigc_publish_plan_id": row.aigc_publish_plan_id,
- "aigc_plan_label": row.aigc_plan_label,
- "create_time": _timestamp(row.create_time),
- "update_time": _timestamp(row.update_time),
- }
- def _run_counts(session: Any, run_ids: list[str]) -> dict[str, dict[str, int]]:
- counts = {
- run_id: {
- "search": 0,
- "candidate": 0,
- "primary": 0,
- "rejected": 0,
- "pending_evaluation": 0,
- }
- for run_id in run_ids
- }
- if not run_ids:
- return counts
- search_stmt = (
- select(VideoDiscoverySearch.run_id, func.count(VideoDiscoverySearch.id))
- .where(VideoDiscoverySearch.run_id.in_(run_ids))
- .group_by(VideoDiscoverySearch.run_id)
- )
- for run_id, count in session.execute(search_stmt):
- counts[str(run_id)]["search"] = int(count)
- candidate_stmt = (
- select(
- VideoDiscoveryCandidate.run_id,
- VideoDiscoveryCandidate.decision_bucket,
- func.count(VideoDiscoveryCandidate.id),
- )
- .where(VideoDiscoveryCandidate.run_id.in_(run_ids))
- .group_by(
- VideoDiscoveryCandidate.run_id,
- VideoDiscoveryCandidate.decision_bucket,
- )
- )
- for run_id, bucket, count in session.execute(candidate_stmt):
- run_counts = counts[str(run_id)]
- run_counts[str(bucket)] = int(count)
- run_counts["candidate"] = run_counts.get("candidate", 0) + int(count)
- return counts
- def list_video_discovery_runs(
- *,
- biz_dt: str | None = None,
- status: str | None = None,
- keyword: str | None = None,
- limit: int = 20,
- offset: int = 0,
- ) -> dict[str, Any]:
- """List find-agent runs with live search and candidate counts."""
- with get_session() as session:
- conditions = [VideoDiscoveryRun.biz_dt >= _MIN_VISIBLE_BIZ_DT]
- if biz_dt:
- conditions.append(VideoDiscoveryRun.biz_dt == biz_dt)
- if status:
- conditions.append(VideoDiscoveryRun.status == status)
- normalized_keyword = (keyword or "").strip().lower()
- if normalized_keyword:
- pattern = f"%{normalized_keyword}%"
- conditions.append(
- or_(
- func.lower(VideoDiscoveryRun.demand_word).like(pattern),
- func.lower(VideoDiscoveryRun.run_id).like(pattern),
- func.lower(func.coalesce(VideoDiscoveryRun.seed_video_title, "")).like(
- pattern
- ),
- )
- )
- total_stmt = select(func.count(VideoDiscoveryRun.id))
- rows_stmt = select(VideoDiscoveryRun)
- if conditions:
- total_stmt = total_stmt.where(*conditions)
- rows_stmt = rows_stmt.where(*conditions)
- rows_stmt = rows_stmt.order_by(
- VideoDiscoveryRun.create_time.desc(),
- VideoDiscoveryRun.id.desc(),
- ).limit(limit).offset(offset)
- total = int(session.scalar(total_stmt) or 0)
- rows = list(session.scalars(rows_stmt).all())
- counts = _run_counts(session, [row.run_id for row in rows])
- return {
- "items": [_serialize_run(row, counts.get(row.run_id, {})) for row in rows],
- "total": total,
- "limit": limit,
- "offset": offset,
- }
- def get_video_discovery_run(run_id: str) -> dict[str, Any] | None:
- with get_session() as session:
- row = session.scalar(
- select(VideoDiscoveryRun).where(
- VideoDiscoveryRun.run_id == run_id,
- VideoDiscoveryRun.biz_dt >= _MIN_VISIBLE_BIZ_DT,
- )
- )
- if row is None:
- return None
- counts = _run_counts(session, [run_id])
- return _serialize_run(row, counts.get(run_id, {}))
- def list_video_discovery_searches(
- run_id: str,
- *,
- keyword: str | None = None,
- limit: int = 20,
- offset: int = 0,
- ) -> dict[str, Any] | None:
- with get_session() as session:
- exists = session.scalar(
- select(VideoDiscoveryRun.id).where(
- VideoDiscoveryRun.run_id == run_id,
- VideoDiscoveryRun.biz_dt >= _MIN_VISIBLE_BIZ_DT,
- )
- )
- if exists is None:
- return None
- conditions = [VideoDiscoverySearch.run_id == run_id]
- normalized_keyword = (keyword or "").strip().lower()
- if normalized_keyword:
- pattern = f"%{normalized_keyword}%"
- conditions.append(
- or_(
- func.lower(VideoDiscoverySearch.keyword).like(pattern),
- func.lower(VideoDiscoverySearch.query_reason).like(pattern),
- func.lower(
- func.coalesce(VideoDiscoverySearch.source_value, "")
- ).like(pattern),
- )
- )
- total = int(
- session.scalar(
- select(func.count(VideoDiscoverySearch.id)).where(*conditions)
- )
- or 0
- )
- rows = list(
- session.scalars(
- select(VideoDiscoverySearch)
- .where(*conditions)
- .order_by(VideoDiscoverySearch.id)
- .limit(limit)
- .offset(offset)
- ).all()
- )
- candidates_by_search: dict[int, list[VideoDiscoveryCandidate]] = {
- int(row.id): [] for row in rows
- }
- if candidates_by_search:
- candidate_rows = session.scalars(
- select(VideoDiscoveryCandidate)
- .options(
- load_only(
- VideoDiscoveryCandidate.id,
- VideoDiscoveryCandidate.search_id,
- VideoDiscoveryCandidate.aweme_id,
- VideoDiscoveryCandidate.title,
- VideoDiscoveryCandidate.content_link,
- VideoDiscoveryCandidate.author_name,
- VideoDiscoveryCandidate.decision_bucket,
- VideoDiscoveryCandidate.publish_at,
- VideoDiscoveryCandidate.duration_seconds,
- VideoDiscoveryCandidate.play_count,
- VideoDiscoveryCandidate.like_count,
- VideoDiscoveryCandidate.comment_count,
- VideoDiscoveryCandidate.collect_count,
- VideoDiscoveryCandidate.share_count,
- VideoDiscoveryCandidate.relevance_score,
- VideoDiscoveryCandidate.elder_score,
- VideoDiscoveryCandidate.share_score,
- VideoDiscoveryCandidate.value_score,
- VideoDiscoveryCandidate.content_50_plus_ratio,
- VideoDiscoveryCandidate.content_portrait_status,
- VideoDiscoveryCandidate.account_50_plus_ratio,
- VideoDiscoveryCandidate.account_portrait_status,
- VideoDiscoveryCandidate.portrait_conflict,
- VideoDiscoveryCandidate.temporal_status,
- VideoDiscoveryCandidate.gate_status,
- VideoDiscoveryCandidate.reject_reason_code,
- )
- )
- .where(VideoDiscoveryCandidate.search_id.in_(candidates_by_search))
- .order_by(
- VideoDiscoveryCandidate.search_id,
- VideoDiscoveryCandidate.id,
- )
- ).all()
- for candidate in candidate_rows:
- if candidate.search_id is not None:
- candidates_by_search[int(candidate.search_id)].append(candidate)
- return {
- "items": [
- _serialize_search(row, candidates_by_search.get(int(row.id), []))
- for row in rows
- ],
- "total": total,
- "limit": limit,
- "offset": offset,
- }
- def list_video_discovery_candidates(
- run_id: str,
- *,
- bucket: str | None = None,
- keyword: str | None = None,
- limit: int = 20,
- offset: int = 0,
- ) -> dict[str, Any] | None:
- with get_session() as session:
- exists = session.scalar(
- select(VideoDiscoveryRun.id).where(
- VideoDiscoveryRun.run_id == run_id,
- VideoDiscoveryRun.biz_dt >= _MIN_VISIBLE_BIZ_DT,
- )
- )
- if exists is None:
- return None
- conditions = [VideoDiscoveryCandidate.run_id == run_id]
- if bucket:
- conditions.append(VideoDiscoveryCandidate.decision_bucket == bucket)
- normalized_keyword = (keyword or "").strip().lower()
- if normalized_keyword:
- pattern = f"%{normalized_keyword}%"
- conditions.append(
- or_(
- func.lower(VideoDiscoveryCandidate.aweme_id).like(pattern),
- func.lower(
- func.coalesce(VideoDiscoveryCandidate.title, "")
- ).like(pattern),
- func.lower(
- func.coalesce(VideoDiscoveryCandidate.author_name, "")
- ).like(pattern),
- )
- )
- total = int(
- session.scalar(
- select(func.count(VideoDiscoveryCandidate.id)).where(*conditions)
- )
- or 0
- )
- rows = list(
- session.scalars(
- select(VideoDiscoveryCandidate)
- .where(*conditions)
- .order_by(
- VideoDiscoveryCandidate.value_score.desc(),
- VideoDiscoveryCandidate.id,
- )
- .limit(limit)
- .offset(offset)
- ).all()
- )
- return {
- "items": [_serialize_candidate(row) for row in rows],
- "total": total,
- "limit": limit,
- "offset": offset,
- }
|