| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- #!/usr/bin/env python3
- """Backfill pipeline trace ledger rows from existing acquisition runs.
- Dry-run is the default. This script never fabricates historical LLM traces:
- old runs only get structural events plus an explicit backfill note.
- """
- from __future__ import annotations
- import argparse
- import json
- import sys
- from dataclasses import asdict, is_dataclass
- from pathlib import Path
- from uuid import UUID
- ROOT = Path(__file__).resolve().parents[1]
- if str(ROOT) not in sys.path:
- sys.path.insert(0, str(ROOT))
- from core.config import CreationDbConfig # noqa: E402
- from core.db_session import transaction # noqa: E402
- from pipeline.postgres import PostgresPipelineRepository # noqa: E402
- from pipeline.tracing import TraceContext # noqa: E402
- def _model_dump(value):
- if hasattr(value, "model_dump"):
- return value.model_dump(mode="json")
- if is_dataclass(value):
- return asdict(value)
- if isinstance(value, dict):
- return value
- return dict(value)
- def _acquisition_runs(conn, *, limit: int | None = None) -> list[dict]:
- sql = """
- SELECT ar.*,
- COUNT(DISTINCT aj.id)::int AS job_count,
- COUNT(DISTINCT ci.id)::int AS candidate_count
- FROM acquisition_runs ar
- LEFT JOIN acquisition_jobs aj ON aj.run_id = ar.id
- LEFT JOIN candidate_items ci ON ci.job_id = aj.id
- GROUP BY ar.id
- ORDER BY ar.created_at
- """
- if limit is not None:
- sql += " LIMIT %s"
- params = (limit,)
- else:
- params = ()
- return PostgresPipelineRepository(conn)._all(sql, params)
- def _candidate_hits(conn, acquisition_run_id: UUID) -> list[dict]:
- return PostgresPipelineRepository(conn)._all(
- """
- SELECT
- aj.id AS acquisition_job_id,
- aj.query_id,
- aj.platform,
- ci.id AS item_id,
- ci.unique_key,
- ci.platform_item_id,
- ci.metadata,
- ci.source_payload
- FROM acquisition_jobs aj
- JOIN candidate_items ci ON ci.job_id = aj.id
- WHERE aj.run_id = %s
- ORDER BY aj.platform, ci.created_at
- """,
- (acquisition_run_id,),
- )
- def backfill(*, env_file: str, apply: bool, limit: int | None = None) -> dict:
- db_config = CreationDbConfig.from_env(env_file)
- with transaction(db_config) as conn:
- repo = PostgresPipelineRepository(conn)
- runs = _acquisition_runs(conn, limit=limit)
- if not apply:
- return {
- "dry_run": True,
- "acquisition_run_count": len(runs),
- "candidate_hit_count": sum(int(row.get("candidate_count") or 0) for row in runs),
- "runs": [
- {
- "acquisition_run_id": str(row["id"]),
- "run_key": row.get("run_key"),
- "status": row.get("status"),
- "job_count": row.get("job_count"),
- "candidate_count": row.get("candidate_count"),
- }
- for row in runs
- ],
- }
- pipeline_count = 0
- hit_count = 0
- for row in runs:
- acquisition_run_id = row["id"]
- run = repo.create_pipeline_run(
- run_key=f"backfill:{acquisition_run_id}",
- batch_id=row.get("batch_id"),
- status=row.get("status") or "done",
- current_stage="ingest",
- metadata={
- "source": "backfill_pipeline_traces",
- "acquisition_run_id": str(acquisition_run_id),
- "original_run_key": row.get("run_key"),
- },
- )
- pipeline_count += 1
- context = TraceContext(
- pipeline_run_id=run.id,
- acquisition_run_id=acquisition_run_id,
- stage="search",
- )
- repo.append_event(
- context=context,
- stage="query",
- event_type="backfilled_without_llm_trace",
- status="done",
- target_table="acquisition_runs",
- target_id=acquisition_run_id,
- payload={
- "reason": "historical raw LLM requests and responses were not stored",
- "job_count": row.get("job_count"),
- "candidate_count": row.get("candidate_count"),
- },
- )
- for hit in _candidate_hits(conn, acquisition_run_id):
- metadata = hit.get("metadata") or {}
- repo.record_candidate_hit(
- context=context.child(
- acquisition_job_id=hit.get("acquisition_job_id"),
- query_id=hit.get("query_id"),
- item_id=hit.get("item_id"),
- platform=hit.get("platform"),
- ),
- item_id=hit.get("item_id"),
- platform=hit.get("platform"),
- unique_key=hit.get("unique_key"),
- platform_item_id=hit.get("platform_item_id"),
- search_provider=metadata.get("search_provider"),
- detail_provider=metadata.get("detail_provider"),
- attempt_index=metadata.get("retry_attempt_index"),
- page_index=metadata.get("page_index"),
- page_rank=metadata.get("page_rank"),
- candidate_rank=metadata.get("candidate_rank"),
- source_cursor=metadata.get("source_cursor"),
- is_duplicate_hit=metadata.get("acquisition_match_status") == "existing",
- raw_candidate=metadata.get("matched_candidate") or {},
- metadata={"source": "backfill_pipeline_traces"},
- )
- hit_count += 1
- return {
- "dry_run": False,
- "pipeline_run_count": pipeline_count,
- "candidate_hit_count": hit_count,
- }
- def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("--env-file", default=".env")
- parser.add_argument("--apply", action="store_true", help="Actually write trace rows. Default is dry-run.")
- parser.add_argument("--limit", type=int, default=None)
- return parser.parse_args(argv)
- def main(argv: list[str] | None = None) -> int:
- args = parse_args(argv)
- print(json.dumps(
- backfill(env_file=args.env_file, apply=args.apply, limit=args.limit),
- ensure_ascii=False,
- default=str,
- indent=2,
- ))
- return 0
- if __name__ == "__main__":
- raise SystemExit(main())
|