diff --git a/.github/workflows/server-sdk-ai.yml b/.github/workflows/server-sdk-ai.yml new file mode 100644 index 0000000..c5013f6 --- /dev/null +++ b/.github/workflows/server-sdk-ai.yml @@ -0,0 +1,43 @@ +name: server-sdk-ai +on: + schedule: + # * is a special character in YAML so you have to quote this string + - cron: '0 9 * * *' + push: + branches: [ main, 'feat/**' ] + paths: + - 'server-sdk-ai/**' + - '.github/workflows/server-sdk-ai.yml' + pull_request: + branches: [ main, 'feat/**' ] + paths: + - 'server-sdk-ai/**' + - '.github/workflows/server-sdk-ai.yml' + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + fail-fast: false + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Setup dotnet build tools + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 + with: + dotnet-version: 8.0 + + # Build-only: these examples call external model providers and need + # provider credentials to run, so CI just compiles them to catch build errors. + - name: Build server-sdk-ai examples + shell: bash + run: | + set -e + for proj in $(find server-sdk-ai -name '*.csproj' | sort); do + echo "::group::Building $proj" + dotnet build "$proj" + echo "::endgroup::" + done diff --git a/CODEOWNERS b/CODEOWNERS index 3c1c6e1..e76d4e9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,5 @@ # Repository Maintainers * @launchdarkly/team-sdk-net + +# AI examples +/server-sdk-ai/ @launchdarkly/team-ai-agents @launchdarkly/team-ai-evals diff --git a/README.md b/README.md index 5e903b0..2cdc8bc 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ or the [.NET SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/ ## SDKs -| SDK | Package | Examples | -|-----------------|--------------------------|--------------------------------| -| .NET Server SDK | `LaunchDarkly.ServerSdk` | [`server-sdk/`](./server-sdk/) | +| SDK | Package | Examples | +|--------------------|-----------------------------|------------------------------------------| +| .NET Server SDK | `LaunchDarkly.ServerSdk` | [`server-sdk/`](./server-sdk/) | +| .NET Server AI SDK | `LaunchDarkly.ServerSdk.Ai` | [`server-sdk-ai/`](./server-sdk-ai/) | ## Requirements diff --git a/server-sdk-ai/README.md b/server-sdk-ai/README.md new file mode 100644 index 0000000..38867ca --- /dev/null +++ b/server-sdk-ai/README.md @@ -0,0 +1,26 @@ +# LaunchDarkly .NET Server AI SDK examples + +Examples for the [`LaunchDarkly.ServerSdk.Ai`](https://www.nuget.org/packages/LaunchDarkly.ServerSdk.Ai) package. + +For more comprehensive instructions, you can visit the [AI Configs Quickstart](https://docs.launchdarkly.com/home/ai-configs/quickstart) or the [.NET AI SDK reference guide](https://docs.launchdarkly.com/sdk/ai/dotnet). + +Each example is a self-contained .NET console application you can run independently. + +## Getting Started + +These examples show how to integrate LaunchDarkly AI with different providers. + +| Provider | Example | Description | +|----------|----------------------------------------------------------|------------------------------------------------------------------| +| OpenAI | [Chat Completions](./frameworks/openai/chat-completions/) | `CompletionConfig` with OpenAI, automatic metrics tracking | +| Bedrock | [Converse](./frameworks/bedrock/converse/) | `CompletionConfig` with the AWS Bedrock Converse API, metrics tracking | + +## Features + +These examples focus on the LaunchDarkly AI SDK itself. They are **provider-agnostic** — they retrieve and resolve AI Configs, then exercise the tracker API with synthetic operations rather than calling any model provider. + +| Example | Description | +|------------------------------------------------------|-----------------------------------------------------------------------------------------| +| [Completion Config](./features/completion-config/) | Default values, Mustache variables, model parameter extraction, tool enumeration, and `TrackMetricsOf` | +| [Agent Config](./features/agent-config/) | Agent instructions and tools; `TrackMetricsOf` and `TrackToolCall` | +| [Judge Config](./features/judge-config/) | Judge config retrieval and `TrackJudgeResult` | diff --git a/server-sdk-ai/features/agent-config/.env.example b/server-sdk-ai/features/agent-config/.env.example new file mode 100644 index 0000000..a4c08ca --- /dev/null +++ b/server-sdk-ai/features/agent-config/.env.example @@ -0,0 +1,5 @@ +# Your LaunchDarkly server-side SDK key +LAUNCHDARKLY_SDK_KEY= + +# Override to use a different AI Config key +LAUNCHDARKLY_AGENT_KEY=sample-agent diff --git a/server-sdk-ai/features/agent-config/AgentConfig.csproj b/server-sdk-ai/features/agent-config/AgentConfig.csproj new file mode 100644 index 0000000..3ec26a6 --- /dev/null +++ b/server-sdk-ai/features/agent-config/AgentConfig.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/server-sdk-ai/features/agent-config/NOTICE b/server-sdk-ai/features/agent-config/NOTICE new file mode 100644 index 0000000..9357970 --- /dev/null +++ b/server-sdk-ai/features/agent-config/NOTICE @@ -0,0 +1,4 @@ +LaunchDarkly .NET Server AI SDK Agent Config example +Copyright 2026 Catamorphic, Co. + +This product includes software developed at LaunchDarkly (https://launchdarkly.com/). diff --git a/server-sdk-ai/features/agent-config/Program.cs b/server-sdk-ai/features/agent-config/Program.cs new file mode 100644 index 0000000..3e69b2f --- /dev/null +++ b/server-sdk-ai/features/agent-config/Program.cs @@ -0,0 +1,152 @@ +using DotNetEnv; +using LaunchDarkly.Sdk; +using LaunchDarkly.Sdk.Server; +using LaunchDarkly.Sdk.Server.Ai; +using LaunchDarkly.Sdk.Server.Ai.Adapters; +using LaunchDarkly.Sdk.Server.Ai.Config; +using LaunchDarkly.Sdk.Server.Ai.Tracking; + +Env.TraversePath().Load(); + +var sdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY"); +if (string.IsNullOrEmpty(sdkKey)) +{ + Console.Error.WriteLine( + "LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again."); + return; +} + +// Set agentKey to the AI config key you want to evaluate. +var agentKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_AGENT_KEY") + ?? "sample-agent"; + +var ldClient = new LdClient(Configuration.Builder(sdkKey).Build()); +if (!ldClient.Initialized) +{ + Console.Error.WriteLine( + "*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo."); + ldClient.Dispose(); + return; +} +Console.WriteLine("*** SDK successfully initialized!"); + +var aiClient = new LdAiClient(new LdClientAdapter(ldClient)); + +// Set up the evaluation context. This context should appear on your +// LaunchDarkly contexts dashboard soon after you run the demo. +var context = Context.Builder(ContextKind.Of("user"), "example-user-key") + .Name("Sandy") + .Build(); + +// Default agent config used when the AI Config is unreachable or missing. +var defaultValue = LdAiAgentConfigDefault.New() + .Enable() + .SetModelName("gpt-4") + .SetModelProviderName("openai") + .SetInstructions("You are a helpful research assistant for {{companyName}}.") + .Build(); + +try +{ + var agentConfig = aiClient.AgentConfig( + agentKey, + context, + defaultValue, + variables: new Dictionary + { + ["companyName"] = "LaunchDarkly" + }); + + if (!agentConfig.Enabled) + { + Console.WriteLine($"Agent config '{agentKey}' is disabled."); + return; + } + + Console.WriteLine(); + Console.WriteLine($"Resolved agent model: {agentConfig.Model.Name} (provider: {agentConfig.Provider.Name})"); + Console.WriteLine(); + Console.WriteLine("Agent instructions (Mustache variables interpolated):"); + Console.WriteLine(agentConfig.Instructions); + + if (agentConfig.Tools.Count > 0) + { + Console.WriteLine(); + Console.WriteLine("Configured tools (map to your provider's function-calling API):"); + foreach (var (name, tool) in agentConfig.Tools) + { + Console.WriteLine($" {name}: {tool.Description} (type: {tool.Type})"); + } + } + + var tracker = agentConfig.CreateTracker(); + + // To keep this example provider-agnostic, the operation below is a + // synthetic stand-in for an actual model call — see the getting-started/* + // examples for end-to-end integrations with OpenAI or AWS Bedrock. The + // tracker API is exercised exactly the same way regardless of provider. + Console.WriteLine(); + Console.WriteLine("Running a synthetic agent invocation wrapped in TrackMetricsOf..."); + + var result = await tracker.TrackMetricsOf( + SyntheticMetrics, + async () => + { + await Task.Delay(150); + return new SyntheticResult( + Content: "[simulated agent response]", + InputTokens: 80, + OutputTokens: 120, + ToolsInvoked: new[] { "search", "calculator" }); + }); + + // Record each tool invocation on the tracker so it appears in the + // metric summary. In a real integration these names would come from + // the provider's tool-use response. + foreach (var toolName in result.ToolsInvoked) + { + tracker.TrackToolCall(toolName); + } + + Console.WriteLine(); + Console.WriteLine("Simulated agent response:"); + Console.WriteLine(result.Content); + + PrintSummary(tracker.Summary); +} +catch (Exception ex) +{ + Console.Error.WriteLine($"Error: {ex.Message}"); +} +finally +{ + ldClient.FlushAndWait(TimeSpan.FromSeconds(5)); + ldClient.Dispose(); +} + +static AiMetrics SyntheticMetrics(SyntheticResult result) => new( + success: true, + tokens: new Usage( + Total: result.InputTokens + result.OutputTokens, + Input: result.InputTokens, + Output: result.OutputTokens)); + +static void PrintSummary(MetricSummary summary) +{ + Console.WriteLine(); + Console.WriteLine("Done! The tracker captured the following metrics:"); + Console.WriteLine($" Duration: {summary.DurationMs}ms"); + Console.WriteLine($" Success: {summary.Success}"); + if (summary.Tokens is { } tokens) + { + Console.WriteLine($" Input tokens: {tokens.Input}"); + Console.WriteLine($" Output tokens: {tokens.Output}"); + Console.WriteLine($" Total tokens: {tokens.Total}"); + } +} + +internal sealed record SyntheticResult( + string Content, + int InputTokens, + int OutputTokens, + IReadOnlyList ToolsInvoked); diff --git a/server-sdk-ai/features/agent-config/README.md b/server-sdk-ai/features/agent-config/README.md new file mode 100644 index 0000000..0ef5aa8 --- /dev/null +++ b/server-sdk-ai/features/agent-config/README.md @@ -0,0 +1,34 @@ +# Agent Config + +This example focuses on the LaunchDarkly AI SDK itself — it is **provider-agnostic** and does not call OpenAI, Bedrock, Anthropic, or any other model. The actual agent invocation is replaced with a synthetic operation so you can see the SDK's tracking surface clearly. For end-to-end provider integrations see the [getting-started examples](../../frameworks). + +It demonstrates: + +- Retrieving agent instructions and tools from LaunchDarkly via `LdAiClient.AgentConfig` +- Using the builder pattern (`LdAiAgentConfigDefault.New()`) to declare a fallback default +- Interpolating Mustache placeholders in agent instructions +- Enumerating the tools attached to the agent (so they can be mapped to your provider's function-calling API) +- Tracking metrics via `tracker.TrackMetricsOf` and recording individual tool invocations via `tracker.TrackToolCall` + +## Prerequisites + +- .NET 8.0 SDK +- A LaunchDarkly account and a server-side SDK key + +## Setup + +1. [Create an AI Config](https://launchdarkly.com/docs/home/ai-configs/create) of type **Agent** in LaunchDarkly with the key `sample-agent`. Set the instructions (you may include Mustache placeholders such as `{{companyName}}`), choose a provider and model. +2. Copy `.env.example` to `.env` and fill in your SDK key: + ```sh + cp .env.example .env + ``` +3. Restore dependencies: + ```sh + dotnet restore + ``` + +## Run + +```sh +dotnet run +``` diff --git a/server-sdk-ai/features/completion-config/.env.example b/server-sdk-ai/features/completion-config/.env.example new file mode 100644 index 0000000..be0ac8c --- /dev/null +++ b/server-sdk-ai/features/completion-config/.env.example @@ -0,0 +1,5 @@ +# Your LaunchDarkly server-side SDK key +LAUNCHDARKLY_SDK_KEY= + +# Override to use a different AI Config key +LAUNCHDARKLY_COMPLETION_KEY=sample-completion diff --git a/server-sdk-ai/features/completion-config/CompletionConfig.csproj b/server-sdk-ai/features/completion-config/CompletionConfig.csproj new file mode 100644 index 0000000..3ec26a6 --- /dev/null +++ b/server-sdk-ai/features/completion-config/CompletionConfig.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/server-sdk-ai/features/completion-config/NOTICE b/server-sdk-ai/features/completion-config/NOTICE new file mode 100644 index 0000000..1b97a9b --- /dev/null +++ b/server-sdk-ai/features/completion-config/NOTICE @@ -0,0 +1,4 @@ +LaunchDarkly .NET Server AI SDK Completion Config example +Copyright 2026 Catamorphic, Co. + +This product includes software developed at LaunchDarkly (https://launchdarkly.com/). diff --git a/server-sdk-ai/features/completion-config/Program.cs b/server-sdk-ai/features/completion-config/Program.cs new file mode 100644 index 0000000..f6a1393 --- /dev/null +++ b/server-sdk-ai/features/completion-config/Program.cs @@ -0,0 +1,163 @@ +using DotNetEnv; +using LaunchDarkly.Sdk; +using LaunchDarkly.Sdk.Server; +using LaunchDarkly.Sdk.Server.Ai; +using LaunchDarkly.Sdk.Server.Ai.Adapters; +using LaunchDarkly.Sdk.Server.Ai.Config; +using LaunchDarkly.Sdk.Server.Ai.Tracking; + +Env.TraversePath().Load(); + +var sdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY"); +if (string.IsNullOrEmpty(sdkKey)) +{ + Console.Error.WriteLine( + "LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again."); + return; +} + +// Set completionKey to the AI config key you want to evaluate. +var completionKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_COMPLETION_KEY") + ?? "sample-completion"; + +var ldClient = new LdClient(Configuration.Builder(sdkKey).Build()); +if (!ldClient.Initialized) +{ + Console.Error.WriteLine( + "*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo."); + ldClient.Dispose(); + return; +} +Console.WriteLine("*** SDK successfully initialized!"); + +var aiClient = new LdAiClient(new LdClientAdapter(ldClient)); + +// Set up the evaluation context. This context should appear on your +// LaunchDarkly contexts dashboard soon after you run the demo. +var context = Context.Builder(ContextKind.Of("user"), "example-user-key") + .Name("Sandy") + .Build(); + +// Build a fallback default config using the builder pattern. The default is +// used when LaunchDarkly is unreachable or the AI Config is missing/disabled. +var defaultValue = LdAiCompletionConfigDefault.New() + .Enable() + .SetModelName("gpt-4") + .SetModelProviderName("openai") + .SetModelParam("temperature", LdValue.Of(0.7)) + .SetModelParam("maxTokens", LdValue.Of(4096)) + .AddMessage("You are a helpful assistant for {{companyName}}.", LdAiConfigTypes.Role.System) + .Build(); + +try +{ + // The variables dictionary supplies values for Mustache placeholders such + // as {{companyName}} that appear in message content. + var config = aiClient.CompletionConfig( + completionKey, + context, + defaultValue, + variables: new Dictionary + { + ["companyName"] = "LaunchDarkly" + }); + + if (!config.Enabled) + { + Console.WriteLine($"AI config '{completionKey}' is disabled."); + return; + } + + Console.WriteLine(); + Console.WriteLine($"Resolved model: {config.Model.Name} (provider: {config.Provider.Name})"); + + // Read typed model parameters set on the AI Config. Falls back to a + // sensible default if the parameter is absent or null. + var temperature = config.Model.Parameters.TryGetValue("temperature", out var tempVal) && !tempVal.IsNull + ? tempVal.AsFloat + : 0.5f; + var maxTokens = config.Model.Parameters.TryGetValue("maxTokens", out var maxVal) && !maxVal.IsNull + ? maxVal.AsInt + : 4096; + + Console.WriteLine($" temperature: {temperature}"); + Console.WriteLine($" maxTokens: {maxTokens}"); + + if (config.Messages.Count > 0) + { + Console.WriteLine(); + Console.WriteLine("Resolved messages (Mustache variables interpolated):"); + foreach (var m in config.Messages) + { + Console.WriteLine($" [{m.Role}] {m.Content}"); + } + } + + if (config.Tools.Count > 0) + { + Console.WriteLine(); + Console.WriteLine("Available tools:"); + foreach (var (name, tool) in config.Tools) + { + Console.WriteLine($" {name}: {tool.Description} (type: {tool.Type})"); + } + } + + var tracker = config.CreateTracker(); + + // To keep this example provider-agnostic, the operation below is a + // synthetic stand-in for an actual model call — see the getting-started/* + // examples for end-to-end integrations with OpenAI or AWS Bedrock. The + // tracker API is exercised exactly the same way regardless of provider. + Console.WriteLine(); + Console.WriteLine("Running a synthetic model call wrapped in TrackMetricsOf..."); + + var result = await tracker.TrackMetricsOf( + SyntheticMetrics, + async () => + { + await Task.Delay(150); + return new SyntheticResult( + Content: "[simulated model response]", + InputTokens: 50, + OutputTokens: 100); + }); + + Console.WriteLine(); + Console.WriteLine("Simulated model response:"); + Console.WriteLine(result.Content); + + PrintSummary(tracker.Summary); +} +catch (Exception ex) +{ + Console.Error.WriteLine($"Error: {ex.Message}"); +} +finally +{ + ldClient.FlushAndWait(TimeSpan.FromSeconds(5)); + ldClient.Dispose(); +} + +static AiMetrics SyntheticMetrics(SyntheticResult result) => new( + success: true, + tokens: new Usage( + Total: result.InputTokens + result.OutputTokens, + Input: result.InputTokens, + Output: result.OutputTokens)); + +static void PrintSummary(MetricSummary summary) +{ + Console.WriteLine(); + Console.WriteLine("Done! The tracker captured the following metrics:"); + Console.WriteLine($" Duration: {summary.DurationMs}ms"); + Console.WriteLine($" Success: {summary.Success}"); + if (summary.Tokens is { } tokens) + { + Console.WriteLine($" Input tokens: {tokens.Input}"); + Console.WriteLine($" Output tokens: {tokens.Output}"); + Console.WriteLine($" Total tokens: {tokens.Total}"); + } +} + +internal sealed record SyntheticResult(string Content, int InputTokens, int OutputTokens); diff --git a/server-sdk-ai/features/completion-config/README.md b/server-sdk-ai/features/completion-config/README.md new file mode 100644 index 0000000..134fd80 --- /dev/null +++ b/server-sdk-ai/features/completion-config/README.md @@ -0,0 +1,34 @@ +# Completion Config with Default Values + +This example focuses on the LaunchDarkly AI SDK itself — it is **provider-agnostic** and does not call OpenAI, Bedrock, Anthropic, or any other model. The actual model call is replaced with a synthetic operation so you can see the SDK's tracking surface clearly. For end-to-end provider integrations see the [getting-started examples](../../frameworks). + +It demonstrates: + +- Building a fallback default config with `LdAiCompletionConfigDefault.New()` +- Using Mustache template variables to interpolate prompt content +- Extracting typed model parameters (`temperature`, `maxTokens`) from the resolved config +- Enumerating tools configured on the AI Config +- Wrapping any provider call in `tracker.TrackMetricsOf` and reading the resulting `MetricSummary` + +## Prerequisites + +- .NET 8.0 SDK +- A LaunchDarkly account and a server-side SDK key + +## Setup + +1. [Create an AI Config](https://launchdarkly.com/docs/home/ai-configs/create) in LaunchDarkly with the key `sample-completion`. Add a system message that contains a Mustache variable (for example `You are a helpful assistant for {{companyName}}.`). Optionally set `temperature` and `maxTokens` model parameters. +2. Copy `.env.example` to `.env` and fill in your SDK key: + ```sh + cp .env.example .env + ``` +3. Restore dependencies: + ```sh + dotnet restore + ``` + +## Run + +```sh +dotnet run +``` diff --git a/server-sdk-ai/features/judge-config/.env.example b/server-sdk-ai/features/judge-config/.env.example new file mode 100644 index 0000000..60e0cb4 --- /dev/null +++ b/server-sdk-ai/features/judge-config/.env.example @@ -0,0 +1,5 @@ +# Your LaunchDarkly server-side SDK key +LAUNCHDARKLY_SDK_KEY= + +# Override to use a different AI Config key +LAUNCHDARKLY_JUDGE_KEY=sample-judge diff --git a/server-sdk-ai/features/judge-config/JudgeConfig.csproj b/server-sdk-ai/features/judge-config/JudgeConfig.csproj new file mode 100644 index 0000000..3ec26a6 --- /dev/null +++ b/server-sdk-ai/features/judge-config/JudgeConfig.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/server-sdk-ai/features/judge-config/NOTICE b/server-sdk-ai/features/judge-config/NOTICE new file mode 100644 index 0000000..401df22 --- /dev/null +++ b/server-sdk-ai/features/judge-config/NOTICE @@ -0,0 +1,4 @@ +LaunchDarkly .NET Server AI SDK Judge Config example +Copyright 2026 Catamorphic, Co. + +This product includes software developed at LaunchDarkly (https://launchdarkly.com/). diff --git a/server-sdk-ai/features/judge-config/Program.cs b/server-sdk-ai/features/judge-config/Program.cs new file mode 100644 index 0000000..c103bad --- /dev/null +++ b/server-sdk-ai/features/judge-config/Program.cs @@ -0,0 +1,165 @@ +using DotNetEnv; +using LaunchDarkly.Sdk; +using LaunchDarkly.Sdk.Server; +using LaunchDarkly.Sdk.Server.Ai; +using LaunchDarkly.Sdk.Server.Ai.Adapters; +using LaunchDarkly.Sdk.Server.Ai.Config; +using LaunchDarkly.Sdk.Server.Ai.Tracking; + +Env.TraversePath().Load(); + +var sdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY"); +if (string.IsNullOrEmpty(sdkKey)) +{ + Console.Error.WriteLine( + "LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again."); + return; +} + +// Set judgeKey to the AI config key you want to evaluate. +var judgeKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_JUDGE_KEY") + ?? "sample-judge"; + +var ldClient = new LdClient(Configuration.Builder(sdkKey).Build()); +if (!ldClient.Initialized) +{ + Console.Error.WriteLine( + "*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo."); + ldClient.Dispose(); + return; +} +Console.WriteLine("*** SDK successfully initialized!"); + +var aiClient = new LdAiClient(new LdClientAdapter(ldClient)); + +// Set up the evaluation context. This context should appear on your +// LaunchDarkly contexts dashboard soon after you run the demo. +var context = Context.Builder(ContextKind.Of("user"), "example-user-key") + .Name("Sandy") + .Build(); + +// Default judge config used as a fallback when LaunchDarkly is unreachable. +var defaultValue = LdAiJudgeConfigDefault.New() + .Enable() + .SetModelName("gpt-4") + .SetModelProviderName("openai") + .SetEvaluationMetricKey("quality") + .AddMessage( + "You are an evaluator. Score the assistant response from 0.0 (poor) to " + + "1.0 (excellent) based on quality and relevance.", + LdAiConfigTypes.Role.System) + .AddMessage("Input given to the assistant: {{message_history}}", LdAiConfigTypes.Role.User) + .AddMessage("Response to evaluate: {{response_to_evaluate}}", LdAiConfigTypes.Role.User) + .Build(); + +// Sample input/output to evaluate. In a real application these would come +// from a previous completion call (typically a tracker resumed via +// LdAiClient.CreateTracker with a resumption token). +const string messageHistory = "How can you help me?"; +const string outputText = "I can answer any question except questions about LaunchDarkly."; + +try +{ + var judgeConfig = aiClient.JudgeConfig( + judgeKey, + context, + defaultValue, + variables: new Dictionary + { + ["message_history"] = messageHistory, + ["response_to_evaluate"] = outputText + }); + + if (!judgeConfig.Enabled) + { + Console.WriteLine($"Judge config '{judgeKey}' is disabled."); + return; + } + + Console.WriteLine(); + Console.WriteLine($"Resolved judge model: {judgeConfig.Model.Name} (provider: {judgeConfig.Provider.Name})"); + Console.WriteLine($"Evaluation metric key: {judgeConfig.EvaluationMetricKey}"); + + if (judgeConfig.Messages.Count > 0) + { + Console.WriteLine(); + Console.WriteLine("Resolved judge prompt (Mustache variables interpolated):"); + foreach (var m in judgeConfig.Messages) + { + Console.WriteLine($" [{m.Role}] {m.Content}"); + } + } + + var tracker = judgeConfig.CreateTracker(); + + // To keep this example provider-agnostic, we wrap a synthetic evaluation in + // TrackMetricsOf and report a hardcoded score. In a real integration the + // operation would call your model provider with the judge's resolved + // messages and parse the score from the response — see the + // getting-started/* examples for end-to-end provider integrations. + Console.WriteLine(); + Console.WriteLine("Running a synthetic judge evaluation wrapped in TrackMetricsOf..."); + + var evaluation = await tracker.TrackMetricsOf( + SyntheticMetrics, + async () => + { + await Task.Delay(150); + return new SyntheticJudgeResult( + Score: 0.8, + Reasoning: "Response was clear but explicitly refused to help with the requested topic.", + InputTokens: 120, + OutputTokens: 60); + }); + + // Emit the judge result back to LaunchDarkly. The event is silently + // dropped when Sampled or Success is false. + tracker.TrackJudgeResult(new JudgeResult( + metricKey: judgeConfig.EvaluationMetricKey, + score: evaluation.Score, + sampled: true, + success: true, + judgeConfigKey: judgeKey)); + + Console.WriteLine(); + Console.WriteLine($"Simulated score: {evaluation.Score}"); + Console.WriteLine($"Reasoning: {evaluation.Reasoning}"); + + PrintSummary(tracker.Summary); +} +catch (Exception ex) +{ + Console.Error.WriteLine($"Error: {ex.Message}"); +} +finally +{ + ldClient.FlushAndWait(TimeSpan.FromSeconds(5)); + ldClient.Dispose(); +} + +static AiMetrics SyntheticMetrics(SyntheticJudgeResult result) => new( + success: true, + tokens: new Usage( + Total: result.InputTokens + result.OutputTokens, + Input: result.InputTokens, + Output: result.OutputTokens)); + +static void PrintSummary(MetricSummary summary) +{ + Console.WriteLine(); + Console.WriteLine("Done! The tracker captured the following metrics:"); + Console.WriteLine($" Duration: {summary.DurationMs}ms"); + Console.WriteLine($" Success: {summary.Success}"); + if (summary.Tokens is { } tokens) + { + Console.WriteLine($" Input tokens: {tokens.Input}"); + Console.WriteLine($" Output tokens: {tokens.Output}"); + Console.WriteLine($" Total tokens: {tokens.Total}"); + } +} + +internal sealed record SyntheticJudgeResult( + double Score, + string Reasoning, + int InputTokens, + int OutputTokens); diff --git a/server-sdk-ai/features/judge-config/README.md b/server-sdk-ai/features/judge-config/README.md new file mode 100644 index 0000000..a5027cf --- /dev/null +++ b/server-sdk-ai/features/judge-config/README.md @@ -0,0 +1,34 @@ +# Judge Config + +This example focuses on the LaunchDarkly AI SDK itself — it is **provider-agnostic** and does not call OpenAI, Bedrock, Anthropic, or any other model. The actual judge evaluation is replaced with a synthetic operation that returns a hardcoded score so you can see the SDK's tracking surface clearly. For end-to-end provider integrations see the [getting-started examples](../../frameworks). + +It demonstrates: + +- Retrieving a judge configuration (prompt messages and evaluation metric key) via `LdAiClient.JudgeConfig` +- Using the builder pattern (`LdAiJudgeConfigDefault.New()`) to declare a fallback default +- Passing template variables for the input under evaluation and the response to evaluate +- Wrapping the judge invocation in `tracker.TrackMetricsOf` +- Emitting the judge result via `tracker.TrackJudgeResult` using the metric key from the config + +## Prerequisites + +- .NET 8.0 SDK +- A LaunchDarkly account and a server-side SDK key + +## Setup + +1. [Create an AI Config](https://launchdarkly.com/docs/home/ai-configs/create) of type **Judge** in LaunchDarkly with the key `sample-judge`. Set an evaluation metric key (for example `quality`), choose a provider and model, and supply judge prompts that may reference Mustache variables such as `{{input}}` and `{{response_to_evaluate}}`. +2. Copy `.env.example` to `.env` and fill in your SDK key: + ```sh + cp .env.example .env + ``` +3. Restore dependencies: + ```sh + dotnet restore + ``` + +## Run + +```sh +dotnet run +``` diff --git a/server-sdk-ai/frameworks/bedrock/converse/.env.example b/server-sdk-ai/frameworks/bedrock/converse/.env.example new file mode 100644 index 0000000..b402924 --- /dev/null +++ b/server-sdk-ai/frameworks/bedrock/converse/.env.example @@ -0,0 +1,10 @@ +# Your LaunchDarkly server-side SDK key +LAUNCHDARKLY_SDK_KEY= + +# AWS credentials (or use the default AWS credential chain) +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 + +# Override to use a different AI Config key +LAUNCHDARKLY_COMPLETION_KEY=sample-completion diff --git a/server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj b/server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj new file mode 100644 index 0000000..e311a16 --- /dev/null +++ b/server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/server-sdk-ai/frameworks/bedrock/converse/NOTICE b/server-sdk-ai/frameworks/bedrock/converse/NOTICE new file mode 100644 index 0000000..92cc136 --- /dev/null +++ b/server-sdk-ai/frameworks/bedrock/converse/NOTICE @@ -0,0 +1,4 @@ +LaunchDarkly .NET Server AI SDK Bedrock Converse example +Copyright 2026 Catamorphic, Co. + +This product includes software developed at LaunchDarkly (https://launchdarkly.com/). diff --git a/server-sdk-ai/frameworks/bedrock/converse/Program.cs b/server-sdk-ai/frameworks/bedrock/converse/Program.cs new file mode 100644 index 0000000..edf046e --- /dev/null +++ b/server-sdk-ai/frameworks/bedrock/converse/Program.cs @@ -0,0 +1,179 @@ +using System.Net; +using Amazon.BedrockRuntime; +using Amazon.BedrockRuntime.Model; +using DotNetEnv; +using LaunchDarkly.Sdk; +using LaunchDarkly.Sdk.Server; +using LaunchDarkly.Sdk.Server.Ai; +using LaunchDarkly.Sdk.Server.Ai.Adapters; +using LaunchDarkly.Sdk.Server.Ai.Config; +using LaunchDarkly.Sdk.Server.Ai.Tracking; + +Env.TraversePath().Load(); + +var sdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY"); +if (string.IsNullOrEmpty(sdkKey)) +{ + Console.Error.WriteLine( + "LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again."); + return; +} + +// Set completionKey to the AI config key you want to evaluate. +var completionKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_COMPLETION_KEY") + ?? "sample-completion"; + +var ldClient = new LdClient(Configuration.Builder(sdkKey).Build()); +if (!ldClient.Initialized) +{ + Console.Error.WriteLine( + "*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo."); + ldClient.Dispose(); + return; +} +Console.WriteLine("*** SDK successfully initialized!"); + +var aiClient = new LdAiClient(new LdClientAdapter(ldClient)); + +// Set up the evaluation context. This context should appear on your +// LaunchDarkly contexts dashboard soon after you run the demo. +var context = Context.Builder(ContextKind.Of("user"), "example-user-key") + .Name("Sandy") + .Build(); + +// The AWS SDK reads credentials and the default region from environment +// variables, the shared credentials file, or IAM role metadata. +var bedrockClient = new AmazonBedrockRuntimeClient(); + +try +{ + // Pass a defaultValue for improved resiliency when the AI config is + // unavailable or LaunchDarkly is unreachable; omit for a disabled default. + // Example: + // var defaultValue = LdAiCompletionConfigDefault.New() + // .Enable() + // .SetModelName("anthropic.claude-3-sonnet-20240229-v1:0") + // .SetModelProviderName("bedrock") + // .AddMessage("You are a helpful assistant.", LdAiConfigTypes.Role.System) + // .Build(); + var config = aiClient.CompletionConfig( + completionKey, + context, + variables: new Dictionary + { + ["myUserVariable"] = "Testing Variable" + }); + + if (!config.Enabled) + { + Console.WriteLine( + $"AI config '{completionKey}' is disabled. Verify the config key exists in " + + "your LaunchDarkly project and is not targeting a disabled variation."); + return; + } + + var tracker = config.CreateTracker(); + + var (chatMessages, systemMessages) = MapMessages(config.Messages); + + var sampleQuestion = "What can you help me with?"; + chatMessages.Add(new Message + { + Role = ConversationRole.User, + Content = new List { new() { Text = sampleQuestion } } + }); + + var modelId = string.IsNullOrEmpty(config.Model.Name) ? "no-model" : config.Model.Name; + + Console.WriteLine(); + Console.WriteLine($"Sending sample question to {modelId}: \"{sampleQuestion}\""); + Console.WriteLine("Waiting for response..."); + + var response = await tracker.TrackMetricsOf( + ExtractBedrockMetrics, + async () => await bedrockClient.ConverseAsync(new ConverseRequest + { + ModelId = modelId, + Messages = chatMessages, + System = systemMessages, + InferenceConfig = BuildInferenceConfig(config.Model.Parameters) + })); + + var aiResponse = response.Output?.Message?.Content?.FirstOrDefault()?.Text ?? ""; + Console.WriteLine(); + Console.WriteLine("Model response:"); + Console.WriteLine(aiResponse); + + PrintSummary(tracker.Summary); +} +catch (Exception ex) +{ + // In production, sanitize before logging — provider errors may include credentials. + Console.Error.WriteLine($"Error: {ex.Message}"); +} +finally +{ + ldClient.FlushAndWait(TimeSpan.FromSeconds(5)); + ldClient.Dispose(); +} + +static AiMetrics ExtractBedrockMetrics(ConverseResponse res) => new( + success: res.HttpStatusCode == HttpStatusCode.OK, + tokens: res.Usage is null + ? null + : new Usage( + Total: res.Usage.TotalTokens, + Input: res.Usage.InputTokens, + Output: res.Usage.OutputTokens)); + +static (List ChatMessages, List SystemMessages) MapMessages( + IReadOnlyList sdkMessages) +{ + var chat = new List(); + var system = new List(); + foreach (var m in sdkMessages) + { + if (m.Role == LdAiConfigTypes.Role.System) + { + system.Add(new SystemContentBlock { Text = m.Content }); + continue; + } + + chat.Add(new Message + { + Role = m.Role == LdAiConfigTypes.Role.Assistant + ? ConversationRole.Assistant + : ConversationRole.User, + Content = new List { new() { Text = m.Content } } + }); + } + return (chat, system); +} + +static InferenceConfiguration BuildInferenceConfig(IReadOnlyDictionary parameters) +{ + var config = new InferenceConfiguration(); + if (parameters.TryGetValue("temperature", out var temp) && !temp.IsNull) + { + config.Temperature = temp.AsFloat; + } + if (parameters.TryGetValue("maxTokens", out var maxTokens) && !maxTokens.IsNull) + { + config.MaxTokens = maxTokens.AsInt; + } + return config; +} + +static void PrintSummary(MetricSummary summary) +{ + Console.WriteLine(); + Console.WriteLine("Done! The AI config was evaluated and the following metrics were tracked:"); + Console.WriteLine($" Duration: {summary.DurationMs}ms"); + Console.WriteLine($" Success: {summary.Success}"); + if (summary.Tokens is { } tokens) + { + Console.WriteLine($" Input tokens: {tokens.Input}"); + Console.WriteLine($" Output tokens: {tokens.Output}"); + Console.WriteLine($" Total tokens: {tokens.Total}"); + } +} diff --git a/server-sdk-ai/frameworks/bedrock/converse/README.md b/server-sdk-ai/frameworks/bedrock/converse/README.md new file mode 100644 index 0000000..883a73d --- /dev/null +++ b/server-sdk-ai/frameworks/bedrock/converse/README.md @@ -0,0 +1,28 @@ +# AWS Bedrock Converse Example + +This example demonstrates how to use the LaunchDarkly AI SDK for .NET with the [AWS Bedrock Converse API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html). + +## Prerequisites + +- .NET 8.0 SDK +- A LaunchDarkly account and a server-side SDK key +- An AWS account with [Bedrock model access enabled](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) +- AWS credentials available to the AWS SDK (environment variables, the shared credentials file, or an IAM role) + +## Setup + +1. [Create an AI Config](https://launchdarkly.com/docs/home/ai-configs/create) in LaunchDarkly with the key `sample-completion`. Select Bedrock as the provider, set the model name to a Bedrock model ID (for example `anthropic.claude-3-sonnet-20240229-v1:0`), and add a system message. +2. Copy `.env.example` to `.env` and fill in your keys: + ```sh + cp .env.example .env + ``` +3. Restore dependencies: + ```sh + dotnet restore + ``` + +## Run + +```sh +dotnet run +``` diff --git a/server-sdk-ai/frameworks/openai/chat-completions/.env.example b/server-sdk-ai/frameworks/openai/chat-completions/.env.example new file mode 100644 index 0000000..f7dc420 --- /dev/null +++ b/server-sdk-ai/frameworks/openai/chat-completions/.env.example @@ -0,0 +1,8 @@ +# Your LaunchDarkly server-side SDK key +LAUNCHDARKLY_SDK_KEY= + +# Your OpenAI API key +OPENAI_API_KEY= + +# Override to use a different AI Config key +LAUNCHDARKLY_COMPLETION_KEY=sample-completion diff --git a/server-sdk-ai/frameworks/openai/chat-completions/NOTICE b/server-sdk-ai/frameworks/openai/chat-completions/NOTICE new file mode 100644 index 0000000..87678f3 --- /dev/null +++ b/server-sdk-ai/frameworks/openai/chat-completions/NOTICE @@ -0,0 +1,4 @@ +LaunchDarkly .NET Server AI SDK OpenAI Chat Completions example +Copyright 2026 Catamorphic, Co. + +This product includes software developed at LaunchDarkly (https://launchdarkly.com/). diff --git a/server-sdk-ai/frameworks/openai/chat-completions/OpenAiChatCompletions.csproj b/server-sdk-ai/frameworks/openai/chat-completions/OpenAiChatCompletions.csproj new file mode 100644 index 0000000..7c15241 --- /dev/null +++ b/server-sdk-ai/frameworks/openai/chat-completions/OpenAiChatCompletions.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + diff --git a/server-sdk-ai/frameworks/openai/chat-completions/Program.cs b/server-sdk-ai/frameworks/openai/chat-completions/Program.cs new file mode 100644 index 0000000..6664a47 --- /dev/null +++ b/server-sdk-ai/frameworks/openai/chat-completions/Program.cs @@ -0,0 +1,139 @@ +using DotNetEnv; +using LaunchDarkly.Sdk; +using LaunchDarkly.Sdk.Server; +using LaunchDarkly.Sdk.Server.Ai; +using LaunchDarkly.Sdk.Server.Ai.Adapters; +using LaunchDarkly.Sdk.Server.Ai.Config; +using LaunchDarkly.Sdk.Server.Ai.Tracking; +using OpenAI.Chat; + +Env.TraversePath().Load(); + +var sdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY"); +if (string.IsNullOrEmpty(sdkKey)) +{ + Console.Error.WriteLine( + "LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again."); + return; +} + +var openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY"); +if (string.IsNullOrEmpty(openAiKey)) +{ + Console.Error.WriteLine( + "OpenAI API key is required: set the OPENAI_API_KEY environment variable and try again."); + return; +} + +// Set completionKey to the AI config key you want to evaluate. +var completionKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_COMPLETION_KEY") + ?? "sample-completion"; + +var ldClient = new LdClient(Configuration.Builder(sdkKey).Build()); +if (!ldClient.Initialized) +{ + Console.Error.WriteLine( + "*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo."); + ldClient.Dispose(); + return; +} +Console.WriteLine("*** SDK successfully initialized!"); + +var aiClient = new LdAiClient(new LdClientAdapter(ldClient)); + +// Set up the evaluation context. This context should appear on your +// LaunchDarkly contexts dashboard soon after you run the demo. +var context = Context.Builder(ContextKind.Of("user"), "example-user-key") + .Name("Sandy") + .Build(); + +try +{ + // Pass a defaultValue for improved resiliency when the AI config is + // unavailable or LaunchDarkly is unreachable; omit for a disabled default. + // Example: + // var defaultValue = LdAiCompletionConfigDefault.New() + // .Enable() + // .SetModelName("gpt-4") + // .SetModelProviderName("openai") + // .AddMessage("You are a helpful assistant.", LdAiConfigTypes.Role.System) + // .Build(); + var config = aiClient.CompletionConfig( + completionKey, + context, + variables: new Dictionary + { + ["myUserVariable"] = "Testing Variable" + }); + + if (!config.Enabled) + { + Console.WriteLine( + $"AI config '{completionKey}' is disabled. Verify the config key exists in " + + "your LaunchDarkly project and is not targeting a disabled variation."); + return; + } + + var tracker = config.CreateTracker(); + + var modelName = string.IsNullOrEmpty(config.Model.Name) ? "gpt-4" : config.Model.Name; + var chatClient = new ChatClient(modelName, openAiKey); + + var sampleQuestion = "What can you help me with?"; + var messages = config.Messages + .Select(ToOpenAiMessage) + .Append(new UserChatMessage(sampleQuestion)) + .ToList(); + + Console.WriteLine(); + Console.WriteLine($"Sending sample question to {modelName}: \"{sampleQuestion}\""); + Console.WriteLine("Waiting for response..."); + + var completion = await tracker.TrackMetricsOf( + result => new AiMetrics( + success: true, + tokens: new Usage( + Total: result.Value.Usage.TotalTokenCount, + Input: result.Value.Usage.InputTokenCount, + Output: result.Value.Usage.OutputTokenCount)), + async () => await chatClient.CompleteChatAsync(messages)); + + var aiResponse = completion.Value.Content[0].Text; + Console.WriteLine(); + Console.WriteLine("Model response:"); + Console.WriteLine(aiResponse); + + PrintSummary(tracker.Summary); +} +catch (Exception ex) +{ + // In production, sanitize before logging — provider errors may include credentials. + Console.Error.WriteLine($"Error: {ex.Message}"); +} +finally +{ + ldClient.FlushAndWait(TimeSpan.FromSeconds(5)); + ldClient.Dispose(); +} + +static ChatMessage ToOpenAiMessage(LdAiConfigTypes.Message m) => m.Role switch +{ + LdAiConfigTypes.Role.System => new SystemChatMessage(m.Content), + LdAiConfigTypes.Role.Assistant => new AssistantChatMessage(m.Content), + LdAiConfigTypes.Role.User => new UserChatMessage(m.Content), + _ => new UserChatMessage(m.Content) +}; + +static void PrintSummary(MetricSummary summary) +{ + Console.WriteLine(); + Console.WriteLine("Done! The AI config was evaluated and the following metrics were tracked:"); + Console.WriteLine($" Duration: {summary.DurationMs}ms"); + Console.WriteLine($" Success: {summary.Success}"); + if (summary.Tokens is { } tokens) + { + Console.WriteLine($" Input tokens: {tokens.Input}"); + Console.WriteLine($" Output tokens: {tokens.Output}"); + Console.WriteLine($" Total tokens: {tokens.Total}"); + } +} diff --git a/server-sdk-ai/frameworks/openai/chat-completions/README.md b/server-sdk-ai/frameworks/openai/chat-completions/README.md new file mode 100644 index 0000000..9375d0f --- /dev/null +++ b/server-sdk-ai/frameworks/openai/chat-completions/README.md @@ -0,0 +1,27 @@ +# OpenAI Chat Completions Example + +This example demonstrates how to use the LaunchDarkly AI SDK for .NET with the [OpenAI Chat Completions API](https://platform.openai.com/docs/api-reference/chat). + +## Prerequisites + +- .NET 8.0 SDK +- A LaunchDarkly account and a server-side SDK key +- An [OpenAI API key](https://platform.openai.com/api-keys) + +## Setup + +1. [Create an AI Config](https://launchdarkly.com/docs/home/ai-configs/create) in LaunchDarkly with the key `sample-completion`. Select OpenAI as the provider, a model such as `gpt-4`, and add a system message. +2. Copy `.env.example` to `.env` and fill in your keys: + ```sh + cp .env.example .env + ``` +3. Restore dependencies: + ```sh + dotnet restore + ``` + +## Run + +```sh +dotnet run +```