Skip to content

Add blueprint judge service#381

Merged
txmed82 merged 2 commits into
masterfrom
codex/blueprint-judge
Jun 4, 2026
Merged

Add blueprint judge service#381
txmed82 merged 2 commits into
masterfrom
codex/blueprint-judge

Conversation

@txmed82

@txmed82 txmed82 commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a BYOK blueprint judge service using the judge role policy
  • canonicalize judge report identity and persist reports plus generation attempts
  • cover success, missing-policy, and failed-attempt audit paths

Tests

  • .venv/bin/python -m pytest tests/test_blueprint_judge.py -q
  • .venv/bin/python -m ruff check src/casecrawler/generation/blueprint_judge.py tests/test_blueprint_judge.py
  • .venv/bin/python -m pytest tests/test_blueprint_judge.py tests/test_blueprint_validator.py tests/test_blueprint_storage.py -q
  • .venv/bin/python -m pytest -q -m 'not optional_backend and not network and not slow'

Summary by CodeRabbit

  • New Features

    • Added an AI-powered blueprint judge that evaluates clinical blueprints and returns structured evaluation reports.
    • Added atomic persistence for judge reports together with their audit attempts.
  • Bug Fixes / Reliability

    • Improved error handling and auditing for failed evaluations to ensure failures are recorded without masking original errors.
  • Tests

    • Added tests covering success, missing policy, provider failures, and persistence edge cases.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds BlueprintJudge to evaluate ClinicalBlueprints with an injected LLM provider: builds deterministic prompts (compact sorted JSON + fixed system prompt), hashes prompt payload, calls generate_structured() for a JudgeReport, canonicalizes the report, and atomically persists JudgeReport and GenerationAttempt records with best-effort failure auditing. Includes tests covering success and multiple failure modes.

Changes

BlueprintJudge component

Layer / File(s) Summary
Type contract and imports
src/casecrawler/generation/blueprint_judge.py
Module imports, logger, and ProviderFactory type alias for provider construction.
Judge.evaluate orchestration
src/casecrawler/generation/blueprint_judge.py
BlueprintJudge.evaluate() selects judge policy, builds prompt and prompt hash, calls provider.generate_structured(), canonicalizes the JudgeReport, and persists succeeded/failed GenerationAttempt and JudgeReport as appropriate.
Prompt building & canonicalization
src/casecrawler/generation/blueprint_judge.py
_build_prompt() serializes blueprint as compact sorted JSON and composes instructions; _prompt_hash() computes SHA-256 over deterministic payload; _canonicalize_report() re-validates and injects report_id, artifact ids, and forces GenerationRole.JUDGE; _JUDGE_SYSTEM_PROMPT constrains judge behavior.
GenerationAttempt creation & best-effort auditing
src/casecrawler/generation/blueprint_judge.py
_attempt() builds attempt records with provider/model, prompt hash, token counts and errors; _missing_policy_attempt() and _save_failed_attempt_best_effort() create and persist failed attempts while swallowing persistence errors.
Atomic storage helper
src/casecrawler/storage/dataset_store.py
DatasetStore.save_judge_report_with_attempt() inserts/replaces judge_reports and generation_attempts under the write lock and commits, rolling back on exception.
Test doubles and fixtures
tests/test_blueprint_judge.py
FakeJudgeProvider, FailingJudgeProvider, BrokenAttemptStore, and helper factories for BlueprintGenerationRequest, ClinicalBlueprint, and JudgeReport.
Behavioral tests: success & failure
tests/test_blueprint_judge.py, tests/test_blueprint_storage.py
Async tests verifying successful evaluation persists report+attempt, missing-policy raises and (with store) audits a failed attempt, provider failure persists failed attempt, and storage-audit failure preserves original provider error; storage test verifies atomic save round-trip.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • txmed82/case-crawler#372: Related dataset-store persistence APIs and tables referenced by the new DatasetStore.save_judge_report_with_attempt and judge-report/tests.

Poem

