feat!: Add AI online evaluations (Judge, Evaluator, IRunner)#301
feat!: Add AI online evaluations (Judge, Evaluator, IRunner)#301mattrmc1 wants to merge 15 commits into
Conversation
…rthy/AIC-2660/aievals-core-judge
…pling normalization
…Key on judge trackers
…rthy/AIC-2660/aievals-core-judge
…unchdarkly/dotnet-core into mmccarthy/AIC-2660/aievals-core-judge
| { | ||
| var defaultValue = LdAiJudgeConfigDefault.Disabled; | ||
| var ldValue = _client.JsonVariation(judgeEntry.Key, context, defaultValue.ToLdValue()); | ||
| var judgeConfig = BuildJudgeConfig(judgeEntry.Key, ldValue, context, defaultValue, variables, graphKey: graphKey); |
There was a problem hiding this comment.
Judge sampling ignores interpolate flag
Medium Severity
BuildEvaluator always calls BuildJudgeConfig with the default interpolate: true, even when the parent config was built with interpolate: false (template APIs). Nested judge configs then run Mustache interpolation (including ldctx) while completion/agent instructions and messages stay as raw templates, so template configs and runnerFactory output can disagree with the documented template behavior.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 6f7d88d. Configure here.
There was a problem hiding this comment.
Not a bug. Attached judges are operational -- they execute against a model provider via IRunner.RunAsync and require interpolated prompts to function. A judge receiving raw {{variable}} tokens would produce nonsense. When fetching a judge directly with interpolation: false, the raw {{variable}} tokens will return correctly
There was a problem hiding this comment.
Messages when fetching toxicity judge as template:
[
{
"content": "You are a business information accuracy and safety expert. Evaluate....",
"role": "system"
},
{
"content": "MESSAGE HISTORY:\n{{message_history}}\n",
"role": "assistant"
},
{
"content": "RESPONSE TO EVALUATE:\n{{response_to_evaluate}}\n",
"role": "user"
}
]|
|
||
| var result = await judge.EvaluateAsync(input, output, judgeEntry.SamplingRate); | ||
| results.Add(result); | ||
| } |
There was a problem hiding this comment.
Duplicate judge keys run twice
Low Severity
If judgeConfiguration.judges lists the same key more than once, BuildEvaluator stores one Judge in a dictionary but builds filteredConfig from the full list without deduplicating. EvaluateAsync then invokes the same judge once per duplicate entry and returns multiple results for one logical judge.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 6f7d88d. Configure here.
There was a problem hiding this comment.
Is this actually a bug? Should the SDK be defensive about this or faithfully evaluate the list it receives (even if duplicates are present)?
cc @jsonbailey
| continue; | ||
| } | ||
|
|
||
| var result = await judge.EvaluateAsync(input, output, judgeEntry.SamplingRate, _random); |
There was a problem hiding this comment.
Shared Random not thread-safe
Medium Severity
Evaluator stores one Random instance and passes it into every concurrent Judge.EvaluateAsync call for sampling. System.Random is not thread-safe, so parallel EvaluateAsync on the same config can corrupt sampling state and yield incorrect skip/run decisions or unstable behavior under load.
Reviewed by Cursor Bugbot for commit 2562911. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 636858c. Configure here.
| : ParseMessages(ldValue.Get("messages")); | ||
| var tools = ParseTools(ldValue.Get("tools")); | ||
| var judgeConfiguration = ParseJudgeConfiguration(ldValue.Get("judgeConfiguration")); | ||
| var evaluator = BuildEvaluator(judgeConfiguration, context, variables); |
There was a problem hiding this comment.
Template fetch builds live evaluators
Medium Severity
CompletionConfigTemplate and AgentConfigTemplate call the factory with interpolate: false, but BuildCompletionConfig and BuildAgentConfig always invoke BuildEvaluator regardless of that flag. With a non-null runnerFactory, template retrieval still evaluates judge flags, interpolates judge prompts, and attaches a runnable Evaluator while completion/agent content stays un-interpolated.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 636858c. Configure here.


