From 1ebe2ee7d2a6231206a5ebdda017fb19c73d673a Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Wed, 17 Jun 2026 11:06:39 -0500 Subject: [PATCH 1/2] feat: Add .NET Server AI SDK examples Adds the server-sdk-ai partition (LaunchDarkly.ServerSdk.Ai) following the EXAM layout: provider integrations under frameworks/ (OpenAI Chat Completions, Bedrock Converse) and provider-agnostic SDK-surface examples under features/ (completion-config, agent-config, judge-config). Sourced from dotnet-core PR #293 (AIC-2789); each example is self-contained and references the published NuGet packages with floating versions. Adds the AI SDK row to the root README, a server-sdk-ai README index, and a CODEOWNERS rule giving the AI teams the folder. --- .github/workflows/server-sdk-ai.yml | 43 +++++ CODEOWNERS | 3 + README.md | 7 +- server-sdk-ai/README.md | 26 +++ .../features/agent-config/.env.example | 5 + .../features/agent-config/AgentConfig.csproj | 16 ++ server-sdk-ai/features/agent-config/NOTICE | 4 + .../features/agent-config/Program.cs | 152 +++++++++++++++ server-sdk-ai/features/agent-config/README.md | 34 ++++ .../features/completion-config/.env.example | 5 + .../completion-config/CompletionConfig.csproj | 16 ++ .../features/completion-config/NOTICE | 4 + .../features/completion-config/Program.cs | 163 ++++++++++++++++ .../features/completion-config/README.md | 34 ++++ .../features/judge-config/.env.example | 5 + .../features/judge-config/JudgeConfig.csproj | 16 ++ server-sdk-ai/features/judge-config/NOTICE | 4 + .../features/judge-config/Program.cs | 165 ++++++++++++++++ server-sdk-ai/features/judge-config/README.md | 34 ++++ .../frameworks/bedrock/converse/.env.example | 10 + .../bedrock/converse/BedrockConverse.csproj | 17 ++ .../frameworks/bedrock/converse/NOTICE | 4 + .../frameworks/bedrock/converse/Program.cs | 179 ++++++++++++++++++ .../frameworks/bedrock/converse/README.md | 28 +++ .../openai/chat-completions/.env.example | 8 + .../frameworks/openai/chat-completions/NOTICE | 4 + .../OpenAiChatCompletions.csproj | 17 ++ .../openai/chat-completions/Program.cs | 139 ++++++++++++++ .../openai/chat-completions/README.md | 27 +++ 29 files changed, 1166 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/server-sdk-ai.yml create mode 100644 server-sdk-ai/README.md create mode 100644 server-sdk-ai/features/agent-config/.env.example create mode 100644 server-sdk-ai/features/agent-config/AgentConfig.csproj create mode 100644 server-sdk-ai/features/agent-config/NOTICE create mode 100644 server-sdk-ai/features/agent-config/Program.cs create mode 100644 server-sdk-ai/features/agent-config/README.md create mode 100644 server-sdk-ai/features/completion-config/.env.example create mode 100644 server-sdk-ai/features/completion-config/CompletionConfig.csproj create mode 100644 server-sdk-ai/features/completion-config/NOTICE create mode 100644 server-sdk-ai/features/completion-config/Program.cs create mode 100644 server-sdk-ai/features/completion-config/README.md create mode 100644 server-sdk-ai/features/judge-config/.env.example create mode 100644 server-sdk-ai/features/judge-config/JudgeConfig.csproj create mode 100644 server-sdk-ai/features/judge-config/NOTICE create mode 100644 server-sdk-ai/features/judge-config/Program.cs create mode 100644 server-sdk-ai/features/judge-config/README.md create mode 100644 server-sdk-ai/frameworks/bedrock/converse/.env.example create mode 100644 server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj create mode 100644 server-sdk-ai/frameworks/bedrock/converse/NOTICE create mode 100644 server-sdk-ai/frameworks/bedrock/converse/Program.cs create mode 100644 server-sdk-ai/frameworks/bedrock/converse/README.md create mode 100644 server-sdk-ai/frameworks/openai/chat-completions/.env.example create mode 100644 server-sdk-ai/frameworks/openai/chat-completions/NOTICE create mode 100644 server-sdk-ai/frameworks/openai/chat-completions/OpenAiChatCompletions.csproj create mode 100644 server-sdk-ai/frameworks/openai/chat-completions/Program.cs create mode 100644 server-sdk-ai/frameworks/openai/chat-completions/README.md 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..fbe5173 --- /dev/null +++ b/server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj @@ -0,0 +1,17 @@ + + + + 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 +``` From 8c16d3c3b3971dc7f3c4ea376bf5954e42473012 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Thu, 18 Jun 2026 11:57:35 -0500 Subject: [PATCH 2/2] fix: Add SSO credential packages for Bedrock example Without AWSSDK.SSO and AWSSDK.SSOOIDC, the .NET AWS SDK cannot resolve AWS SSO credentials and falls through to the EC2 Instance Metadata Service, which fails on local machines. Co-authored-by: Cursor --- .../frameworks/bedrock/converse/BedrockConverse.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj b/server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj index fbe5173..e311a16 100644 --- a/server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj +++ b/server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj @@ -11,6 +11,9 @@ + + +