From 41977c9dde5d462aaaadbcafcea2affaf107a245 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Fri, 6 Sep 2024 09:57:25 +0200 Subject: [PATCH 1/7] Added first anthropic sample --- .../Anthropic_ChatCompletion.cs | 71 +++++++++++++++++++ dotnet/samples/Concepts/Concepts.csproj | 1 + .../InternalUtilities/TestConfiguration.cs | 7 ++ 3 files changed, 79 insertions(+) create mode 100644 dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletion.cs diff --git a/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletion.cs b/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletion.cs new file mode 100644 index 000000000000..026df090e829 --- /dev/null +++ b/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletion.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace ChatCompletion; + +public sealed class Anthropic_ChatCompletion(ITestOutputHelper output) : BaseTest(output) +{ + [Fact] + public async Task SampleAsync() + { + Console.WriteLine("============= Anthropic - Claude Chat Completion ============="); + + string apiKey = TestConfiguration.AnthropicAI.ApiKey; + string modelId = TestConfiguration.AnthropicAI.ModelId; + + if (apiKey is null || modelId is null) + { + Console.WriteLine("Anthropic credentials not found. Skipping example."); + return; + } + + Kernel kernel = Kernel.CreateBuilder() + .AddAnthropicChatCompletion( + modelId: modelId, + apiKey: apiKey) + .Build(); + + await SimpleChatAsync(kernel); + } + + private async Task SimpleChatAsync(Kernel kernel) + { + Console.WriteLine("======== Simple Chat ========"); + + var chatHistory = new ChatHistory("You are an expert in the tool shop."); + var chat = kernel.GetRequiredService(); + + // First user message + chatHistory.AddUserMessage("Hi, I'm looking for new power tools, any suggestion?"); + await MessageOutputAsync(chatHistory); + + // First bot assistant message + var reply = await chat.GetChatMessageContentAsync(chatHistory); + chatHistory.Add(reply); + await MessageOutputAsync(chatHistory); + + // Second user message + chatHistory.AddUserMessage("I'm looking for a drill, a screwdriver and a hammer."); + await MessageOutputAsync(chatHistory); + + // Second bot assistant message + reply = await chat.GetChatMessageContentAsync(chatHistory); + chatHistory.Add(reply); + await MessageOutputAsync(chatHistory); + } + + /// + /// Outputs the last message of the chat history + /// + private Task MessageOutputAsync(ChatHistory chatHistory) + { + var message = chatHistory.Last(); + + Console.WriteLine($"{message.Role}: {message.Content}"); + Console.WriteLine("------------------------"); + + return Task.CompletedTask; + } +} diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index dd43184b6612..2d4aea18c108 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -49,6 +49,7 @@ + diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs index 1a86413a5e05..53c0ba33475a 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs @@ -41,6 +41,7 @@ public static void Initialize(IConfigurationRoot configRoot) public static ChatGPTRetrievalPluginConfig ChatGPTRetrievalPlugin => LoadSection(); public static MsGraphConfiguration MSGraph => LoadSection(); public static MistralAIConfig MistralAI => LoadSection(); + public static AnthropicAIConfig AnthropicAI => LoadSection(); public static GoogleAIConfig GoogleAI => LoadSection(); public static VertexAIConfig VertexAI => LoadSection(); public static AzureCosmosDbMongoDbConfig AzureCosmosDbMongoDb => LoadSection(); @@ -194,6 +195,12 @@ public class MistralAIConfig public string EmbeddingModelId { get; set; } } + public class AnthropicAIConfig + { + public string ApiKey { get; set; } + public string ModelId { get; set; } + } + public class GoogleAIConfig { public string ApiKey { get; set; } From efaa7dba06856cf3d56ec80e178634211d3dd74b Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Fri, 6 Sep 2024 10:02:06 +0200 Subject: [PATCH 2/7] Added more samples --- .../Anthropic_ChatCompletionStream.cs | 93 +++++++++++++++++++ .../ChatCompletion/Anthropic_Vision.cs | 55 +++++++++++ 2 files changed, 148 insertions(+) create mode 100644 dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletionStream.cs create mode 100644 dotnet/samples/Concepts/ChatCompletion/Anthropic_Vision.cs diff --git a/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletionStream.cs b/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletionStream.cs new file mode 100644 index 000000000000..b3c77c5bf88a --- /dev/null +++ b/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletionStream.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace ChatCompletion; + +public sealed class Anthropic_ChatCompletionStream(ITestOutputHelper output) : BaseTest(output) +{ + [Fact] + public async Task SampleAsync() + { + Console.WriteLine("============= Anthropic - Claude Chat Streaming ============="); + + string apiKey = TestConfiguration.AnthropicAI.ApiKey; + string modelId = TestConfiguration.AnthropicAI.ModelId; + + if (apiKey is null || modelId is null) + { + Console.WriteLine("Anthropic credentials not found. Skipping example."); + return; + } + + Kernel kernel = Kernel.CreateBuilder() + .AddAnthropicChatCompletion( + modelId: modelId, + apiKey: apiKey) + .Build(); + + await this.StreamingChatAsync(kernel); + } + + private async Task StreamingChatAsync(Kernel kernel) + { + Console.WriteLine("======== Streaming Chat ========"); + + var chatHistory = new ChatHistory("You are an expert in the tool shop."); + var chat = kernel.GetRequiredService(); + + // First user message + chatHistory.AddUserMessage("Hi, I'm looking for alternative coffee brew methods, can you help me?"); + await MessageOutputAsync(chatHistory); + + // First bot assistant message + var streamingChat = chat.GetStreamingChatMessageContentsAsync(chatHistory); + var reply = await MessageOutputAsync(streamingChat); + chatHistory.Add(reply); + + // Second user message + chatHistory.AddUserMessage("Give me the best speciality coffee roasters."); + await MessageOutputAsync(chatHistory); + + // Second bot assistant message + streamingChat = chat.GetStreamingChatMessageContentsAsync(chatHistory); + reply = await MessageOutputAsync(streamingChat); + chatHistory.Add(reply); + } + + /// + /// Outputs the last message of the chat history + /// + private Task MessageOutputAsync(ChatHistory chatHistory) + { + var message = chatHistory.Last(); + + Console.WriteLine($"{message.Role}: {message.Content}"); + Console.WriteLine("------------------------"); + + return Task.CompletedTask; + } + + private async Task MessageOutputAsync(IAsyncEnumerable streamingChat) + { + bool first = true; + StringBuilder messageBuilder = new(); + await foreach (var chatMessage in streamingChat) + { + if (first) + { + Console.Write($"{chatMessage.Role}: "); + first = false; + } + + Console.Write(chatMessage.Content); + messageBuilder.Append(chatMessage.Content); + } + + Console.WriteLine(); + Console.WriteLine("------------------------"); + return new ChatMessageContent(AuthorRole.Assistant, messageBuilder.ToString()); + } +} diff --git a/dotnet/samples/Concepts/ChatCompletion/Anthropic_Vision.cs b/dotnet/samples/Concepts/ChatCompletion/Anthropic_Vision.cs new file mode 100644 index 000000000000..6fe42457aa07 --- /dev/null +++ b/dotnet/samples/Concepts/ChatCompletion/Anthropic_Vision.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Resources; + +namespace ChatCompletion; + +public sealed class Anthropic_Vision(ITestOutputHelper output) : BaseTest(output) +{ + [Fact] + public async Task SampleAsync() + { + Console.WriteLine("============= Anthropic - Claude Chat Completion ============="); + + string apiKey = TestConfiguration.AnthropicAI.ApiKey; + string modelId = TestConfiguration.AnthropicAI.ModelId; + + if (apiKey is null || modelId is null) + { + Console.WriteLine("Anthropic credentials not found. Skipping example."); + return; + } + + Kernel kernel = Kernel.CreateBuilder() + .AddAnthropicChatCompletion( + modelId: modelId, + apiKey: apiKey) + .Build(); + + var chatHistory = new ChatHistory("Your job is describing images."); + var chatCompletionService = kernel.GetRequiredService(); + + // Load the image from the resources + await using var stream = EmbeddedResource.ReadStream("sample_image.jpg")!; + using var binaryReader = new BinaryReader(stream); + var bytes = binaryReader.ReadBytes((int)stream.Length); + + chatHistory.AddUserMessage( + [ + new TextContent("What’s in this image?"), + // Vertex AI Gemini API supports both base64 and URI format + // You have to always provide the mimeType for the image + new ImageContent(bytes, "image/jpeg"), + // The Cloud Storage URI of the image to include in the prompt. + // The bucket that stores the file must be in the same Google Cloud project that's sending the request. + // new ImageContent(new Uri("gs://generativeai-downloads/images/scones.jpg"), + // metadata: new Dictionary { { "mimeType", "image/jpeg" } }) + ]); + + var reply = await chatCompletionService.GetChatMessageContentAsync(chatHistory); + + Console.WriteLine(reply.Content); + } +} From b169619ae903f8ee248f983b8a794bbc812dcc27 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Fri, 6 Sep 2024 10:08:51 +0200 Subject: [PATCH 3/7] sample about providers --- .../Anthropic_ProvidersSetup.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 dotnet/samples/Concepts/ChatCompletion/Anthropic_ProvidersSetup.cs diff --git a/dotnet/samples/Concepts/ChatCompletion/Anthropic_ProvidersSetup.cs b/dotnet/samples/Concepts/ChatCompletion/Anthropic_ProvidersSetup.cs new file mode 100644 index 000000000000..753d7af61c79 --- /dev/null +++ b/dotnet/samples/Concepts/ChatCompletion/Anthropic_ProvidersSetup.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; + +namespace ChatCompletion; + +/// +/// This sample shows how to setup different providers for anthropic. +/// +public sealed class Anthropic_ProvidersSetup(ITestOutputHelper output) : BaseTest(output) +{ + public void AnthropicProvider() + { + var kernel = Kernel.CreateBuilder() + .AddAnthropicChatCompletion( + modelId: "modelId", + apiKey: "apiKey") + .Build(); + } + + /// + /// For more information on how to setup the Vertex AI provider, go to sample. + /// + public void VertexAiProvider() + { + var kernel = Kernel.CreateBuilder() + .AddAnthropicVertextAIChatCompletion( + modelId: "modelId", + bearerTokenProvider: () => ValueTask.FromResult("bearer"), + endpoint: new Uri("https://your-endpoint")) + .Build(); + } +} From a07154731130938e90732072cd48b43b685d3df9 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Fri, 6 Sep 2024 20:13:00 +0200 Subject: [PATCH 4/7] Addresses feedback --- .../Concepts/ChatCompletion/Anthropic_ChatCompletion.cs | 7 ++----- ...ionStream.cs => Anthropic_ChatCompletionStreaming.cs} | 9 +++------ .../samples/Concepts/ChatCompletion/Anthropic_Vision.cs | 7 ++----- dotnet/samples/Concepts/README.md | 4 ++++ 4 files changed, 11 insertions(+), 16 deletions(-) rename dotnet/samples/Concepts/ChatCompletion/{Anthropic_ChatCompletionStream.cs => Anthropic_ChatCompletionStreaming.cs} (91%) diff --git a/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletion.cs b/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletion.cs index 026df090e829..c3bf9a0a19d8 100644 --- a/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletion.cs +++ b/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletion.cs @@ -15,11 +15,8 @@ public async Task SampleAsync() string apiKey = TestConfiguration.AnthropicAI.ApiKey; string modelId = TestConfiguration.AnthropicAI.ModelId; - if (apiKey is null || modelId is null) - { - Console.WriteLine("Anthropic credentials not found. Skipping example."); - return; - } + Assert.NotNull(apiKey); + Assert.NotNull(modelId); Kernel kernel = Kernel.CreateBuilder() .AddAnthropicChatCompletion( diff --git a/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletionStream.cs b/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletionStreaming.cs similarity index 91% rename from dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletionStream.cs rename to dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletionStreaming.cs index b3c77c5bf88a..471107d8281b 100644 --- a/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletionStream.cs +++ b/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletionStreaming.cs @@ -6,7 +6,7 @@ namespace ChatCompletion; -public sealed class Anthropic_ChatCompletionStream(ITestOutputHelper output) : BaseTest(output) +public sealed class Anthropic_ChatCompletionStreaming(ITestOutputHelper output) : BaseTest(output) { [Fact] public async Task SampleAsync() @@ -16,11 +16,8 @@ public async Task SampleAsync() string apiKey = TestConfiguration.AnthropicAI.ApiKey; string modelId = TestConfiguration.AnthropicAI.ModelId; - if (apiKey is null || modelId is null) - { - Console.WriteLine("Anthropic credentials not found. Skipping example."); - return; - } + Assert.NotNull(apiKey); + Assert.NotNull(modelId); Kernel kernel = Kernel.CreateBuilder() .AddAnthropicChatCompletion( diff --git a/dotnet/samples/Concepts/ChatCompletion/Anthropic_Vision.cs b/dotnet/samples/Concepts/ChatCompletion/Anthropic_Vision.cs index 6fe42457aa07..324992ed17df 100644 --- a/dotnet/samples/Concepts/ChatCompletion/Anthropic_Vision.cs +++ b/dotnet/samples/Concepts/ChatCompletion/Anthropic_Vision.cs @@ -16,11 +16,8 @@ public async Task SampleAsync() string apiKey = TestConfiguration.AnthropicAI.ApiKey; string modelId = TestConfiguration.AnthropicAI.ModelId; - if (apiKey is null || modelId is null) - { - Console.WriteLine("Anthropic credentials not found. Skipping example."); - return; - } + Assert.NotNull(apiKey); + Assert.NotNull(modelId); Kernel kernel = Kernel.CreateBuilder() .AddAnthropicChatCompletion( diff --git a/dotnet/samples/Concepts/README.md b/dotnet/samples/Concepts/README.md index 77427c605193..94af2d6f55ce 100644 --- a/dotnet/samples/Concepts/README.md +++ b/dotnet/samples/Concepts/README.md @@ -43,6 +43,10 @@ Down below you can find the code snippets that demonstrate the usage of many Sem - [Google_GeminiChatCompletionStreaming](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionStreaming.cs) - [Google_GeminiGetModelResult](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiGetModelResult.cs) - [Google_GeminiVision](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiVision.cs) +- [Anthropic_ChatCompletion](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletion.cs) +- [Anthropic_ChatCompletionStreaming](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Anthropic_ChatCompletionStreaming.cs) +- [Anthropic_Vision](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Anthropic_Vision.cs) +- [Anthropic_ProvidersSetup](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Anthropic_ProvidersSetup.cs) - [OpenAI_ChatCompletion](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletion.cs) - [OpenAI_ChatCompletionMultipleChoices](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionMultipleChoices.cs) - [OpenAI_ChatCompletionStreaming](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionStreaming.cs) From ab855ad32f5bf84e2e3407695682e370ae07d1df Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Fri, 6 Sep 2024 22:05:01 +0200 Subject: [PATCH 5/7] Added anthropic to AI router demo --- dotnet/samples/Demos/AIModelRouter/AIModelRouter.csproj | 2 ++ dotnet/samples/Demos/AIModelRouter/Program.cs | 3 ++- dotnet/samples/Demos/AIModelRouter/README.md | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dotnet/samples/Demos/AIModelRouter/AIModelRouter.csproj b/dotnet/samples/Demos/AIModelRouter/AIModelRouter.csproj index fb5862e3270a..6470829f01ef 100644 --- a/dotnet/samples/Demos/AIModelRouter/AIModelRouter.csproj +++ b/dotnet/samples/Demos/AIModelRouter/AIModelRouter.csproj @@ -6,6 +6,7 @@ enable enable 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + SKEXP0070 @@ -14,6 +15,7 @@ + diff --git a/dotnet/samples/Demos/AIModelRouter/Program.cs b/dotnet/samples/Demos/AIModelRouter/Program.cs index 5bafa4934883..7cb6227d95a6 100644 --- a/dotnet/samples/Demos/AIModelRouter/Program.cs +++ b/dotnet/samples/Demos/AIModelRouter/Program.cs @@ -25,6 +25,7 @@ private static async Task Main(string[] args) .AddOpenAIChatCompletion(serviceId: "lmstudio", modelId: "N/A", endpoint: new Uri("http://localhost:1234"), apiKey: null) .AddOpenAIChatCompletion(serviceId: "ollama", modelId: "phi3", endpoint: new Uri("http://localhost:11434"), apiKey: null) .AddOpenAIChatCompletion(serviceId: "openai", modelId: "gpt-4o", apiKey: config["OpenAI:ApiKey"]!) + .AddAnthropicChatCompletion(serviceId: "claude", modelId: "claude-3-5-sonnet-20240620", apiKey: config["Anthropic:ApiKey"]!) // Adding a custom filter to capture router selected service id .Services.AddSingleton(new SelectedServiceFilter()); @@ -43,7 +44,7 @@ private static async Task Main(string[] args) // Find the best service to use based on the user's input KernelArguments arguments = new(new PromptExecutionSettings() { - ServiceId = router.FindService(userMessage, ["lmstudio", "ollama", "openai"]) + ServiceId = router.FindService(userMessage, ["lmstudio", "ollama", "openai", "claude"]) }); // Invoke the prompt and print the response diff --git a/dotnet/samples/Demos/AIModelRouter/README.md b/dotnet/samples/Demos/AIModelRouter/README.md index 92ac37e7c81e..63ddb9e420bc 100644 --- a/dotnet/samples/Demos/AIModelRouter/README.md +++ b/dotnet/samples/Demos/AIModelRouter/README.md @@ -23,6 +23,9 @@ The sample can be configured by using the command line with .NET [Secret Manager ```powershell # OpenAI (Not required if using Azure OpenAI) dotnet user-secrets set "OpenAI:ApiKey" "... your api key ... " + +# Anthropic +dotnet user-secrets set "Anthropic:ApiKey" "... your api key ... " ``` ## Running the sample From d9831b8fd115688eca98462572c97db006eb1dd0 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz <60486987+Krzysztof318@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:44:31 +0200 Subject: [PATCH 6/7] Update dotnet/samples/Demos/AIModelRouter/Program.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- dotnet/samples/Demos/AIModelRouter/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/Demos/AIModelRouter/Program.cs b/dotnet/samples/Demos/AIModelRouter/Program.cs index 7cb6227d95a6..4efabc102dc3 100644 --- a/dotnet/samples/Demos/AIModelRouter/Program.cs +++ b/dotnet/samples/Demos/AIModelRouter/Program.cs @@ -44,7 +44,7 @@ private static async Task Main(string[] args) // Find the best service to use based on the user's input KernelArguments arguments = new(new PromptExecutionSettings() { - ServiceId = router.FindService(userMessage, ["lmstudio", "ollama", "openai", "claude"]) + ServiceId = router.FindService(userMessage, ["lmstudio", "ollama", "openai", "anthropic"]) }); // Invoke the prompt and print the response From d36c69bd6ef37fa7140d0d588d1acb2dadaebb5c Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:46:26 +0100 Subject: [PATCH 7/7] Fix warnings --- dotnet/samples/Demos/AIModelRouter/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/Demos/AIModelRouter/Program.cs b/dotnet/samples/Demos/AIModelRouter/Program.cs index 6cdab1352e5b..612c13b251c3 100644 --- a/dotnet/samples/Demos/AIModelRouter/Program.cs +++ b/dotnet/samples/Demos/AIModelRouter/Program.cs @@ -77,10 +77,10 @@ private static async Task Main(string[] args) if (config["Anthropic:ApiKey"] is not null) { services.AddAnthropicChatCompletion( - serviceId: "anthropic", - modelId: config["Anthropic:ModelId"] ?? "claude-3-5-sonnet-20240620", + serviceId: "anthropic", + modelId: config["Anthropic:ModelId"] ?? "claude-3-5-sonnet-20240620", apiKey: config["Anthropic:ApiKey"]!); - + Console.WriteLine("• Anthropic Added - Use \"anthropic\" in the prompt."); }