|
@@ -8,16 +8,15 @@ import uuid
|
|
|
from decimal import Decimal
|
|
from decimal import Decimal
|
|
|
from typing import Any
|
|
from typing import Any
|
|
|
|
|
|
|
|
-from sqlalchemy.exc import OperationalError, ProgrammingError
|
|
|
|
|
-
|
|
|
|
|
from agents.find_agent.tools.decision_support import (
|
|
from agents.find_agent.tools.decision_support import (
|
|
|
audit_video_discovery_process,
|
|
audit_video_discovery_process,
|
|
|
)
|
|
)
|
|
|
from supply_agent.tools import tool
|
|
from supply_agent.tools import tool
|
|
|
-from supply_infra.db.repositories.video_discovery_repo import (
|
|
|
|
|
- VideoDiscoveryRepository,
|
|
|
|
|
|
|
+from supply_infra.services.video_discovery_service import (
|
|
|
|
|
+ RunNotFoundError,
|
|
|
|
|
+ format_db_error,
|
|
|
|
|
+ get_video_discovery_service,
|
|
|
)
|
|
)
|
|
|
-from supply_infra.db.session import get_session
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
@@ -52,15 +51,6 @@ def _load_json(value: str | None, default: Any) -> Any:
|
|
|
return default
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _db_error_message(error: Exception) -> str:
|
|
|
|
|
- if isinstance(error, (OperationalError, ProgrammingError)):
|
|
|
|
|
- return (
|
|
|
|
|
- f"数据库表尚未初始化或不可用: {error}。"
|
|
|
|
|
- "请先运行 `.venv/bin/python -m supply_infra.db`。"
|
|
|
|
|
- )
|
|
|
|
|
- return str(error)
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
def _clean_text(value: Any, *, max_length: int | None = None) -> str | None:
|
|
def _clean_text(value: Any, *, max_length: int | None = None) -> str | None:
|
|
|
if value is None:
|
|
if value is None:
|
|
|
return None
|
|
return None
|
|
@@ -146,71 +136,6 @@ def _candidate_from_search_result(item: dict[str, Any], keyword: str) -> dict[st
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _serialize_candidate(item: Any) -> dict[str, Any]:
|
|
|
|
|
- decision_bucket = (
|
|
|
|
|
- "pending_evaluation"
|
|
|
|
|
- if item.decision_bucket == "unreviewed"
|
|
|
|
|
- else item.decision_bucket
|
|
|
|
|
- )
|
|
|
|
|
- return {
|
|
|
|
|
- "aweme_id": item.aweme_id,
|
|
|
|
|
- "title": item.title,
|
|
|
|
|
- "content_link": item.content_link,
|
|
|
|
|
- "author_name": item.author_name,
|
|
|
|
|
- "author_sec_uid": item.author_sec_uid,
|
|
|
|
|
- "source_keywords": _load_json(item.source_keywords_json, []),
|
|
|
|
|
- "source_search_ids": _load_json(item.source_search_ids_json, []),
|
|
|
|
|
- "tags": _load_json(item.tags_json, []),
|
|
|
|
|
- "hit_points": _load_json(item.hit_points_json, []),
|
|
|
|
|
- "play_count": item.play_count,
|
|
|
|
|
- "like_count": item.like_count,
|
|
|
|
|
- "comment_count": item.comment_count,
|
|
|
|
|
- "collect_count": item.collect_count,
|
|
|
|
|
- "share_count": item.share_count,
|
|
|
|
|
- "relevance_score": (
|
|
|
|
|
- float(item.relevance_score) if item.relevance_score is not None else None
|
|
|
|
|
- ),
|
|
|
|
|
- "elder_score": float(item.elder_score) if item.elder_score is not None else None,
|
|
|
|
|
- "share_score": float(item.share_score) if item.share_score is not None else None,
|
|
|
|
|
- "value_score": float(item.value_score) if item.value_score is not None else None,
|
|
|
|
|
- "confidence": item.confidence,
|
|
|
|
|
- "decision_bucket": decision_bucket,
|
|
|
|
|
- "content_age_evidence": _load_json(item.content_age_evidence_json, {}),
|
|
|
|
|
- "account_age_evidence": _load_json(item.account_age_evidence_json, {}),
|
|
|
|
|
- "age_normalization": _load_json(item.age_normalization_json, {}),
|
|
|
|
|
- "detail_verified": bool(item.detail_verified),
|
|
|
|
|
- "content_portrait_attempted": bool(item.content_portrait_attempted),
|
|
|
|
|
- "account_portrait_attempted": bool(item.account_portrait_attempted),
|
|
|
|
|
- "age_portraits_normalized": bool(item.age_portraits_normalized),
|
|
|
|
|
- "expansion_worthy_tags": _load_json(
|
|
|
|
|
- item.expansion_worthy_tags_json, []
|
|
|
|
|
- ),
|
|
|
|
|
- "relevance_reason": item.relevance_reason,
|
|
|
|
|
- "elder_reason": item.elder_reason,
|
|
|
|
|
- "share_reason": item.share_reason,
|
|
|
|
|
- "decision_reason": item.decision_reason,
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def _serialize_search(item: Any) -> dict[str, Any]:
|
|
|
|
|
- return {
|
|
|
|
|
- "search_id": int(item.id),
|
|
|
|
|
- "keyword": item.keyword,
|
|
|
|
|
- "query_reason": item.query_reason,
|
|
|
|
|
- "source_type": item.source_type,
|
|
|
|
|
- "source_value": item.source_value,
|
|
|
|
|
- "parent_search_id": item.parent_search_id,
|
|
|
|
|
- "provider": item.provider,
|
|
|
|
|
- "provider_state": _load_json(item.provider_state_json, {}),
|
|
|
|
|
- "cursor": item.cursor,
|
|
|
|
|
- "page_no": item.page_no,
|
|
|
|
|
- "results_count": item.results_count,
|
|
|
|
|
- "new_candidate_count": item.new_candidate_count,
|
|
|
|
|
- "has_more": bool(item.has_more),
|
|
|
|
|
- "next_cursor": item.next_cursor,
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
@tool
|
|
@tool
|
|
|
def create_video_discovery_run(
|
|
def create_video_discovery_run(
|
|
|
demand_word: str,
|
|
demand_word: str,
|
|
@@ -239,24 +164,24 @@ def create_video_discovery_run(
|
|
|
Returns:
|
|
Returns:
|
|
|
JSON,包含后续存储工具必须使用的 run_id。
|
|
JSON,包含后续存储工具必须使用的 run_id。
|
|
|
"""
|
|
"""
|
|
|
|
|
+ service = get_video_discovery_service()
|
|
|
cleaned_run_id = _clean_text(run_id, max_length=64)
|
|
cleaned_run_id = _clean_text(run_id, max_length=64)
|
|
|
if cleaned_run_id:
|
|
if cleaned_run_id:
|
|
|
try:
|
|
try:
|
|
|
- with get_session() as session:
|
|
|
|
|
- existing = VideoDiscoveryRepository(session).get_run(cleaned_run_id)
|
|
|
|
|
|
|
+ existing = service.lookup_run(cleaned_run_id)
|
|
|
if existing is not None:
|
|
if existing is not None:
|
|
|
return _json(
|
|
return _json(
|
|
|
{
|
|
{
|
|
|
"title": "视频发现运行已存在",
|
|
"title": "视频发现运行已存在",
|
|
|
"run_id": cleaned_run_id,
|
|
"run_id": cleaned_run_id,
|
|
|
- "status": existing.status,
|
|
|
|
|
|
|
+ "status": existing["status"],
|
|
|
"pre_created": True,
|
|
"pre_created": True,
|
|
|
"output": f"run_id={cleaned_run_id}",
|
|
"output": f"run_id={cleaned_run_id}",
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
logger.error("create_video_discovery_run lookup failed: %s", exc, exc_info=True)
|
|
logger.error("create_video_discovery_run lookup failed: %s", exc, exc_info=True)
|
|
|
- return _json({"error": _db_error_message(exc), "title": "查询视频发现运行失败"})
|
|
|
|
|
|
|
+ return _json({"error": format_db_error(exc), "title": "查询视频发现运行失败"})
|
|
|
|
|
|
|
|
demand = _clean_text(demand_word, max_length=256)
|
|
demand = _clean_text(demand_word, max_length=256)
|
|
|
if not demand:
|
|
if not demand:
|
|
@@ -274,19 +199,18 @@ def create_video_discovery_run(
|
|
|
"status": "running",
|
|
"status": "running",
|
|
|
}
|
|
}
|
|
|
try:
|
|
try:
|
|
|
- with get_session() as session:
|
|
|
|
|
- VideoDiscoveryRepository(session).create_run(values)
|
|
|
|
|
|
|
+ created = service.create_run(values)
|
|
|
return _json(
|
|
return _json(
|
|
|
{
|
|
{
|
|
|
"title": "视频发现运行已创建",
|
|
"title": "视频发现运行已创建",
|
|
|
- "run_id": new_run_id,
|
|
|
|
|
- "status": "running",
|
|
|
|
|
- "output": f"run_id={new_run_id}",
|
|
|
|
|
|
|
+ "run_id": created["run_id"],
|
|
|
|
|
+ "status": created["status"],
|
|
|
|
|
+ "output": f"run_id={created['run_id']}",
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
logger.error("create_video_discovery_run failed: %s", exc, exc_info=True)
|
|
logger.error("create_video_discovery_run failed: %s", exc, exc_info=True)
|
|
|
- return _json({"error": _db_error_message(exc), "title": "创建视频发现运行失败"})
|
|
|
|
|
|
|
+ return _json({"error": format_db_error(exc), "title": "创建视频发现运行失败"})
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool
|
|
@tool
|
|
@@ -378,32 +302,25 @@ def record_video_search_page(
|
|
|
search_values["search_key"] = _search_key(search_values)
|
|
search_values["search_key"] = _search_key(search_values)
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
- with get_session() as session:
|
|
|
|
|
- repo = VideoDiscoveryRepository(session)
|
|
|
|
|
- if repo.get_run(run_text) is None:
|
|
|
|
|
- return _input_error(f"run_id 不存在: {run_text}")
|
|
|
|
|
- search, new_count = repo.save_search_page(search_values, candidate_rows)
|
|
|
|
|
- payload = {
|
|
|
|
|
- "title": "搜索页已保存",
|
|
|
|
|
- "search_id": int(search.id),
|
|
|
|
|
- "run_id": run_text,
|
|
|
|
|
- "keyword": keyword_text,
|
|
|
|
|
- "source_type": search.source_type,
|
|
|
|
|
- "parent_search_id": search.parent_search_id,
|
|
|
|
|
- "page_no": search.page_no,
|
|
|
|
|
- "results_count": search.results_count,
|
|
|
|
|
- "new_candidate_count": new_count,
|
|
|
|
|
- "has_more": bool(search.has_more),
|
|
|
|
|
- "next_cursor": search.next_cursor,
|
|
|
|
|
- "output": (
|
|
|
|
|
- f"search_id={search.id},本页 {search.results_count} 条,"
|
|
|
|
|
- f"新增候选 {new_count} 条"
|
|
|
|
|
- ),
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ saved = get_video_discovery_service().save_search_page(
|
|
|
|
|
+ run_text,
|
|
|
|
|
+ search_values,
|
|
|
|
|
+ candidate_rows,
|
|
|
|
|
+ )
|
|
|
|
|
+ payload = {
|
|
|
|
|
+ "title": "搜索页已保存",
|
|
|
|
|
+ **saved,
|
|
|
|
|
+ "output": (
|
|
|
|
|
+ f"search_id={saved['search_id']},本页 {saved['results_count']} 条,"
|
|
|
|
|
+ f"新增候选 {saved['new_candidate_count']} 条"
|
|
|
|
|
+ ),
|
|
|
|
|
+ }
|
|
|
return _json(payload)
|
|
return _json(payload)
|
|
|
|
|
+ except RunNotFoundError as exc:
|
|
|
|
|
+ return _input_error(str(exc))
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
logger.error("record_video_search_page failed: %s", exc, exc_info=True)
|
|
logger.error("record_video_search_page failed: %s", exc, exc_info=True)
|
|
|
- return _json({"error": _db_error_message(exc), "title": "保存搜索页失败"})
|
|
|
|
|
|
|
+ return _json({"error": format_db_error(exc), "title": "保存搜索页失败"})
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_evaluation(item: dict[str, Any]) -> dict[str, Any]:
|
|
def _normalize_evaluation(item: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -513,44 +430,37 @@ def batch_save_video_candidate_evaluations(
|
|
|
errors.append(f"[{index}] {exc}")
|
|
errors.append(f"[{index}] {exc}")
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
- with get_session() as session:
|
|
|
|
|
- repo = VideoDiscoveryRepository(session)
|
|
|
|
|
- if repo.get_run(run_text) is None:
|
|
|
|
|
- return _input_error(f"run_id 不存在: {run_text}")
|
|
|
|
|
- if rows:
|
|
|
|
|
- saved, audit_relevant_changed = repo.save_candidate_evaluations(
|
|
|
|
|
- run_text, rows
|
|
|
|
|
- )
|
|
|
|
|
- else:
|
|
|
|
|
- saved, audit_relevant_changed = 0, False
|
|
|
|
|
- run = repo.finish_run(
|
|
|
|
|
- run_text,
|
|
|
|
|
- status=run_status,
|
|
|
|
|
- intent_summary=_clean_text(intent_summary),
|
|
|
|
|
- stop_reason=_clean_text(stop_reason),
|
|
|
|
|
- )
|
|
|
|
|
- payload = {
|
|
|
|
|
- "title": "候选评估已保存",
|
|
|
|
|
- "run_id": run_text,
|
|
|
|
|
- "saved_count": saved,
|
|
|
|
|
- "error_count": len(errors),
|
|
|
|
|
- "errors": errors,
|
|
|
|
|
- "input_error": bool(errors and not rows),
|
|
|
|
|
- "audit_relevant_changed": audit_relevant_changed,
|
|
|
|
|
- "status": run.status,
|
|
|
|
|
- "search_count": run.search_count,
|
|
|
|
|
- "primary_count": run.primary_count,
|
|
|
|
|
- "output": (
|
|
|
|
|
- f"保存 {saved} 条;主推荐 {run.primary_count} 条;"
|
|
|
|
|
- f"状态 {run.status}"
|
|
|
|
|
- ),
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ saved = get_video_discovery_service().save_evaluations_and_finish(
|
|
|
|
|
+ run_text,
|
|
|
|
|
+ rows,
|
|
|
|
|
+ status=run_status,
|
|
|
|
|
+ intent_summary=_clean_text(intent_summary),
|
|
|
|
|
+ stop_reason=_clean_text(stop_reason),
|
|
|
|
|
+ )
|
|
|
|
|
+ payload = {
|
|
|
|
|
+ "title": "候选评估已保存",
|
|
|
|
|
+ "run_id": run_text,
|
|
|
|
|
+ "saved_count": saved["saved_count"],
|
|
|
|
|
+ "error_count": len(errors),
|
|
|
|
|
+ "errors": errors,
|
|
|
|
|
+ "input_error": bool(errors and not rows),
|
|
|
|
|
+ "audit_relevant_changed": saved["audit_relevant_changed"],
|
|
|
|
|
+ "status": saved["status"],
|
|
|
|
|
+ "search_count": saved["search_count"],
|
|
|
|
|
+ "primary_count": saved["primary_count"],
|
|
|
|
|
+ "output": (
|
|
|
|
|
+ f"保存 {saved['saved_count']} 条;主推荐 {saved['primary_count']} 条;"
|
|
|
|
|
+ f"状态 {saved['status']}"
|
|
|
|
|
+ ),
|
|
|
|
|
+ }
|
|
|
return _json(payload)
|
|
return _json(payload)
|
|
|
|
|
+ except RunNotFoundError as exc:
|
|
|
|
|
+ return _input_error(str(exc))
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
logger.error(
|
|
logger.error(
|
|
|
"batch_save_video_candidate_evaluations failed: %s", exc, exc_info=True
|
|
"batch_save_video_candidate_evaluations failed: %s", exc, exc_info=True
|
|
|
)
|
|
)
|
|
|
- return _json({"error": _db_error_message(exc), "title": "保存候选评估失败"})
|
|
|
|
|
|
|
+ return _json({"error": format_db_error(exc), "title": "保存候选评估失败"})
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool
|
|
@tool
|
|
@@ -568,41 +478,27 @@ def query_video_discovery_state(
|
|
|
if not run_text:
|
|
if not run_text:
|
|
|
return _json({"error": "run_id 不能为空"})
|
|
return _json({"error": "run_id 不能为空"})
|
|
|
try:
|
|
try:
|
|
|
- with get_session() as session:
|
|
|
|
|
- repo = VideoDiscoveryRepository(session)
|
|
|
|
|
- run = repo.get_run(run_text)
|
|
|
|
|
- if run is None:
|
|
|
|
|
- return _json({"error": f"run_id 不存在: {run_text}"})
|
|
|
|
|
- searches = repo.list_searches(run_text)
|
|
|
|
|
- buckets = (
|
|
|
|
|
- ("primary", "rejected", "pending_evaluation", "unreviewed")
|
|
|
|
|
- if include_rejected
|
|
|
|
|
- else ("primary", "pending_evaluation", "unreviewed")
|
|
|
|
|
- )
|
|
|
|
|
- candidates = repo.list_candidates(
|
|
|
|
|
- run_text, buckets=buckets, limit=max(1, min(int(limit), 500))
|
|
|
|
|
- )
|
|
|
|
|
- payload = {
|
|
|
|
|
- "title": f"视频发现状态: {run_text}",
|
|
|
|
|
- "run": {
|
|
|
|
|
- "run_id": run.run_id,
|
|
|
|
|
- "demand_word": run.demand_word,
|
|
|
|
|
- "intent_summary": run.intent_summary,
|
|
|
|
|
- "status": run.status,
|
|
|
|
|
- "search_count": run.search_count,
|
|
|
|
|
- "primary_count": run.primary_count,
|
|
|
|
|
- "stop_reason": run.stop_reason,
|
|
|
|
|
- },
|
|
|
|
|
- "searches": [_serialize_search(item) for item in searches],
|
|
|
|
|
- "candidates": [_serialize_candidate(item) for item in candidates],
|
|
|
|
|
- "output": (
|
|
|
|
|
- f"搜索页 {run.search_count};主推荐 {run.primary_count}"
|
|
|
|
|
- ),
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ state = get_video_discovery_service().get_full_state(
|
|
|
|
|
+ run_text,
|
|
|
|
|
+ include_rejected=include_rejected,
|
|
|
|
|
+ limit=limit,
|
|
|
|
|
+ )
|
|
|
|
|
+ run = state["run"]
|
|
|
|
|
+ payload = {
|
|
|
|
|
+ "title": f"视频发现状态: {run_text}",
|
|
|
|
|
+ "run": run,
|
|
|
|
|
+ "searches": state["searches"],
|
|
|
|
|
+ "candidates": state["candidates"],
|
|
|
|
|
+ "output": (
|
|
|
|
|
+ f"搜索页 {run['search_count']};主推荐 {run['primary_count']}"
|
|
|
|
|
+ ),
|
|
|
|
|
+ }
|
|
|
return _json(payload)
|
|
return _json(payload)
|
|
|
|
|
+ except RunNotFoundError:
|
|
|
|
|
+ return _json({"error": f"run_id 不存在: {run_text}"})
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
logger.error("query_video_discovery_state failed: %s", exc, exc_info=True)
|
|
logger.error("query_video_discovery_state failed: %s", exc, exc_info=True)
|
|
|
- return _json({"error": _db_error_message(exc), "title": "查询视频发现状态失败"})
|
|
|
|
|
|
|
+ return _json({"error": format_db_error(exc), "title": "查询视频发现状态失败"})
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool
|
|
@tool
|
|
@@ -633,29 +529,12 @@ def audit_video_discovery_run(
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
- with get_session() as session:
|
|
|
|
|
- repo = VideoDiscoveryRepository(session)
|
|
|
|
|
- run = repo.get_run(run_text)
|
|
|
|
|
- if run is None:
|
|
|
|
|
- return _input_error(f"run_id 不存在: {run_text}")
|
|
|
|
|
- searches = [
|
|
|
|
|
- _serialize_search(item)
|
|
|
|
|
- for item in repo.list_searches(run_text)
|
|
|
|
|
- ]
|
|
|
|
|
- candidates = [
|
|
|
|
|
- _serialize_candidate(item)
|
|
|
|
|
- for item in repo.list_candidates(run_text, limit=500)
|
|
|
|
|
- ]
|
|
|
|
|
- persisted_run = {
|
|
|
|
|
- "status": run.status,
|
|
|
|
|
- "search_count": run.search_count,
|
|
|
|
|
- "primary_count": run.primary_count,
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ snapshot = get_video_discovery_service().get_audit_snapshot(run_text)
|
|
|
|
|
+ persisted_run = snapshot["persisted_run"]
|
|
|
result = _load_json(
|
|
result = _load_json(
|
|
|
audit_video_discovery_process(
|
|
audit_video_discovery_process(
|
|
|
- searches=searches,
|
|
|
|
|
- candidates=candidates,
|
|
|
|
|
|
|
+ searches=snapshot["searches"],
|
|
|
|
|
+ candidates=snapshot["candidates"],
|
|
|
intended_status=intended_status,
|
|
intended_status=intended_status,
|
|
|
),
|
|
),
|
|
|
{},
|
|
{},
|
|
@@ -679,6 +558,8 @@ def audit_video_discovery_run(
|
|
|
result["critical_violations"] = list(dict.fromkeys(violations))
|
|
result["critical_violations"] = list(dict.fromkeys(violations))
|
|
|
result["can_finish"] = False
|
|
result["can_finish"] = False
|
|
|
return _json(result)
|
|
return _json(result)
|
|
|
|
|
+ except RunNotFoundError as exc:
|
|
|
|
|
+ return _input_error(str(exc))
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
logger.error(
|
|
logger.error(
|
|
|
"audit_video_discovery_run failed: %s",
|
|
"audit_video_discovery_run failed: %s",
|
|
@@ -686,5 +567,5 @@ def audit_video_discovery_run(
|
|
|
exc_info=True,
|
|
exc_info=True,
|
|
|
)
|
|
)
|
|
|
return _json(
|
|
return _json(
|
|
|
- {"error": _db_error_message(exc), "title": "数据库运行审计失败"}
|
|
|
|
|
|
|
+ {"error": format_db_error(exc), "title": "数据库运行审计失败"}
|
|
|
)
|
|
)
|