decode_runner.py 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. """Pipeline adapter for decoding creation candidate items."""
  2. from __future__ import annotations
  3. from dataclasses import dataclass, field
  4. from typing import Protocol
  5. from uuid import UUID
  6. from acquisition.domain import CandidateItem, MediaAsset
  7. from core.text_limits import ERROR_MESSAGE_MAX_CHARS, clip_text
  8. from decode_content.readers.service import post_from_candidate_item
  9. from decode_content.service import DecodeService, DecodeWorkflowOutput
  10. from pipeline.dedupe import dedupe_candidate_items, should_decode_item
  11. class DecodeCandidateRepository(Protocol):
  12. def list_creation_candidate_items(
  13. self,
  14. *,
  15. run_id: UUID | None = None,
  16. limit: int = 100,
  17. ) -> list[CandidateItem]:
  18. ...
  19. def list_media_assets_for_item(self, item_id: UUID) -> list[MediaAsset]:
  20. ...
  21. @dataclass(frozen=True)
  22. class DecodeBatchResult:
  23. total: int
  24. decoded: int
  25. skipped: int
  26. failed: int
  27. outputs: list[DecodeWorkflowOutput]
  28. failures: list[dict[str, str]] = field(default_factory=list)
  29. def _record_failure(decode_service: DecodeService, item: CandidateItem, exc: Exception) -> dict[str, str]:
  30. message = clip_text(str(exc) or exc.__class__.__name__, ERROR_MESSAGE_MAX_CHARS)
  31. failure = {
  32. "item_id": str(item.id),
  33. "platform": item.platform,
  34. "title": item.title or "",
  35. "error": message,
  36. }
  37. repo = getattr(decode_service, "repository", None)
  38. if repo is not None and item.id is not None:
  39. mark_jobs = getattr(repo, "mark_running_decode_jobs_failed", None)
  40. if mark_jobs is not None:
  41. mark_jobs(item.id, message)
  42. save_result = getattr(repo, "save_decode_result", None)
  43. if save_result is not None:
  44. save_result(
  45. item_id=item.id,
  46. read_result={"is_empty": True, "text": "", "metadata": {"decode_error": message}},
  47. gate_result={"passed": False, "reason": "decode_failed", "details": {"error": message}},
  48. framing_result={"error": message},
  49. status="failed",
  50. )
  51. return failure
  52. def run_decode_stage(
  53. *,
  54. candidate_repo: DecodeCandidateRepository,
  55. decode_service: DecodeService,
  56. run_id: UUID | None = None,
  57. limit: int = 100,
  58. decoded_item_ids: set[str] | None = None,
  59. ) -> DecodeBatchResult:
  60. items = dedupe_candidate_items(candidate_repo.list_creation_candidate_items(run_id=run_id, limit=limit))
  61. outputs: list[DecodeWorkflowOutput] = []
  62. failures: list[dict[str, str]] = []
  63. decoded = skipped = failed = 0
  64. for item in items:
  65. if item.id is None:
  66. skipped += 1
  67. continue
  68. decision = should_decode_item(item, decoded_item_ids=decoded_item_ids)
  69. if not decision.keep:
  70. skipped += 1
  71. continue
  72. try:
  73. media = candidate_repo.list_media_assets_for_item(item.id)
  74. post = post_from_candidate_item(item, media)
  75. outputs.append(decode_service.decode_post(item_id=item.id, post=post))
  76. decoded += 1
  77. except Exception as exc:
  78. failed += 1
  79. failures.append(_record_failure(decode_service, item, exc))
  80. return DecodeBatchResult(
  81. total=len(items),
  82. decoded=decoded,
  83. skipped=skipped,
  84. failed=failed,
  85. outputs=outputs,
  86. failures=failures,
  87. )