feat!: Add AI online evaluations (Judge, Evaluator, IRunner)#301
feat!: Add AI online evaluations (Judge, Evaluator, IRunner)#301mattrmc1 wants to merge 16 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.
If there are duplicates, it would be a bug in the server as it should never allow duplicates. I don't think there is any harm in being defensive about it, but I also don't think its really necessary. I would agree this is low severity and may not be worth the extra check.
| 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.
| : 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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 5 total unresolved issues (including 4 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 2f97f42. Configure here.
| if ((random ?? new Random()).NextDouble() > effectiveRate) | ||
| { | ||
| return new JudgeResult(sampled: false, judgeConfigKey: Config.Key); | ||
| } |
There was a problem hiding this comment.
Zero sampling still runs judges
Medium Severity
When samplingRate normalizes to 0.0, evaluation is skipped only if NextDouble() is greater than zero. Because Random.NextDouble() can return exactly 0.0, a judge configured with zero sampling may still invoke the runner and charge provider usage, contradicting “never evaluate” semantics and tests such as EvaluateAsync_ExplicitZeroSamplingRate_AlwaysSkips.
Reviewed by Cursor Bugbot for commit 2f97f42. Configure here.
|
Converting to draft for now |
tanderson-ld
left a comment
There was a problem hiding this comment.
Not reviewing yet in draft.


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 path triggers extra model calls for judges and changes
JudgeResultdefault semantics, which can affect tracking behavior for existing callers.Overview
Adds AI online evaluations to the server AI SDK: callers can pass an optional
runnerFactoryintoLdAiClient, andConfigFactorybuilds a realEvaluatorfrom each flag’sjudgeConfiguration(fetching judge configs, skipping disabled/null/failed judges) orEvaluator.Noop()when no factory or no judges.New public surface:
IRunner,RunnerResult,Judge(model call with structured score/reasoning schema, sampling, validation), andEvaluator(EvaluateAsyncover configured judges; does not callTrackJudgeResult). Completion and agent configs (including default/fallback paths and graph agents) expose a non-nullEvaluatoronLdAiConfig; judge configs always get noop.Config/parsing tweaks:
JudgeConfiguration.Judge.SamplingRateisdouble?(omit/null → evaluate 100%); defaults only serializesamplingRatewhen set. Judge prompt variables from the parent config are forwarded when building nested judge configs.Breaking (
JudgeResult): defaults forSampledandSuccessflip tofalse;MetricKey/Scoreoptional; newErrorMessageandReasoningfields per AIEVALS.Reviewed by Cursor Bugbot for commit 2f97f42. Bugbot is set up for automated code reviews on this repo. Configure here.