run_creation_singleton.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. #!/usr/bin/env python3
  2. """Run one real f1/f2 creation-knowledge pipeline sample to dry-run ingest."""
  3. from __future__ import annotations
  4. import argparse
  5. import json
  6. from dataclasses import asdict, is_dataclass
  7. from datetime import datetime
  8. from pathlib import Path
  9. import sys
  10. from typing import Any
  11. ROOT = Path(__file__).resolve().parents[1]
  12. if str(ROOT) not in sys.path:
  13. sys.path.insert(0, str(ROOT))
  14. from acquisition.queries.builder import ( # noqa: E402
  15. QueryBuildOptions,
  16. TREES,
  17. build_creation_query_batch,
  18. persist_query_batch,
  19. )
  20. from acquisition.repositories.postgres import PostgresAcquisitionRepository # noqa: E402
  21. from acquisition.runner import DEFAULT_PLATFORMS, run_batch # noqa: E402
  22. from core.config import CreationDbConfig, Settings # noqa: E402
  23. from core.db_session import transaction # noqa: E402
  24. from decode_content.repositories.postgres import PostgresDecodeRepository # noqa: E402
  25. from decode_content.service import DecodeService # noqa: E402
  26. from pipeline.decode_runner import run_decode_stage # noqa: E402
  27. def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
  28. parser = argparse.ArgumentParser(description=__doc__)
  29. parser.add_argument("--env-file", default=".env")
  30. parser.add_argument("--tree-path", type=Path, default=TREES)
  31. parser.add_argument("--per", type=int, default=1, help="Queries per active family")
  32. parser.add_argument("--batch-n", type=int, default=30)
  33. parser.add_argument("--seed", type=int, default=7)
  34. parser.add_argument(
  35. "--platform",
  36. action="append",
  37. choices=DEFAULT_PLATFORMS,
  38. help="Platform to run. Repeat for multiple platforms. Default: xiaohongshu.",
  39. )
  40. parser.add_argument("--search-limit", type=int, default=1)
  41. parser.add_argument("--display-limit", type=int, default=1)
  42. parser.add_argument("--decode-limit", type=int, default=100)
  43. parser.add_argument("--name", default="")
  44. parser.add_argument("--frontend-base", default="http://127.0.0.1:5180/app/")
  45. return parser.parse_args(argv)
  46. def _now_key() -> str:
  47. return datetime.now().strftime("%Y%m%d-%H%M%S")
  48. def _model_dump(value: Any) -> dict[str, Any]:
  49. if hasattr(value, "model_dump"):
  50. return value.model_dump(mode="json")
  51. if is_dataclass(value):
  52. return asdict(value)
  53. if isinstance(value, dict):
  54. return value
  55. return dict(value)
  56. def _dry_ingest_payloads(
  57. repo: PostgresDecodeRepository,
  58. outputs: list[Any],
  59. ) -> list[dict[str, Any]]:
  60. records: list[dict[str, Any]] = []
  61. for output in outputs:
  62. for draft in output.payload_drafts:
  63. if draft.id is None:
  64. continue
  65. repo.mark_payload_draft_ingested(draft.id)
  66. record = repo.save_ingest_record(
  67. payload_draft_id=draft.id,
  68. target_system="dry-run",
  69. target_id=str(draft.id),
  70. status="ingested",
  71. response_payload={
  72. "dry_run": True,
  73. "note": "payload generated by singleton pipeline; external ingest API not called",
  74. "payload": draft.payload,
  75. },
  76. )
  77. records.append(_model_dump(record))
  78. return records
  79. def main(argv: list[str] | None = None) -> int:
  80. args = parse_args(argv)
  81. settings = Settings.from_env(args.env_file)
  82. db_config = CreationDbConfig.from_env(args.env_file)
  83. platforms = tuple(args.platform or ("xiaohongshu",))
  84. run_key_suffix = _now_key()
  85. name = args.name or f"creation-singleton-{run_key_suffix}"
  86. generated = build_creation_query_batch(
  87. settings,
  88. tree_path=args.tree_path,
  89. options=QueryBuildOptions(
  90. per=args.per,
  91. batch_n=args.batch_n,
  92. seed=args.seed,
  93. active_family_keys=("f1", "f2"),
  94. ),
  95. )
  96. query_count = sum(len(family.get("items") or []) for family in generated.get("families") or [])
  97. kept_count = sum(
  98. 1
  99. for family in generated.get("families") or []
  100. for item in family.get("items") or []
  101. if item.get("keep", True)
  102. )
  103. with transaction(db_config) as conn:
  104. acquisition_repo = PostgresAcquisitionRepository(conn)
  105. batch, persisted_count = persist_query_batch(
  106. acquisition_repo,
  107. generated,
  108. name=name,
  109. source_type="generated",
  110. generation_method="creation_singleton_v1",
  111. target_platforms=list(platforms),
  112. )
  113. with transaction(db_config) as conn:
  114. acquisition_repo = PostgresAcquisitionRepository(conn)
  115. acquisition = run_batch(
  116. acquisition_repo,
  117. batch_id=batch.id,
  118. settings=settings,
  119. platforms=platforms,
  120. search_limit=args.search_limit,
  121. display_limit=args.display_limit,
  122. classify=True,
  123. resume=False,
  124. skip_done=False,
  125. run_key=f"singleton-acquisition:{batch.id}:{run_key_suffix}",
  126. )
  127. with transaction(db_config) as conn:
  128. acquisition_repo = PostgresAcquisitionRepository(conn)
  129. decode_repo = PostgresDecodeRepository(conn)
  130. decode_service = DecodeService(settings=settings, repository=decode_repo)
  131. decode = run_decode_stage(
  132. candidate_repo=acquisition_repo,
  133. decode_service=decode_service,
  134. run_id=acquisition.run_id,
  135. limit=args.decode_limit,
  136. )
  137. ingest_records = _dry_ingest_payloads(decode_repo, decode.outputs)
  138. decoded_items = [str(output.item_id) for output in decode.outputs]
  139. first_item_id = decoded_items[0] if decoded_items else None
  140. summary = {
  141. "batch_id": str(batch.id),
  142. "run_id": str(acquisition.run_id),
  143. "active_family_keys": ["f1", "f2"],
  144. "query_count": query_count,
  145. "kept_count": kept_count,
  146. "persisted_queries": persisted_count,
  147. "platforms": list(platforms),
  148. "acquisition": _model_dump(acquisition),
  149. "decode": {
  150. "total": decode.total,
  151. "decoded": decode.decoded,
  152. "skipped": decode.skipped,
  153. "failed": decode.failed,
  154. "decoded_items": decoded_items,
  155. "payload_count": sum(len(output.payload_drafts) for output in decode.outputs),
  156. },
  157. "dry_run_ingest_count": len(ingest_records),
  158. "detail_url": (
  159. f"{args.frontend_base.rstrip('/')}/#/decode-item/{first_item_id}"
  160. if first_item_id
  161. else None
  162. ),
  163. }
  164. print(json.dumps(summary, ensure_ascii=False, indent=2, default=str))
  165. return 0 if first_item_id else 2
  166. if __name__ == "__main__":
  167. raise SystemExit(main())