output_sync.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. """Persist the recommendation buckets stated by the model's final response."""
  2. from __future__ import annotations
  3. import json
  4. import re
  5. from typing import Any
  6. from supply_agent.types import AgentResult, Role
  7. from supply_infra.db.repositories.video_discovery_repo import (
  8. VideoDiscoveryRepository,
  9. )
  10. from supply_infra.db.session import get_session
  11. _VIDEO_ID_PATTERN = re.compile(r"(?<!\d)\d{15,22}(?!\d)")
  12. _PRIMARY_LABELS = ("主推荐", "正式推荐")
  13. _BACKUP_LABELS = ("补充推荐", "人工备选")
  14. _END_LABELS = (
  15. "淘汰",
  16. "最有竞争力",
  17. "搜索轨迹",
  18. "搜索树",
  19. "缺失数据",
  20. "局限",
  21. "总结",
  22. )
  23. def _normalized_heading(line: str) -> str:
  24. text = line.strip()
  25. text = re.sub(r"^#{1,6}\s*", "", text)
  26. text = re.sub(r"^\d+\s*[.、]\s*", "", text)
  27. return text.replace("*", "").strip()
  28. def extract_model_recommendation_ids(
  29. content: str,
  30. ) -> tuple[list[str], list[str]]:
  31. """Extract video ids from the model's primary and backup sections."""
  32. primary: list[str] = []
  33. backup: list[str] = []
  34. section: str | None = None
  35. for line in (content or "").splitlines():
  36. heading = _normalized_heading(line)
  37. if any(heading.startswith(label) for label in _PRIMARY_LABELS):
  38. section = "primary"
  39. elif any(heading.startswith(label) for label in _BACKUP_LABELS):
  40. section = "backup"
  41. elif any(heading.startswith(label) for label in _END_LABELS):
  42. section = None
  43. if section is None:
  44. continue
  45. target = primary if section == "primary" else backup
  46. for aweme_id in _VIDEO_ID_PATTERN.findall(line):
  47. if aweme_id not in target:
  48. target.append(aweme_id)
  49. primary_set = set(primary)
  50. return primary, [aweme_id for aweme_id in backup if aweme_id not in primary_set]
  51. def _latest_run_id(result: AgentResult) -> str | None:
  52. for message in reversed(result.messages):
  53. if message.role != Role.TOOL or not message.content:
  54. continue
  55. try:
  56. payload = json.loads(message.content)
  57. except (TypeError, json.JSONDecodeError):
  58. continue
  59. if not isinstance(payload, dict):
  60. continue
  61. run_id = payload.get("run_id")
  62. if run_id:
  63. return str(run_id)
  64. return None
  65. def persist_model_recommendations(result: AgentResult) -> dict[str, Any]:
  66. """
  67. Make the model's final recommendation sections authoritative in the database.
  68. The final response itself is never validated or rewritten. Video ids listed
  69. under primary/backup are persisted to those buckets exactly as stated.
  70. """
  71. run_id = _latest_run_id(result)
  72. primary_ids, backup_ids = extract_model_recommendation_ids(result.content)
  73. if not run_id or not (primary_ids or backup_ids):
  74. return {
  75. "run_id": run_id,
  76. "primary_ids": primary_ids,
  77. "backup_ids": backup_ids,
  78. "saved_count": 0,
  79. }
  80. rows = [
  81. {"aweme_id": aweme_id, "decision_bucket": "primary"}
  82. for aweme_id in primary_ids
  83. ]
  84. rows.extend(
  85. {"aweme_id": aweme_id, "decision_bucket": "backup"}
  86. for aweme_id in backup_ids
  87. )
  88. with get_session() as session:
  89. repo = VideoDiscoveryRepository(session)
  90. if repo.get_run(run_id) is None:
  91. return {
  92. "run_id": run_id,
  93. "primary_ids": primary_ids,
  94. "backup_ids": backup_ids,
  95. "saved_count": 0,
  96. }
  97. saved_count, _ = repo.save_candidate_evaluations(run_id, rows)
  98. return {
  99. "run_id": run_id,
  100. "primary_ids": primary_ids,
  101. "backup_ids": backup_ids,
  102. "saved_count": saved_count,
  103. }