🐰 I hop through blueprints, prompt hashed tight,
I ask the judge LLM to judge what’s right,
Reports get canonicalized and stored away,
Attempts counted, even when things fray,
A bunny applauds the tests that saved the day.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add blueprint judge service' directly and clearly describes the main change: introduction of a new BlueprintJudge module that evaluates clinical blueprints.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/blueprint-judge

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/casecrawler/generation/blueprint_judge.py`:
- Around line 56-65: The current audit write inside the exception handler for
generation failures can raise and replace the original provider exception;
update the block that calls store.save_generation_attempt(...) (the call that
builds self._attempt with GenerationAttemptStatus.FAILED and errors=[str(err)])
so the audit write is best-effort: perform the save_generation_attempt in its
own try/except that catches and logs/storage-errors (or records them separately)
but does not raise or overwrite the original exception—ensure the original
exception (err) is re-raised or propagated after attempting the audit write and
only the audit failure is suppressed/handled.
- Around line 68-79: The two separate writes (store.save_judge_report and
store.save_generation_attempt) can leave a persisted JudgeReport without its
GenerationAttempt on failure; change the call site to use an atomic API on the
store instead: construct the GenerationAttempt via self._attempt(...) with
status GenerationAttemptStatus.SUCCEEDED and call a single transactional method
(e.g. store.save_judge_report_with_attempt(report, attempt) or use
store.transaction(...) to perform both saves inside one transaction) so both
records are persisted atomically; update or add that atomic method to the store
implementation to perform the two inserts within a DB transaction.
- Around line 41-42: The code currently hashes only the user prompt
(prompt_hash) which will not change when _JUDGE_SYSTEM_PROMPT or other model
inputs change; update the hashing to include the full model input (at minimum
include self._JUDGE_SYSTEM_PROMPT plus the prompt returned by _build_prompt, or
better yet serialize the complete messages/parameters used for the model call)
and compute the sha256 over that combined payload; locate the prompt_hash
calculation(s) in blueprint_judge.py (the prompt = self._build_prompt(...) /
prompt_hash = ... site and the second occurrence around line 48) and replace the
single-prompt hash with a deterministic serialization of system prompt + prompt
(and any model params) before hashing so persisted attempts reflect any
system-prompt changes.
- Around line 36-38: The code raises ValueError when
request.policy_for(GenerationRole.JUDGE) returns None but does not persist an
audit when a store is provided; update the branch to, when store is not None,
write a failed-judge audit record (e.g., call store.save_audit(...) or
store.record_audit(...) with details like request id, role=GenerationRole.JUDGE,
reason="missing_policy") before raising the ValueError so the missing-policy
failure is reconstructible from the audit trail; keep the existing raise
ValueError after the persistence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bfcbb1a2-bdbd-45e1-afaa-c6e394a1e8e2

📥 Commits

Reviewing files that changed from the base of the PR and between 53b5ef3 and b86b811.

📒 Files selected for processing (2)
  • src/casecrawler/generation/blueprint_judge.py
  • tests/test_blueprint_judge.py

Comment thread src/casecrawler/generation/blueprint_judge.py
Comment thread src/casecrawler/generation/blueprint_judge.py Outdated
Comment thread src/casecrawler/generation/blueprint_judge.py
Comment thread src/casecrawler/generation/blueprint_judge.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
tests/test_blueprint_judge.py (3)

105-107: ⚡ Quick win

Assert the factory receives the configured provider/model.

This test proves the generate_structured() call uses the right schema and temperature, but it does not pin down the BYOK selection path itself. If BlueprintJudge.evaluate() stopped passing policy.provider / policy.model into provider_factory, this would still go green.

Suggested test hardening
     store = DatasetStore(db_path=str(tmp_path / "datasets.db"))
     provider = FakeJudgeProvider(_raw_report())
-    judge = BlueprintJudge(provider_factory=lambda provider_name, model: provider)
+    factory_calls = []
+
+    def provider_factory(provider_name, model):
+        factory_calls.append((provider_name, model))
+        return provider
+
+    judge = BlueprintJudge(provider_factory=provider_factory)
@@
     assert attempts[0].model == "gpt-4.1-mini"
     assert attempts[0].artifact_id == "bp-1"
     assert attempts[0].total_tokens == 110
     assert attempts[0].prompt_hash
+    assert factory_calls == [("openai", "gpt-4.1-mini")]
     assert provider.calls[0]["schema"] is JudgeReport
     assert provider.calls[0]["kwargs"]["temperature"] == 0.0
     assert "bp-1" in provider.calls[0]["prompt"]

Also applies to: 129-132

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_blueprint_judge.py` around lines 105 - 107, The test currently
passes a provider_factory lambda that always returns FakeJudgeProvider, which
doesn't assert the provider/model args; modify the test to supply a
provider_factory implementation that captures or asserts its inputs
(provider_name and model) against the configured policy values before returning
the FakeJudgeProvider so BlueprintJudge(provider_factory=...) (and specifically
BlueprintJudge.evaluate()) is verified to forward policy.provider and
policy.model; apply the same change to the other instance at lines 129-132
referencing DatasetStore, FakeJudgeProvider, and provider_factory.

43-45: ⚡ Quick win

Make the failed-audit test prove the audit write was attempted.

Right now this passes even if evaluate() never calls save_generation_attempt() at all, because the original provider error would still be raised. Recording one call on BrokenAttemptStore would turn this into a real regression test for the best-effort audit path.

