Make EasyCom adjudication resumable

This commit is contained in:
Xin Wang
2026-09-15 03:25:12 +08:00
parent 31d3fc1757
commit e2c305facc
3 changed files with 443 additions and 53 deletions
+145 -1
View File
@@ -3,18 +3,45 @@
from __future__ import annotations
import unittest
from unittest import mock
from pathlib import Path
import json
import tempfile
from adjudicate_easycom_eot_int_candidates import (
PROMPT_VERSION, SYSTEM_PROMPT, _progress_line, _transport_rows, build_user_prompt,
APIRequestError, BatchValidationError, PROMPT_VERSION, SYSTEM_PROMPT,
_progress_line, _transport_rows, add_suggestions, build_user_prompt,
exclude_reason_group, read_candidates,
requires_semantic_adjudication, validate_decisions,
)
class EntryINTAdjudicationTests(unittest.TestCase):
@staticmethod
def _rows(count: int) -> list[dict[str, object]]:
return [
{"uid": f"u{index}", "candidate_source": "WEARER_ONSET", "task": "wearer_entry"}
for index in range(count)
]
@staticmethod
def _decisions(rows: list[dict[str, object]]) -> list[dict[str, object]]:
return [
{
"uid": row["uid"],
"verdict": "EXCLUDE",
"event_type": None,
"subtype": None,
"exclude_reason": "INSUFFICIENT_EVIDENCE",
"exclude_reason_group": "INSUFFICIENT_EVIDENCE",
"selected_opportunity_uid": None,
"confidence": 0.5,
"reason": "Needs human context.",
"needs_human_review": True,
}
for row in rows
]
def test_progress_line_reports_items_percent_pass_and_elapsed_time(self) -> None:
line = _progress_line(25, 100, label="pass 1/2 · batch 4/13", elapsed_seconds=65)
self.assertIn("25/100 ( 25.0%)", line)
@@ -258,6 +285,123 @@ class EntryINTAdjudicationTests(unittest.TestCase):
self.assertFalse(requires_semantic_adjudication({"candidate_source": "WEARER_ONSET_CONTROL"}))
self.assertFalse(requires_semantic_adjudication({"candidate_source": "WEARER_RESUME"}))
def test_validation_failure_recursively_splits_batch_to_singletons(self) -> None:
rows = self._rows(4)
batch_sizes: list[int] = []
def fake_post(**kwargs):
batch = list(kwargs["rows"])
batch_sizes.append(len(batch))
if len(batch) > 1:
raise BatchValidationError("one item has an invalid subtype")
return self._decisions(batch), {"total_tokens": 10}
with tempfile.TemporaryDirectory() as temporary, mock.patch(
"adjudicate_easycom_eot_int_candidates._post", side_effect=fake_post
):
output, pending, usage = add_suggestions(
rows,
api_key="test",
model="model",
base_url="https://example.test/v1",
passes=1,
batch_size=4,
timeout_seconds=1,
max_retries=0,
checkpoint_path=Path(temporary) / "checkpoint.json",
)
self.assertEqual(batch_sizes, [4, 2, 1, 1, 2, 1, 1])
self.assertEqual(len(output), 4)
self.assertEqual(pending, [])
self.assertEqual(usage["total_tokens"], 40)
def test_singleton_validation_failure_goes_to_pending_queue(self) -> None:
rows = self._rows(1)
with tempfile.TemporaryDirectory() as temporary, mock.patch(
"adjudicate_easycom_eot_int_candidates._post",
side_effect=BatchValidationError("invalid singleton label"),
):
checkpoint = Path(temporary) / "checkpoint.json"
pending_path = Path(temporary) / "pending.jsonl"
output, pending, _ = add_suggestions(
rows,
api_key="test",
model="model",
base_url="https://example.test/v1",
passes=2,
batch_size=8,
timeout_seconds=1,
max_retries=0,
checkpoint_path=checkpoint,
pending_output_path=pending_path,
)
state = json.loads(checkpoint.read_text(encoding="utf-8"))
saved_pending = [
json.loads(line)
for line in pending_path.read_text(encoding="utf-8").splitlines()
]
self.assertEqual(output, [])
self.assertEqual([row["uid"] for row in pending], ["u0"])
self.assertIn("invalid singleton label", pending[0]["llm_adjudication_failure"]["error"])
self.assertEqual(state["status"], "complete")
self.assertIn("u0", state["pending"])
self.assertEqual([row["uid"] for row in saved_pending], ["u0"])
def test_resume_skips_batches_already_saved_in_checkpoint(self) -> None:
rows = self._rows(4)
with tempfile.TemporaryDirectory() as temporary:
checkpoint = Path(temporary) / "checkpoint.json"
first_sizes: list[int] = []
def interrupted_post(**kwargs):
batch = list(kwargs["rows"])
first_sizes.append(len(batch))
if len(first_sizes) == 2:
raise APIRequestError("temporary provider outage")
return self._decisions(batch), {"total_tokens": 20}
with mock.patch(
"adjudicate_easycom_eot_int_candidates._post", side_effect=interrupted_post
), self.assertRaises(APIRequestError):
add_suggestions(
rows,
api_key="test",
model="model",
base_url="https://example.test/v1",
passes=1,
batch_size=2,
timeout_seconds=1,
max_retries=0,
checkpoint_path=checkpoint,
)
resumed_uids: list[list[str]] = []
def resumed_post(**kwargs):
batch = list(kwargs["rows"])
resumed_uids.append([str(row["uid"]) for row in batch])
return self._decisions(batch), {"total_tokens": 20}
with mock.patch(
"adjudicate_easycom_eot_int_candidates._post", side_effect=resumed_post
):
output, pending, usage = add_suggestions(
rows,
api_key="test",
model="model",
base_url="https://example.test/v1",
passes=1,
batch_size=2,
timeout_seconds=1,
max_retries=0,
checkpoint_path=checkpoint,
)
self.assertEqual(first_sizes, [2, 2])
self.assertEqual(resumed_uids, [["u2", "u3"]])
self.assertEqual(len(output), 4)
self.assertEqual(pending, [])
self.assertEqual(usage["total_tokens"], 40)
if __name__ == "__main__":
unittest.main()