Skip to content

Commit acfa1a5

Browse files
authored
refactor: implement AI Client (#36)
This implements the guts of the AI client, including fetching Model configuration, interpolation, and tests.
1 parent 0ba54e4 commit acfa1a5

19 files changed

+1349
-21
lines changed

pkgs/sdk/server-ai/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ The .NET build tools should automatically load the most appropriate build of the
2222

2323
## Getting started
2424

25-
Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/server-side/dotnet#getting-started) for instructions on getting started with using the SDK.
25+
Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/ai/dotnet) for instructions on getting started with using the SDK.
2626

2727
## Signing
2828

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using LaunchDarkly.Logging;
2+
using LaunchDarkly.Sdk.Server.Ai.Interfaces;
3+
4+
namespace LaunchDarkly.Sdk.Server.Ai.Adapters;
5+
6+
/// <summary>
7+
/// Adapts an <see cref="LdClient"/> to the requirements of <see cref="LdAiClient"/>.
8+
/// </summary>
9+
public class LdClientAdapter : ILaunchDarklyClient
10+
{
11+
private readonly LdClient _client;
12+
13+
/// <summary>
14+
/// Constructs the adapter from an existing client.
15+
/// </summary>
16+
/// <param name="client">the adapter</param>
17+
public LdClientAdapter(LdClient client)
18+
{
19+
_client = client;
20+
}
21+
22+
/// <inheritdoc/>
23+
public LdValue JsonVariation(string key, Context context, LdValue defaultValue)
24+
=> _client.JsonVariation(key, context, defaultValue);
25+
26+
/// <inheritdoc/>
27+
public void Track(string name, Context context, LdValue data, double metricValue)
28+
=> _client.Track(name, context, data, metricValue);
29+
30+
/// <inheritdoc/>
31+
public ILogger GetLogger() => new LoggerAdapter(_client.GetLogger());
32+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using LaunchDarkly.Logging;
2+
using LaunchDarkly.Sdk.Server.Ai.Interfaces;
3+
4+
namespace LaunchDarkly.Sdk.Server.Ai.Adapters;
5+
6+
/// <summary>
7+
/// Adapts a <see cref="Logger"/> to the requirements of the <see cref="LdAiClient"/>'s
8+
/// logger.
9+
/// </summary>
10+
internal class LoggerAdapter : ILogger
11+
{
12+
private readonly Logger _logger;
13+
14+
/// <summary>
15+
/// Creates a new adapter.
16+
/// </summary>
17+
/// <param name="logger">the existing logger</param>
18+
public LoggerAdapter(Logger logger)
19+
{
20+
_logger = logger;
21+
}
22+
23+
/// <inheritdoc/>
24+
public void Error(string format, params object[] allParams) => _logger.Error(format, allParams);
25+
26+
/// <inheritdoc/>
27+
public void Warn(string format, params object[] allParams) => _logger.Warn(format, allParams);
28+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using LaunchDarkly.Sdk.Server.Ai.DataModel;
4+
5+
namespace LaunchDarkly.Sdk.Server.Ai.Config;
6+
7+
/// <summary>
8+
/// Represents an AI configuration, which contains model parameters and prompt messages.
9+
/// </summary>
10+
public record LdAiConfig
11+
{
12+
13+
/// <summary>
14+
/// Represents a single message, which is part of a prompt.
15+
/// </summary>
16+
public record Message
17+
{
18+
/// <summary>
19+
/// The content of the message, which may contain Mustache templates.
20+
/// </summary>
21+
public readonly string Content;
22+
23+
/// <summary>
24+
/// The role of the message.
25+
/// </summary>
26+
public readonly Role Role;
27+
28+
internal Message(string content, Role role)
29+
{
30+
Content = content;
31+
Role = role;
32+
}
33+
}
34+
35+
/// <summary>
36+
/// Builder for constructing an LdAiConfig instance, which can be passed as the default
37+
/// value to the AI Client's <see cref="LdAiClient.ModelConfig"/> method.
38+
/// </summary>
39+
public class Builder
40+
{
41+
private bool _enabled;
42+
private readonly List<Message> _prompt;
43+
private readonly Dictionary<string, object> _modelParams;
44+
45+
internal Builder()
46+
{
47+
_enabled = false;
48+
_prompt = new List<Message>();
49+
_modelParams = new Dictionary<string, object>();
50+
}
51+
52+
/// <summary>
53+
/// Adds a prompt message with the given content and role. The default role is <see cref="Role.User"/>.
54+
/// </summary>
55+
/// <param name="content">the content, which may contain Mustache templates</param>
56+
/// <param name="role">the role</param>
57+
/// <returns>a new builder</returns>
58+
public Builder AddPromptMessage(string content, Role role = Role.User)
59+
{
60+
_prompt.Add(new Message(content, role));
61+
return this;
62+
}
63+
64+
/// <summary>
65+
/// Disables the config.
66+
/// </summary>
67+
/// <returns>the builder</returns>
68+
public Builder Disable() => SetEnabled(false);
69+
70+
/// <summary>
71+
/// Enables the config.
72+
/// </summary>
73+
/// <returns>the builder</returns>
74+
public Builder Enable() => SetEnabled(true);
75+
76+
/// <summary>
77+
/// Sets the enabled state of the config based on a boolean.
78+
/// </summary>
79+
/// <param name="enabled">whether the config is enabled</param>
80+
/// <returns>the builder</returns>
81+
public Builder SetEnabled(bool enabled)
82+
{
83+
_enabled = enabled;
84+
return this;
85+
}
86+
87+
/// <summary>
88+
/// Sets a parameter for the model. The value may be any object.
89+
/// </summary>
90+
/// <param name="name">the parameter name</param>
91+
/// <param name="value">the parameter value</param>
92+
/// <returns>the builder</returns>
93+
public Builder SetModelParam(string name, object value)
94+
{
95+
_modelParams[name] = value;
96+
return this;
97+
}
98+
99+
/// <summary>
100+
/// Builds the LdAiConfig instance.
101+
/// </summary>
102+
/// <returns>a new LdAiConfig</returns>
103+
public LdAiConfig Build()
104+
{
105+
return new LdAiConfig(_enabled, _prompt, new Meta(), _modelParams);
106+
}
107+
}
108+
109+
/// <summary>
110+
/// The prompts associated with the config.
111+
/// </summary>
112+
public readonly IReadOnlyList<Message> Prompt;
113+
114+
/// <summary>
115+
/// The model parameters associated with the config.
116+
/// </summary>
117+
public readonly IReadOnlyDictionary<string, object> Model;
118+
119+
120+
121+
internal LdAiConfig(bool enabled, IEnumerable<Message> prompt, Meta meta, IReadOnlyDictionary<string, object> model)
122+
{
123+
Model = model ?? new Dictionary<string, object>();
124+
Prompt = prompt?.ToList() ?? new List<Message>();
125+
VersionKey = meta?.VersionKey ?? "";
126+
Enabled = enabled;
127+
}
128+
129+
private static LdValue ObjectToValue(object obj)
130+
{
131+
if (obj == null)
132+
{
133+
return LdValue.Null;
134+
}
135+
136+
return obj switch
137+
{
138+
bool b => LdValue.Of(b),
139+
double d => LdValue.Of(d),
140+
string s => LdValue.Of(s),
141+
IEnumerable<object> list => LdValue.ArrayFrom(list.Select(ObjectToValue)),
142+
IDictionary<string, object> dict => LdValue.ObjectFrom(dict.ToDictionary(kv => kv.Key,
143+
kv => ObjectToValue(kv.Value))),
144+
_ => LdValue.Null
145+
};
146+
}
147+
148+
internal LdValue ToLdValue()
149+
{
150+
return LdValue.ObjectFrom(new Dictionary<string, LdValue>
151+
{
152+
{ "_ldMeta", LdValue.ObjectFrom(
153+
new Dictionary<string, LdValue>
154+
{
155+
{ "versionKey", LdValue.Of(VersionKey) },
156+
{ "enabled", LdValue.Of(Enabled) }
157+
}) },
158+
{ "prompt", LdValue.ArrayFrom(Prompt.Select(m => LdValue.ObjectFrom(new Dictionary<string, LdValue>
159+
{
160+
{ "content", LdValue.Of(m.Content) },
161+
{ "role", LdValue.Of(m.Role.ToString()) }
162+
}))) },
163+
{ "model", ObjectToValue(Model) }
164+
});
165+
}
166+
167+
/// <summary>
168+
/// Creates a new LdAiConfig builder.
169+
/// </summary>
170+
/// <returns>a new builder</returns>
171+
public static Builder New() => new();
172+
173+
/// <summary>
174+
/// Returns true if the config is enabled.
175+
/// </summary>
176+
/// <returns>true if enabled</returns>
177+
public bool Enabled { get; }
178+
179+
180+
/// <summary>
181+
/// This field meant for internal LaunchDarkly usage.
182+
/// </summary>
183+
public string VersionKey { get; }
184+
185+
/// <summary>
186+
/// Convenient helper that returns a disabled LdAiConfig.
187+
/// </summary>
188+
public static LdAiConfig Disabled = New().Disable().Build();
189+
190+
191+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System.Collections.Generic;
2+
using System.Text.Json.Serialization;
3+
4+
namespace LaunchDarkly.Sdk.Server.Ai.DataModel;
5+
6+
/// <summary>
7+
/// Represents the role of the prompt message.
8+
/// </summary>
9+
public enum Role
10+
{
11+
/// <summary>
12+
/// User role.
13+
/// </summary>
14+
User,
15+
/// <summary>
16+
/// System role.
17+
/// </summary>
18+
System,
19+
/// <summary>
20+
/// Assistant role.
21+
/// </summary>
22+
Assistant
23+
}
24+
25+
/// <summary>
26+
/// Represents the JSON serialization of the Meta field.
27+
/// </summary>
28+
public class Meta
29+
{
30+
/// <summary>
31+
/// The version key.
32+
/// </summary>
33+
[JsonPropertyName("versionKey")]
34+
public string VersionKey { get; set; }
35+
36+
/// <summary>
37+
/// If the config is enabled.
38+
/// </summary>
39+
[JsonPropertyName("enabled")]
40+
public bool Enabled { get; set; }
41+
}
42+
43+
/// <summary>
44+
/// Represents the JSON serialization of a Message.
45+
/// </summary>
46+
public class Message
47+
{
48+
/// <summary>
49+
/// The content.
50+
/// </summary>
51+
[JsonPropertyName("content")]
52+
public string Content { get; set; }
53+
54+
/// <summary>
55+
/// The role.
56+
/// </summary>
57+
[JsonPropertyName("role")]
58+
[JsonConverter(typeof(JsonStringEnumConverter))]
59+
public Role Role { get; set; }
60+
}
61+
62+
63+
/// <summary>
64+
/// Represents the JSON serialization of an AiConfig.
65+
/// </summary>
66+
67+
public class AiConfig
68+
{
69+
/// <summary>
70+
/// The prompt.
71+
/// </summary>
72+
[JsonPropertyName("prompt")]
73+
public List<Message> Prompt { get; set; }
74+
75+
/// <summary>
76+
/// LaunchDarkly metadata.
77+
/// </summary>
78+
[JsonPropertyName("_ldMeta")]
79+
public Meta Meta { get; set; }
80+
81+
/// <summary>
82+
/// The model params;
83+
/// </summary>
84+
[JsonPropertyName("model")]
85+
public Dictionary<string, object> Model { get; set; }
86+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
namespace LaunchDarkly.Sdk.Server.Ai.Interfaces;
2+
3+
/// <summary>
4+
/// Interface representing capabilities needed by the AI Client. These are usually provided
5+
/// by the LaunchDarkly Server SDK.
6+
/// </summary>
7+
public interface ILaunchDarklyClient
8+
{
9+
/// <summary>
10+
/// Returns a JSON variation.
11+
/// </summary>
12+
/// <param name="key">the flag key</param>
13+
/// <param name="context">the context</param>
14+
/// <param name="defaultValue">the default value</param>
15+
/// <returns>the evaluation result</returns>
16+
LdValue JsonVariation(string key, Context context, LdValue defaultValue);
17+
18+
/// <summary>
19+
/// Tracks a metric.
20+
/// </summary>
21+
/// <param name="name">metric name</param>
22+
/// <param name="context">context</param>
23+
/// <param name="data">associated data</param>
24+
/// <param name="metricValue">metric value</param>
25+
void Track(string name, Context context, LdValue data, double metricValue);
26+
27+
/// <summary>
28+
/// Returns a logger.
29+
/// </summary>
30+
/// <returns>a logger</returns>
31+
ILogger GetLogger();
32+
}

0 commit comments

Comments
 (0)