query_generation.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. """Formal query-generation preview API routes."""
  2. from __future__ import annotations
  3. from typing import Any
  4. from uuid import UUID
  5. from fastapi import APIRouter, Depends, Query
  6. from acquisition.queries.builder import QueryBuildOptions, TREES, build_creation_query_batch
  7. from app.dependencies import _env_file, get_acquisition_repository
  8. from app.routes.acquisition import _model_dump
  9. from app.schemas import QuerySchema
  10. from core.config import Settings
  11. router = APIRouter(prefix="/api/query-generation", tags=["query-generation"])
  12. def _summary(generated: dict[str, Any]) -> dict[str, Any]:
  13. families = generated.get("families") or []
  14. return {
  15. "family_count": len(families),
  16. "family_keys": [family.get("key") for family in families],
  17. "query_count": sum(len(family.get("items") or []) for family in families),
  18. "kept_count": sum(
  19. 1
  20. for family in families
  21. for item in family.get("items") or []
  22. if item.get("keep", True)
  23. ),
  24. }
  25. LIGHT_ITEM_METADATA_KEYS = {
  26. "page_index",
  27. "page_rank",
  28. "source_cursor",
  29. "content_mode",
  30. "search_provider",
  31. "detail_provider",
  32. "acquisition_match_status",
  33. "matched_unique_key",
  34. "skip_reason",
  35. "unsupported_raw_type",
  36. "video_url_missing",
  37. }
  38. def _light_metadata(metadata: dict[str, Any] | None) -> dict[str, Any]:
  39. if not isinstance(metadata, dict):
  40. return {}
  41. return {key: metadata[key] for key in LIGHT_ITEM_METADATA_KEYS if key in metadata}
  42. def _normalize_light_query_detail(detail: dict[str, Any]) -> dict[str, Any]:
  43. media_by_item = {media["item_id"]: media for media in detail.get("media_assets") or []}
  44. classification_by_item = {row["item_id"]: row for row in detail.get("classifications") or []}
  45. decode_by_item = {row["item_id"]: row for row in detail.get("decode_summaries") or []}
  46. platforms: dict[str, dict[str, Any]] = {}
  47. for raw_job in detail.get("jobs") or []:
  48. job = _model_dump(raw_job)
  49. platform = job.get("platform")
  50. if not platform:
  51. continue
  52. platforms.setdefault(
  53. platform,
  54. {
  55. "platform": platform,
  56. "status": job.get("status") or "pending",
  57. "attempt_count": job.get("attempt_count"),
  58. "display_limit": job.get("display_limit"),
  59. "search_limit": job.get("search_limit"),
  60. "error_message": job.get("error_message"),
  61. "item_ids": [],
  62. "item_count": 0,
  63. },
  64. )
  65. items: list[dict[str, Any]] = []
  66. for row in detail.get("items") or []:
  67. item = _model_dump(row)
  68. item_id = item["id"]
  69. classification = classification_by_item.get(item_id)
  70. payload = {
  71. "id": item_id,
  72. "platform": item.get("platform"),
  73. "title": item.get("title"),
  74. "raw_summary": item.get("raw_summary"),
  75. "status": item.get("status"),
  76. "content_mode": item.get("content_mode"),
  77. "metadata": _light_metadata(item.get("metadata")),
  78. "classification": _model_dump(classification) if classification else None,
  79. "decode_summary": _model_dump(decode_by_item[item_id]) if item_id in decode_by_item else None,
  80. "media_assets": [],
  81. }
  82. if item_id in media_by_item:
  83. payload["media_assets"] = [_model_dump(media_by_item[item_id])]
  84. items.append(payload)
  85. platform = payload["platform"]
  86. group = platforms.setdefault(
  87. platform,
  88. {"platform": platform, "status": "done", "item_ids": [], "item_count": 0},
  89. )
  90. if group.get("status") in {None, "pending"}:
  91. group["status"] = "done"
  92. group.setdefault("item_ids", []).append(item_id)
  93. group["item_count"] = len(group["item_ids"])
  94. return {
  95. "query": QuerySchema.model_validate(detail["query"]).model_dump(mode="json"),
  96. "run": _model_dump(detail["run"]) if detail.get("run") else None,
  97. "jobs": [_model_dump(job) for job in detail.get("jobs") or []],
  98. "items": items,
  99. "platforms": platforms,
  100. }
  101. @router.get("/preview")
  102. def query_generation_preview(
  103. per: int = Query(default=0, ge=0, le=10000),
  104. batch_n: int = Query(default=0, ge=0, le=1000),
  105. ) -> dict[str, Any]:
  106. """Preview the currently active formal query families without writing DB rows."""
  107. settings = Settings.from_env(_env_file())
  108. generated = build_creation_query_batch(
  109. settings,
  110. tree_path=TREES,
  111. options=QueryBuildOptions(
  112. per=per,
  113. batch_n=batch_n,
  114. active_family_keys=("f1", "f2"),
  115. ),
  116. )
  117. generated["summary"] = _summary(generated)
  118. return generated
  119. @router.get("/latest-singleton")
  120. def latest_singleton_overview(repo: Any = Depends(get_acquisition_repository)) -> dict[str, Any]:
  121. """Return links from the query preview board to the latest real singleton run."""
  122. getter = getattr(repo, "get_latest_singleton_overview", None)
  123. if getter is None:
  124. return {"batch": None, "run": None, "queries": [], "decoded_items": []}
  125. return getter()
  126. @router.get("/latest/queries/{query_id}")
  127. def latest_query_detail(
  128. query_id: UUID,
  129. repo: Any = Depends(get_acquisition_repository),
  130. ) -> dict[str, Any]:
  131. """Return search material for one query in the latest real query board batch."""
  132. detail_getter = getattr(repo, "get_latest_query_result_list", None)
  133. if detail_getter is None:
  134. return {"query": None, "jobs": [], "items": [], "platforms": {}}
  135. return _normalize_light_query_detail(detail_getter(query_id))