BEGIN_COMMIT_OVERRIDE
feat: Add AI online evaluations (Judge, Evaluator, IRunner)
feat!: JudgeResult defaults for sampled and success changed from true to false per AIEVALS spec
END_COMMIT_OVERRIDE
Summary
Adds AI online evaluations to
LaunchDarkly.ServerSdk.Ai. A caller that supplies arunnerFactorytoLdAiClientgets automatic judge evaluation wired into everyCompletionConfigandAgentConfig— each returned config carries anEvaluatorthat can score model output against the judges declared in the flag'sjudgeConfiguration. When norunnerFactoryis provided (or no judges are configured), configs receive a noopEvaluatorso callers never need null checks.Implements the AIEVALS and AIRUNNER specs (sections 1.1–1.4).
createJudge(AIEVALS 1.2) is intentionally omitted per .NET/Java convention — judges are created internally by the SDK, not by user code.New public types
LdAiClientchangesWhen
runnerFactoryis non-null,ConfigFactory.BuildEvaluatoriterates the flag'sjudgeConfiguration, evaluates each judge key as a flag variation, creates aJudge+IRunnerpair per enabled judge, and attaches the resultingEvaluatorto the config. Disabled judges, null runners, and initialization exceptions are logged and skipped — no single judge failure prevents the others from being built.LdAiConfigbase classAll config types (
LdAiCompletionConfig,LdAiAgentConfig,LdAiJudgeConfig) now carry anEvaluatorproperty via the base class.LdAiJudgeConfigalways receivesEvaluator.Noop()(judges don't evaluate themselves).JudgeResultchangesUpdated to match AIEVALS 1.3.1 defaults:
MetricKeynull)Score0.0)SampledtruefalseSuccesstruefalseErrorMessageReasoningNull safety (ref: java-core#175 discussion)
BuildEvaluatorhandles every failure mode without throwing:runnerFactory == nullor emptyjudgeConfiguration→Evaluator.Noop()null→ warn + skip judgeevaluationMetricKey→JudgeResult(success: false, errorMessage: ...)[0, 1]range →JudgeResult(success: false, errorMessage: ...)NaN/Infinity/negative/> 1.0→ normalized to safe boundsJudgeconstructor still usesArgumentNullExceptionguards, but these are never hit in practice becauseBuildEvaluatorvalidates inputs before construction.Migration
None required. The
LdAiClientconstructor gains an optionalrunnerFactoryparameter (defaultnull) — existing callers are unaffected.JudgeResultdefault changes are source-compatible (all parameters are now optional with safe defaults). No members removed or renamed.Test plan
dotnet test pkgs/sdk/server-ai/test/LaunchDarkly.ServerSdk.Ai.Tests.csproj --framework net8.0passesJudgeTest(435 lines) covers: successful evaluation with score/reasoning extraction, sampling skip path,samplingRateedge cases (NaN, negative,> 1.0), runner exception handling, missingevaluationMetricKeyvalidation, out-of-range score rejection witherrorMessage,evaluateMessagesformatting, null/empty message handlingEvaluatorTest(210 lines) covers: noop returns empty list, multi-judge execution with per-judge sampling, missing judge key logs warning and skips, noop does not log warningsLdAiCompletionConfigTest(189 lines added) covers:Evaluatorattached whenrunnerFactoryprovided, noopEvaluatorwhen no runner factory, noop whenjudgeConfigurationis empty, disabled judge skipped, null runner skippedLdAiJudgeConfigTestcovers:JudgeResultErrorMessageandReasoningfields, judge config always receives noopEvaluatorLdAiConfigTrackerTestcovers:TrackJudgeResultwith new optional fieldsNote
Medium Risk
New public API and config-build path that performs extra flag evaluations and optional model calls via IRunner; failures are mostly logged/skipped but behavior and JudgeResult default changes affect metrics emission semantics.
Overview
Adds AI online evaluations to the server AI SDK: completion and agent configs can carry a non-null
Evaluatorthat runs configured judges against model input/output when callers supply an optionalrunnerFactoryonLdAiClient.Wiring:
ConfigFactory.BuildEvaluatorresolves each judge key via flag variation, buildsJudge+IRunnerpairs (skipping disabled judges, null runners, and init failures), and attaches the result to normal and default fallback paths for completion/agent configs. Agent graph builds passgraphKeyinto judge trackers. Without a factory or judges, configs getEvaluator.Noop().New surface:
IRunner,RunnerResult,Judge(structured score/reasoning via runner, sampling, metrics tracking), andEvaluator.EvaluateAsync(does not callTrackJudgeResult—callers emit events).Spec-aligned tweaks:
JudgeResultdefaultsSampled/Successto false; optionalErrorMessage/Reasoning. JudgesamplingRateis nullable (omit = 100%); parsing and defaultToLdValueonly serialize rate when set.Reviewed by Cursor Bugbot for commit 636858c. Bugbot is set up for automated code reviews on this repo. Configure here.