-
Notifications
You must be signed in to change notification settings - Fork 9
feat!: Add AI online evaluations (Judge, Evaluator, IRunner) #301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a625933
db62ad3
a680fb4
f920e9d
aa959ce
b814097
66e2c31
f5f064e
3bea03d
b3bad64
c06c4b9
6f7d88d
fae8b52
2562911
636858c
2f97f42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| using System.Collections.Generic; | ||
| using System.Collections.Immutable; | ||
| using System.Linq; | ||
| using LaunchDarkly.Sdk.Server.Ai.Evals; | ||
| using LaunchDarkly.Sdk.Server.Ai.Interfaces; | ||
| using Mustache; | ||
|
|
||
|
|
@@ -21,11 +22,14 @@ internal sealed class ConfigFactory | |
|
|
||
| private readonly ILaunchDarklyClient _client; | ||
| private readonly ILogger _logger; | ||
| private readonly Func<LdAiJudgeConfig, IRunner> _runnerFactory; | ||
|
|
||
| public ConfigFactory(ILaunchDarklyClient client, ILogger logger) | ||
| public ConfigFactory(ILaunchDarklyClient client, ILogger logger, | ||
| Func<LdAiJudgeConfig, IRunner> runnerFactory = null) | ||
| { | ||
| _client = client; | ||
| _logger = logger; | ||
| _runnerFactory = runnerFactory; | ||
| } | ||
|
|
||
| public LdAiCompletionConfig BuildCompletionConfig( | ||
|
|
@@ -44,7 +48,7 @@ public LdAiCompletionConfig BuildCompletionConfig( | |
| _logger.Error( | ||
| "AI Config '{0}': variation result is not an object (got {1}); using caller's default.", | ||
| key, ldValue.Type); | ||
| return BuildCompletionFromDefault(key, defaultValue, mergedVars, trackerFactory, interpolate); | ||
| return BuildCompletionFromDefault(key, defaultValue, mergedVars, trackerFactory, context, variables, interpolate); | ||
| } | ||
|
|
||
| var (enabled, variationKey, version, mode) = ParseMeta(ldValue); | ||
|
|
@@ -54,7 +58,7 @@ public LdAiCompletionConfig BuildCompletionConfig( | |
| _logger.Warn( | ||
| "AI Config mode mismatch for {0}: expected {1}, got {2}. Returning caller's default.", | ||
| key, LdAiCompletionConfig.Mode, mode); | ||
| return BuildCompletionFromDefault(key, defaultValue, mergedVars, trackerFactory, interpolate); | ||
| return BuildCompletionFromDefault(key, defaultValue, mergedVars, trackerFactory, context, variables, interpolate); | ||
| } | ||
|
|
||
| var model = ParseModel(ldValue.Get("model")); | ||
|
|
@@ -64,6 +68,7 @@ public LdAiCompletionConfig BuildCompletionConfig( | |
| : ParseMessages(ldValue.Get("messages")); | ||
| var tools = ParseTools(ldValue.Get("tools")); | ||
| var judgeConfiguration = ParseJudgeConfiguration(ldValue.Get("judgeConfiguration")); | ||
| var evaluator = BuildEvaluator(judgeConfiguration, context, variables); | ||
|
|
||
| return new LdAiCompletionConfig( | ||
| key, | ||
|
|
@@ -75,21 +80,25 @@ public LdAiCompletionConfig BuildCompletionConfig( | |
| judgeConfiguration, | ||
| model, | ||
| provider, | ||
| trackerFactory); | ||
| trackerFactory, | ||
| evaluator); | ||
| } | ||
|
|
||
| private LdAiCompletionConfig BuildCompletionFromDefault( | ||
| string key, | ||
| LdAiCompletionConfigDefault defaultValue, | ||
| IReadOnlyDictionary<string, object> mergedVars, | ||
| Func<LdAiConfig, ILdAiConfigTracker> trackerFactory, | ||
| Context context, | ||
| IReadOnlyDictionary<string, object> variables = null, | ||
| bool interpolate = true) | ||
| { | ||
| // Caller-supplied default messages can contain Mustache templates too; interpolate | ||
| // with the same per-message fallback as server-returned configs. | ||
| var messages = interpolate | ||
| ? InterpolateMessages(defaultValue.Messages, mergedVars, key) | ||
| : (defaultValue.Messages ?? new List<LdAiConfigTypes.Message>()); | ||
| var evaluator = BuildEvaluator(defaultValue.JudgeConfiguration, context, variables); | ||
| return new LdAiCompletionConfig( | ||
| key, | ||
| defaultValue.Enabled ?? true, | ||
|
|
@@ -100,7 +109,8 @@ private LdAiCompletionConfig BuildCompletionFromDefault( | |
| defaultValue.JudgeConfiguration, | ||
| defaultValue.Model, | ||
| defaultValue.Provider, | ||
| trackerFactory); | ||
| trackerFactory, | ||
| evaluator); | ||
| } | ||
|
|
||
| public LdAiAgentConfig BuildAgentConfig( | ||
|
|
@@ -120,7 +130,7 @@ public LdAiAgentConfig BuildAgentConfig( | |
| _logger.Error( | ||
| "AI Config '{0}': variation result is not an object (got {1}); using caller's default.", | ||
| key, ldValue.Type); | ||
| return BuildAgentFromDefault(key, defaultValue, mergedVars, trackerFactory, interpolate); | ||
| return BuildAgentFromDefault(key, defaultValue, mergedVars, trackerFactory, context, variables, interpolate, graphKey); | ||
| } | ||
|
|
||
| var (enabled, variationKey, version, mode) = ParseMeta(ldValue); | ||
|
|
@@ -130,7 +140,7 @@ public LdAiAgentConfig BuildAgentConfig( | |
| _logger.Warn( | ||
| "AI Config mode mismatch for {0}: expected {1}, got {2}. Returning caller's default.", | ||
| key, LdAiAgentConfig.Mode, mode); | ||
| return BuildAgentFromDefault(key, defaultValue, mergedVars, trackerFactory, interpolate); | ||
| return BuildAgentFromDefault(key, defaultValue, mergedVars, trackerFactory, context, variables, interpolate, graphKey); | ||
| } | ||
|
|
||
| var model = ParseModel(ldValue.Get("model")); | ||
|
|
@@ -140,6 +150,7 @@ public LdAiAgentConfig BuildAgentConfig( | |
| ? InterpolateInstructions(ParseInstructions(ldValue.Get("instructions")), mergedVars, key) | ||
| : ParseInstructions(ldValue.Get("instructions")); | ||
| var judgeConfiguration = ParseJudgeConfiguration(ldValue.Get("judgeConfiguration")); | ||
| var evaluator = BuildEvaluator(judgeConfiguration, context, variables, graphKey); | ||
|
|
||
| return new LdAiAgentConfig( | ||
| key, | ||
|
|
@@ -151,19 +162,24 @@ public LdAiAgentConfig BuildAgentConfig( | |
| model, | ||
| provider, | ||
| judgeConfiguration, | ||
| trackerFactory); | ||
| trackerFactory, | ||
| evaluator); | ||
| } | ||
|
|
||
| internal LdAiAgentConfig BuildAgentFromDefault( | ||
| string key, | ||
| LdAiAgentConfigDefault defaultValue, | ||
| IReadOnlyDictionary<string, object> mergedVars, | ||
| Func<LdAiConfig, ILdAiConfigTracker> trackerFactory, | ||
| bool interpolate = true) | ||
| Context context, | ||
| IReadOnlyDictionary<string, object> variables = null, | ||
| bool interpolate = true, | ||
| string graphKey = null) | ||
| { | ||
| var instructions = interpolate | ||
| ? InterpolateInstructions(defaultValue.Instructions, mergedVars, key) | ||
| : defaultValue.Instructions; | ||
| var evaluator = BuildEvaluator(defaultValue.JudgeConfiguration, context, variables, graphKey); | ||
| return new LdAiAgentConfig( | ||
| key, | ||
| defaultValue.Enabled ?? true, | ||
|
|
@@ -174,7 +190,8 @@ internal LdAiAgentConfig BuildAgentFromDefault( | |
| defaultValue.Model, | ||
| defaultValue.Provider, | ||
| defaultValue.JudgeConfiguration, | ||
| trackerFactory); | ||
| trackerFactory, | ||
| evaluator); | ||
| } | ||
|
|
||
| public LdAiJudgeConfig BuildJudgeConfig( | ||
|
|
@@ -183,10 +200,11 @@ public LdAiJudgeConfig BuildJudgeConfig( | |
| Context context, | ||
| LdAiJudgeConfigDefault defaultValue, | ||
| IReadOnlyDictionary<string, object> variables, | ||
| bool interpolate = true) | ||
| bool interpolate = true, | ||
| string graphKey = null) | ||
| { | ||
| var mergedVars = interpolate ? MergeVariables(variables, context) : null; | ||
| var trackerFactory = TrackerFactoryFor(context); | ||
| var trackerFactory = TrackerFactoryFor(context, graphKey); | ||
|
|
||
| if (ldValue.Type != LdValueType.Object) | ||
| { | ||
|
|
@@ -247,6 +265,49 @@ private LdAiJudgeConfig BuildJudgeFromDefault( | |
| trackerFactory); | ||
| } | ||
|
|
||
| private Evaluator BuildEvaluator(LdAiConfigTypes.JudgeConfiguration judgeConfiguration, Context context, | ||
| IReadOnlyDictionary<string, object> variables = null, string graphKey = null) | ||
| { | ||
| if (_runnerFactory == null || judgeConfiguration == null || judgeConfiguration.Judges.Count == 0) | ||
| { | ||
| return Evaluator.Noop(); | ||
| } | ||
|
|
||
| var judges = new Dictionary<string, Judge>(); | ||
| foreach (var judgeEntry in judgeConfiguration.Judges) | ||
| { | ||
| try | ||
| { | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Judge sampling ignores interpolate flagMedium Severity
Additional Locations (2)Reviewed by Cursor Bugbot for commit 6f7d88d. Configure here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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"
}
] |
||
|
|
||
| if (!judgeConfig.Enabled) | ||
| { | ||
| _logger?.Debug("Judge '{0}' is disabled; skipping.", judgeEntry.Key); | ||
| continue; | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| var runner = _runnerFactory(judgeConfig); | ||
| if (runner == null) | ||
| { | ||
| _logger?.Warn("Runner factory returned null for judge '{0}'; skipping.", judgeEntry.Key); | ||
| continue; | ||
| } | ||
|
|
||
| judges[judgeEntry.Key] = new Judge(judgeConfig, runner, _logger); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger?.Warn("Failed to initialize judge '{0}': {1}", judgeEntry.Key, ex.Message); | ||
| } | ||
| } | ||
|
|
||
| var filteredConfig = new LdAiConfigTypes.JudgeConfiguration( | ||
| judgeConfiguration.Judges.Where(j => judges.ContainsKey(j.Key)).ToList()); | ||
| return new Evaluator(judges, filteredConfig, _logger); | ||
| } | ||
|
|
||
| private string InterpolateInstructions( | ||
| string instructions, | ||
| IReadOnlyDictionary<string, object> mergedVars, | ||
|
|
@@ -401,9 +462,13 @@ internal static LdAiConfigTypes.JudgeConfiguration ParseJudgeConfiguration(LdVal | |
| { | ||
| var j = judgesArray.Get(i); | ||
| if (j.Type != LdValueType.Object) continue; | ||
| var samplingRateValue = j.Get("samplingRate"); | ||
| double? samplingRate = samplingRateValue.Type == LdValueType.Number | ||
| ? samplingRateValue.AsDouble | ||
| : (double?)null; | ||
| entries.Add(new LdAiConfigTypes.JudgeConfiguration.Judge( | ||
| j.Get("key").AsString ?? "", | ||
| j.Get("samplingRate").AsDouble)); | ||
| samplingRate)); | ||
| } | ||
| return new LdAiConfigTypes.JudgeConfiguration(entries); | ||
| } | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Template fetch builds live evaluators
Medium Severity
CompletionConfigTemplateandAgentConfigTemplatecall the factory withinterpolate: false, butBuildCompletionConfigandBuildAgentConfigalways invokeBuildEvaluatorregardless of that flag. With a non-nullrunnerFactory, template retrieval still evaluates judge flags, interpolates judge prompts, and attaches a runnableEvaluatorwhile completion/agent content stays un-interpolated.Additional Locations (2)
pkgs/sdk/server-ai/src/Config/ConfigFactory.cs#L152-L153pkgs/sdk/server-ai/src/Config/ConfigFactory.cs#L100-L101Reviewed by Cursor Bugbot for commit 636858c. Configure here.