Skip to content

Commit

Permalink
refactor: implement AI Client (#36)
Browse files Browse the repository at this point in the history
This implements the guts of the AI client, including fetching Model
configuration, interpolation, and tests.
  • Loading branch information
cwaldren-ld authored Nov 8, 2024
1 parent 0ba54e4 commit acfa1a5
Show file tree
Hide file tree
Showing 19 changed files with 1,349 additions and 21 deletions.
2 changes: 1 addition & 1 deletion pkgs/sdk/server-ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The .NET build tools should automatically load the most appropriate build of the

## Getting started

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

## Signing

Expand Down
32 changes: 32 additions & 0 deletions pkgs/sdk/server-ai/src/Adapters/LdClientAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using LaunchDarkly.Logging;
using LaunchDarkly.Sdk.Server.Ai.Interfaces;

namespace LaunchDarkly.Sdk.Server.Ai.Adapters;

/// <summary>
/// Adapts an <see cref="LdClient"/> to the requirements of <see cref="LdAiClient"/>.
/// </summary>
public class LdClientAdapter : ILaunchDarklyClient
{
private readonly LdClient _client;

/// <summary>
/// Constructs the adapter from an existing client.
/// </summary>
/// <param name="client">the adapter</param>
public LdClientAdapter(LdClient client)
{
_client = client;
}

/// <inheritdoc/>
public LdValue JsonVariation(string key, Context context, LdValue defaultValue)
=> _client.JsonVariation(key, context, defaultValue);

/// <inheritdoc/>
public void Track(string name, Context context, LdValue data, double metricValue)
=> _client.Track(name, context, data, metricValue);

/// <inheritdoc/>
public ILogger GetLogger() => new LoggerAdapter(_client.GetLogger());
}
28 changes: 28 additions & 0 deletions pkgs/sdk/server-ai/src/Adapters/LoggerAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using LaunchDarkly.Logging;
using LaunchDarkly.Sdk.Server.Ai.Interfaces;

namespace LaunchDarkly.Sdk.Server.Ai.Adapters;

/// <summary>
/// Adapts a <see cref="Logger"/> to the requirements of the <see cref="LdAiClient"/>'s
/// logger.
/// </summary>
internal class LoggerAdapter : ILogger
{
private readonly Logger _logger;

/// <summary>
/// Creates a new adapter.
/// </summary>
/// <param name="logger">the existing logger</param>
public LoggerAdapter(Logger logger)
{
_logger = logger;
}

/// <inheritdoc/>
public void Error(string format, params object[] allParams) => _logger.Error(format, allParams);

/// <inheritdoc/>
public void Warn(string format, params object[] allParams) => _logger.Warn(format, allParams);
}
191 changes: 191 additions & 0 deletions pkgs/sdk/server-ai/src/Config/LdAiConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
using System.Collections.Generic;
using System.Linq;
using LaunchDarkly.Sdk.Server.Ai.DataModel;

namespace LaunchDarkly.Sdk.Server.Ai.Config;

/// <summary>
/// Represents an AI configuration, which contains model parameters and prompt messages.
/// </summary>
public record LdAiConfig
{

/// <summary>
/// Represents a single message, which is part of a prompt.
/// </summary>
public record Message
{
/// <summary>
/// The content of the message, which may contain Mustache templates.
/// </summary>
public readonly string Content;

/// <summary>
/// The role of the message.
/// </summary>
public readonly Role Role;

internal Message(string content, Role role)
{
Content = content;
Role = role;
}
}

/// <summary>
/// Builder for constructing an LdAiConfig instance, which can be passed as the default
/// value to the AI Client's <see cref="LdAiClient.ModelConfig"/> method.
/// </summary>
public class Builder
{
private bool _enabled;
private readonly List<Message> _prompt;
private readonly Dictionary<string, object> _modelParams;

internal Builder()
{
_enabled = false;
_prompt = new List<Message>();
_modelParams = new Dictionary<string, object>();
}

/// <summary>
/// Adds a prompt message with the given content and role. The default role is <see cref="Role.User"/>.
/// </summary>
/// <param name="content">the content, which may contain Mustache templates</param>
/// <param name="role">the role</param>
/// <returns>a new builder</returns>
public Builder AddPromptMessage(string content, Role role = Role.User)
{
_prompt.Add(new Message(content, role));
return this;
}

/// <summary>
/// Disables the config.
/// </summary>
/// <returns>the builder</returns>
public Builder Disable() => SetEnabled(false);

/// <summary>
/// Enables the config.
/// </summary>
/// <returns>the builder</returns>
public Builder Enable() => SetEnabled(true);

/// <summary>
/// Sets the enabled state of the config based on a boolean.
/// </summary>
/// <param name="enabled">whether the config is enabled</param>
/// <returns>the builder</returns>
public Builder SetEnabled(bool enabled)
{
_enabled = enabled;
return this;
}

/// <summary>
/// Sets a parameter for the model. The value may be any object.
/// </summary>
/// <param name="name">the parameter name</param>
/// <param name="value">the parameter value</param>
/// <returns>the builder</returns>
public Builder SetModelParam(string name, object value)
{
_modelParams[name] = value;
return this;
}

/// <summary>
/// Builds the LdAiConfig instance.
/// </summary>
/// <returns>a new LdAiConfig</returns>
public LdAiConfig Build()
{
return new LdAiConfig(_enabled, _prompt, new Meta(), _modelParams);
}
}

/// <summary>
/// The prompts associated with the config.
/// </summary>
public readonly IReadOnlyList<Message> Prompt;

/// <summary>
/// The model parameters associated with the config.
/// </summary>
public readonly IReadOnlyDictionary<string, object> Model;



internal LdAiConfig(bool enabled, IEnumerable<Message> prompt, Meta meta, IReadOnlyDictionary<string, object> model)
{
Model = model ?? new Dictionary<string, object>();
Prompt = prompt?.ToList() ?? new List<Message>();
VersionKey = meta?.VersionKey ?? "";
Enabled = enabled;
}

private static LdValue ObjectToValue(object obj)
{
if (obj == null)
{
return LdValue.Null;
}

return obj switch
{
bool b => LdValue.Of(b),
double d => LdValue.Of(d),
string s => LdValue.Of(s),
IEnumerable<object> list => LdValue.ArrayFrom(list.Select(ObjectToValue)),
IDictionary<string, object> dict => LdValue.ObjectFrom(dict.ToDictionary(kv => kv.Key,
kv => ObjectToValue(kv.Value))),
_ => LdValue.Null
};
}

internal LdValue ToLdValue()
{
return LdValue.ObjectFrom(new Dictionary<string, LdValue>
{
{ "_ldMeta", LdValue.ObjectFrom(
new Dictionary<string, LdValue>
{
{ "versionKey", LdValue.Of(VersionKey) },
{ "enabled", LdValue.Of(Enabled) }
}) },
{ "prompt", LdValue.ArrayFrom(Prompt.Select(m => LdValue.ObjectFrom(new Dictionary<string, LdValue>
{
{ "content", LdValue.Of(m.Content) },
{ "role", LdValue.Of(m.Role.ToString()) }
}))) },
{ "model", ObjectToValue(Model) }
});
}

/// <summary>
/// Creates a new LdAiConfig builder.
/// </summary>
/// <returns>a new builder</returns>
public static Builder New() => new();

/// <summary>
/// Returns true if the config is enabled.
/// </summary>
/// <returns>true if enabled</returns>
public bool Enabled { get; }


/// <summary>
/// This field meant for internal LaunchDarkly usage.
/// </summary>
public string VersionKey { get; }

/// <summary>
/// Convenient helper that returns a disabled LdAiConfig.
/// </summary>
public static LdAiConfig Disabled = New().Disable().Build();


}
86 changes: 86 additions & 0 deletions pkgs/sdk/server-ai/src/DataModel/DataModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace LaunchDarkly.Sdk.Server.Ai.DataModel;

/// <summary>
/// Represents the role of the prompt message.
/// </summary>
public enum Role
{
/// <summary>
/// User role.
/// </summary>
User,
/// <summary>
/// System role.
/// </summary>
System,
/// <summary>
/// Assistant role.
/// </summary>
Assistant
}

/// <summary>
/// Represents the JSON serialization of the Meta field.
/// </summary>
public class Meta
{
/// <summary>
/// The version key.
/// </summary>
[JsonPropertyName("versionKey")]
public string VersionKey { get; set; }

/// <summary>
/// If the config is enabled.
/// </summary>
[JsonPropertyName("enabled")]
public bool Enabled { get; set; }
}

/// <summary>
/// Represents the JSON serialization of a Message.
/// </summary>
public class Message
{
/// <summary>
/// The content.
/// </summary>
[JsonPropertyName("content")]
public string Content { get; set; }

/// <summary>
/// The role.
/// </summary>
[JsonPropertyName("role")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public Role Role { get; set; }
}


/// <summary>
/// Represents the JSON serialization of an AiConfig.
/// </summary>

public class AiConfig
{
/// <summary>
/// The prompt.
/// </summary>
[JsonPropertyName("prompt")]
public List<Message> Prompt { get; set; }

/// <summary>
/// LaunchDarkly metadata.
/// </summary>
[JsonPropertyName("_ldMeta")]
public Meta Meta { get; set; }

/// <summary>
/// The model params;
/// </summary>
[JsonPropertyName("model")]
public Dictionary<string, object> Model { get; set; }
}
32 changes: 32 additions & 0 deletions pkgs/sdk/server-ai/src/Interfaces/ILaunchDarklyClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace LaunchDarkly.Sdk.Server.Ai.Interfaces;

/// <summary>
/// Interface representing capabilities needed by the AI Client. These are usually provided
/// by the LaunchDarkly Server SDK.
/// </summary>
public interface ILaunchDarklyClient
{
/// <summary>
/// Returns a JSON variation.
/// </summary>
/// <param name="key">the flag key</param>
/// <param name="context">the context</param>
/// <param name="defaultValue">the default value</param>
/// <returns>the evaluation result</returns>
LdValue JsonVariation(string key, Context context, LdValue defaultValue);

/// <summary>
/// Tracks a metric.
/// </summary>
/// <param name="name">metric name</param>
/// <param name="context">context</param>
/// <param name="data">associated data</param>
/// <param name="metricValue">metric value</param>
void Track(string name, Context context, LdValue data, double metricValue);

/// <summary>
/// Returns a logger.
/// </summary>
/// <returns>a logger</returns>
ILogger GetLogger();
}
Loading

0 comments on commit acfa1a5

Please sign in to comment.