| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- #!/usr/bin/env python3
- """Ingest existing payload drafts for one acquisition run.
- This script does not search, classify, or decode. It only reads payload_drafts
- already produced for candidate items under the given run_id.
- """
- 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, IngestApiConfig # noqa: E402
- from core.db_session import transaction # noqa: E402
- from decode_content.ingest import KnowledgeIngestClient, ingest_payload_draft # noqa: E402
- from decode_content.models import PayloadDraft # noqa: E402
- from decode_content.repositories.postgres import PostgresDecodeRepository # noqa: E402
- from pipeline.postgres import PostgresPipelineRepository # noqa: E402
- from pipeline.tracing import TraceContext, new_trace_writer # 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 list_payload_drafts_for_run(
- repo: PostgresDecodeRepository,
- *,
- run_id: UUID,
- include_real_ingested: bool = False,
- limit: int | None = None,
- ) -> list[PayloadDraft]:
- skip_real_ingested = "" if include_real_ingested else """
- AND NOT EXISTS (
- SELECT 1 FROM ingest_records ir
- WHERE ir.payload_draft_id = pd.id
- AND ir.target_system = 'knowledge-ingest-api'
- AND ir.status = 'ingested'
- )
- """
- limit_sql = "LIMIT %s" if limit is not None else ""
- params: tuple = (run_id, limit) if limit is not None else (run_id,)
- rows = repo._all(
- f"""
- SELECT pd.*
- FROM payload_drafts pd
- JOIN candidate_items ci ON ci.id = pd.item_id
- JOIN acquisition_jobs aj ON aj.id = ci.job_id
- WHERE aj.run_id = %s
- {skip_real_ingested}
- ORDER BY ci.created_at, pd.created_at, pd.id
- {limit_sql}
- """,
- params,
- )
- return [PayloadDraft.model_validate(row) for row in rows]
- def count_run_payloads(repo: PostgresDecodeRepository, *, run_id: UUID) -> dict[str, int]:
- row = repo._one(
- """
- SELECT
- COUNT(DISTINCT pd.id)::int AS total_payloads,
- COUNT(DISTINCT pd.id) FILTER (
- WHERE EXISTS (
- SELECT 1 FROM ingest_records ir
- WHERE ir.payload_draft_id = pd.id
- AND ir.target_system = 'knowledge-ingest-api'
- AND ir.status = 'ingested'
- )
- )::int AS already_real_ingested
- FROM payload_drafts pd
- JOIN candidate_items ci ON ci.id = pd.item_id
- JOIN acquisition_jobs aj ON aj.id = ci.job_id
- WHERE aj.run_id = %s
- """,
- (run_id,),
- )
- return {
- "total_payloads": int(row.get("total_payloads") or 0),
- "already_real_ingested": int(row.get("already_real_ingested") or 0),
- }
- def ingest_existing_payloads(
- repo: PostgresDecodeRepository,
- drafts: list[PayloadDraft],
- *,
- dry_run: bool,
- env_file: str,
- trace_writer=None,
- trace_context: TraceContext | None = None,
- ) -> list[dict]:
- client = None
- if not dry_run:
- config = IngestApiConfig.from_env(env_file)
- if not config.url:
- raise RuntimeError("CK_INGEST_API_URL 未配置,真实 ingest 已关闭")
- client = KnowledgeIngestClient(config)
- records: list[dict] = []
- for draft in drafts:
- if draft.id is None:
- continue
- context = (trace_context or TraceContext(stage="ingest")).child(
- item_id=draft.item_id,
- payload_draft_id=draft.id,
- stage="ingest",
- )
- if trace_writer is not None:
- trace_writer.event(
- context=context,
- stage="ingest",
- event_type="ingest_started",
- status="running",
- target_table="payload_drafts",
- target_id=draft.id,
- payload={"dry_run": dry_run, "entrypoint": "scripts/ingest_payloads.py"},
- )
- record = ingest_payload_draft(repo, draft, dry_run=dry_run, client=client)
- if trace_writer is not None:
- trace_writer.event(
- context=context.child(ingest_record_id=record.id),
- stage="ingest",
- event_type="ingest_recorded",
- status=record.status,
- target_table="ingest_records",
- target_id=record.id,
- payload={"dry_run": dry_run, "target_system": record.target_system},
- error_message=record.error_message,
- )
- records.append(_model_dump(record))
- return records
- def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("--run-id", required=True, help="acquisition_runs.id whose existing payloads should be ingested")
- parser.add_argument("--pipeline-run-id", help="Optional pipeline_runs.id for trace events")
- parser.add_argument("--env-file", default=".env")
- parser.add_argument("--real", action="store_true", help="Call CK_INGEST_API_URL. Default is dry-run.")
- parser.add_argument("--limit", type=int, default=None, help="Optional max payload drafts to process")
- parser.add_argument(
- "--include-real-ingested",
- action="store_true",
- help="Also process drafts that already have successful knowledge-ingest-api records.",
- )
- return parser.parse_args(argv)
- def main(argv: list[str] | None = None) -> int:
- args = parse_args(argv)
- run_id = UUID(args.run_id)
- db_config = CreationDbConfig.from_env(args.env_file)
- pipeline_run_id = UUID(args.pipeline_run_id) if args.pipeline_run_id else None
- if pipeline_run_id is None:
- try:
- with transaction(db_config) as conn:
- pipeline_run_id = PostgresPipelineRepository(conn).get_pipeline_run_by_acquisition_run(run_id).id
- except Exception:
- pipeline_run_id = None
- trace_writer = new_trace_writer(db_config, env_file=args.env_file)
- trace_context = TraceContext(
- pipeline_run_id=pipeline_run_id,
- acquisition_run_id=run_id,
- stage="ingest",
- )
- with transaction(db_config) as conn:
- repo = PostgresDecodeRepository(conn)
- counts = count_run_payloads(repo, run_id=run_id)
- drafts = list_payload_drafts_for_run(
- repo,
- run_id=run_id,
- include_real_ingested=args.include_real_ingested,
- limit=args.limit,
- )
- records = ingest_existing_payloads(
- repo,
- drafts,
- dry_run=not args.real,
- env_file=args.env_file,
- trace_writer=trace_writer,
- trace_context=trace_context,
- )
- ingested_count = sum(1 for record in records if record.get("status") == "ingested")
- failed_count = sum(1 for record in records if record.get("status") == "failed")
- skipped_already_real = (
- 0
- if args.include_real_ingested
- else max(0, counts["already_real_ingested"])
- )
- print(json.dumps(
- {
- "run_id": str(run_id),
- "pipeline_run_id": str(pipeline_run_id) if pipeline_run_id else None,
- "dry_run": not args.real,
- "total_payloads": counts["total_payloads"],
- "already_real_ingested": counts["already_real_ingested"],
- "selected_payloads": len(drafts),
- "skipped_already_real_ingested": skipped_already_real,
- "ingested_count": ingested_count,
- "failed_count": failed_count,
- "records": records,
- },
- ensure_ascii=False,
- default=str,
- indent=2,
- ))
- return 0
- if __name__ == "__main__":
- raise SystemExit(main())
|