feat!: Add AI online evaluations (Judge, Evaluator, IRunner)#301
feat!: Add AI online evaluations (Judge, Evaluator, IRunner)#301mattrmc1 wants to merge 14 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
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 2562911. Configure here.
| 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.


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 optional API is backward compatible, but judge evaluation triggers extra model calls and changes JudgeResult default semantics for code that relied on implicit success/sampled=true.
Overview
Adds online AI evaluations to the server AI SDK: completion and agent configs can run configured judges against model input/output when callers supply a
runnerFactoryonLdAiClient.LdAiClientgains an optionalFunc<LdAiJudgeConfig, IRunner> runnerFactory.ConfigFactoryuses it in newBuildEvaluatorlogic: for each entry injudgeConfiguration, it fetches the judge flag, builds aJudge+IRunner, and attaches anEvaluatorto completion/agent configs (including default/fallback paths). Missing factory, empty judges, disabled judges, null runners, and init errors degrade toEvaluator.Noop()or skipped judges with logging—no throw. Judge prompt variables from the parent config are passed through; agent graph builds passgraphKeyfor judge tracking.New public surface:
IRunner,RunnerResult,Judge,Evaluator.LdAiConfig.Evaluatoris always non-null (noop when unused).JudgeResultgains optionalErrorMessage/Reasoningand spec-aligned defaults (Sampled/Successdefault false).samplingRateon judge config isdouble?(omit = 100%); parsing and defaultToLdValueonly emit rate when set.Judgeruns the model via structured score/reasoning schema, sampling, score validation, and metrics via the config tracker; it does not callTrackJudgeResult(caller responsibility). ExtensiveJudgeTest/EvaluatorTestand config integration tests added.Reviewed by Cursor Bugbot for commit 2562911. Bugbot is set up for automated code reviews on this repo. Configure here.