Skip to content
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

refactor: implement AI Client #36

Merged
merged 31 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f25033c
chore: add project skeleton for server-ai
cwaldren-ld Nov 1, 2024
ccdbada
make it build
cwaldren-ld Nov 1, 2024
8049816
ci
cwaldren-ld Nov 1, 2024
8c34ad5
basic unit test
cwaldren-ld Nov 2, 2024
43aa7aa
add Mustache dep
cwaldren-ld Nov 4, 2024
25142ac
add InterpolateTemplate
cwaldren-ld Nov 4, 2024
0d58582
add implementation and a unit test
cwaldren-ld Nov 5, 2024
37f395d
more tests
cwaldren-ld Nov 5, 2024
c74c9e3
rename config tracker to LdAiClient
cwaldren-ld Nov 5, 2024
7b0e446
handle null client argument
cwaldren-ld Nov 5, 2024
44956bc
more interpolation tests
cwaldren-ld Nov 6, 2024
42a6b05
more interpolation
cwaldren-ld Nov 6, 2024
59dc7e7
Merge branch 'main' into cw/sdk-875-ai-config-tracker
cwaldren-ld Nov 6, 2024
949c635
fix tests
cwaldren-ld Nov 6, 2024
e7d8fc2
adding track methods
cwaldren-ld Nov 6, 2024
36152b8
all the tests
cwaldren-ld Nov 6, 2024
96b8fb0
docs
cwaldren-ld Nov 6, 2024
3abb9e5
data model docs
cwaldren-ld Nov 6, 2024
bdfe78a
docs
cwaldren-ld Nov 7, 2024
dc4fe3d
refactoring
cwaldren-ld Nov 7, 2024
796b85f
decrease flakyness
cwaldren-ld Nov 7, 2024
fa56649
unused includes
cwaldren-ld Nov 7, 2024
aa1310a
missing docs
cwaldren-ld Nov 7, 2024
b5a354d
rename Statistics -> Metrics
cwaldren-ld Nov 7, 2024
e6e4ee7
add adapters
cwaldren-ld Nov 7, 2024
d9b343b
docs
cwaldren-ld Nov 7, 2024
6ec3f94
internal builder constructor + remove random lines
cwaldren-ld Nov 8, 2024
8c18ca5
add an ILdAiClient interface
cwaldren-ld Nov 8, 2024
ce64ccf
add ALL of the interfaces
cwaldren-ld Nov 8, 2024
c513a2f
pass LdValue as default value
cwaldren-ld Nov 8, 2024
dd3492d
rename Metrics namespace to Provider
cwaldren-ld Nov 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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](TODO) for instructions on getting started with using the SDK.
cwaldren-ld marked this conversation as resolved.
Show resolved Hide resolved

## 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);
}
159 changes: 159 additions & 0 deletions pkgs/sdk/server-ai/src/Config/LdAiConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
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;


/// <summary>
/// Constructs a new builder. By default, the config will be disabled, with no prompt
/// messages or model parameters.
/// </summary>
public 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>();
cwaldren-ld marked this conversation as resolved.
Show resolved Hide resolved
Prompt = prompt?.ToList() ?? new List<Message>();
VersionKey = meta?.VersionKey ?? "";
Enabled = enabled;
}


/// <summary>
/// Creates a new LdAiConfig builder.
/// </summary>
/// <returns>a new builder</returns>
public static Builder New() => new();
cwaldren-ld marked this conversation as resolved.
Show resolved Hide resolved

/// <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();


}
89 changes: 89 additions & 0 deletions pkgs/sdk/server-ai/src/DataModel/DataModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Collections.Generic;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some duplication here. This file is meant to contain classes that can be directly JSON deserialized, but this doesn't lend itself to the user-facing types. For instance, these fields have get; set; and I don't want that to be accessible to users. So, I deserialize these and then convert them into LdAiConfig.

I can't make these internal (deserialization seems to fail) and I can't use init; (we still support .net 4.6.2.)

I may be missing a more idiomatic way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to tell it to use a parameterized constructor JsonContructor which allows creating immutable types, but you would have to have a full parameterized constructor for the records (because no init).

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>

cwaldren-ld marked this conversation as resolved.
Show resolved Hide resolved
[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; }
}
33 changes: 33 additions & 0 deletions pkgs/sdk/server-ai/src/Interfaces/ILaunchDarklyClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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();

cwaldren-ld marked this conversation as resolved.
Show resolved Hide resolved
}
22 changes: 22 additions & 0 deletions pkgs/sdk/server-ai/src/Interfaces/ILogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace LaunchDarkly.Sdk.Server.Ai.Interfaces;

/// <summary>
/// Log interface required by the AI Client.
/// </summary>
public interface ILogger
{

cwaldren-ld marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Log an error.
/// </summary>
/// <param name="format">format string</param>
/// <param name="allParams">parameters</param>
void Error(string format, params object[] allParams);

/// <summary>
/// Log a warning.
/// </summary>
/// <param name="format">format string</param>
/// <param name="allParams">parameters</param>
void Warn(string format, params object[] allParams);
}
3 changes: 2 additions & 1 deletion pkgs/sdk/server-ai/src/LaunchDarkly.ServerSdk.Ai.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<OutputType>Library</OutputType>
<PackageId>LaunchDarkly.ServerSdk.Ai</PackageId>
<RootNamespace>LaunchDarkly.Sdk.Server.Ai</RootNamespace>
<LangVersion>7.3</LangVersion>
<LangVersion>11</LangVersion>
<Description>LaunchDarkly Server-Side .NET AI SDK</Description>
<Authors>LaunchDarkly</Authors>
<Owners>LaunchDarkly</Owners>
Expand All @@ -39,6 +39,7 @@

<ItemGroup>
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.*" />
<PackageReference Include="Mustache.NETStandard" Version="1.1.0" />
</ItemGroup>

<PropertyGroup Condition="'$(Configuration)'=='Release'">
Expand Down
Loading
Loading