Make EasyCom adjudication resumable
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user