| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781 |
- """Formal acquisition runner backed by the repository boundary."""
- from __future__ import annotations
- from dataclasses import dataclass
- from typing import Any, Callable, Iterable
- from uuid import UUID
- from acquisition.classification.coarse import ClassificationResult, coarse_classify_item
- from acquisition.content_mode import guard_content_for_processing, infer_content_mode
- from acquisition.crawler import RateLimiter
- from acquisition.domain import AcquisitionJob, Query
- from acquisition.media.service import StabilizedMedia, stabilize_media_urls
- from acquisition.platforms import PlatformAdapter, get_platform_adapter
- from acquisition.platforms.base import PlatformCandidate, PlatformSearchPage
- from acquisition.repositories.base import AcquisitionRepository
- from acquisition.unique_key import build_unique_key
- from core.config import Settings
- from core.text_limits import (
- BODY_TEXT_MAX_CHARS,
- ERROR_MESSAGE_MAX_CHARS,
- RAW_SUMMARY_MAX_CHARS,
- clip_text,
- )
- DEFAULT_PLATFORMS = ("xiaohongshu", "weixin", "douyin")
- PAGINATION_STRATEGY = "creation_ratio_two_page_v1"
- PAGINATION_CREATION_THRESHOLD = 0.4
- PAGINATION_MAX_PAGES = 2
- LOW_RESULT_RETRY_THRESHOLD = 5
- LOW_RESULT_MAX_RETRIES = 1
- AdapterFactory = Callable[[str], PlatformAdapter]
- Classifier = Callable[..., ClassificationResult]
- MediaStabilizer = Callable[..., list[StabilizedMedia]]
- RateLimiterFactory = Callable[[str], Any]
- @dataclass(frozen=True)
- class RunBatchResult:
- run_id: UUID
- total_jobs: int
- done: int
- partial: int
- failed: int
- skipped: int
- @dataclass(frozen=True)
- class RecordCandidateResult:
- displayed: bool
- skipped_processing: bool = False
- classified: bool = False
- is_creation_knowledge: bool | None = None
- class PlatformRateLimiter:
- """Share one rate bucket per platform across search and detail calls."""
- def __init__(self, platform: str, delegate: RateLimiter | None = None) -> None:
- self.platform = platform
- self.delegate = delegate or RateLimiter(
- min_interval_seconds=10.0,
- max_interval_seconds=12.0,
- )
- def wait(self, bucket: str) -> None:
- self.delegate.wait(self.platform)
- def _query_id(query: Query) -> UUID:
- if query.id is None:
- raise RuntimeError("formal acquisition queries must have id before running")
- return query.id
- def _job_id(job: AcquisitionJob) -> UUID:
- if job.id is None:
- raise RuntimeError("formal acquisition jobs must have id before running")
- return job.id
- def _best_video_url(media: list[StabilizedMedia]) -> str:
- for row in media:
- if row.media_type == "video" and row.status == "ready" and row.cdn_url:
- return row.cdn_url
- return ""
- def _image_urls(media: list[StabilizedMedia]) -> list[str]:
- return [row.cdn_url for row in media if row.media_type == "image" and row.cdn_url]
- def _source_payload(candidate: Any, detail: Any) -> dict[str, Any]:
- return {
- "candidate": candidate.model_dump() if hasattr(candidate, "model_dump") else {},
- "detail": detail.raw if isinstance(getattr(detail, "raw", None), dict) else {},
- }
- def _candidate_provider(candidate: Any) -> str:
- provider = getattr(candidate, "provider", "") or ""
- raw = getattr(candidate, "raw", None)
- if not provider and isinstance(raw, dict):
- provider = raw.get("search_provider") or ""
- return provider
- def _detail_provider(detail: Any, candidate: Any) -> str:
- provider = getattr(detail, "provider", "") or ""
- if not provider:
- raw = getattr(detail, "raw", None)
- if isinstance(raw, dict):
- provider = raw.get("detail_provider") or ""
- return provider or _candidate_provider(candidate)
- def _candidate_page_metadata(candidate: Any) -> dict[str, Any]:
- raw = getattr(candidate, "raw", None)
- if not isinstance(raw, dict):
- return {}
- out: dict[str, Any] = {}
- for key in ("page_index", "page_rank", "source_cursor"):
- value = raw.get(key)
- if value not in (None, ""):
- out[key] = value
- return out
- def _content_mode(platform: str, detail: Any) -> str:
- explicit = getattr(detail, "content_mode", "") or ""
- if explicit:
- return explicit
- return infer_content_mode(
- platform=platform,
- content_type=getattr(detail, "content_type", "") or "",
- body_text=getattr(detail, "body_text", "") or "",
- image_urls=getattr(detail, "image_urls", []) or [],
- video_urls=getattr(detail, "video_urls", []) or [],
- raw=getattr(detail, "raw", {}) if isinstance(getattr(detail, "raw", None), dict) else {},
- )
- def _skip_reason_message(reason: str) -> str:
- if reason == "video_url_missing":
- return "Video post has no usable video URL; skipped before classification."
- if reason == "video_oss_failed":
- return "Video OSS transfer failed after retries; skipped before classification."
- if reason == "unsupported_content_mode":
- return "Unsupported content mode; skipped before classification."
- return reason or "Skipped before classification."
- def _requires_strict_video_oss(platform: str, content_mode: str) -> bool:
- return content_mode == "video_post"
- def _video_media_errors(media_rows: list[StabilizedMedia]) -> list[dict[str, Any]]:
- errors: list[dict[str, Any]] = []
- for row in media_rows:
- if row.media_type != "video" or row.status == "ready":
- continue
- errors.append(
- {
- "source_url": row.source_url,
- "status": row.status,
- "error_message": row.error_message,
- }
- )
- return errors
- def _record_candidate(
- repo: AcquisitionRepository,
- *,
- job: AcquisitionJob,
- query: Query,
- platform: str,
- candidate: Any,
- detail: Any,
- settings: Settings,
- media_stabilizer: MediaStabilizer,
- classifier: Classifier,
- classify: bool,
- attempt_index: int = 1,
- ) -> RecordCandidateResult:
- source_payload = _source_payload(candidate, detail)
- platform_item_id = detail.source_id or candidate.source_id or None
- canonical_url = detail.url or candidate.url or None
- content_mode = _content_mode(platform, detail)
- guard = guard_content_for_processing(
- content_mode=content_mode,
- video_urls=detail.video_urls,
- )
- metadata = {
- "candidate_rank": candidate.rank,
- "acquisition_match_status": "created",
- "content_mode": content_mode,
- "retry_attempt_index": attempt_index,
- **_candidate_page_metadata(candidate),
- }
- search_provider = _candidate_provider(candidate)
- detail_provider = _detail_provider(detail, candidate)
- if search_provider:
- metadata["search_provider"] = search_provider
- if detail_provider:
- metadata["detail_provider"] = detail_provider
- if guard.metadata:
- metadata.update(guard.metadata)
- body_text = clip_text(detail.body_text or "", BODY_TEXT_MAX_CHARS)
- item = repo.upsert_candidate_item(
- platform=platform,
- job_id=_job_id(job),
- query_id=_query_id(query),
- platform_item_id=platform_item_id,
- unique_key=build_unique_key(
- platform,
- platform_item_id=platform_item_id,
- url=canonical_url,
- raw_payload=source_payload,
- ),
- canonical_url=canonical_url,
- content_type=detail.content_type or None,
- content_mode=content_mode,
- title=detail.title or None,
- author_name=detail.author or None,
- body_text=body_text or None,
- raw_summary=clip_text(body_text, RAW_SUMMARY_MAX_CHARS) or None,
- status="candidate" if guard.can_process else "skipped",
- source_payload=source_payload,
- metadata=metadata,
- error_message=None if guard.can_process else guard.reason,
- )
- if item.id is None:
- raise RuntimeError("repository returned candidate without id")
- if not guard.can_process:
- repo.add_item_classification(
- item_id=item.id,
- is_creation_knowledge=None,
- label=guard.label,
- confidence=None,
- reason=_skip_reason_message(guard.reason),
- model_name=None,
- prompt_version="content_mode_guard",
- result_payload={
- "content_mode": content_mode,
- "skip_reason": guard.reason,
- **(guard.metadata or {}),
- },
- status="skipped",
- error_message=guard.reason,
- )
- return RecordCandidateResult(displayed=True, skipped_processing=True)
- strict_video_oss = _requires_strict_video_oss(platform, content_mode)
- media_rows = media_stabilizer(
- image_urls=detail.image_urls,
- video_urls=detail.video_urls,
- settings=settings,
- video_fallback_to_source=not strict_video_oss,
- video_retry_delays_seconds=(
- settings.video_oss_retry_delays_seconds if strict_video_oss else ()
- ),
- )
- for row in media_rows:
- repo.add_media_asset(
- item_id=item.id,
- media_type=row.media_type,
- source_url=row.source_url,
- oss_url=row.cdn_url,
- cdn_url=row.cdn_url,
- position=row.position,
- status=row.status,
- source_payload={},
- metadata={"error_message": row.error_message} if row.error_message else {},
- )
- if strict_video_oss and not _best_video_url(media_rows):
- repo.add_item_classification(
- item_id=item.id,
- is_creation_knowledge=None,
- label="video_oss_failed",
- confidence=None,
- reason=_skip_reason_message("video_oss_failed"),
- model_name=None,
- prompt_version="media_stabilization_guard",
- result_payload={
- "content_mode": content_mode,
- "skip_reason": "video_oss_failed",
- "video_oss_retry_delays_seconds": list(
- settings.video_oss_retry_delays_seconds
- ),
- "media_errors": _video_media_errors(media_rows),
- },
- status="skipped",
- error_message="video_oss_failed",
- )
- return RecordCandidateResult(displayed=True, skipped_processing=True)
- if classify:
- result = classifier(
- platform=platform,
- content_mode=content_mode,
- title=detail.title,
- body_text=detail.body_text,
- image_urls=_image_urls(media_rows),
- video_url=_best_video_url(media_rows),
- settings=settings,
- )
- repo.add_item_classification(
- item_id=item.id,
- is_creation_knowledge=result.is_creation_knowledge,
- label=result.label,
- confidence=result.confidence,
- reason=result.reason,
- model_name=settings.video_model,
- prompt_version=result.prompt_version,
- result_payload=result.result_payload,
- status=result.status,
- error_message=result.error_message,
- )
- return RecordCandidateResult(
- displayed=True,
- classified=result.status == "classified" and result.is_creation_knowledge is not None,
- is_creation_knowledge=result.is_creation_knowledge,
- )
- return RecordCandidateResult(displayed=True)
- def _candidate_unique_key(platform: str, candidate: PlatformCandidate) -> str | None:
- return build_unique_key(
- platform,
- platform_item_id=candidate.source_id or None,
- url=candidate.url or None,
- raw_payload={"candidate": candidate.model_dump()},
- )
- def _candidate_dedupe_key(platform: str, candidate: PlatformCandidate) -> str | None:
- unique_key = _candidate_unique_key(platform, candidate)
- if unique_key:
- return f"unique:{unique_key}"
- if candidate.source_id:
- return f"source:{platform}:{candidate.source_id}"
- if candidate.url:
- return f"url:{platform}:{candidate.url}"
- return None
- def _attach_existing_candidate(
- repo: AcquisitionRepository,
- *,
- job: AcquisitionJob,
- query: Query,
- candidate: PlatformCandidate,
- unique_key: str,
- attempt_index: int = 1,
- ) -> bool:
- get_by_key = getattr(repo, "get_candidate_item_by_unique_key", None)
- attach = getattr(repo, "attach_existing_candidate_item", None)
- if get_by_key is None or attach is None:
- return False
- existing = get_by_key(unique_key)
- if existing is None or existing.id is None:
- return False
- attach(
- existing.id,
- job_id=_job_id(job),
- query_id=_query_id(query),
- metadata={
- "acquisition_match_status": "existing",
- "matched_unique_key": unique_key,
- "matched_candidate": candidate.model_dump(),
- "candidate_rank": candidate.rank,
- "retry_attempt_index": attempt_index,
- **_candidate_page_metadata(candidate),
- **({"search_provider": _candidate_provider(candidate)} if _candidate_provider(candidate) else {}),
- },
- )
- return True
- def _pagination_config(threshold: float, max_pages: int) -> dict[str, Any]:
- return {
- "strategy": PAGINATION_STRATEGY,
- "creation_threshold": threshold,
- "max_pages": max_pages,
- }
- def _ratio(numerator: int, denominator: int) -> float | None:
- if denominator <= 0:
- return None
- return numerator / denominator
- def _search_pages(
- adapter: PlatformAdapter,
- query_text: str,
- *,
- settings: Settings,
- limit: int,
- rate_limiter: Any,
- max_pages: int,
- ) -> Iterable[PlatformSearchPage]:
- search_pages = getattr(adapter, "search_pages", None)
- if callable(search_pages):
- return search_pages(
- query_text,
- settings=settings,
- limit=limit,
- rate_limiter=rate_limiter,
- max_pages=max_pages,
- )
- candidates = adapter.search(
- query_text,
- settings=settings,
- limit=limit,
- rate_limiter=rate_limiter,
- )
- return [
- PlatformSearchPage(
- page_index=1,
- candidates=candidates,
- raw_count=len(candidates),
- has_more=False,
- )
- ]
- def run_batch(
- repo: AcquisitionRepository,
- *,
- batch_id: UUID,
- settings: Settings,
- platforms: tuple[str, ...] | list[str] = DEFAULT_PLATFORMS,
- search_limit: int = 10,
- display_limit: int = 5,
- classify: bool = True,
- resume: bool = True,
- skip_done: bool = True,
- run_key: str | None = None,
- adapter_factory: AdapterFactory = get_platform_adapter,
- media_stabilizer: MediaStabilizer = stabilize_media_urls,
- classifier: Classifier = coarse_classify_item,
- rate_limiter_factory: RateLimiterFactory | None = None,
- pagination_creation_threshold: float = PAGINATION_CREATION_THRESHOLD,
- pagination_max_pages: int = PAGINATION_MAX_PAGES,
- low_result_retry_threshold: int = LOW_RESULT_RETRY_THRESHOLD,
- low_result_max_retries: int = LOW_RESULT_MAX_RETRIES,
- ) -> RunBatchResult:
- """Run query x platform acquisition and write formal cloud-state rows."""
- queries = repo.list_queries_for_batch(batch_id, keep=True)
- run = repo.create_acquisition_run(
- batch_id=batch_id,
- run_key=run_key or f"acquisition:{batch_id}",
- status="running",
- metadata={
- "platforms": list(platforms),
- "search_limit": search_limit,
- "display_limit": display_limit,
- "classify": classify,
- "resume": resume,
- "pagination": _pagination_config(
- pagination_creation_threshold,
- pagination_max_pages,
- ),
- "low_result_retry": {
- "threshold": low_result_retry_threshold,
- "max_retries": low_result_max_retries,
- },
- },
- )
- if run.id is None:
- raise RuntimeError("repository returned acquisition run without id")
- total_jobs = len(queries) * len(platforms)
- done = partial = failed = skipped = 0
- gates: dict[str, Any] = {}
- for query in queries:
- query_id = _query_id(query)
- for platform in platforms:
- job = repo.ensure_acquisition_job(
- run_id=run.id,
- query_id=query_id,
- platform=platform,
- search_limit=search_limit,
- display_limit=display_limit,
- status="pending",
- metadata={"query_text": query.query_text},
- )
- if skip_done and job.status == "done":
- skipped += 1
- continue
- attempts = job.attempt_count + 1
- job = repo.update_acquisition_job(
- _job_id(job),
- status="running",
- attempt_count=attempts,
- error_message=None,
- )
- errors: list[str] = []
- display_count = 0
- searched_count = 0
- raw_searched_count = 0
- skipped_item_count = 0
- search_page_count = 0
- page_summaries: list[dict[str, Any]] = []
- first_page_classified_count = 0
- first_page_creation_count = 0
- first_page_creation_ratio: float | None = None
- requested_second_page = False
- pagination_stop_reason = ""
- try:
- adapter = adapter_factory(platform)
- gate = gates.get(platform)
- if gate is None:
- gate = (
- rate_limiter_factory(platform)
- if rate_limiter_factory
- else PlatformRateLimiter(platform)
- )
- gates[platform] = gate
- retry_enabled = (
- low_result_max_retries > 0
- and low_result_retry_threshold >= 0
- and search_limit > low_result_retry_threshold
- )
- max_attempts = 1 + (low_result_max_retries if retry_enabled else 0)
- seen_candidate_keys: set[str] = set()
- retry_attempts: list[dict[str, Any]] = []
- retry_triggered = False
- for attempt_index in range(1, max_attempts + 1):
- attempt_display_start = display_count
- attempt_searched_start = searched_count
- attempt_raw_start = raw_searched_count
- attempt_pages_start = search_page_count
- attempt_page_summaries: list[dict[str, Any]] = []
- attempt_first_page_classified_count = 0
- attempt_first_page_creation_count = 0
- attempt_first_page_creation_ratio: float | None = None
- attempt_requested_second_page = False
- attempt_stop_reason = ""
- pages = _search_pages(
- adapter,
- query.query_text,
- settings=settings,
- limit=search_limit,
- rate_limiter=gate,
- max_pages=pagination_max_pages,
- )
- for page in pages:
- search_page_count += 1
- raw_searched_count += page.raw_count
- page_classified_count = 0
- page_creation_count = 0
- unique_candidate_count = 0
- for candidate in page.candidates[:search_limit]:
- dedupe_key = _candidate_dedupe_key(platform, candidate)
- if dedupe_key and dedupe_key in seen_candidate_keys:
- continue
- if dedupe_key:
- seen_candidate_keys.add(dedupe_key)
- unique_candidate_count += 1
- searched_count += 1
- try:
- unique_key = _candidate_unique_key(platform, candidate)
- if unique_key and _attach_existing_candidate(
- repo,
- job=job,
- query=query,
- candidate=candidate,
- unique_key=unique_key,
- attempt_index=attempt_index,
- ):
- display_count += 1
- continue
- detail = adapter.fetch_detail(
- candidate,
- settings=settings,
- rate_limiter=gate,
- )
- recorded = _record_candidate(
- repo,
- job=job,
- query=query,
- platform=platform,
- candidate=candidate,
- detail=detail,
- settings=settings,
- media_stabilizer=media_stabilizer,
- classifier=classifier,
- classify=classify,
- attempt_index=attempt_index,
- )
- if recorded.displayed:
- display_count += 1
- if recorded.skipped_processing:
- skipped_item_count += 1
- if recorded.classified:
- page_classified_count += 1
- if recorded.is_creation_knowledge is True:
- page_creation_count += 1
- except Exception as exc:
- errors.append(clip_text(exc, ERROR_MESSAGE_MAX_CHARS))
- continue
- page_ratio = _ratio(page_creation_count, page_classified_count)
- page_summary = {
- "attempt_index": attempt_index,
- "page_index": page.page_index,
- "raw_count": page.raw_count,
- "candidate_count": len(page.candidates),
- "unique_candidate_count": unique_candidate_count,
- "classified_count": page_classified_count,
- "creation_count": page_creation_count,
- "creation_ratio": page_ratio,
- "has_more": page.has_more,
- "next_cursor": page.next_cursor,
- "provider": page.provider,
- }
- page_summaries.append(page_summary)
- attempt_page_summaries.append(page_summary)
- if page.page_index == 1:
- attempt_first_page_classified_count = page_classified_count
- attempt_first_page_creation_count = page_creation_count
- attempt_first_page_creation_ratio = page_ratio
- if pagination_max_pages <= 1:
- attempt_stop_reason = "max_pages_reached"
- break
- if not page.has_more or not page.next_cursor:
- attempt_stop_reason = "no_more_pages"
- break
- if page_classified_count <= 0:
- attempt_stop_reason = "no_classified_candidates"
- break
- if page_ratio is None or page_ratio < pagination_creation_threshold:
- attempt_stop_reason = "below_threshold"
- break
- attempt_requested_second_page = True
- continue
- attempt_stop_reason = "max_pages_reached"
- break
- if not attempt_stop_reason:
- attempt_stop_reason = "pages_exhausted"
- first_page_classified_count = attempt_first_page_classified_count
- first_page_creation_count = attempt_first_page_creation_count
- first_page_creation_ratio = attempt_first_page_creation_ratio
- requested_second_page = attempt_requested_second_page
- pagination_stop_reason = attempt_stop_reason
- attempt_display_count = display_count - attempt_display_start
- retry_attempts.append(
- {
- "attempt_index": attempt_index,
- "display_count": attempt_display_count,
- "searched_count": searched_count - attempt_searched_start,
- "raw_searched_count": raw_searched_count - attempt_raw_start,
- "search_page_count": search_page_count - attempt_pages_start,
- "stop_reason": attempt_stop_reason,
- "requested_second_page": attempt_requested_second_page,
- "first_page_classified_count": attempt_first_page_classified_count,
- "first_page_creation_count": attempt_first_page_creation_count,
- "first_page_creation_ratio": attempt_first_page_creation_ratio,
- "pages": attempt_page_summaries,
- }
- )
- if (
- attempt_index <= low_result_max_retries
- and display_count <= low_result_retry_threshold
- ):
- retry_triggered = True
- continue
- break
- status = "done" if display_count else "failed"
- if status == "done":
- done += 1
- else:
- failed += 1
- repo.update_acquisition_job(
- _job_id(job),
- status=status,
- attempt_count=attempts,
- error_message=None if status != "failed" else "; ".join(errors[-3:]),
- metadata={
- "query_text": query.query_text,
- "searched_count": searched_count,
- "raw_searched_count": raw_searched_count,
- "display_count": display_count,
- "skipped_item_count": skipped_item_count,
- "search_page_count": search_page_count,
- "pagination": {
- **_pagination_config(
- pagination_creation_threshold,
- pagination_max_pages,
- ),
- "first_page_classified_count": first_page_classified_count,
- "first_page_creation_count": first_page_creation_count,
- "first_page_creation_ratio": first_page_creation_ratio,
- "requested_second_page": requested_second_page,
- "stop_reason": pagination_stop_reason,
- "pages": page_summaries,
- },
- "low_result_retry": {
- "threshold": low_result_retry_threshold,
- "max_retries": low_result_max_retries if retry_enabled else 0,
- "triggered": retry_triggered,
- "attempt_count": len(retry_attempts),
- "attempts": retry_attempts,
- },
- "errors": errors[-3:],
- },
- )
- except Exception as exc:
- failed += 1
- repo.update_acquisition_job(
- _job_id(job),
- status="failed",
- attempt_count=attempts,
- error_message=clip_text(exc, ERROR_MESSAGE_MAX_CHARS),
- metadata={
- "query_text": query.query_text,
- "searched_count": searched_count,
- "raw_searched_count": raw_searched_count,
- "display_count": display_count,
- "skipped_item_count": skipped_item_count,
- "search_page_count": search_page_count,
- "pagination": {
- **_pagination_config(
- pagination_creation_threshold,
- pagination_max_pages,
- ),
- "first_page_classified_count": first_page_classified_count,
- "first_page_creation_count": first_page_creation_count,
- "first_page_creation_ratio": first_page_creation_ratio,
- "requested_second_page": requested_second_page,
- "stop_reason": pagination_stop_reason or "failed",
- "pages": page_summaries,
- },
- "errors": errors[-3:],
- },
- )
- run_status = "done" if done > 0 else "failed"
- update_run = getattr(repo, "update_acquisition_run", None)
- if update_run:
- update_run(
- run.id,
- status=run_status,
- metadata={
- "total_jobs": total_jobs,
- "done": done,
- "partial": partial,
- "failed": failed,
- "skipped": skipped,
- "pagination": _pagination_config(
- pagination_creation_threshold,
- pagination_max_pages,
- ),
- },
- )
- return RunBatchResult(
- run_id=run.id,
- total_jobs=total_jobs,
- done=done,
- partial=partial,
- failed=failed,
- skipped=skipped,
- )
|