From 9b1649b48a78af8e80668137d9a593ea32b5e73d Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Jun 2024 20:56:08 +0200 Subject: [PATCH] Implemented the Anthropic provider (#17) --- .../Provider/Anthropic/ChatRequest.cs | 19 ++ .../Provider/Anthropic/ProviderAnthropic.cs | 166 ++++++++++++++++++ .../Provider/Anthropic/ResponseStreamLine.cs | 17 ++ app/MindWork AI Studio/Provider/Providers.cs | 4 + 4 files changed, 206 insertions(+) create mode 100644 app/MindWork AI Studio/Provider/Anthropic/ChatRequest.cs create mode 100644 app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs create mode 100644 app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs diff --git a/app/MindWork AI Studio/Provider/Anthropic/ChatRequest.cs b/app/MindWork AI Studio/Provider/Anthropic/ChatRequest.cs new file mode 100644 index 0000000..0a15098 --- /dev/null +++ b/app/MindWork AI Studio/Provider/Anthropic/ChatRequest.cs @@ -0,0 +1,19 @@ +using AIStudio.Provider.OpenAI; + +namespace AIStudio.Provider.Anthropic; + +/// +/// The Anthropic chat request model. +/// +/// Which model to use for chat completion. +/// The chat messages. +/// The maximum number of tokens to generate. +/// Whether to stream the chat completion. +/// The system prompt for the chat completion. +public readonly record struct ChatRequest( + string Model, + IList Messages, + int MaxTokens, + bool Stream, + string System +); \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs new file mode 100644 index 0000000..89c6fe0 --- /dev/null +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -0,0 +1,166 @@ +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; + +using AIStudio.Chat; +using AIStudio.Provider.OpenAI; +using AIStudio.Settings; + +namespace AIStudio.Provider.Anthropic; + +public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.com/v1/"), IProvider +{ + private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + }; + + #region Implementation of IProvider + + public string Id => "Anthropic"; + + public string InstanceName { get; set; } = "Anthropic"; + + /// + public async IAsyncEnumerable StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) + { + // Get the API key: + var requestedSecret = await settings.GetAPIKey(jsRuntime, this); + if(!requestedSecret.Success) + yield break; + + // Prepare the Anthropic HTTP chat request: + var chatRequest = JsonSerializer.Serialize(new ChatRequest + { + Model = chatModel.Id, + + // Build the messages: + Messages = [..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => text.Text, + _ => string.Empty, + } + }).ToList()], + + System = chatThread.SystemPrompt, + MaxTokens = 4_096, + + // Right now, we only support streaming completions: + Stream = true, + }, JSON_SERIALIZER_OPTIONS); + + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, "messages"); + + // Set the authorization header: + request.Headers.Add("x-api-key", requestedSecret.Secret); + + // Set the Anthropic version: + request.Headers.Add("anthropic-version", "2023-06-01"); + + // Set the content: + request.Content = new StringContent(chatRequest, Encoding.UTF8, "application/json"); + + // Send the request with the ResponseHeadersRead option. + // This allows us to read the stream as soon as the headers are received. + // This is important because we want to stream the responses. + var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + + // Open the response stream: + var stream = await response.Content.ReadAsStreamAsync(token); + + // Add a stream reader to read the stream, line by line: + var streamReader = new StreamReader(stream); + + // Read the stream, line by line: + while(!streamReader.EndOfStream) + { + // Check if the token is canceled: + if(token.IsCancellationRequested) + yield break; + + // Read the next line: + var line = await streamReader.ReadLineAsync(token); + + // Skip empty lines: + if(string.IsNullOrWhiteSpace(line)) + continue; + + // Check for the end of the stream: + if(line.StartsWith("event: message_stop", StringComparison.InvariantCulture)) + yield break; + + // Skip lines that do not start with "data: ". Regard + // to the specification, we only want to read the data lines: + if(!line.StartsWith("data: ", StringComparison.InvariantCulture)) + continue; + + // Ignore any type except "content_block_delta": + if(!line.Contains("\"content_block_delta\"", StringComparison.InvariantCulture)) + continue; + + ResponseStreamLine anthropicResponse; + try + { + // We know that the line starts with "data: ". Hence, we can + // skip the first 6 characters to get the JSON data after that. + var jsonData = line[6..]; + + // Deserialize the JSON data: + anthropicResponse = JsonSerializer.Deserialize(jsonData, JSON_SERIALIZER_OPTIONS); + } + catch + { + // Skip invalid JSON data: + continue; + } + + // Skip empty responses: + if(anthropicResponse == default || string.IsNullOrWhiteSpace(anthropicResponse.Delta.Text)) + continue; + + // Yield the response: + yield return anthropicResponse.Delta.Text; + } + } + + #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + /// + public async IAsyncEnumerable StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default) + { + yield break; + } + #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + /// + public Task> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) + { + return Task.FromResult(new[] + { + new Model("claude-3-5-sonnet-20240620"), + new Model("claude-3-opus-20240229"), + new Model("claude-3-sonnet-20240229"), + new Model("claude-3-haiku-20240307"), + }.AsEnumerable()); + } + + #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + /// + public Task> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) + { + return Task.FromResult(Enumerable.Empty()); + } + #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs b/app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs new file mode 100644 index 0000000..041d53a --- /dev/null +++ b/app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs @@ -0,0 +1,17 @@ +// ReSharper disable NotAccessedPositionalProperty.Global +namespace AIStudio.Provider.Anthropic; + +/// +/// Represents a response stream line. +/// +/// The type of the response line. +/// The index of the response line. +/// The delta of the response line. +public readonly record struct ResponseStreamLine(string Type, int Index, Delta Delta); + +/// +/// The delta object of a response line. +/// +/// The type of the delta. +/// The text of the delta. +public readonly record struct Delta(string Type, string Text); \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Providers.cs b/app/MindWork AI Studio/Provider/Providers.cs index 0c93f42..e61713a 100644 --- a/app/MindWork AI Studio/Provider/Providers.cs +++ b/app/MindWork AI Studio/Provider/Providers.cs @@ -1,3 +1,4 @@ +using AIStudio.Provider.Anthropic; using AIStudio.Provider.OpenAI; namespace AIStudio.Provider; @@ -9,6 +10,7 @@ public enum Providers { NONE, OPEN_AI, + ANTHROPIC, } /// @@ -24,6 +26,7 @@ public static class ExtensionsProvider public static string ToName(this Providers provider) => provider switch { Providers.OPEN_AI => "OpenAI", + Providers.ANTHROPIC => "Anthropic", Providers.NONE => "No provider selected", _ => "Unknown", @@ -38,6 +41,7 @@ public static class ExtensionsProvider public static IProvider CreateProvider(this Providers provider, string instanceName) => provider switch { Providers.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName }, + Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName }, _ => new NoProvider(), };