Skip to content

Commit a744d60

Browse files
authored
chore: Add LdAiAgentConfig and LdAiJudgeConfig types with ConfigFactory build methods (2 of 6) (#280)
## Summary Adds the **typed config shapes and factory build methods** for agents and judges. With this change, `ConfigFactory` can produce fully-evaluated `LdAiAgentConfig` and `LdAiJudgeConfig` objects from a flag's JSON. Four new public types slot into the existing `LdAiConfigBase` / `LdAiConfigDefaultBase` parallel hierarchy: - **`LdAiAgentConfig`** extends `LdAiConfigBase`. Adds `Instructions` (interpolated) and `Tools`. Internal constructor — SDK-built only. - **`LdAiAgentConfigDefault`** extends `LdAiConfigDefaultBase`. Public `Builder` with `SetInstructions`, `SetModelParam`, `SetCustomModelParam`, `SetModelName`, `SetModelProviderName`, `Enable` / `Disable`. `New()` factory and `Disabled` helper. `ToLdValue()` serializes to the `_ldMeta { enabled, mode }` envelope that the SDK reads, so a caller default round-trips through `JsonVariation`'s fallback path correctly. - **`LdAiJudgeConfig`** extends `LdAiConfigBase`. Adds `Messages` and `EvaluationMetricKey`. - **`LdAiJudgeConfigDefault`** mirrors the agent default, but the builder takes `AddMessage(content, role)` instead of `SetInstructions`. Each config type carries a `Mode` constant (`"agent"` / `"judge"`) used by its build method to validate `_ldMeta.mode` and fall back to the caller's default on mismatch. ### Factory build methods `ConfigFactory.BuildAgentConfig` and `BuildJudgeConfig` follow the same shape as the existing `BuildCompletionConfig`: 1. Merge user variables with the LD context (the `ldctx` key). 2. Synthesize a tracker factory bound to that context. 3. Reject non-object variations → log error, return caller's default. 4. Reject mode mismatch → log warning, return caller's default. 5. Otherwise parse model/provider plus the type-specific fields (`instructions` + `tools` for agents; `messages` + `evaluationMetricKey` for judges) and interpolate Mustache templates. A new private `InterpolateInstructions` helper mirrors the per-field tolerance of the existing `InterpolateMessages`: a malformed template in `instructions` keeps the raw string and logs a warning rather than dropping the whole config. ### Defaults Both new default builders set `_enabled = true` initially. The base type's `Enabled` is `bool?` so the round-trip through `ToLdValue()` → `JsonVariation` fallback can distinguish "unset" from "explicitly disabled". ## Test plan - [ ] `dotnet build` succeeds across `netstandard2.0`, `net462`, `net8.0` - [ ] `dotnet test --framework net8.0` passes - [ ] `LdAiAgentConfigTest` covers happy path, mode mismatch, non-object variation, instructions interpolation, and `LdAiAgentConfigDefault.ToLdValue` round-trip - [ ] `LdAiJudgeConfigTest` covers the same matrix for judges plus default-type behavior - [ ] Reviewer confirms parallel hierarchy lines up with the cross-SDK pattern (python, ruby) and that builder method names match the Completion side <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Additive SDK surface and factory logic parallel to existing completion handling; extensive tests and safe fallbacks on bad variations. > > **Overview** > Adds **agent** and **judge** AI config types to the server-ai SDK, mirroring the existing completion config pattern. > > **`ConfigFactory`** gains `BuildAgentConfig` and `BuildJudgeConfig`, which parse flag JSON (`_ldMeta.mode` of `agent` / `judge`), merge Mustache variables with LD context, attach trackers, and fall back to caller defaults on non-object variations or mode mismatch. Agents get interpolated **instructions**, **tools**, and optional **judgeConfiguration**; judges get **messages** and **evaluationMetricKey**. **`InterpolateInstructions`** applies the same malformed-template tolerance as message interpolation. > > New public types: **`LdAiAgentConfig`** / **`LdAiAgentConfigDefault`** (builder with `SetInstructions`, `SetJudgeConfiguration`, model fields; `ToLdValue()` for `JsonVariation` fallback) and **`LdAiJudgeConfig`** / **`LdAiJudgeConfigDefault`** (builder with `AddMessage`, metric key). Unit tests cover happy paths, fallbacks, interpolation, judge config round-trip, and default serialization. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit daaa648. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 12c0b7a commit a744d60

7 files changed

Lines changed: 1187 additions & 4 deletions

File tree

pkgs/sdk/server-ai/src/Config/ConfigFactory.cs

Lines changed: 151 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ namespace LaunchDarkly.Sdk.Server.Ai.Config;
1010
/// <summary>
1111
/// Owns the translation from an evaluated <see cref="LdValue"/> to a typed AI Config.
1212
/// Holds the underlying LaunchDarkly client and logger so each per-mode build method
13-
/// (currently only <see cref="BuildCompletionConfig"/>; future agent and judge builders will
14-
/// follow the same pattern) can synthesize a tracker factory and merge in
15-
/// context-derived prompt variables without the public <see cref="LdAiClient"/>
16-
/// having to know any of those details.
13+
/// can synthesize a tracker factory and merge in context-derived prompt variables
14+
/// without the public <see cref="LdAiClient"/> having to know any of those details.
1715
/// </summary>
1816
internal sealed class ConfigFactory
1917
{
@@ -93,6 +91,155 @@ private LdAiCompletionConfig BuildCompletionFromDefault(
9391
trackerFactory);
9492
}
9593

94+
public LdAiAgentConfig BuildAgentConfig(
95+
string key,
96+
LdValue ldValue,
97+
Context context,
98+
LdAiAgentConfigDefault defaultValue,
99+
IReadOnlyDictionary<string, object> variables)
100+
{
101+
var mergedVars = MergeVariables(variables, context);
102+
var trackerFactory = TrackerFactoryFor(context);
103+
104+
if (ldValue.Type != LdValueType.Object)
105+
{
106+
_logger.Error(
107+
"AI Config '{0}': variation result is not an object (got {1}); using caller's default.",
108+
key, ldValue.Type);
109+
return BuildAgentFromDefault(key, defaultValue, mergedVars, trackerFactory);
110+
}
111+
112+
var (enabled, variationKey, version, mode) = ParseMeta(ldValue);
113+
114+
if (mode != LdAiAgentConfig.Mode)
115+
{
116+
_logger.Warn(
117+
"AI Config mode mismatch for {0}: expected {1}, got {2}. Returning caller's default.",
118+
key, LdAiAgentConfig.Mode, mode);
119+
return BuildAgentFromDefault(key, defaultValue, mergedVars, trackerFactory);
120+
}
121+
122+
var model = ParseModel(ldValue.Get("model"));
123+
var provider = ParseProvider(ldValue.Get("provider"));
124+
var tools = ParseTools(ldValue.Get("tools"));
125+
var instructions = InterpolateInstructions(ParseInstructions(ldValue.Get("instructions")), mergedVars, key);
126+
var judgeConfiguration = ParseJudgeConfiguration(ldValue.Get("judgeConfiguration"));
127+
128+
return new LdAiAgentConfig(
129+
key,
130+
enabled,
131+
variationKey,
132+
version,
133+
instructions,
134+
tools,
135+
model,
136+
provider,
137+
judgeConfiguration,
138+
trackerFactory);
139+
}
140+
141+
private LdAiAgentConfig BuildAgentFromDefault(
142+
string key,
143+
LdAiAgentConfigDefault defaultValue,
144+
IReadOnlyDictionary<string, object> mergedVars,
145+
Func<LdAiConfig, ILdAiConfigTracker> trackerFactory)
146+
{
147+
var instructions = InterpolateInstructions(defaultValue.Instructions, mergedVars, key);
148+
return new LdAiAgentConfig(
149+
key,
150+
defaultValue.Enabled ?? true,
151+
variationKey: "",
152+
version: 1,
153+
instructions,
154+
tools: ImmutableDictionary<string, LdAiConfigTypes.Tool>.Empty,
155+
defaultValue.Model,
156+
defaultValue.Provider,
157+
defaultValue.JudgeConfiguration,
158+
trackerFactory);
159+
}
160+
161+
public LdAiJudgeConfig BuildJudgeConfig(
162+
string key,
163+
LdValue ldValue,
164+
Context context,
165+
LdAiJudgeConfigDefault defaultValue,
166+
IReadOnlyDictionary<string, object> variables)
167+
{
168+
var mergedVars = MergeVariables(variables, context);
169+
var trackerFactory = TrackerFactoryFor(context);
170+
171+
if (ldValue.Type != LdValueType.Object)
172+
{
173+
_logger.Error(
174+
"AI Config '{0}': variation result is not an object (got {1}); using caller's default.",
175+
key, ldValue.Type);
176+
return BuildJudgeFromDefault(key, defaultValue, mergedVars, trackerFactory);
177+
}
178+
179+
var (enabled, variationKey, version, mode) = ParseMeta(ldValue);
180+
181+
if (mode != LdAiJudgeConfig.Mode)
182+
{
183+
_logger.Warn(
184+
"AI Config mode mismatch for {0}: expected {1}, got {2}. Returning caller's default.",
185+
key, LdAiJudgeConfig.Mode, mode);
186+
return BuildJudgeFromDefault(key, defaultValue, mergedVars, trackerFactory);
187+
}
188+
189+
var model = ParseModel(ldValue.Get("model"));
190+
var provider = ParseProvider(ldValue.Get("provider"));
191+
var messages = InterpolateMessages(ParseMessages(ldValue.Get("messages")), mergedVars, key);
192+
var evaluationMetricKey = ParseEvaluationMetricKey(ldValue);
193+
194+
return new LdAiJudgeConfig(
195+
key,
196+
enabled,
197+
variationKey,
198+
version,
199+
messages,
200+
evaluationMetricKey,
201+
model,
202+
provider,
203+
trackerFactory);
204+
}
205+
206+
private LdAiJudgeConfig BuildJudgeFromDefault(
207+
string key,
208+
LdAiJudgeConfigDefault defaultValue,
209+
IReadOnlyDictionary<string, object> mergedVars,
210+
Func<LdAiConfig, ILdAiConfigTracker> trackerFactory)
211+
{
212+
var messages = InterpolateMessages(defaultValue.Messages, mergedVars, key);
213+
return new LdAiJudgeConfig(
214+
key,
215+
defaultValue.Enabled ?? true,
216+
variationKey: "",
217+
version: 1,
218+
messages,
219+
defaultValue.EvaluationMetricKey,
220+
defaultValue.Model,
221+
defaultValue.Provider,
222+
trackerFactory);
223+
}
224+
225+
private string InterpolateInstructions(
226+
string instructions,
227+
IReadOnlyDictionary<string, object> mergedVars,
228+
string key)
229+
{
230+
if (instructions == null) return null;
231+
try
232+
{
233+
return InterpolateTemplate(instructions, mergedVars);
234+
}
235+
catch (Exception ex)
236+
{
237+
_logger.Warn(
238+
$"AI Config '{key}': skipping interpolation of malformed instructions template: {ex.Message}");
239+
return instructions;
240+
}
241+
}
242+
96243
private IReadOnlyList<LdAiConfigTypes.Message> InterpolateMessages(
97244
IReadOnlyList<LdAiConfigTypes.Message> messages,
98245
IReadOnlyDictionary<string, object> mergedVars,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using LaunchDarkly.Sdk.Server.Ai.Interfaces;
5+
6+
namespace LaunchDarkly.Sdk.Server.Ai.Config;
7+
8+
/// <summary>
9+
/// Represents an AI Agent Config returned by the SDK, containing model parameters,
10+
/// agent instructions, available tools, and a factory that produces a tracker for
11+
/// reporting events related to model usage.
12+
///
13+
/// Instances of this type are produced by the AI client's agent config method;
14+
/// they are not constructed directly by users. To supply a fallback default to the
15+
/// client, use <see cref="LdAiAgentConfigDefault"/>.
16+
/// </summary>
17+
public sealed class LdAiAgentConfig : LdAiConfig
18+
{
19+
/// <summary>
20+
/// The mode tag emitted in <c>_ldMeta.mode</c> for this config type.
21+
/// </summary>
22+
internal const string Mode = "agent";
23+
24+
/// <summary>
25+
/// The agent's system instructions, which may have been interpolated with Mustache variables.
26+
/// </summary>
27+
public string Instructions { get; }
28+
29+
/// <summary>
30+
/// The tools available to the agent, keyed by tool name.
31+
/// </summary>
32+
public IReadOnlyDictionary<string, LdAiConfigTypes.Tool> Tools { get; }
33+
34+
/// <summary>
35+
/// The judge configuration attached to this agent config, or <c>null</c> if none is configured.
36+
/// </summary>
37+
public LdAiConfigTypes.JudgeConfiguration JudgeConfiguration { get; }
38+
39+
internal LdAiAgentConfig(
40+
string key,
41+
bool enabled,
42+
string variationKey,
43+
int version,
44+
string instructions,
45+
IReadOnlyDictionary<string, LdAiConfigTypes.Tool> tools,
46+
LdAiConfigTypes.ModelConfig model,
47+
LdAiConfigTypes.ProviderConfig provider,
48+
LdAiConfigTypes.JudgeConfiguration judgeConfiguration,
49+
Func<LdAiConfig, ILdAiConfigTracker> trackerFactory)
50+
: base(key, enabled, variationKey, version, model, provider, trackerFactory)
51+
{
52+
Instructions = instructions;
53+
Tools = tools?.ToImmutableDictionary() ?? ImmutableDictionary<string, LdAiConfigTypes.Tool>.Empty;
54+
JudgeConfiguration = judgeConfiguration;
55+
}
56+
}

0 commit comments

Comments
 (0)