diff --git a/Anthropic.Playground/Program.cs b/Anthropic.Playground/Program.cs index 95a1697..65ce902 100644 --- a/Anthropic.Playground/Program.cs +++ b/Anthropic.Playground/Program.cs @@ -1,4 +1,5 @@ using Anthropic.Extensions; +using Anthropic.ObjectModels.RequestModels; using Anthropic.Playground.TestHelpers; using Anthropic.Services; using LaserCatEyes.HttpClientListener; @@ -28,10 +29,8 @@ var serviceProvider = serviceCollection.BuildServiceProvider(); var sdk = serviceProvider.GetRequiredService(); - -//await ChatTestHelper.RunSimpleChatTest(sdk); -await ChatTestHelper.RunSimpleCompletionStreamTest(sdk); - - +await ChatTestHelper.RunChatCompletionTest(sdk); +await ChatTestHelper.RunChatCompletionStreamTest(sdk); +await ChatToolUseTestHelper.RunChatCompletionWithToolUseTest(sdk); Console.WriteLine("Press any key to exit..."); Console.ReadLine(); \ No newline at end of file diff --git a/Anthropic.Playground/SampleModels/WeatherInput.cs b/Anthropic.Playground/SampleModels/WeatherInput.cs new file mode 100644 index 0000000..cc4e92a --- /dev/null +++ b/Anthropic.Playground/SampleModels/WeatherInput.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Anthropic.Playground.SampleModels; + +public class WeatherInput +{ + [JsonPropertyName("location")] + public string Location { get; set; } + + [JsonPropertyName("unit")] + public string? Unit { get; set; } +} \ No newline at end of file diff --git a/Anthropic.Playground/TestHelpers/ChatTestHelper.cs b/Anthropic.Playground/TestHelpers/ChatTestHelper.cs index a13bbca..7dfcc45 100644 --- a/Anthropic.Playground/TestHelpers/ChatTestHelper.cs +++ b/Anthropic.Playground/TestHelpers/ChatTestHelper.cs @@ -1,92 +1,108 @@ -using Anthropic.ObjectModels; +using Anthropic.ObjectModels.ResponseModels; +using Anthropic.ObjectModels.SharedModels; using Anthropic.Services; namespace Anthropic.Playground.TestHelpers; +/// +/// Helper class for testing chat functionalities of the Anthropic SDK. +/// internal static class ChatTestHelper { - public static async Task RunSimpleChatTest(IAnthropicService sdk) + /// + /// Runs a simple chat completion test using the Anthropic SDK. + /// + /// The Anthropic service instance. + public static async Task RunChatCompletionTest(IAnthropicService anthropicService) { - Console.WriteLine("Messsaging Testing is starting:"); - + Console.WriteLine("Starting Chat Completion Test:"); try { - Console.WriteLine("Chat Completion Test:", ConsoleColor.DarkCyan); - var completionResult = await sdk.Messages.Create(new() + Console.WriteLine("Sending chat completion request..."); + var chatCompletionResult = await anthropicService.Messages.Create(new() { - Messages = new List - { + Messages = + [ Message.FromUser("Hello there."), Message.FromAssistant("Hi, I'm Claude. How can I help you?"), Message.FromUser("Can you explain LLMs in plain English?") - }, - MaxTokens = 50, + ], + MaxTokens = 200, Model = "claude-3-5-sonnet-20240620" }); - if (completionResult.Successful) + if (chatCompletionResult.Successful) { - Console.WriteLine(completionResult.Content.First().Text); + Console.WriteLine("Chat Completion Response:"); + Console.WriteLine(chatCompletionResult.ToString()); } else { - if (completionResult.Error == null) + if (chatCompletionResult.Error == null) { - throw new("Unknown Error"); + throw new("Unknown Error Occurred"); } - Console.WriteLine($"{completionResult.HttpStatusCode}: {completionResult.Error.Message}"); + Console.WriteLine($"Error: {chatCompletionResult.HttpStatusCode}: {chatCompletionResult.Error.Message}"); } } - catch (Exception e) + catch (Exception ex) { - Console.WriteLine(e); + Console.WriteLine($"Exception occurred: {ex}"); throw; } + Console.WriteLine("---- 0 ----"); + } - public static async Task RunSimpleCompletionStreamTest(IAnthropicService sdk) + /// + /// Runs a simple chat completion stream test using the Anthropic SDK. + /// + /// The Anthropic service instance. + public static async Task RunChatCompletionStreamTest(IAnthropicService anthropicService) { - Console.WriteLine("Chat Completion Stream Testing is starting:", ConsoleColor.Cyan); + Console.WriteLine("Starting Chat Completion Stream Test:"); try { - Console.WriteLine("Chat Completion Stream Test:", ConsoleColor.DarkCyan); - var completionResult = sdk.Messages.CreateAsStream(new() + Console.WriteLine("Initiating chat completion stream..."); + var chatCompletionStream = anthropicService.Messages.CreateAsStream(new() { - Messages = new List - { + Messages = + [ Message.FromUser("Hello there."), Message.FromAssistant("Hi, I'm Claude. How can I help you?"), - Message.FromUser("Tell me really long story about how a purple color became human?(in Turkish)") - }, - MaxTokens = 1024, + Message.FromUser("Tell me a really long story about how the color purple became human.") + ], + MaxTokens = 10, Model = "claude-3-5-sonnet-20240620" }); - await foreach (var completion in completionResult) + await foreach (var streamResponse in chatCompletionStream) { - if (completion.Successful) + if (streamResponse.IsMessageResponse()) { - Console.Write(completion?.Content?.First().Text); + var messageCompletion = streamResponse.As(); + Console.Write(messageCompletion.ToString()); } - else + else if (streamResponse.IsError()) { - if (completion.Error == null) - { - throw new("Unknown Error"); - } - - Console.WriteLine($"{completion.HttpStatusCode}: {completion.Error.Message}"); + var errorResponse = streamResponse.As(); + Console.WriteLine($"Error: {errorResponse.HttpStatusCode}: {errorResponse.Error?.Message}"); + } + else if (streamResponse.IsPingResponse()) + { + // Optionally handle ping responses + // Console.WriteLine("Ping received"); } } - Console.WriteLine(""); - Console.WriteLine("Complete"); + Console.WriteLine("\nStream completed"); } - catch (Exception e) + catch (Exception ex) { - Console.WriteLine(e); + Console.WriteLine($"Exception occurred: {ex}"); throw; } + Console.WriteLine("---- 0 ----"); } -} +} \ No newline at end of file diff --git a/Anthropic.Playground/TestHelpers/ChatToolUseTestHelper.cs b/Anthropic.Playground/TestHelpers/ChatToolUseTestHelper.cs new file mode 100644 index 0000000..5af9f45 --- /dev/null +++ b/Anthropic.Playground/TestHelpers/ChatToolUseTestHelper.cs @@ -0,0 +1,95 @@ +using Anthropic.Extensions; +using Anthropic.ObjectModels.ResponseModels; +using Anthropic.ObjectModels.SharedModels; +using Anthropic.Playground.SampleModels; +using Anthropic.Services; + +namespace Anthropic.Playground.TestHelpers; + +/// +/// Helper class for testing chat functionalities with tool use in the Anthropic SDK. +/// +internal static class ChatToolUseTestHelper +{ + /// + /// Runs a chat completion stream test with tool use using the Anthropic SDK. + /// + /// The Anthropic service instance. + public static async Task RunChatCompletionWithToolUseTest(IAnthropicService anthropicService) + { + Console.WriteLine("Starting Chat Completion Stream Test with Tool Use:"); + try + { + Console.WriteLine("Initiating chat completion stream..."); + var chatCompletionStream = anthropicService.Messages.CreateAsStream(new() + { + Messages = [Message.FromUser("What is the weather like in San Francisco?")], + MaxTokens = 2024, + Model = "claude-3-5-sonnet-20240620", + Tools = + [ + new() + { + Name = "get_weather", + Description = "Get the current weather in a given location", + InputSchema = PropertyDefinition.DefineObject(new Dictionary + { + { "location", PropertyDefinition.DefineString("The city and state, e.g. San Francisco, CA") } + }, ["location"], null, null, null) + } + ] + // ToolChoice = new() { Type = "any" } + }); + + await foreach (var streamResponse in chatCompletionStream) + { + if (streamResponse.IsMessageResponse()) + { + var messageResponse = streamResponse.As(); + var content = messageResponse.Content?.FirstOrDefault(); + if (content == null) + { + continue; + } + switch (content.Type) + { + case "text": + Console.Write(content.Text); + break; + case "tool_use" when content.Name == "get_weather": + if (content.TryGetInputAs(out var weatherInput)) + { + Console.WriteLine($"\nWeather request for \"{weatherInput.Location}\" in \"{weatherInput.Unit}\""); + } + + break; + case "tool_use": + throw new InvalidOperationException($"Unknown tool use: {content.Name}"); + default: + Console.WriteLine($"\nUnknown content type: {content.Type}"); + break; + } + } + else if (streamResponse.IsError()) + { + var errorResponse = streamResponse.As(); + Console.WriteLine($"Error: {errorResponse.HttpStatusCode}: {errorResponse.Error?.Message}"); + } + else if (streamResponse.IsPingResponse()) + { + // Optionally handle ping responses + // Console.WriteLine("Ping received"); + } + } + + Console.WriteLine("\nStream completed"); + } + catch (Exception ex) + { + Console.WriteLine($"Exception occurred: {ex}"); + throw; + } + + Console.WriteLine("---- 0 ----"); + } +} \ No newline at end of file diff --git a/Anthropic/Anthropic.csproj b/Anthropic/Anthropic.csproj index a6b18c1..a0626e4 100644 --- a/Anthropic/Anthropic.csproj +++ b/Anthropic/Anthropic.csproj @@ -1,50 +1,57 @@  - - net8.0;net6.0;netstandard2.0 - enable - enable - Latest - Betalgo Up Ltd. - https://www.anthropic.com/ - true - Anthropic SDK by Betalgo - 0.0.1 - Tolga Kayhan, Betalgo - Betalgo Up Ltd. - Anthropic Claude dotnet SDK - Dotnet SDK for Anthropic Claude - https://github.com/betalgo/anthropic/ - Anthropic,Claude,ai,betalgo,NLP - Betalgo.Ranul.Anthropic - Readme.md - True - https://github.com/betalgo/anthropic.git - git - True - MIT - true - true - snupkg - + + net8.0;net6.0;netstandard2.0 + enable + enable + Latest + Betalgo Up Ltd. + https://www.anthropic.com/ + Ranul-Anthropic-Icon.png + true + Anthropic Claude C# .NET library by Ranul Betalgo + 8.0.0 + Tolga Kayhan, Betalgo, Ranul + Betalgo Up Ltd. + Anthropic Claude C# .NET library by Ranul Betalgo + Anthropic Claude C# .NET library by Ranul Betalgo + https://github.com/betalgo/anthropic/ + Anthropic,Claude,ai,betalgo,NLP,Ranul + Betalgo.Ranul.Anthropic + Readme.md + True + https://github.com/betalgo/anthropic.git + git + True + MIT + true + true + snupkg + - - - + + + + + True \ - + + True + \ + + + + + + + - - - - - - + \ No newline at end of file diff --git a/Anthropic/EndpointProviders/AnthropicEndpointProvider.cs b/Anthropic/EndpointProviders/AnthropicEndpointProvider.cs index 672aa40..e99e2c5 100644 --- a/Anthropic/EndpointProviders/AnthropicEndpointProvider.cs +++ b/Anthropic/EndpointProviders/AnthropicEndpointProvider.cs @@ -2,7 +2,6 @@ public class AnthropicEndpointProvider : IAnthropicEndpointProvider { - private readonly string _apiVersion; public AnthropicEndpointProvider(string apiVersion) diff --git a/Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs b/Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs index 262e836..6eced21 100644 --- a/Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs +++ b/Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Anthropic.Services; +using Anthropic.ObjectModels; +using Anthropic.Services; using Microsoft.Extensions.DependencyInjection; namespace Anthropic.Extensions; diff --git a/Anthropic/Extensions/AsyncDisposableStream.cs b/Anthropic/Extensions/AsyncDisposableStream.cs index 9bc0f16..ec3a24d 100644 --- a/Anthropic/Extensions/AsyncDisposableStream.cs +++ b/Anthropic/Extensions/AsyncDisposableStream.cs @@ -1,20 +1,68 @@ #if NETSTANDARD2_0 -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - namespace Anthropic.Extensions { public class AsyncDisposableStream : Stream, IAsyncDisposable { private readonly Stream _innerStream; + /// public AsyncDisposableStream(Stream stream) { _innerStream = stream; } + /// + public override bool CanRead => _innerStream.CanRead; + + /// + public override bool CanSeek => _innerStream.CanSeek; + + /// + public override bool CanTimeout => _innerStream.CanTimeout; + + /// + public override bool CanWrite => _innerStream.CanWrite; + + /// + public override long Length => _innerStream.Length; + + /// + public override long Position + { + get => _innerStream.Position; + set => _innerStream.Position = value; + } + + /// + public override int ReadTimeout + { + get => _innerStream.ReadTimeout; + set => _innerStream.ReadTimeout = value; + } + + /// + public override int WriteTimeout + { + get => _innerStream.WriteTimeout; + set => _innerStream.WriteTimeout = value; + } + + /// + public async ValueTask DisposeAsync() + { + if (_innerStream != null) + { + if (_innerStream is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + } + else + { + await Task.Run(() => _innerStream.Dispose()).ConfigureAwait(false); + } + } + } + public new Task CopyToAsync(Stream destination) { return _innerStream.CopyToAsync(destination); @@ -25,6 +73,7 @@ public AsyncDisposableStream(Stream stream) return _innerStream.CopyToAsync(destination, bufferSize); } + /// public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); @@ -40,6 +89,7 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio _innerStream.CopyTo(destination, bufferSize); } + /// public override void Close() { _innerStream.Close(); @@ -50,6 +100,7 @@ public override void Close() _innerStream.Dispose(); } + /// public override void Flush() { _innerStream.Flush(); @@ -60,16 +111,19 @@ public override void Flush() return _innerStream.FlushAsync(); } + /// public override Task FlushAsync(CancellationToken cancellationToken) { return _innerStream.FlushAsync(cancellationToken); } + /// public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { return _innerStream.BeginRead(buffer, offset, count, callback, state); } + /// public override int EndRead(IAsyncResult asyncResult) { return _innerStream.EndRead(asyncResult); @@ -80,16 +134,19 @@ public override int EndRead(IAsyncResult asyncResult) return _innerStream.ReadAsync(buffer, offset, count); } + /// public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return _innerStream.ReadAsync(buffer, offset, count, cancellationToken); } + /// public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { return _innerStream.BeginWrite(buffer, offset, count, callback, state); } + /// public override void EndWrite(IAsyncResult asyncResult) { _innerStream.EndWrite(asyncResult); @@ -100,83 +157,47 @@ public override void EndWrite(IAsyncResult asyncResult) return _innerStream.WriteAsync(buffer, offset, count); } + /// public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); } + /// public override long Seek(long offset, SeekOrigin origin) { return _innerStream.Seek(offset, origin); } + /// public override void SetLength(long value) { _innerStream.SetLength(value); } + /// public override int Read(byte[] buffer, int offset, int count) { return _innerStream.Read(buffer, offset, count); } + /// public override int ReadByte() { return _innerStream.ReadByte(); } + /// public override void Write(byte[] buffer, int offset, int count) { _innerStream.Write(buffer, offset, count); } + /// public override void WriteByte(byte value) { _innerStream.WriteByte(value); } - - public override bool CanRead => _innerStream.CanRead; - - public override bool CanSeek => _innerStream.CanSeek; - - public override bool CanTimeout => _innerStream.CanTimeout; - - public override bool CanWrite => _innerStream.CanWrite; - - public override long Length => _innerStream.Length; - - public override long Position - { - get => _innerStream.Position; - set => _innerStream.Position = value; - } - - public override int ReadTimeout - { - get => _innerStream.ReadTimeout; - set => _innerStream.ReadTimeout = value; - } - - public override int WriteTimeout - { - get => _innerStream.WriteTimeout; - set => _innerStream.WriteTimeout = value; - } - - public async ValueTask DisposeAsync() - { - if (_innerStream != null) - { - if (_innerStream is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync().ConfigureAwait(false); - } - else - { - await Task.Run(() => _innerStream.Dispose()).ConfigureAwait(false); - } - } - } } } #endif \ No newline at end of file diff --git a/Anthropic/Extensions/ContentBlockExtensions.cs b/Anthropic/Extensions/ContentBlockExtensions.cs new file mode 100644 index 0000000..88b174c --- /dev/null +++ b/Anthropic/Extensions/ContentBlockExtensions.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using Anthropic.ObjectModels.SharedModels; + +namespace Anthropic.Extensions; + +public static class ContentBlockExtensions +{ + public static T? GetInputAs(this ContentBlock contentBlock) + { + if (contentBlock.Input is JsonElement jsonElement) + { + return JsonSerializer.Deserialize(jsonElement.GetRawText()); + } + + return default; + } + + public static bool TryGetInputAs(this ContentBlock contentBlock, out T? result) + { + result = default; + if (contentBlock.Input is JsonElement jsonElement) + { + try + { + result = JsonSerializer.Deserialize(jsonElement.GetRawText()); + return true; + } + catch + { + return false; + } + } + + return false; + } +} \ No newline at end of file diff --git a/Anthropic/Extensions/HttpClientExtensions.cs b/Anthropic/Extensions/HttpClientExtensions.cs index d954449..79b9c06 100644 --- a/Anthropic/Extensions/HttpClientExtensions.cs +++ b/Anthropic/Extensions/HttpClientExtensions.cs @@ -2,7 +2,7 @@ using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; -using Anthropic.ObjectModels; +using Anthropic.ObjectModels.ResponseModels; namespace Anthropic.Extensions; diff --git a/Anthropic/Extensions/JsonToObjectRouterExtension.cs b/Anthropic/Extensions/JsonToObjectRouterExtension.cs deleted file mode 100644 index 08115f3..0000000 --- a/Anthropic/Extensions/JsonToObjectRouterExtension.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json; -using Anthropic.ObjectModels; - -namespace Anthropic.Extensions; - -internal static class JsonToObjectRouterExtension -{ - public static Type Route(string json) - { - var apiResponse = JsonSerializer.Deserialize(json); - - return apiResponse?.Type switch - { - "message_start" => typeof(MessageResponse), - "message_delta" => typeof(MessageResponse), - "message_stop" => typeof(MessageResponse), - "content_block_start" => typeof(MessageResponse), - "content_block_delta" => typeof(MessageResponse), - "content_block_stop" => typeof(MessageResponse), - "ping" => typeof(MessageResponse), - _ => typeof(BaseResponse) - }; - } -} \ No newline at end of file diff --git a/Anthropic/Extensions/ModelExtension.cs b/Anthropic/Extensions/ModelExtension.cs index 36b5774..d3fc8e6 100644 --- a/Anthropic/Extensions/ModelExtension.cs +++ b/Anthropic/Extensions/ModelExtension.cs @@ -1,11 +1,10 @@ -using System; -using Anthropic.ObjectModels; +using Anthropic.ObjectModels; namespace Anthropic.Extensions; -public static class ModelExtension +internal static class ModelExtension { - public static void ProcessModelId(this IObjectModels.IModel modelFromObject, string? defaultModelId, bool allowNull = false) + internal static void ProcessModelId(this IObjectInterfaces.IModel modelFromObject, string? defaultModelId, bool allowNull = false) { if (allowNull) { diff --git a/Anthropic/Extensions/StreamHandleExtension.cs b/Anthropic/Extensions/StreamHandleExtension.cs index 817e5dd..0a85d0d 100644 --- a/Anthropic/Extensions/StreamHandleExtension.cs +++ b/Anthropic/Extensions/StreamHandleExtension.cs @@ -1,17 +1,30 @@ using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Anthropic.ObjectModels; +using Anthropic.ObjectModels.ResponseModels; +using Anthropic.ObjectModels.SharedModels; +using static Anthropic.Extensions.StreamPartialResponse; namespace Anthropic.Extensions; + public static class StreamHandleExtension { - public static async IAsyncEnumerable AsStream(this HttpResponseMessage response, [EnumeratorCancellation] CancellationToken cancellationToken = default) + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + public static async IAsyncEnumerable AsStream(this HttpResponseMessage response, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - using var reader = new StreamReader(stream); + var reader = new StreamReader(stream); - string? currentEvent = null; + var currentEvent = string.Empty; + var currentEventType = string.Empty; + string? currentEventSubType = null; + var toolUseBuilder = new StreamToolUseJsonBuilder(); while (!reader.EndOfStream) { @@ -20,99 +33,276 @@ public static async IAsyncEnumerable AsStream(this HttpResponse var line = await reader.ReadLineAsync(); if (string.IsNullOrEmpty(line)) continue; - if (line.StartsWith("event: ")) + if (line.StartsWith(StaticValues.StreamConstants.Event, StringComparison.Ordinal)) + { +#if NET6_0_OR_GREATER + (currentEventType, currentEventSubType) = ParseEvent(line.AsSpan(StaticValues.StreamConstants.EventCharCount)); +#else + (currentEventType, currentEventSubType) = ParseEvent(line.Substring(StaticValues.StreamConstants.EventCharCount)); +#endif + currentEvent = line.Substring(StaticValues.StreamConstants.EventCharCount); + } + else if (line.StartsWith(StaticValues.StreamConstants.Data, StringComparison.Ordinal)) + { + var data = line.Substring(StaticValues.StreamConstants.DataCharCount); + var yieldResponse = ProcessEventData(data, currentEvent, currentEventType, currentEventSubType, toolUseBuilder); + if (yieldResponse == null) + { + continue; + } + yield return yieldResponse; + + } + } + } + + private static IStreamResponse? ProcessEventData(string data, string currentEvent, string currentEventType, string? currentEventSubType, StreamToolUseJsonBuilder toolUseBuilder) + { + + return currentEventSubType switch + { + StaticValues.EventSubTypes.Ping => new PingResponse { StreamEvent = currentEvent }, + StaticValues.EventSubTypes.Start => ProcessStartEvent(data, currentEvent, currentEventType, toolUseBuilder), + StaticValues.EventSubTypes.Delta => ProcessDeltaEvent(data, currentEvent, currentEventType, toolUseBuilder), + StaticValues.EventSubTypes.Stop => ProcessStopEvent(data, currentEvent, currentEventType, toolUseBuilder), + _ => DeserializeMessageResponse(data, currentEvent) + }; + } + + private static IStreamResponse ProcessStartEvent(string data, string currentEvent, string currentEventType, StreamToolUseJsonBuilder toolUseBuilder) + { + if (currentEventType == StaticValues.TypeConstants.ContentBlock) + { + var startBlock = JsonSerializer.Deserialize(data, JsonOptions); + if (startBlock.ContentBlock.Type == StaticValues.TypeConstants.ToolUse) + { + toolUseBuilder.StartBlock(startBlock.Index, startBlock.ContentBlock); + return new MessageResponse(); + } + + return new MessageResponse + { + Type = StaticValues.TypeConstants.Message, + StreamEvent = currentEvent, + Content = [startBlock.ContentBlock] + }; + } + + if (currentEventType == StaticValues.TypeConstants.Message) + { + var messageEvent = JsonSerializer.Deserialize(data, JsonOptions); + var response = messageEvent?.Message ?? new MessageResponse(); + response.StreamEvent = currentEvent; + return response; + } + + return DeserializeMessageResponse(data, currentEvent); + } + + private static IStreamResponse? ProcessDeltaEvent(string data, string currentEvent, string currentEventType, StreamToolUseJsonBuilder toolUseBuilder) + { + var deltaBase = JsonSerializer.Deserialize>(data, JsonOptions); + if (deltaBase?.Delta?.Type == null) + { + return null; + } + var (deltaEventType, deltaEventSubType) = ParseEvent(deltaBase.Delta.Type); + + switch (currentEventType) + { + case StaticValues.TypeConstants.Message: { - currentEvent = line.Substring(7); - // yield return new() { StreamEvent = currentEvent }; + var deltaItemMessage = JsonSerializer.Deserialize>(data, JsonOptions); + var response = deltaItemMessage!.Delta; + if (response != null) + { + response.Usage = deltaItemMessage.Usage; + } + return response; } - else if (line.StartsWith("data: ")) + case StaticValues.TypeConstants.ContentBlock when deltaEventSubType == StaticValues.EventSubTypes.Delta: { - var data = line.Substring(6); - var jsonElement = JsonSerializer.Deserialize(data); - var eventType = jsonElement.GetProperty("type").GetString(); + if (deltaEventType == StaticValues.TypeConstants.Text) + { + var delta = JsonSerializer.Deserialize>(data, JsonOptions); + return new MessageResponse + { + Type = StaticValues.TypeConstants.Message, + StreamEvent = currentEventType, + Content = [ContentBlock.CreateText(delta.Delta.Text)] + }; + } - switch (eventType) + if (deltaEventType == StaticValues.TypeConstants.InputJson) { - case "content_block_delta": - var contentBlockDelta = JsonSerializer.Deserialize(data); - if (contentBlockDelta.Delta.Type == "text_delta") - { - yield return new() - { - StreamEvent = "content_block_delta", - Content = [new() { Text = contentBlockDelta.Delta.Text }] - }; - } - break; - case "message_start": - case "message_delta": - case "message_stop": - yield return JsonSerializer.Deserialize(data) ?? new MessageResponse(); - break; - default: - yield return JsonSerializer.Deserialize(data) ?? new MessageResponse(); - break; + var delta = JsonSerializer.Deserialize>(data, JsonOptions); + toolUseBuilder.AppendJson(delta?.Index, delta?.Delta?.PartialJson); } + + break; } } + + return new MessageResponse(); } -} -public class Delta -{ - [JsonPropertyName("index")] - public int Index { get; set; } -} -public class ContentBlockStart -{ - [JsonPropertyName("type")] - public string Type { get; set; } + private static IStreamResponse ProcessStopEvent(string data, string currentEvent, string currentEventType, StreamToolUseJsonBuilder toolUseBuilder) + { + if (currentEventType == StaticValues.TypeConstants.ContentBlock) + { + var stopBlock = JsonSerializer.Deserialize(data, JsonOptions); + if (toolUseBuilder.IsActiveBlock(stopBlock.Index)) + { + var finalBlock = toolUseBuilder.FinishBlock(stopBlock.Index); + return new MessageResponse + { + Type = StaticValues.TypeConstants.Message, + StreamEvent = currentEvent, + Content = [finalBlock] + }; + } + } - [JsonPropertyName("index")] - public int Index { get; set; } + return DeserializeMessageResponse(data, currentEvent); + } - [JsonPropertyName("content_block")] - public ContentBlock ContentBlock { get; set; } -} + private static MessageResponse DeserializeMessageResponse(string data, string currentEvent) + { + var response = JsonSerializer.Deserialize(data, JsonOptions) ?? new MessageResponse(); + response.StreamEvent = currentEvent; + response.Type = StaticValues.TypeConstants.Message; + return response; + } -public class ContentBlockDelta -{ - [JsonPropertyName("type")] - public string Type { get; set; } +#if NET6_0_OR_GREATER + private static (string EventType, string SubType) ParseEvent(ReadOnlySpan eventName) + { + if (eventName.Equals("ping", StringComparison.Ordinal)) + { + return ("ping", null); + } - [JsonPropertyName("index")] - public int Index { get; set; } + var lastUnderscoreIndex = eventName.LastIndexOf('_'); - [JsonPropertyName("delta")] - public Delta Delta { get; set; } -} + if (lastUnderscoreIndex != -1) + { + var eventType = eventName.Slice(0, lastUnderscoreIndex); + var subType = eventName.Slice(lastUnderscoreIndex + 1); -public class Delta -{ - [JsonPropertyName("type")] - public string Type { get; set; } + if (subType.Equals("delta", StringComparison.Ordinal) || subType.Equals("start", StringComparison.Ordinal) || subType.Equals("stop", StringComparison.Ordinal)) + { + return (eventType.ToString(), subType.ToString()); + } + } - [JsonPropertyName("text")] - public string Text { get; set; } + return (eventName.ToString(), null); + } +#else + private static (string EventType, string? SubType) ParseEvent(string eventName) + { + if (string.Equals(eventName, StaticValues.EventSubTypes.Ping, StringComparison.Ordinal)) + { + return (StaticValues.EventSubTypes.Ping, null); + } + + var lastUnderscoreIndex = eventName.LastIndexOf('_'); + + if (lastUnderscoreIndex != -1) + { + var eventType = eventName.Substring(0, lastUnderscoreIndex); + var subType = eventName.Substring(lastUnderscoreIndex + 1); + + if (string.Equals(subType, "delta", StringComparison.Ordinal) || string.Equals(subType, "start", StringComparison.Ordinal) || string.Equals(subType, "stop", StringComparison.Ordinal)) + { + return (eventType, subType); + } + } + + return (eventName, null); + } +#endif } -public class MessageDelta +internal class StreamPartialResponse { - [JsonPropertyName("type")] - public string Type { get; set; } + public class StreamArrayItemBase : TypeBaseResponse + { + [JsonPropertyName("index")] + public int? Index { get; set; } - [JsonPropertyName("delta")] - public DeltaInfo Delta { get; set; } + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + } - [JsonPropertyName("usage")] - public Usage Usage { get; set; } + public class DeltaItem : StreamArrayItemBase + { + [JsonPropertyName("delta")] + public T? Delta { get; set; } + } + + public class InputJsonDelta + { + [JsonPropertyName("partial_json")] + public string? PartialJson { get; set; } + } + + public class MessageEventItem : TypeBaseResponse + { + [JsonPropertyName("message")] + public MessageResponse? Message { get; set; } + } + + public class ContentBlockItem : StreamArrayItemBase + { + [JsonPropertyName("content_block")] + public ContentBlock? ContentBlock { get; set; } + } } -public class DeltaInfo +internal class StreamToolUseJsonBuilder { - [JsonPropertyName("stop_reason")] - public string StopReason { get; set; } + private readonly HashSet _activeIndices = []; + private readonly Dictionary _blocks = []; + + public void StartBlock(int? index, ContentBlock initialBlock) + { + index ??= int.MaxValue; + _blocks[index] = (initialBlock, new()); + _activeIndices.Add(index); + } - [JsonPropertyName("stop_sequence")] - public string StopSequence { get; set; } + public void AppendJson(int? index, string? partialJson) + { + index ??= int.MaxValue; + if (_blocks.TryGetValue(index, out var block)) + { + block.JsonBuilder.Append(partialJson); + } + } + + public ContentBlock FinishBlock(int? index) + { + index ??= int.MaxValue; + if (_blocks.TryGetValue(index, out var block)) + { + _activeIndices.Remove(index); + _blocks.Remove(index); + var finalJson = block.JsonBuilder.ToString(); + var inputJson = string.IsNullOrEmpty(finalJson) ? block.InitialBlock.Input : JsonSerializer.Deserialize(finalJson); + return new() + { + Type = block.InitialBlock.Type, + Id = block.InitialBlock.Id, + Name = block.InitialBlock.Name, + Input = inputJson + }; + } + return new(); + } + + public bool IsActiveBlock(int? index) + { + index ??= int.MaxValue; + return _activeIndices.Contains(index); + } } \ No newline at end of file diff --git a/Anthropic/Extensions/StringExtensions.cs b/Anthropic/Extensions/StringExtensions.cs deleted file mode 100644 index 6f85f7d..0000000 --- a/Anthropic/Extensions/StringExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Anthropic.Extensions; - -/// -/// Extension methods for string manipulation -/// -internal static class StringExtensions -{ - /// - /// Remove the search string from the beginning of string if it exists - /// - /// - /// - /// - public static string RemoveIfStartWith(this string text, string search) - { - var pos = text.IndexOf(search, StringComparison.Ordinal); - return pos != 0 ? text : text.Substring(search.Length); - } -} \ No newline at end of file diff --git a/Anthropic/Services/AnthropicOptions.cs b/Anthropic/ObjectModels/AnthropicOptions.cs similarity index 72% rename from Anthropic/Services/AnthropicOptions.cs rename to Anthropic/ObjectModels/AnthropicOptions.cs index 80947b5..43bf844 100644 --- a/Anthropic/Services/AnthropicOptions.cs +++ b/Anthropic/ObjectModels/AnthropicOptions.cs @@ -1,10 +1,9 @@ -using System; - -namespace Anthropic.Services; +namespace Anthropic.ObjectModels; public class AnthropicOptions { private const string AnthropicDefaultApiVersion = "v1"; + private const string AnthropicDefaultVersion = "2023-06-01"; private const string AnthropicDefaultBaseDomain = "https://api.anthropic.com/"; @@ -12,12 +11,13 @@ public class AnthropicOptions /// Setting key for Json Setting Bindings /// public static readonly string SettingKey = "AnthropicServiceOptions"; - + + private string? _providerVersion; private string? _apiVersion; private string? _baseDomain; - public ProviderType ProviderType { get; set; } = ProviderType.Anthropic; + public AnthropicProviderType ProviderType { get; set; } = AnthropicProviderType.Anthropic; public string ApiKey { get; set; } = null!; @@ -27,7 +27,7 @@ public string ApiVersion { return _apiVersion ??= ProviderType switch { - ProviderType.Anthropic => AnthropicDefaultApiVersion, + AnthropicProviderType.Anthropic => AnthropicDefaultApiVersion, _ => throw new ArgumentOutOfRangeException(nameof(ProviderType)) }; } @@ -44,13 +44,26 @@ public string BaseDomain { return _baseDomain ??= ProviderType switch { - ProviderType.Anthropic => AnthropicDefaultBaseDomain, + AnthropicProviderType.Anthropic => AnthropicDefaultBaseDomain, _ => throw new ArgumentOutOfRangeException(nameof(ProviderType)) }; } set => _baseDomain = value; } - + + public string ProviderVersion + { + get + { + return _providerVersion ??= ProviderType switch + { + AnthropicProviderType.Anthropic => AnthropicDefaultVersion, + _ => throw new ArgumentOutOfRangeException(nameof(ProviderType)) + }; + } + set => _providerVersion = value; + } + public bool ValidateApiOptions { get; set; } = true; /// @@ -58,6 +71,7 @@ public string BaseDomain /// public string? DefaultModelId { get; set; } + /// /// Validate Settings /// diff --git a/Anthropic/ObjectModels/AnthropicProviderType.cs b/Anthropic/ObjectModels/AnthropicProviderType.cs new file mode 100644 index 0000000..7d5f5e2 --- /dev/null +++ b/Anthropic/ObjectModels/AnthropicProviderType.cs @@ -0,0 +1,11 @@ +namespace Anthropic.ObjectModels; + +/// +/// Anthropic Provider Type +/// +public enum AnthropicProviderType +{ + Anthropic = 1, + VertexAI = 2, + AmazonBedrock +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/BaseResponse.cs b/Anthropic/ObjectModels/BaseResponse.cs deleted file mode 100644 index cef558b..0000000 --- a/Anthropic/ObjectModels/BaseResponse.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System.Net; -using System.Net.Http.Headers; -using System.Text.Json.Serialization; -using Anthropic.Extensions; - -namespace Anthropic.ObjectModels; -public class TypeBaseResponse -{ - /// - /// Gets or sets the object type. For Messages, this is always "message". - /// - [JsonPropertyName("type")] - public string Type { get; set; } - -} -public class BaseResponse: TypeBaseResponse -{ - /// - /// Gets or sets the unique object identifier. - /// - [JsonPropertyName("id")] - public string Id { get; set; } - - /// - /// Gets or sets the usage information for billing and rate-limiting. - /// - [JsonPropertyName("usage")] - public Usage Usage { get; set; } - - [JsonPropertyName("StreamEvent")] - public string? StreamEvent { get; set; } - public bool IsDelta => StreamEvent?.EndsWith("delta") ?? false; - - public HttpStatusCode HttpStatusCode { get; set; } - public ResponseHeaderValues? HeaderValues { get; set; } - - [JsonPropertyName("error")] - public Error? Error { get; set; } - - public bool Successful => Error == null; -} - -public class Error -{ - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; - - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; -} - -/// -/// Represents the values of various headers in an HTTP response. -/// -public class ResponseHeaderValues -{ - /// - /// Gets the date and time at which the message was originated. - /// - public DateTimeOffset? Date { get; set; } - - /// - /// Gets the media type of the body of the response. - /// - public string? ContentType { get; set; } - - /// - /// Gets the transfer encoding applied to the body of the response. - /// - public string? TransferEncoding { get; set; } - - /// - /// Gets the connection type of the response. - /// - public string? Connection { get; set; } - - /// - /// Gets the unique identifier for the request. - /// - public string? RequestId { get; set; } - - /// - /// Gets the cloud trace context for the request. - /// - public string? XCloudTraceContext { get; set; } - - /// - /// Gets information about proxies used in handling the request. - /// - public string? Via { get; set; } - - /// - /// Gets the Cloudflare cache status for the request. - /// - public string? CFCacheStatus { get; set; } - - /// - /// Gets information about the server that handled the request. - /// - public string? Server { get; set; } - - /// - /// Gets the Cloudflare Ray ID for the request. - /// - public string? CF_RAY { get; set; } - - /// - /// Gets the content encoding applied to the body of the response. - /// - public string? ContentEncoding { get; set; } - - /// - /// Gets a dictionary containing all headers from the response. - /// - public Dictionary>? All { get; set; } - - /// - /// Gets the Anthropic-specific headers from the response. - /// - public AnthropicHeaders? Anthropic { get; set; } - - /// - /// Initializes a new instance of the ResponseHeaderValues class from an HttpResponseMessage. - /// - /// The HTTP response message containing the headers. - public ResponseHeaderValues(HttpResponseMessage response) - { - Date = response.Headers.Date; - ContentType = response.Content.Headers.ContentType?.ToString(); - TransferEncoding = response.Headers.TransferEncoding?.ToString(); - Connection = response.Headers.Connection?.ToString(); - RequestId = response.Headers.GetHeaderValue("request-id"); - XCloudTraceContext = response.Headers.GetHeaderValue("x-cloud-trace-context"); - Via = response.Headers.Via?.ToString(); - CFCacheStatus = response.Headers.GetHeaderValue("cf-cache-status"); - Server = response.Headers.Server?.ToString(); - CF_RAY = response.Headers.GetHeaderValue("cf-ray"); - ContentEncoding = response.Content.Headers.ContentEncoding?.ToString(); - All = response.Headers.ToDictionary(x => x.Key, x => x.Value.AsEnumerable()); - Anthropic = new AnthropicHeaders(response.Headers); - } -} - -/// -/// Represents Anthropic-specific headers in an HTTP response. -/// -public class AnthropicHeaders -{ - /// - /// Gets information about rate limits applied to the request. - /// - public RateLimitInfo? RateLimit { get; } - - /// - /// Initializes a new instance of the AnthropicHeaders class from HttpResponseHeaders. - /// - /// The HTTP response headers. - public AnthropicHeaders(HttpResponseHeaders headers) - { - RateLimit = new RateLimitInfo(headers); - } -} - -/// -/// Represents rate limit information from Anthropic API headers. -/// -public class RateLimitInfo -{ - /// - /// Gets the maximum number of requests allowed within any rate limit period. - /// - public int? RequestsLimit { get; } - - /// - /// Gets the number of requests remaining before being rate limited. - /// - public int? RequestsRemaining { get; } - - /// - /// Gets the time when the request rate limit will reset, provided in RFC 3339 format. - /// - public DateTimeOffset? RequestsReset { get; } - - /// - /// Gets the maximum number of tokens allowed within any rate limit period. - /// - public int? TokensLimit { get; } - - /// - /// Gets the number of tokens remaining (rounded to the nearest thousand) before being rate limited. - /// - public int? TokensRemaining { get; } - - /// - /// Gets the time when the token rate limit will reset, provided in RFC 3339 format. - /// - public DateTimeOffset? TokensReset { get; } - - /// - /// Gets the number of seconds until you can retry the request. - /// - public int? RetryAfter { get; } - - /// - /// Initializes a new instance of the RateLimitInfo class from HttpResponseHeaders. - /// - /// The HTTP response headers. - public RateLimitInfo(HttpResponseHeaders headers) - { - RequestsLimit = ParseIntHeader(headers, "anthropic-ratelimit-requests-limit"); - RequestsRemaining = ParseIntHeader(headers, "anthropic-ratelimit-requests-remaining"); - RequestsReset = ParseDateTimeOffsetHeader(headers, "anthropic-ratelimit-requests-reset"); - TokensLimit = ParseIntHeader(headers, "anthropic-ratelimit-tokens-limit"); - TokensRemaining = ParseIntHeader(headers, "anthropic-ratelimit-tokens-remaining"); - TokensReset = ParseDateTimeOffsetHeader(headers, "anthropic-ratelimit-tokens-reset"); - RetryAfter = ParseIntHeader(headers, "retry-after"); - } - - private static int? ParseIntHeader(HttpResponseHeaders headers, string headerName) - { - if (int.TryParse(headers.GetHeaderValue(headerName), out int value)) - { - return value; - } - return null; - } - - private static DateTimeOffset? ParseDateTimeOffsetHeader(HttpResponseHeaders headers, string headerName) - { - if (DateTimeOffset.TryParse(headers.GetHeaderValue(headerName), out DateTimeOffset value)) - { - return value; - } - return null; - } -} \ No newline at end of file diff --git a/Anthropic/ObjectModels/IObjectModels.cs b/Anthropic/ObjectModels/IObjectInterfaces.cs similarity index 76% rename from Anthropic/ObjectModels/IObjectModels.cs rename to Anthropic/ObjectModels/IObjectInterfaces.cs index 2ba4186..bb5cefc 100644 --- a/Anthropic/ObjectModels/IObjectModels.cs +++ b/Anthropic/ObjectModels/IObjectInterfaces.cs @@ -1,6 +1,6 @@ namespace Anthropic.ObjectModels; -public interface IObjectModels +public interface IObjectInterfaces { public interface IModel { diff --git a/Anthropic/ObjectModels/JsonConverters.cs b/Anthropic/ObjectModels/JsonConverters.cs new file mode 100644 index 0000000..ae87da2 --- /dev/null +++ b/Anthropic/ObjectModels/JsonConverters.cs @@ -0,0 +1,58 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Anthropic.ObjectModels.SharedModels; + +namespace Anthropic.ObjectModels; + +internal class JsonConverters +{ + public class ContentConverter : JsonConverter> + { + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException("Expected start of array"); + } + + var content = new List(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.EndArray: + return content; + case JsonTokenType.StartObject: + content.Add(JsonSerializer.Deserialize(ref reader, options)!); + break; + case JsonTokenType.String: + // Handle the case where content is a single string + content.Add(new() { Type = "text", Text = reader.GetString() }); + return content; // Return immediately as this is a single-item array + } + } + + throw new JsonException("Expected end of array"); + } + + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, options); + } + } + + internal class InputJsonConverter : JsonConverter + { + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var doc = JsonDocument.ParseValue(ref reader); + return doc.RootElement.Clone(); + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, options); + } + } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/MessageRequest.cs b/Anthropic/ObjectModels/MessageRequest.cs deleted file mode 100644 index 08675a6..0000000 --- a/Anthropic/ObjectModels/MessageRequest.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Anthropic.ObjectModels; - -/// -/// Represents a request to create a message using the Anthropic API. -/// -public class MessageRequest : IObjectModels.IModel -{ - /// - /// Gets or sets the maximum number of tokens to generate before stopping. - /// - [JsonPropertyName("max_tokens")] - public int MaxTokens { get; set; } - - /// - /// Gets or sets the input messages for the conversation. - /// - [JsonPropertyName("messages")] - public List Messages { get; set; } - - /// - /// Gets or sets the metadata about the request. - /// - [JsonPropertyName("metadata")] - public Metadata Metadata { get; set; } - - /// - /// Gets or sets the custom text sequences that will cause the model to stop generating. - /// - [JsonPropertyName("stop_sequences")] - public List StopSequences { get; set; } - - /// - /// Gets or sets a value indicating whether to incrementally stream the response using server-sent events. - /// - [JsonPropertyName("stream")] - public bool Stream { get; set; } - - /// - /// Gets or sets the system prompt. - /// - [JsonPropertyName("system")] - public string System { get; set; } - - /// - /// Gets or sets the amount of randomness injected into the response. - /// - [JsonPropertyName("temperature")] - public float Temperature { get; set; } - - /// - /// Gets or sets how the model should use the provided tools. - /// - [JsonPropertyName("tool_choice")] - public ToolChoice ToolChoice { get; set; } - - /// - /// Gets or sets the definitions of tools that the model may use. - /// - [JsonPropertyName("tools")] - public List Tools { get; set; } - - /// - /// Gets or sets the model that will complete the prompt. - /// - [JsonPropertyName("model")] - public string Model { get; set; } -} - -public class Message -{ - private Message(string assistant, string content) - { - Role = assistant; - Content = new(content); - } - - private Message(string assistant, List contents) - { - Role = assistant; - Content = new(contents); - } - - [JsonPropertyName("role")] - public string Role { get; set; } - - [JsonPropertyName("content")] - public MessageContentOneOfType Content { get; set; } - - - public static Message FromAssistant(string content) - { - return new("assistant", content); - } - - public static Message FromUser(string content) - { - return new("user", content); - } - - public static Message FromUser(List contents) - { - return new("user", contents); - } -} - -/// -/// Represents metadata about the request. -/// -public class Metadata -{ - /// - /// Gets or sets an external identifier for the user associated with the request. - /// - [JsonPropertyName("user_id")] - public string UserId { get; set; } -} - -/// -/// Represents how the model should use the provided tools. -/// -public class ToolChoice -{ - /// - /// Gets or sets the type of tool choice. - /// - [JsonPropertyName("type")] - public string Type { get; set; } -} - -/// -/// Represents a tool definition that the model may use. -/// -public class Tool -{ - /// - /// Gets or sets the name of the tool. - /// - [JsonPropertyName("name")] - public string Name { get; set; } - - /// - /// Gets or sets the description of the tool. - /// - [JsonPropertyName("description")] - public string Description { get; set; } - - /// - /// Gets or sets the JSON schema for the tool's input. - /// - [JsonPropertyName("input_schema")] - public InputSchema InputSchema { get; set; } -} - -/// -/// Represents the JSON schema for a tool's input. -/// -public class InputSchema -{ - /// - /// Gets or sets the type of the input schema. - /// - [JsonPropertyName("type")] - public string Type { get; set; } - - /// - /// Gets or sets the properties of the input schema. - /// - [JsonPropertyName("properties")] - public Dictionary Properties { get; set; } -} - -public class MessageContentConverter : JsonConverter -{ - public override MessageContentOneOfType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return reader.TokenType switch - { - JsonTokenType.String => new() { AsString = reader.GetString() }, - JsonTokenType.StartArray => new() { AsBlocks = JsonSerializer.Deserialize>(ref reader, options) }, - _ => throw new JsonException("Invalid token type for MessageContent") - }; - } - - public override void Write(Utf8JsonWriter writer, MessageContentOneOfType? value, JsonSerializerOptions options) - { - if (value?.AsString != null) - { - writer.WriteStringValue(value.AsString); - } - else if (value?.AsBlocks != null) - { - JsonSerializer.Serialize(writer, value.AsBlocks, options); - } - else - { - writer.WriteNullValue(); - } - } -} - -[JsonConverter(typeof(MessageContentConverter))] -public class MessageContentOneOfType -{ - public MessageContentOneOfType() - { - } - - public MessageContentOneOfType(string asString) - { - AsString = asString; - } - - public MessageContentOneOfType(List asBlocks) - { - AsBlocks = asBlocks; - } - - [JsonIgnore] - public string? AsString { get; set; } - - [JsonIgnore] - public List? AsBlocks { get; set; } -} \ No newline at end of file diff --git a/Anthropic/ObjectModels/MessageResponse.cs b/Anthropic/ObjectModels/MessageResponse.cs deleted file mode 100644 index 9e7b0d5..0000000 --- a/Anthropic/ObjectModels/MessageResponse.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Anthropic.ObjectModels; - -/// -/// Represents a response from the Anthropic API for a message request. -/// -public class MessageResponse:BaseResponse -{ - /// - /// Gets or sets the conversational role of the generated message. This will always be "assistant". - /// - [JsonPropertyName("role")] - public string Role { get; set; } - - /// - /// Gets or sets the content generated by the model. - /// - [JsonPropertyName("content")] - public List Content { get; set; } - - /// - /// Gets or sets the model that handled the request. - /// - [JsonPropertyName("model")] - public string Model { get; set; } - - /// - /// Gets or sets the reason that the generation stopped. - /// - [JsonPropertyName("stop_reason")] - public string StopReason { get; set; } - - /// - /// Gets or sets the custom stop sequence that was generated, if any. - /// - [JsonPropertyName("stop_sequence")] - public string StopSequence { get; set; } -} - -/// -/// Represents a content block in the response. -/// -public class ContentBlock: TypeBaseResponse -{ - /// - /// Gets or sets the text content of the block. - /// - [JsonPropertyName("text")] - public string Text { get; set; } - [JsonPropertyName("name")] - public string Name{ get; set; } - - public string Id { get; set; } -} - -/// -/// Represents the usage information for billing and rate-limiting. -/// -public class Usage -{ - /// - /// Gets or sets the number of input tokens used. - /// - [JsonPropertyName("input_tokens")] - public int InputTokens { get; set; } - - /// - /// Gets or sets the number of output tokens used. - /// - [JsonPropertyName("output_tokens")] - public int OutputTokens { get; set; } -} \ No newline at end of file diff --git a/Anthropic/ObjectModels/RequestModels/MessageRequest.cs b/Anthropic/ObjectModels/RequestModels/MessageRequest.cs new file mode 100644 index 0000000..c3eb797 --- /dev/null +++ b/Anthropic/ObjectModels/RequestModels/MessageRequest.cs @@ -0,0 +1,92 @@ +using System.Text.Json.Serialization; +using Anthropic.ObjectModels.SharedModels; + +namespace Anthropic.ObjectModels.RequestModels; + +/// +/// Represents a request to create a message using the Anthropic API. +/// +public class MessageRequest : IObjectInterfaces.IModel +{ + public MessageRequest() + { + } + + /// + /// + /// + /// + /// + /// Model is required for the request but can be set later by using the default model. + public MessageRequest(List messages, int maxTokens, string? model = null) + { + Model = model; + Messages = messages; + MaxTokens = maxTokens; + } + + + /// + /// Gets or sets the maximum number of tokens to generate before stopping. + /// + /// Required + [JsonPropertyName("max_tokens")] + public int MaxTokens { get; set; } + + /// + /// Gets or sets the input messages for the conversation. + /// + /// Required + [JsonPropertyName("messages")] + public List Messages { get; set; } + + /// + /// Gets or sets the metadata about the request. + /// + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + /// + /// Gets or sets the custom text sequences that will cause the model to stop generating. + /// + [JsonPropertyName("stop_sequences")] + public List? StopSequences { get; set; } + + /// + /// The library will automatically set this value. + /// Gets or sets a value indicating whether to incrementally stream the response using server-sent events. + /// + [JsonPropertyName("stream")] + public bool? Stream { get; internal set; } + + /// + /// Gets or sets the system prompt. + /// + [JsonPropertyName("system")] + public string? System { get; set; } + + /// + /// Gets or sets the amount of randomness injected into the response. + /// + [JsonPropertyName("temperature")] + public float? Temperature { get; set; } + + /// + /// Gets or sets how the model should use the provided tools. + /// + [JsonPropertyName("tool_choice")] + public ToolChoice? ToolChoice { get; set; } + + /// + /// Gets or sets the definitions of tools that the model may use. + /// + [JsonPropertyName("tools")] + public List? Tools { get; set; } + + /// + /// Gets or sets the model that will complete the prompt. + /// + /// Required + [JsonPropertyName("model")] + public string Model { get; set; } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/ResponseModels/BaseResponse.cs b/Anthropic/ObjectModels/ResponseModels/BaseResponse.cs new file mode 100644 index 0000000..3c8a0ff --- /dev/null +++ b/Anthropic/ObjectModels/ResponseModels/BaseResponse.cs @@ -0,0 +1,120 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json.Serialization; +using Anthropic.ObjectModels.SharedModels; + +namespace Anthropic.ObjectModels.ResponseModels; +public class TypeBaseResponse:IType +{ + [JsonPropertyName("type")] + public string Type { get; set; } +} +public interface IType +{ + [JsonPropertyName("type")] + public string Type { get; set; } +} + +public interface IStreamResponse:IType +{ + public string? StreamEvent { get; set; } +} +public static class StreamResponseExtensions +{ + public static T As(this IStreamResponse response) where T : class, IStreamResponse + { + if (response is T typedResponse) + { + return typedResponse; + } + + throw new InvalidCastException($"Cannot cast {response.GetType().Name} to {typeof(T).Name}"); + } + + public static bool TryAs(this IStreamResponse response, out T result) where T : class, IStreamResponse + { + if (response is T typedResponse) + { + result = typedResponse; + return true; + } + + result = null; + return false; + } + +} +public static class StreamExtensions +{ + public static bool IsMessageResponse(this IStreamResponse response) + { + return response.Type == "message"; + } + public static bool IsPingResponse(this IStreamResponse response) + { + return response.Type == "ping"; + } + public static bool IsError(this IStreamResponse response) + { + return response.Type == "error"; + } + +} +public class BaseResponse : TypeBaseResponse, IStreamResponse +{ + /// + /// Gets or sets the unique object identifier. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// Gets or sets the usage information for billing and rate-limiting. + /// + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonPropertyName("StreamEvent")] + public string? StreamEvent { get; set; } + public bool IsDelta => StreamEvent?.EndsWith("delta") ?? false; + + public HttpStatusCode HttpStatusCode { get; set; } + public ResponseHeaderValues? HeaderValues { get; set; } + + [JsonPropertyName("error")] + public Error? Error { get; set; } + + public bool Successful => Error == null; +} + +public class Error:TypeBaseResponse +{ + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; +} + +/// +/// Represents Anthropic-specific headers in an HTTP response. +/// +/// +/// Initializes a new instance of the AnthropicHeaders class from HttpResponseHeaders. +/// +public class AnthropicHeaders +{ + /// + /// Represents Anthropic-specific headers in an HTTP response. + /// + /// + /// Initializes a new instance of the AnthropicHeaders class from HttpResponseHeaders. + /// + /// The HTTP response headers. + public AnthropicHeaders(HttpResponseHeaders headers) + { + RateLimit = new(headers); + } + + /// + /// Gets information about rate limits applied to the request. + /// + public RateLimitInfo? RateLimit { get; } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/ResponseModels/MessageResponse.cs b/Anthropic/ObjectModels/ResponseModels/MessageResponse.cs new file mode 100644 index 0000000..146c58a --- /dev/null +++ b/Anthropic/ObjectModels/ResponseModels/MessageResponse.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; +using Anthropic.ObjectModels.SharedModels; + +namespace Anthropic.ObjectModels.ResponseModels; + +public class PingResponse: BaseResponse, IStreamResponse +{ +} +public class MessageResponse : BaseResponse, IStreamResponse +{ + + public override string? ToString() + { + return Content?.FirstOrDefault()?.Text; + } + + [JsonPropertyName("content")] + [JsonConverter(typeof(JsonConverters.ContentConverter))] + public List? Content { get; set; } + + [JsonPropertyName("model")] + public string? Model { get; set; } + + [JsonPropertyName("role")] + public string? Role { get; set; } + + [JsonPropertyName("stop_reason")] + public string? StopReason { get; set; } + + [JsonPropertyName("stop_sequence")] + public string? StopSequence { get; set; } + + public Message ToMessage() + { + return new() + { + Role = Role, + Content = Content + }; + } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/ResponseModels/RateLimitInfo.cs b/Anthropic/ObjectModels/ResponseModels/RateLimitInfo.cs new file mode 100644 index 0000000..af713cf --- /dev/null +++ b/Anthropic/ObjectModels/ResponseModels/RateLimitInfo.cs @@ -0,0 +1,84 @@ +using System.Net.Http.Headers; +using Anthropic.Extensions; + +namespace Anthropic.ObjectModels.ResponseModels; + +/// +/// Represents rate limit information from Anthropic API headers. +/// +/// +/// Initializes a new instance of the RateLimitInfo class from HttpResponseHeaders. +/// +public class RateLimitInfo +{ + /// + /// Represents rate limit information from Anthropic API headers. + /// + /// + /// Initializes a new instance of the RateLimitInfo class from HttpResponseHeaders. + /// + /// The HTTP response headers. + public RateLimitInfo(HttpResponseHeaders headers) + { + RequestsLimit = ParseIntHeader(headers, "anthropic-ratelimit-requests-limit"); + RequestsRemaining = ParseIntHeader(headers, "anthropic-ratelimit-requests-remaining"); + RequestsReset = ParseDateTimeOffsetHeader(headers, "anthropic-ratelimit-requests-reset"); + TokensLimit = ParseIntHeader(headers, "anthropic-ratelimit-tokens-limit"); + TokensRemaining = ParseIntHeader(headers, "anthropic-ratelimit-tokens-remaining"); + TokensReset = ParseDateTimeOffsetHeader(headers, "anthropic-ratelimit-tokens-reset"); + RetryAfter = ParseIntHeader(headers, "retry-after"); + } + + /// + /// Gets the maximum number of requests allowed within any rate limit period. + /// + public int? RequestsLimit { get; } + + /// + /// Gets the number of requests remaining before being rate limited. + /// + public int? RequestsRemaining { get; } + + /// + /// Gets the time when the request rate limit will reset, provided in RFC 3339 format. + /// + public DateTimeOffset? RequestsReset { get; } + + /// + /// Gets the maximum number of tokens allowed within any rate limit period. + /// + public int? TokensLimit { get; } + + /// + /// Gets the number of tokens remaining (rounded to the nearest thousand) before being rate limited. + /// + public int? TokensRemaining { get; } + + /// + /// Gets the time when the token rate limit will reset, provided in RFC 3339 format. + /// + public DateTimeOffset? TokensReset { get; } + + /// + /// Gets the number of seconds until you can retry the request. + /// + public int? RetryAfter { get; } + + private static int? ParseIntHeader(HttpResponseHeaders headers, string headerName) + { + if (int.TryParse(headers.GetHeaderValue(headerName), out var value)) + { + return value; + } + return null; + } + + private static DateTimeOffset? ParseDateTimeOffsetHeader(HttpResponseHeaders headers, string headerName) + { + if (DateTimeOffset.TryParse(headers.GetHeaderValue(headerName), out var value)) + { + return value; + } + return null; + } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/ResponseModels/ResponseHeaderValues.cs b/Anthropic/ObjectModels/ResponseModels/ResponseHeaderValues.cs new file mode 100644 index 0000000..de3c50a --- /dev/null +++ b/Anthropic/ObjectModels/ResponseModels/ResponseHeaderValues.cs @@ -0,0 +1,101 @@ +using Anthropic.Extensions; + +namespace Anthropic.ObjectModels.ResponseModels; + +/// +/// Represents the values of various headers in an HTTP response. +/// +/// +/// Initializes a new instance of the ResponseHeaderValues class from an HttpResponseMessage. +/// +public class ResponseHeaderValues +{ + /// + /// Represents the values of various headers in an HTTP response. + /// + /// + /// Initializes a new instance of the ResponseHeaderValues class from an HttpResponseMessage. + /// + /// The HTTP response message containing the headers. + public ResponseHeaderValues(HttpResponseMessage response) + { + Date = response.Headers.Date; + ContentType = response.Content.Headers.ContentType?.ToString(); + TransferEncoding = response.Headers.TransferEncoding?.ToString(); + Connection = response.Headers.Connection?.ToString(); + RequestId = response.Headers.GetHeaderValue("request-id"); + XCloudTraceContext = response.Headers.GetHeaderValue("x-cloud-trace-context"); + Via = response.Headers.Via?.ToString(); + CFCacheStatus = response.Headers.GetHeaderValue("cf-cache-status"); + Server = response.Headers.Server?.ToString(); + CF_RAY = response.Headers.GetHeaderValue("cf-ray"); + ContentEncoding = response.Content.Headers.ContentEncoding?.ToString(); + All = response.Headers.ToDictionary(x => x.Key, x => x.Value.AsEnumerable()); + Anthropic = new(response.Headers); + } + + /// + /// Gets the date and time at which the message was originated. + /// + public DateTimeOffset? Date { get; set; } + + /// + /// Gets the media type of the body of the response. + /// + public string? ContentType { get; set; } + + /// + /// Gets the transfer encoding applied to the body of the response. + /// + public string? TransferEncoding { get; set; } + + /// + /// Gets the connection type of the response. + /// + public string? Connection { get; set; } + + /// + /// Gets the unique identifier for the request. + /// + public string? RequestId { get; set; } + + /// + /// Gets the cloud trace context for the request. + /// + public string? XCloudTraceContext { get; set; } + + /// + /// Gets information about proxies used in handling the request. + /// + public string? Via { get; set; } + + /// + /// Gets the Cloudflare cache status for the request. + /// + public string? CFCacheStatus { get; set; } + + /// + /// Gets information about the server that handled the request. + /// + public string? Server { get; set; } + + /// + /// Gets the Cloudflare Ray ID for the request. + /// + public string? CF_RAY { get; set; } + + /// + /// Gets the content encoding applied to the body of the response. + /// + public string? ContentEncoding { get; set; } + + /// + /// Gets a dictionary containing all headers from the response. + /// + public Dictionary>? All { get; set; } + + /// + /// Gets the Anthropic-specific headers from the response. + /// + public AnthropicHeaders? Anthropic { get; set; } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/SharedModels/ContentBlock.cs b/Anthropic/ObjectModels/SharedModels/ContentBlock.cs new file mode 100644 index 0000000..f8e9c99 --- /dev/null +++ b/Anthropic/ObjectModels/SharedModels/ContentBlock.cs @@ -0,0 +1,65 @@ +using System.Text.Json.Serialization; +using Anthropic.ObjectModels.ResponseModels; + +namespace Anthropic.ObjectModels.SharedModels; + +public class ContentBlock : TypeBaseResponse +{ + [JsonPropertyName("text")] + public string? Text { get; set; } + + [JsonPropertyName("source")] + public ImageSource? Source { get; set; } + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("input")] + [JsonConverter(typeof(JsonConverters.InputJsonConverter))] + public object? Input { get; set; } + + [JsonIgnore] + public bool IsText => Type == "text"; + + [JsonIgnore] + public bool IsImage => Type == "image"; + + [JsonIgnore] + public bool IsToolUse => Type == "tool_use"; + + public static ContentBlock CreateText(string? text) + { + return new() + { + Type = "text", + Text = text + }; + } + + public static ContentBlock CreateImage(string mediaType, string data) + { + return new() + { + Type = "image", + Source = new() + { + MediaType = mediaType, + Data = data + } + }; + } + + public static ContentBlock CreateToolUse(object input, string? id, string? name) + { + return new() + { + Type = "tool_use", + Id = id, + Name = name, + Input = input + }; + } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/SharedModels/ImageSource.cs b/Anthropic/ObjectModels/SharedModels/ImageSource.cs new file mode 100644 index 0000000..760620a --- /dev/null +++ b/Anthropic/ObjectModels/SharedModels/ImageSource.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; +using Anthropic.ObjectModels.ResponseModels; + +namespace Anthropic.ObjectModels.SharedModels; + +public class ImageSource : TypeBaseResponse +{ + [JsonPropertyName("media_type")] + public string? MediaType { get; set; } + + [JsonPropertyName("data")] + public string? Data { get; set; } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/SharedModels/Message.cs b/Anthropic/ObjectModels/SharedModels/Message.cs new file mode 100644 index 0000000..6d6c873 --- /dev/null +++ b/Anthropic/ObjectModels/SharedModels/Message.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace Anthropic.ObjectModels.SharedModels; + +public class Message +{ + public Message() + { + } + + private Message(string assistant, string content) + { + Role = assistant; + Content = [ContentBlock.CreateText(content)]; + } + + private Message(string assistant, List contents) + { + Role = assistant; + Content = [.. contents]; + } + + [JsonPropertyName("role")] + public string Role { get; set; } + + [JsonPropertyName("content")] + public List Content { get; set; } + + + public static Message FromAssistant(string content) + { + return new("assistant", content); + } + + public static Message FromUser(string content) + { + return new("user", content); + } + + public static Message FromUser(List contents) + { + return new("user", contents); + } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/SharedModels/Metadata.cs b/Anthropic/ObjectModels/SharedModels/Metadata.cs new file mode 100644 index 0000000..96e2d35 --- /dev/null +++ b/Anthropic/ObjectModels/SharedModels/Metadata.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Anthropic.ObjectModels.SharedModels; + +/// +/// Represents metadata about the request. +/// +public class Metadata +{ + /// + /// Gets or sets an external identifier for the user associated with the request. + /// + [JsonPropertyName("user_id")] + public string UserId { get; set; } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/SharedModels/Tool.cs b/Anthropic/ObjectModels/SharedModels/Tool.cs new file mode 100644 index 0000000..6f4f705 --- /dev/null +++ b/Anthropic/ObjectModels/SharedModels/Tool.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Serialization; + +namespace Anthropic.ObjectModels.SharedModels; + +/// +/// Represents a tool definition that the model may use. +/// +public class Tool +{ + /// + /// Gets or sets the name of the tool. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Gets or sets the description of the tool. + /// + [JsonPropertyName("description")] + public string Description { get; set; } + + /// + /// Gets or sets the JSON schema for the tool's input. + /// + [JsonPropertyName("input_schema")] + public PropertyDefinition InputSchema { get; set; } +} + +/// +/// Represents the JSON schema for a tool's input. +/// +public class InputSchema +{ + /// + /// Gets or sets the type of the input schema. + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// Gets or sets the properties of the input schema. + /// + [JsonPropertyName("properties")] + public Dictionary Properties { get; set; } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/SharedModels/ToolChoice.cs b/Anthropic/ObjectModels/SharedModels/ToolChoice.cs new file mode 100644 index 0000000..a3d010e --- /dev/null +++ b/Anthropic/ObjectModels/SharedModels/ToolChoice.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Anthropic.ObjectModels.SharedModels; + +/// +/// Represents how the model should use the provided tools. +/// +public class ToolChoice +{ + /// + /// Gets or sets the type of tool choice. + /// + [JsonPropertyName("type")] + public string Type { get; set; } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/SharedModels/ToolParameters.cs b/Anthropic/ObjectModels/SharedModels/ToolParameters.cs new file mode 100644 index 0000000..49da2e4 --- /dev/null +++ b/Anthropic/ObjectModels/SharedModels/ToolParameters.cs @@ -0,0 +1,177 @@ +using System.Text.Json.Serialization; + +namespace Anthropic.ObjectModels.SharedModels; + +/// +/// Function parameter is a JSON Schema object. +/// https://json-schema.org/understanding-json-schema/reference/object.html +/// +public class PropertyDefinition +{ + public enum FunctionObjectTypes + { + String, + Integer, + Number, + Object, + Array, + Boolean, + Null + } + + /// + /// Required. Function parameter object type. Default value is "object". + /// + [JsonPropertyName("type")] + public string Type { get; set; } = "object"; + + /// + /// Optional. List of "function arguments", as a dictionary that maps from argument name + /// to an object that describes the type, maybe possible enum values, and so on. + /// + [JsonPropertyName("properties")] + public IDictionary? Properties { get; set; } + + /// + /// Optional. List of "function arguments" which are required. + /// + [JsonPropertyName("required")] + public IList? Required { get; set; } + + /// + /// Optional. Whether additional properties are allowed. Default value is true. + /// + [JsonPropertyName("additionalProperties")] + public bool? AdditionalProperties { get; set; } + + /// + /// Optional. Argument description. + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// Optional. List of allowed values for this argument. + /// + [JsonPropertyName("enum")] + public IList? Enum { get; set; } + + /// + /// The number of properties on an object can be restricted using the minProperties and maxProperties keywords. Each of + /// these must be a non-negative integer. + /// + [JsonPropertyName("minProperties")] + public int? MinProperties { get; set; } + + /// + /// The number of properties on an object can be restricted using the minProperties and maxProperties keywords. Each of + /// these must be a non-negative integer. + /// + [JsonPropertyName("maxProperties")] + public int? MaxProperties { get; set; } + + /// + /// If type is "array", this specifies the element type for all items in the array. + /// If type is not "array", this should be null. + /// For more details, see https://json-schema.org/understanding-json-schema/reference/array.html + /// + [JsonPropertyName("items")] + public PropertyDefinition? Items { get; set; } + + public static PropertyDefinition DefineArray(PropertyDefinition? arrayItems = null) + { + return new() + { + Items = arrayItems, + Type = ConvertTypeToString(FunctionObjectTypes.Array) + }; + } + + public static PropertyDefinition DefineEnum(List enumList, string? description = null) + { + return new() + { + Description = description, + Enum = enumList, + Type = ConvertTypeToString(FunctionObjectTypes.String) + }; + } + + public static PropertyDefinition DefineInteger(string? description = null) + { + return new() + { + Description = description, + Type = ConvertTypeToString(FunctionObjectTypes.Integer) + }; + } + + public static PropertyDefinition DefineNumber(string? description = null) + { + return new() + { + Description = description, + Type = ConvertTypeToString(FunctionObjectTypes.Number) + }; + } + + public static PropertyDefinition DefineString(string? description = null) + { + return new() + { + Description = description, + Type = ConvertTypeToString(FunctionObjectTypes.String) + }; + } + + public static PropertyDefinition DefineBoolean(string? description = null) + { + return new() + { + Description = description, + Type = ConvertTypeToString(FunctionObjectTypes.Boolean) + }; + } + + public static PropertyDefinition DefineNull(string? description = null) + { + return new() + { + Description = description, + Type = ConvertTypeToString(FunctionObjectTypes.Null) + }; + } + + public static PropertyDefinition DefineObject(IDictionary? properties, IList? required, bool? additionalProperties, string? description, IList? @enum) + { + return new() + { + Properties = properties, + Required = required, + AdditionalProperties = additionalProperties, + Description = description, + Enum = @enum, + Type = ConvertTypeToString(FunctionObjectTypes.Object) + }; + } + + /// + /// Converts a FunctionObjectTypes enumeration value to its corresponding string representation. + /// + /// The type to convert + /// The string representation of the given type + public static string ConvertTypeToString(FunctionObjectTypes type) + { + return type switch + { + FunctionObjectTypes.String => "string", + FunctionObjectTypes.Integer => "integer", + FunctionObjectTypes.Number => "number", + FunctionObjectTypes.Object => "object", + FunctionObjectTypes.Array => "array", + FunctionObjectTypes.Boolean => "boolean", + FunctionObjectTypes.Null => "null", + _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type}") + }; + } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/SharedModels/Usage.cs b/Anthropic/ObjectModels/SharedModels/Usage.cs new file mode 100644 index 0000000..12861a4 --- /dev/null +++ b/Anthropic/ObjectModels/SharedModels/Usage.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Anthropic.ObjectModels.SharedModels; + +public class Usage +{ + [JsonPropertyName("input_tokens")] + public int InputTokens { get; set; } + + [JsonPropertyName("output_tokens")] + public int OutputTokens { get; set; } +} \ No newline at end of file diff --git a/Anthropic/ObjectModels/StaticValues.cs b/Anthropic/ObjectModels/StaticValues.cs new file mode 100644 index 0000000..7075de2 --- /dev/null +++ b/Anthropic/ObjectModels/StaticValues.cs @@ -0,0 +1,30 @@ +namespace Anthropic.ObjectModels; + +internal class StaticValues +{ + public static class TypeConstants + { + public const string Message = "message"; + public const string ContentBlock = "content_block"; + public const string Text = "text"; + public const string ToolUse = "tool_use"; + public const string InputJson = "input_json"; + public const string Unknown = "unknown"; + } + + public static class StreamConstants + { + public const string Event = "event: "; + public const int EventCharCount = 7; + public const string Data = "data: "; + public const int DataCharCount = 6; + } + + public static class EventSubTypes + { + public const string Ping = "ping"; + public const string Start = "start"; + public const string Delta = "delta"; + public const string Stop = "stop"; + } +} \ No newline at end of file diff --git a/Anthropic/Ranul-Anthropic-Icon.png b/Anthropic/Ranul-Anthropic-Icon.png new file mode 100644 index 0000000..1aa1185 Binary files /dev/null and b/Anthropic/Ranul-Anthropic-Icon.png differ diff --git a/Anthropic/Services/AnthropicMessagesService.cs b/Anthropic/Services/AnthropicMessagesService.cs index 03591fe..8404991 100644 --- a/Anthropic/Services/AnthropicMessagesService.cs +++ b/Anthropic/Services/AnthropicMessagesService.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; using Anthropic.Extensions; -using Anthropic.ObjectModels; +using Anthropic.ObjectModels.RequestModels; +using Anthropic.ObjectModels.ResponseModels; namespace Anthropic.Services; @@ -14,23 +15,22 @@ public async Task Create(MessageRequest request, CancellationTo } /// - public async IAsyncEnumerable CreateAsStream(MessageRequest request, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable CreateAsStream(MessageRequest request, [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Mark the request as streaming request.Stream = true; - // Send the request to the CompletionCreate endpoint request.ProcessModelId(_defaultModelId); using var response = _httpClient.PostAsStreamAsync(_endpointProvider.CreateMessage(), request, cancellationToken); if (!response.IsSuccessStatusCode) { - yield return await response.HandleResponseContent(cancellationToken); + yield return await response.HandleResponseContent(cancellationToken); yield break; } - await foreach (var baseResponse in response.AsStream(cancellationToken: cancellationToken)) yield return baseResponse; + await foreach (var streamResponse in response.AsStream(cancellationToken: cancellationToken)) yield return streamResponse; } public IMessagesService Messages => this; @@ -62,5 +62,5 @@ public interface IMessagesService /// /// /// - public IAsyncEnumerable CreateAsStream(MessageRequest request, [EnumeratorCancellation] CancellationToken cancellationToken = default); + public IAsyncEnumerable CreateAsStream(MessageRequest request, [EnumeratorCancellation] CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Anthropic/Services/AnthropicService.cs b/Anthropic/Services/AnthropicService.cs index fb8658e..ea7bbcf 100644 --- a/Anthropic/Services/AnthropicService.cs +++ b/Anthropic/Services/AnthropicService.cs @@ -1,4 +1,5 @@ using Anthropic.EndpointProviders; +using Anthropic.ObjectModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -31,18 +32,18 @@ public AnthropicService(AnthropicOptions settings, HttpClient? httpClient = null } _httpClient.BaseAddress = new(settings.BaseDomain); - _httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01"); + _httpClient.DefaultRequestHeaders.Add("anthropic-version", settings.ProviderVersion); switch (settings.ProviderType) { - case ProviderType.Anthropic: + case AnthropicProviderType.Anthropic: _httpClient.DefaultRequestHeaders.Add("x-api-key", settings.ApiKey); break; } _endpointProvider = settings.ProviderType switch { - ProviderType.Anthropic => new AnthropicEndpointProvider(settings.ApiVersion), - _ => throw new ArgumentOutOfRangeException() + AnthropicProviderType.Anthropic => new AnthropicEndpointProvider(settings.ApiVersion), + _ => throw new ArgumentOutOfRangeException( nameof(settings.ProviderType)) }; _defaultModelId = settings.DefaultModelId; @@ -54,7 +55,6 @@ public void SetDefaultModelId(string modelId) { _defaultModelId = modelId; } - public IMessagesService ChatCompletion => this; /// public void Dispose() @@ -73,24 +73,4 @@ protected virtual void Dispose(bool disposing) } } } -} - -/// -/// Provider Type -/// -public enum ProviderType -{ - Anthropic = 1, - VertexAI = 2, - AmazonBedrock -} - -public interface IAnthropicService -{ - public IMessagesService Messages { get; } - /// - /// Set default model - /// - /// - void SetDefaultModelId(string modelId); } \ No newline at end of file diff --git a/Anthropic/Services/IAnthropicService.cs b/Anthropic/Services/IAnthropicService.cs new file mode 100644 index 0000000..0178e55 --- /dev/null +++ b/Anthropic/Services/IAnthropicService.cs @@ -0,0 +1,11 @@ +namespace Anthropic.Services; + +public interface IAnthropicService +{ + public IMessagesService Messages { get; } + /// + /// Set default model + /// + /// + void SetDefaultModelId(string modelId); +} \ No newline at end of file