Suggested test hardening
 class BrokenAttemptStore:
+    def __init__(self) -> None:
+        self.save_attempt_calls = 0
+
     def save_generation_attempt(self, attempt):
+        self.save_attempt_calls += 1
         raise RuntimeError("storage boom")
@@
 async def test_blueprint_judge_preserves_provider_error_when_failed_audit_fails():
     from casecrawler.generation.blueprint_judge import BlueprintJudge
 
     judge = BlueprintJudge(
         provider_factory=lambda provider_name, model: FailingJudgeProvider()
     )
+    store = BrokenAttemptStore()
 
     with pytest.raises(RuntimeError, match="judge boom"):
-        await judge.evaluate(_request(), _blueprint(), store=BrokenAttemptStore())
+        await judge.evaluate(_request(), _blueprint(), store=store)
+    assert store.save_attempt_calls == 1

Also applies to: 199-208

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_blueprint_judge.py` around lines 43 - 45, The test's
BrokenAttemptStore should record that save_generation_attempt was invoked so the
failure proves the audit write was attempted: modify BrokenAttemptStore to set a
flag or increment a counter in save_generation_attempt (e.g., self.called = True
or self.call_count += 1), run evaluate() as before (expecting the provider
error), and after the exception assert that the flag/counter indicates at least
one call; reference the BrokenAttemptStore class and its save_generation_attempt
method and the evaluate() call in the test to locate where to add the assertion.

189-196: ⚡ Quick win

Also verify provider/model on the failed attempt.

The success path already checks these fields, but the runtime-failure audit path does not. A regression that records failed judge attempts as "unconfigured" would still pass here and weaken the audit trail.

Suggested assertions
     assert len(attempts) == 1
     assert attempts[0].role == GenerationRole.JUDGE
     assert attempts[0].status == GenerationAttemptStatus.FAILED
     assert attempts[0].artifact_id == "bp-1"
+    assert attempts[0].provider == "openai"
+    assert attempts[0].model == "gpt-4.1-mini"
     assert attempts[0].errors == ["judge boom"]
     assert attempts[0].total_tokens == 0
     assert attempts[0].prompt_hash
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_blueprint_judge.py` around lines 189 - 196, The failed-judge audit
assertions are missing checks for provider and model; update the test in
tests/test_blueprint_judge.py (the block that inspects attempts =
store.list_generation_attempts(...)) to assert that attempts[0].provider and
attempts[0].model match the expected provider and model used in this test (same
values asserted on the success path) so runtime-failure attempts aren’t recorded
as "unconfigured".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_blueprint_judge.py`:
- Around line 105-107: The test currently passes a provider_factory lambda that
always returns FakeJudgeProvider, which doesn't assert the provider/model args;
modify the test to supply a provider_factory implementation that captures or
asserts its inputs (provider_name and model) against the configured policy
values before returning the FakeJudgeProvider so
BlueprintJudge(provider_factory=...) (and specifically
BlueprintJudge.evaluate()) is verified to forward policy.provider and
policy.model; apply the same change to the other instance at lines 129-132
referencing DatasetStore, FakeJudgeProvider, and provider_factory.
- Around line 43-45: The test's BrokenAttemptStore should record that
save_generation_attempt was invoked so the failure proves the audit write was
attempted: modify BrokenAttemptStore to set a flag or increment a counter in
save_generation_attempt (e.g., self.called = True or self.call_count += 1), run
evaluate() as before (expecting the provider error), and after the exception
assert that the flag/counter indicates at least one call; reference the
BrokenAttemptStore class and its save_generation_attempt method and the
evaluate() call in the test to locate where to add the assertion.
- Around line 189-196: The failed-judge audit assertions are missing checks for
provider and model; update the test in tests/test_blueprint_judge.py (the block
that inspects attempts = store.list_generation_attempts(...)) to assert that
attempts[0].provider and attempts[0].model match the expected provider and model
used in this test (same values asserted on the success path) so runtime-failure
attempts aren’t recorded as "unconfigured".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: be740bf9-d294-4a1f-9790-451ac1265e0c

📥 Commits

Reviewing files that changed from the base of the PR and between b86b811 and 140f503.

📒 Files selected for processing (4)
  • src/casecrawler/generation/blueprint_judge.py
  • src/casecrawler/storage/dataset_store.py
  • tests/test_blueprint_judge.py
  • tests/test_blueprint_storage.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/casecrawler/generation/blueprint_judge.py

@txmed82 txmed82 merged commit a9fa1fd into master Jun 4, 2026
4 checks passed
@txmed82 txmed82 deleted the codex/blueprint-judge branch June 4, 2026 01:35
@coderabbitai coderabbitai Bot mentioned this pull request Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant