feat(evaluations): add LD judge event support - #63
Draft
donei003 wants to merge 5 commits into
Draft
Conversation
donei003
force-pushed
the
feature/ld-judges-phase3
branch
from
September 2, 2026 20:38
e7fffd7 to
614adca
Compare
Contributor
Author
|
Addressed the feedback in the draft:
Validation after changes:
|
donei003
force-pushed
the
feature/ld-judges-phase3
branch
from
September 2, 2026 21:14
614adca to
6637d48
Compare
Contributor
Author
|
Updated for the usage feedback:
Validation:
|
donei003
force-pushed
the
feature/ld-judges-phase3
branch
from
September 2, 2026 21:56
6637d48 to
6104ae0
Compare
Contributor
Author
|
Updated Validation:
|
donei003
force-pushed
the
feature/ld-judges-phase3
branch
from
September 2, 2026 21:56
6104ae0 to
7ed3d05
Compare
Contributor
Author
|
Updated
Validation:
|
donei003
force-pushed
the
feature/ld-judges-phase3
branch
from
September 2, 2026 22:15
7ed3d05 to
ccfdc0a
Compare
Contributor
Author
|
Updated based on the latest feedback:
Validation:
|
Rename the public run() parameter judges= to criteria= (with JudgeReference -> Criterion and evaluations/judges.py -> criteria.py): the wire format already calls these criteria, and a Scorer is not a judge. Scorer callbacks now receive the public DatasetRow instead of the internal result dict, so internal key renames cannot break customer scorers; Criterion and DatasetRow are exported. Extract the judge response contract (formatting instructions, JSON score parsing, finite-number guard) into judge_scoring.py shared by the online judge path and the offline evaluations runner. The two copies had already drifted textually. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dering
- Resolve LD judges with a valid {kind: evaluation, key: project} context.
The previous empty context is invalid to the real LD SDK, so every
resolution returned the None default and failed as 'not found'. The
resolution error now also carries the underlying cause instead of
always claiming the judge does not exist.
- Validate judge scores when the judge responds: non-JSON output,
non-numeric, non-finite, and out-of-range scores become per-criterion
ERROR events with cause codes (invalid_judge_output, invalid_score,
handler_raised, generation_incomplete, scorer_raised) instead of
crashing the run at event-build time after all LLM spend.
- Pass judge configs to the handler unrendered. The handler owns the
single template pass, so {{...}} sequences inside generated output or
dataset values can no longer be expanded into the judge prompt.
Absent judge variables render as empty strings rather than leaving
literal mustache in the prompt.
- Reject duplicate criterion identities (judge keys / scorer names)
before any records are created; they would share event identity.
- Flush queued generation events in a finally so they reach LaunchDarkly
even when the criteria phase fails.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrite the evaluation event payloads as frozen dataclasses with an explicit to_track_payload(), matching the Usage/RunSummary wire pattern used everywhere else in the SDK, and remove the pydantic>=2 dependency. The models validated the SDK's own dicts, and a validation failure surfaced as a run-aborting crash at emission time; payload construction is now also wrapped per result so one bad criterion result is logged and skipped instead of dropping the whole batch. ERROR events carry a top-level errorMessage for parity with generation events. Run (row x criterion) pairs through the same ConcurrencyController and concurrency parameter the generation phase uses, instead of one criterion at a time: a 200-row dataset with 3 judges was 600 serial LLM calls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Emit per-event telemetry lines through the module logger instead of printing to the host application's stdout, refresh the run() docstring (no longer generation-only), and cover the shared judge response parser with unit tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
JudgeandScorercriterion references for SDK-run evaluations, passed viarun(criteria=[...]){kind: "evaluation"}context) before mutating evaluation/run state; resolution failures fail fast and carry the underlying causeconcurrencyparameter as generation — and emit one$ld:ai:offline-evals:evaluationevent per(row, criterion)resultDesign notes
criteria=(union typeCriterion = Judge | Scorer) to match the wire format (criteria/criterionType); a scorer is not a judge.Scorer.fnreceives the publicDatasetRowdataclass plus the generated output, so internal result-dict keys are not part of the customer contract.parse_templatepass, so{{...}}sequences inside generated output or dataset values are never expanded into judge prompts.invalid_judge_output,invalid_score,handler_raised,scorer_raised,generation_incomplete) and a top-levelerrorMessage, mirroring generation events — it never aborts the run after LLM spend. Queued generation events are flushed in afinally.{score, reasoning}parsing) is shared with the online judge path viajudge_scoring.py.Notes
(row, criterionType), not via batch ingest.error.code(string cause codes here vs. numeric5001on generation events).Validation
uv run ruff format ./uv run ruff check .uv run pytest -q— 1151 passed, 11 skippeduv run mypy packages/client/src/launchdarkly_ai_server/evaluations packages/client/src/launchdarkly_ai_server/judge_scoring.py packages/client/src/launchdarkly_ai_server/judges.py packages/client/src/launchdarkly_ai_server/__init__.pyldclientthat the judge-resolution context is valid (Context.from_dict({"kind": "evaluation", "key": ...}).valid is True)Full
uv run mypy .currently fails before checking due to duplicate testconftestmodule names across packages, which appears unrelated to this change.