run_creation_singleton.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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. "--dry-query-filter",
  36. action="store_true",
  37. help="Skip only the query-filter LLM; search/classify/decode remain real.",
  38. )
  39. parser.add_argument(
  40. "--platform",
  41. action="append",
  42. choices=DEFAULT_PLATFORMS,
  43. help="Platform to run. Repeat for multiple platforms. Default: xiaohongshu.",
  44. )
  45. parser.add_argument("--search-limit", type=int, default=1)
  46. parser.add_argument("--display-limit", type=int, default=1)
  47. parser.add_argument("--decode-limit", type=int, default=1)
  48. parser.add_argument("--name", default="")
  49. parser.add_argument("--frontend-base", default="http://127.0.0.1:5180/app/")
  50. return parser.parse_args(argv)
  51. def _now_key() -> str:
  52. return datetime.now().strftime("%Y%m%d-%H%M%S")
  53. def _model_dump(value: Any) -> dict[str, Any]:
  54. if hasattr(value, "model_dump"):
  55. return value.model_dump(mode="json")
  56. if is_dataclass(value):
  57. return asdict(value)
  58. if isinstance(value, dict):
  59. return value
  60. return dict(value)
  61. def _dry_ingest_payloads(
  62. repo: PostgresDecodeRepository,
  63. outputs: list[Any],
  64. ) -> list[dict[str, Any]]:
  65. records: list[dict[str, Any]] = []
  66. for output in outputs:
  67. for draft in output.payload_drafts:
  68. if draft.id is None:
  69. continue
  70. repo.mark_payload_draft_ingested(draft.id)
  71. record = repo.save_ingest_record(
  72. payload_draft_id=draft.id,
  73. target_system="dry-run",
  74. target_id=str(draft.id),
  75. status="ingested",
  76. response_payload={
  77. "dry_run": True,
  78. "note": "payload generated by singleton pipeline; external ingest API not called",
  79. "payload": draft.payload,
  80. },
  81. )
  82. records.append(_model_dump(record))
  83. return records
  84. def main(argv: list[str] | None = None) -> int:
  85. args = parse_args(argv)
  86. settings = Settings.from_env(args.env_file)
  87. db_config = CreationDbConfig.from_env(args.env_file)
  88. platforms = tuple(args.platform or ("xiaohongshu",))
  89. run_key_suffix = _now_key()
  90. name = args.name or f"creation-singleton-{run_key_suffix}"
  91. generated = build_creation_query_batch(
  92. settings,
  93. tree_path=args.tree_path,
  94. options=QueryBuildOptions(
  95. per=args.per,
  96. batch_n=args.batch_n,
  97. seed=args.seed,
  98. dry=args.dry_query_filter,
  99. active_family_keys=("f1", "f2"),
  100. ),
  101. )
  102. query_count = sum(len(family.get("items") or []) for family in generated.get("families") or [])
  103. kept_count = sum(
  104. 1
  105. for family in generated.get("families") or []
  106. for item in family.get("items") or []
  107. if item.get("keep", True)
  108. )
  109. with transaction(db_config) as conn:
  110. acquisition_repo = PostgresAcquisitionRepository(conn)
  111. batch, persisted_count = persist_query_batch(
  112. acquisition_repo,
  113. generated,
  114. name=name,
  115. source_type="generated",
  116. generation_method="creation_singleton_v1",
  117. target_platforms=list(platforms),
  118. )
  119. with transaction(db_config) as conn:
  120. acquisition_repo = PostgresAcquisitionRepository(conn)
  121. acquisition = run_batch(
  122. acquisition_repo,
  123. batch_id=batch.id,
  124. settings=settings,
  125. platforms=platforms,
  126. search_limit=args.search_limit,
  127. display_limit=args.display_limit,
  128. classify=True,
  129. resume=False,
  130. skip_done=False,
  131. run_key=f"singleton-acquisition:{batch.id}:{run_key_suffix}",
  132. )
  133. with transaction(db_config) as conn:
  134. acquisition_repo = PostgresAcquisitionRepository(conn)
  135. decode_repo = PostgresDecodeRepository(conn)
  136. decode_service = DecodeService(settings=settings, repository=decode_repo)
  137. decode = run_decode_stage(
  138. candidate_repo=acquisition_repo,
  139. decode_service=decode_service,
  140. run_id=acquisition.run_id,
  141. limit=args.decode_limit,
  142. )
  143. ingest_records = _dry_ingest_payloads(decode_repo, decode.outputs)
  144. decoded_items = [str(output.item_id) for output in decode.outputs]
  145. first_item_id = decoded_items[0] if decoded_items else None
  146. summary = {
  147. "batch_id": str(batch.id),
  148. "run_id": str(acquisition.run_id),
  149. "active_family_keys": ["f1", "f2"],
  150. "query_count": query_count,
  151. "kept_count": kept_count,
  152. "persisted_queries": persisted_count,
  153. "platforms": list(platforms),
  154. "acquisition": _model_dump(acquisition),
  155. "decode": {
  156. "total": decode.total,
  157. "decoded": decode.decoded,
  158. "skipped": decode.skipped,
  159. "failed": decode.failed,
  160. "decoded_items": decoded_items,
  161. "payload_count": sum(len(output.payload_drafts) for output in decode.outputs),
  162. },
  163. "dry_run_ingest_count": len(ingest_records),
  164. "detail_url": (
  165. f"{args.frontend_base.rstrip('/')}/#/decode-item/{first_item_id}"
  166. if first_item_id
  167. else None
  168. ),
  169. }
  170. print(json.dumps(summary, ensure_ascii=False, indent=2, default=str))
  171. return 0 if first_item_id else 2
  172. if __name__ == "__main__":
  173. raise SystemExit(main())