From 0cc642377588ed94f5a596b13f88eb3c5c001540 Mon Sep 17 00:00:00 2001 From: Kasper Marstal Date: Tue, 22 Oct 2024 22:45:51 +0200 Subject: [PATCH] feat: Add support for OpenAI tools (#18) * Rewrite clients to MediatR request handlers * Add glob tool --- .gitignore | 1 + src/Cellm/AddIn/ArgumentParser.cs | 1 - src/Cellm/AddIn/{Cellm.cs => CellmAddIn.cs} | 4 +- src/Cellm/AddIn/CellmConfiguration.cs | 2 +- src/Cellm/AddIn/CellmFunctions.cs | 36 +- src/Cellm/AddIn/{Prompts => }/CellmPrompts.cs | 2 +- src/Cellm/AddIn/Prompts/Prompt.cs | 12 - src/Cellm/Cellm.csproj | 4 + src/Cellm/Models/Anthropic/AnthropicClient.cs | 167 --- .../Models/Anthropic/AnthropicRequest.cs | 5 + .../Anthropic/AnthropicRequestHandler.cs | 114 ++ .../Models/Anthropic/AnthropicResponse.cs | 5 + src/Cellm/Models/Anthropic/Models.cs | 59 + src/Cellm/Models/Client.cs | 29 +- src/Cellm/Models/ClientFactory.cs | 23 - src/Cellm/Models/GoogleAi/GoogleAiClient.cs | 171 --- src/Cellm/Models/GoogleAi/GoogleAiRequest.cs | 5 + .../Models/GoogleAi/GoogleAiRequestHandler.cs | 116 ++ src/Cellm/Models/GoogleAi/GoogleAiResponse.cs | 5 + src/Cellm/Models/GoogleAi/Models.cs | 57 + src/Cellm/Models/IClient.cs | 4 +- src/Cellm/Models/IClientFactory.cs | 6 - src/Cellm/Models/IModelRequest.cs | 9 + src/Cellm/Models/IModelRequestHandler.cs | 11 + src/Cellm/Models/IModelResponse.cs | 8 + src/Cellm/Models/IProviderRequest.cs | 7 + src/Cellm/Models/IProviderRequestHandler.cs | 8 + src/Cellm/Models/IProviderResponse.cs | 5 + .../Models/Llamafile/LlamafileRequest.cs | 5 + ...leClient.cs => LlamafileRequestHandler.cs} | 40 +- .../Models/Llamafile/LlamafileResponse.cs | 5 + .../ModelRequestBehavior/CachingBehavior.cs | 32 + .../ModelRequestBehavior/SentryBehavior.cs | 22 + .../ModelRequestBehavior/ToolBehavior.cs | 46 + src/Cellm/Models/OpenAi/Extensions.cs | 50 + src/Cellm/Models/OpenAi/Models.cs | 53 + src/Cellm/Models/OpenAi/OpenAiClient.cs | 178 --- .../Models/OpenAi/OpenAiConfiguration.cs | 5 +- src/Cellm/Models/OpenAi/OpenAiRequest.cs | 5 + .../Models/OpenAi/OpenAiRequestHandler.cs | 124 ++ src/Cellm/Models/OpenAi/OpenAiResponse.cs | 5 + src/Cellm/Models/Serde.cs | 11 +- src/Cellm/Prompts/Message.cs | 3 + src/Cellm/Prompts/Prompt.cs | 3 + .../{AddIn => }/Prompts/PromptBuilder.cs | 38 +- src/Cellm/Prompts/Roles.cs | 9 + src/Cellm/Prompts/Tool.cs | 5 + src/Cellm/Prompts/ToolCall.cs | 3 + src/Cellm/Services/ServiceLocator.cs | 24 +- src/Cellm/Tools/Glob.cs | 48 + src/Cellm/Tools/ITools.cs | 10 + src/Cellm/Tools/Tools.cs | 73 ++ src/Cellm/appsettings.Local.Anthropic.json | 2 +- src/Cellm/appsettings.json | 4 +- src/Cellm/packages.lock.json | 1041 +++++++++-------- 55 files changed, 1603 insertions(+), 1117 deletions(-) rename src/Cellm/AddIn/{Cellm.cs => CellmAddIn.cs} (83%) rename src/Cellm/AddIn/{Prompts => }/CellmPrompts.cs (95%) delete mode 100644 src/Cellm/AddIn/Prompts/Prompt.cs delete mode 100644 src/Cellm/Models/Anthropic/AnthropicClient.cs create mode 100644 src/Cellm/Models/Anthropic/AnthropicRequest.cs create mode 100644 src/Cellm/Models/Anthropic/AnthropicRequestHandler.cs create mode 100644 src/Cellm/Models/Anthropic/AnthropicResponse.cs create mode 100644 src/Cellm/Models/Anthropic/Models.cs delete mode 100644 src/Cellm/Models/ClientFactory.cs delete mode 100644 src/Cellm/Models/GoogleAi/GoogleAiClient.cs create mode 100644 src/Cellm/Models/GoogleAi/GoogleAiRequest.cs create mode 100644 src/Cellm/Models/GoogleAi/GoogleAiRequestHandler.cs create mode 100644 src/Cellm/Models/GoogleAi/GoogleAiResponse.cs create mode 100644 src/Cellm/Models/GoogleAi/Models.cs delete mode 100644 src/Cellm/Models/IClientFactory.cs create mode 100644 src/Cellm/Models/IModelRequest.cs create mode 100644 src/Cellm/Models/IModelRequestHandler.cs create mode 100644 src/Cellm/Models/IModelResponse.cs create mode 100644 src/Cellm/Models/IProviderRequest.cs create mode 100644 src/Cellm/Models/IProviderRequestHandler.cs create mode 100644 src/Cellm/Models/IProviderResponse.cs create mode 100644 src/Cellm/Models/Llamafile/LlamafileRequest.cs rename src/Cellm/Models/Llamafile/{LlamafileClient.cs => LlamafileRequestHandler.cs} (85%) create mode 100644 src/Cellm/Models/Llamafile/LlamafileResponse.cs create mode 100644 src/Cellm/Models/ModelRequestBehavior/CachingBehavior.cs create mode 100644 src/Cellm/Models/ModelRequestBehavior/SentryBehavior.cs create mode 100644 src/Cellm/Models/ModelRequestBehavior/ToolBehavior.cs create mode 100644 src/Cellm/Models/OpenAi/Extensions.cs create mode 100644 src/Cellm/Models/OpenAi/Models.cs delete mode 100644 src/Cellm/Models/OpenAi/OpenAiClient.cs create mode 100644 src/Cellm/Models/OpenAi/OpenAiRequest.cs create mode 100644 src/Cellm/Models/OpenAi/OpenAiRequestHandler.cs create mode 100644 src/Cellm/Models/OpenAi/OpenAiResponse.cs create mode 100644 src/Cellm/Prompts/Message.cs create mode 100644 src/Cellm/Prompts/Prompt.cs rename src/Cellm/{AddIn => }/Prompts/PromptBuilder.cs (58%) create mode 100644 src/Cellm/Prompts/Roles.cs create mode 100644 src/Cellm/Prompts/Tool.cs create mode 100644 src/Cellm/Prompts/ToolCall.cs create mode 100644 src/Cellm/Tools/Glob.cs create mode 100644 src/Cellm/Tools/ITools.cs create mode 100644 src/Cellm/Tools/Tools.cs diff --git a/.gitignore b/.gitignore index 1812c4e..e601a22 100644 --- a/.gitignore +++ b/.gitignore @@ -367,3 +367,4 @@ appsettings.Local.json docker/ollama-cache docker/vllm-cache *.xlsx +TODO.md diff --git a/src/Cellm/AddIn/ArgumentParser.cs b/src/Cellm/AddIn/ArgumentParser.cs index f63e48b..5597ea8 100644 --- a/src/Cellm/AddIn/ArgumentParser.cs +++ b/src/Cellm/AddIn/ArgumentParser.cs @@ -1,6 +1,5 @@ using System.Text; using Cellm.AddIn.Exceptions; -using Cellm.AddIn.Prompts; using Cellm.Services.Configuration; using ExcelDna.Integration; using Microsoft.Extensions.Configuration; diff --git a/src/Cellm/AddIn/Cellm.cs b/src/Cellm/AddIn/CellmAddIn.cs similarity index 83% rename from src/Cellm/AddIn/Cellm.cs rename to src/Cellm/AddIn/CellmAddIn.cs index 42ffc2b..589bd5f 100644 --- a/src/Cellm/AddIn/Cellm.cs +++ b/src/Cellm/AddIn/CellmAddIn.cs @@ -3,7 +3,7 @@ namespace Cellm.AddIn; -public class Cellm : IExcelAddIn +public class CellmAddIn : IExcelAddIn { public void AutoOpen() { @@ -13,6 +13,8 @@ public void AutoOpen() SentrySdk.CaptureException(ex); return ex.Message; }); + + _ = ServiceLocator.ServiceProvider; } public void AutoClose() diff --git a/src/Cellm/AddIn/CellmConfiguration.cs b/src/Cellm/AddIn/CellmConfiguration.cs index e756ccd..913eef6 100644 --- a/src/Cellm/AddIn/CellmConfiguration.cs +++ b/src/Cellm/AddIn/CellmConfiguration.cs @@ -10,7 +10,7 @@ public class CellmConfiguration public double DefaultTemperature { get; init; } - public int MaxTokens { get; init; } + public int MaxOutputTokens { get; init; } public int CacheTimeoutInSeconds { get; init; } } diff --git a/src/Cellm/AddIn/CellmFunctions.cs b/src/Cellm/AddIn/CellmFunctions.cs index 5ccb13e..d304404 100644 --- a/src/Cellm/AddIn/CellmFunctions.cs +++ b/src/Cellm/AddIn/CellmFunctions.cs @@ -1,10 +1,12 @@ -using System.Text; +using System.Diagnostics; +using System.Text; using Cellm.AddIn.Exceptions; -using Cellm.AddIn.Prompts; using Cellm.Models; +using Cellm.Prompts; using Cellm.Services; +using Cellm.Services.Configuration; using ExcelDna.Integration; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Configuration; namespace Cellm.AddIn; @@ -31,17 +33,23 @@ public static object Prompt( [ExcelArgument(Name = "InstructionsOrTemperature", Description = "A cell or range of cells with instructions or a temperature")] object instructionsOrTemperature, [ExcelArgument(Name = "Temperature", Description = "Temperature")] object temperature) { - var cellmConfiguration = ServiceLocator.Get>().Value; + var configuration = ServiceLocator.Get(); + + var provider = configuration.GetSection(nameof(CellmConfiguration)).GetValue(nameof(CellmConfiguration.DefaultProvider)) + ?? throw new ArgumentException(nameof(CellmConfiguration.DefaultProvider)); + + var model = configuration.GetSection($"{provider}Configuration").GetValue(nameof(IProviderConfiguration.DefaultModel)) + ?? throw new ArgumentException(nameof(IProviderConfiguration.DefaultModel)); return PromptWith( - $"{cellmConfiguration.DefaultProvider}/{cellmConfiguration.DefaultModel}", + $"{provider}/{model}", context, instructionsOrTemperature, temperature); } /// - /// Sends a prompt to a specific model. + /// Sends a prompt to the specified model. /// /// The provider and model in the format "provider/model". /// A cell or range of cells containing the context for the prompt. @@ -79,15 +87,16 @@ public static object PromptWith( .ToString(); var prompt = new PromptBuilder() + .SetModel(arguments.Model) .SetSystemMessage(CellmPrompts.SystemMessage) .SetTemperature(arguments.Temperature) .AddUserMessage(userMessage) .Build(); // ExcelAsyncUtil yields Excel's main thread, Task.Run enables async/await in inner code - return ExcelAsyncUtil.Run(nameof(Prompt), new object[] { context, instructionsOrTemperature, temperature }, () => + return ExcelAsyncUtil.Run(nameof(PromptWith), new object[] { providerAndModel, context, instructionsOrTemperature, temperature }, () => { - return Task.Run(async () => await CallModelAsync(prompt, arguments.Provider, arguments.Model)).GetAwaiter().GetResult(); + return Task.Run(async () => await CallModelAsync(prompt, arguments.Provider)).GetAwaiter().GetResult(); }); } catch (CellmException ex) @@ -106,20 +115,23 @@ public static object PromptWith( /// A task that represents the asynchronous operation. The task result contains the model's response as a string. /// Thrown when an unexpected error occurs during the operation. - private static async Task CallModelAsync(Prompt prompt, string? provider = null, string? model = null, Uri? baseAddress = null) + private static async Task CallModelAsync(Prompt prompt, string? provider = null, Uri? baseAddress = null) { try { var client = ServiceLocator.Get(); - var response = await client.Send(prompt, provider, model, baseAddress); - return response.Messages.Last().Content; + var response = await client.Send(prompt, provider, baseAddress); + var content = response.Messages.Last().Content; + return content; } - catch (CellmException) + catch (CellmException ex) { + Debug.WriteLine(ex); throw; } catch (Exception ex) { + Debug.WriteLine(ex); throw new CellmException("An unexpected error occurred", ex); } } diff --git a/src/Cellm/AddIn/Prompts/CellmPrompts.cs b/src/Cellm/AddIn/CellmPrompts.cs similarity index 95% rename from src/Cellm/AddIn/Prompts/CellmPrompts.cs rename to src/Cellm/AddIn/CellmPrompts.cs index ec96fc3..f32c015 100644 --- a/src/Cellm/AddIn/Prompts/CellmPrompts.cs +++ b/src/Cellm/AddIn/CellmPrompts.cs @@ -1,4 +1,4 @@ -namespace Cellm.AddIn.Prompts; +namespace Cellm.AddIn; internal static class CellmPrompts { diff --git a/src/Cellm/AddIn/Prompts/Prompt.cs b/src/Cellm/AddIn/Prompts/Prompt.cs deleted file mode 100644 index 83c84ce..0000000 --- a/src/Cellm/AddIn/Prompts/Prompt.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Cellm.AddIn.Prompts; - -public enum Role -{ - System, - User, - Assistant -} - -public record Message(string Content, Role Role); - -public record Prompt(string SystemMessage, List Messages, double Temperature); diff --git a/src/Cellm/Cellm.csproj b/src/Cellm/Cellm.csproj index 771b63d..d6f41d9 100644 --- a/src/Cellm/Cellm.csproj +++ b/src/Cellm/Cellm.csproj @@ -20,10 +20,14 @@ + + + + diff --git a/src/Cellm/Models/Anthropic/AnthropicClient.cs b/src/Cellm/Models/Anthropic/AnthropicClient.cs deleted file mode 100644 index 38c3312..0000000 --- a/src/Cellm/Models/Anthropic/AnthropicClient.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System.Text; -using System.Text.Json.Serialization; -using Cellm.AddIn; -using Cellm.AddIn.Exceptions; -using Cellm.AddIn.Prompts; -using Microsoft.Extensions.Options; - -namespace Cellm.Models.Anthropic; - -internal class AnthropicClient : IClient -{ - private readonly AnthropicConfiguration _anthropicConfiguration; - private readonly CellmConfiguration _cellmConfiguration; - private readonly HttpClient _httpClient; - private readonly ICache _cache; - private readonly ISerde _serde; - - public AnthropicClient( - IOptions anthropicConfiguration, - IOptions cellmConfiguration, - HttpClient httpClient, - ICache cache, - ISerde serde) - { - _anthropicConfiguration = anthropicConfiguration.Value; - _cellmConfiguration = cellmConfiguration.Value; - _httpClient = httpClient; - _cache = cache; - _serde = serde; - } - - public async Task Send(Prompt prompt, string? provider, string? model, Uri? baseAddress) - { - var transaction = SentrySdk.StartTransaction(typeof(AnthropicClient).Name, nameof(Send)); - SentrySdk.ConfigureScope(scope => scope.Transaction = transaction); - - var requestBody = new RequestBody - { - System = prompt.SystemMessage, - Messages = prompt.Messages.Select(x => new Message { Content = x.Content, Role = x.Role.ToString().ToLower() }).ToList(), - Model = model ?? _anthropicConfiguration.DefaultModel, - MaxTokens = _cellmConfiguration.MaxTokens, - Temperature = prompt.Temperature - }; - - if (_cache.TryGetValue(requestBody, out object? value) && value is Prompt assistantPrompt) - { - return assistantPrompt; - } - - var json = _serde.Serialize(requestBody); - var jsonAsString = new StringContent(json, Encoding.UTF8, "application/json"); - - const string path = "/v1/messages"; - var address = baseAddress is null ? new Uri(path, UriKind.Relative) : new Uri(baseAddress, path); - - var response = await _httpClient.PostAsync(address, jsonAsString); - var responseBodyAsString = await response.Content.ReadAsStringAsync(); - - if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException(responseBodyAsString, null, response.StatusCode); - } - - var responseBody = _serde.Deserialize(responseBodyAsString); - var assistantMessage = responseBody?.Content?.Last()?.Text ?? throw new CellmException("#EMPTY_RESPONSE?"); - - if (assistantMessage.StartsWith("#INSTRUCTION_ERROR?")) - { - throw new CellmException(assistantMessage); - } - - assistantPrompt = new PromptBuilder(prompt) - .AddAssistantMessage(assistantMessage) - .Build(); - - _cache.Set(requestBody, assistantPrompt); - - var inputTokens = responseBody?.Usage?.InputTokens ?? -1; - if (inputTokens > 0) - { - SentrySdk.Metrics.Distribution("InputTokens", - inputTokens, - unit: MeasurementUnit.Custom("token"), - tags: new Dictionary { - { nameof(provider), provider?.ToLower() ?? _cellmConfiguration.DefaultProvider }, - { nameof(model), model?.ToLower() ?? _anthropicConfiguration.DefaultModel }, - { nameof(_httpClient.BaseAddress), _httpClient.BaseAddress?.ToString() ?? string.Empty } - } - ); - } - - var outputTokens = responseBody?.Usage?.OutputTokens ?? -1; - if (outputTokens > 0) - { - SentrySdk.Metrics.Distribution("OutputTokens", - outputTokens, - unit: MeasurementUnit.Custom("token"), - tags: new Dictionary { - { nameof(provider), provider?.ToLower() ?? _cellmConfiguration.DefaultProvider }, - { nameof(model), model?.ToLower() ?? _anthropicConfiguration.DefaultModel }, - { nameof(_httpClient.BaseAddress), _httpClient.BaseAddress?.ToString() ?? string.Empty } - } - ); - } - - transaction.Finish(); - - return assistantPrompt; - } - - private class ResponseBody - { - public List? Content { get; set; } - - public string? Id { get; set; } - - public string? Model { get; set; } - - public string? Role { get; set; } - - [JsonPropertyName("stop_reason")] - public string? StopReason { get; set; } - - [JsonPropertyName("stop_sequence")] - public string? StopSequence { get; set; } - - public string? Type { get; set; } - - public Usage? Usage { get; set; } - } - - private class RequestBody - { - public List? Messages { get; set; } - - public string? System { get; set; } - - public string? Model { get; set; } - - [JsonPropertyName("max_tokens")] - public int MaxTokens { get; set; } - - public double Temperature { get; set; } - } - - private class Message - { - public string? Role { get; set; } - - public string? Content { get; set; } - } - - private class Content - { - public string? Text { get; set; } - - public string? Type { get; set; } - } - - private class Usage - { - public int InputTokens { get; set; } - - public int OutputTokens { get; set; } - } -} diff --git a/src/Cellm/Models/Anthropic/AnthropicRequest.cs b/src/Cellm/Models/Anthropic/AnthropicRequest.cs new file mode 100644 index 0000000..f0d1f9b --- /dev/null +++ b/src/Cellm/Models/Anthropic/AnthropicRequest.cs @@ -0,0 +1,5 @@ +using Cellm.Prompts; + +namespace Cellm.Models.Anthropic; + +internal record AnthropicRequest(Prompt Prompt, string? Provider, Uri? BaseAddress) : IModelRequest; \ No newline at end of file diff --git a/src/Cellm/Models/Anthropic/AnthropicRequestHandler.cs b/src/Cellm/Models/Anthropic/AnthropicRequestHandler.cs new file mode 100644 index 0000000..b466e83 --- /dev/null +++ b/src/Cellm/Models/Anthropic/AnthropicRequestHandler.cs @@ -0,0 +1,114 @@ +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Cellm.AddIn; +using Cellm.AddIn.Exceptions; +using Cellm.Models.Anthropic.Models; +using Cellm.Prompts; +using Microsoft.Extensions.Options; + +namespace Cellm.Models.Anthropic; + +internal class AnthropicRequestHandler : IModelRequestHandler +{ + private readonly AnthropicConfiguration _anthropicConfiguration; + private readonly CellmConfiguration _cellmConfiguration; + private readonly HttpClient _httpClient; + private readonly ICache _cache; + private readonly ISerde _serde; + + public AnthropicRequestHandler( + IOptions anthropicConfiguration, + IOptions cellmConfiguration, + HttpClient httpClient, + ICache cache, + ISerde serde) + { + _anthropicConfiguration = anthropicConfiguration.Value; + _cellmConfiguration = cellmConfiguration.Value; + _httpClient = httpClient; + _cache = cache; + _serde = serde; + } + + public async Task Handle(AnthropicRequest request, CancellationToken cancellationToken) + { + const string path = "/v1/messages"; + var address = request.BaseAddress is null ? new Uri(path, UriKind.Relative) : new Uri(request.BaseAddress, path); + + var json = Serialize(request); + var jsonAsStringContent = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync(address, jsonAsStringContent, cancellationToken); + var responseBodyAsString = await response.Content.ReadAsStringAsync(cancellationToken); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"{nameof(AnthropicRequest)} failed: {responseBodyAsString}", null, response.StatusCode); + } + + return Deserialize(request, responseBodyAsString); + } + + public string Serialize(AnthropicRequest request) + { + var requestBody = new AnthropicRequestBody + { + System = request.Prompt.SystemMessage, + Messages = request.Prompt.Messages.Select(x => new AnthropicMessage { Content = x.Content, Role = x.Role.ToString().ToLower() }).ToList(), + Model = request.Prompt.Model ?? _anthropicConfiguration.DefaultModel, + MaxTokens = _cellmConfiguration.MaxOutputTokens, + Temperature = request.Prompt.Temperature + }; + + return _serde.Serialize(requestBody, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + } + + public AnthropicResponse Deserialize(AnthropicRequest request, string response) + { + var responseBody = _serde.Deserialize(response, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + + var tags = new Dictionary { + { nameof(request.Provider), request.Provider?.ToLower() ?? _cellmConfiguration.DefaultProvider }, + { nameof(request.Prompt.Model), request.Prompt.Model ?.ToLower() ?? _anthropicConfiguration.DefaultModel }, + { nameof(_httpClient.BaseAddress), _httpClient.BaseAddress?.ToString() ?? string.Empty } + }; + + var inputTokens = responseBody?.Usage?.InputTokens ?? -1; + if (inputTokens > 0) + { + SentrySdk.Metrics.Distribution("InputTokens", + inputTokens, + unit: MeasurementUnit.Custom("token"), + tags); + } + + var outputTokens = responseBody?.Usage?.OutputTokens ?? -1; + if (outputTokens > 0) + { + SentrySdk.Metrics.Distribution("OutputTokens", + outputTokens, + unit: MeasurementUnit.Custom("token"), + tags); + } + + var assistantMessage = responseBody?.Content?.Last()?.Text ?? throw new CellmException("#EMPTY_RESPONSE?"); + + var prompt = new PromptBuilder(request.Prompt) + .AddAssistantMessage(assistantMessage) + .Build(); + + return new AnthropicResponse(prompt); + } +} diff --git a/src/Cellm/Models/Anthropic/AnthropicResponse.cs b/src/Cellm/Models/Anthropic/AnthropicResponse.cs new file mode 100644 index 0000000..78dbc41 --- /dev/null +++ b/src/Cellm/Models/Anthropic/AnthropicResponse.cs @@ -0,0 +1,5 @@ +using Cellm.Prompts; + +namespace Cellm.Models.Anthropic; + +internal record AnthropicResponse(Prompt Prompt) : IModelResponse; \ No newline at end of file diff --git a/src/Cellm/Models/Anthropic/Models.cs b/src/Cellm/Models/Anthropic/Models.cs new file mode 100644 index 0000000..8d83591 --- /dev/null +++ b/src/Cellm/Models/Anthropic/Models.cs @@ -0,0 +1,59 @@ +using System.Text.Json.Serialization; + +namespace Cellm.Models.Anthropic.Models; + +public class AnthropicResponseBody +{ + public List? Content { get; set; } + + public string? Id { get; set; } + + public string? Model { get; set; } + + public string? Role { get; set; } + + [JsonPropertyName("stop_reason")] + public string? StopReason { get; set; } + + [JsonPropertyName("stop_sequence")] + public string? StopSequence { get; set; } + + public string? Type { get; set; } + + public AnthropicUsage? Usage { get; set; } +} + +public class AnthropicRequestBody +{ + public List? Messages { get; set; } + + public string? System { get; set; } + + public string? Model { get; set; } + + [JsonPropertyName("max_tokens")] + public int MaxTokens { get; set; } + + public double Temperature { get; set; } +} + +public class AnthropicMessage +{ + public string? Role { get; set; } + + public string? Content { get; set; } +} + +public class AnthropicContent +{ + public string? Text { get; set; } + + public string? Type { get; set; } +} + +public class AnthropicUsage +{ + public int InputTokens { get; set; } + + public int OutputTokens { get; set; } +} \ No newline at end of file diff --git a/src/Cellm/Models/Client.cs b/src/Cellm/Models/Client.cs index 6bfed38..d8be9f7 100644 --- a/src/Cellm/Models/Client.cs +++ b/src/Cellm/Models/Client.cs @@ -1,7 +1,12 @@ using System.Text.Json; using Cellm.AddIn; using Cellm.AddIn.Exceptions; -using Cellm.AddIn.Prompts; +using Cellm.Models.Anthropic; +using Cellm.Models.GoogleAi; +using Cellm.Models.Llamafile; +using Cellm.Models.OpenAi; +using Cellm.Prompts; +using MediatR; using Microsoft.Extensions.Options; using Polly.Timeout; @@ -9,21 +14,31 @@ namespace Cellm.Models; internal class Client : IClient { - private readonly IClientFactory _clientFactory; private readonly CellmConfiguration _cellmConfiguration; + private readonly ISender _sender; - public Client(IClientFactory clientFactory, IOptions cellmConfiguration) + public Client(IOptions cellmConfiguration, ISender sender) { - _clientFactory = clientFactory; _cellmConfiguration = cellmConfiguration.Value; + _sender = sender; } - public async Task Send(Prompt prompt, string? provider, string? model, Uri? baseAddress) + public async Task Send(Prompt prompt, string? provider, Uri? baseAddress) { try { - var client = _clientFactory.GetClient(provider ?? _cellmConfiguration.DefaultProvider); - return await client.Send(prompt, provider, model, baseAddress); + provider ??= _cellmConfiguration.DefaultProvider; + + IModelResponse response = provider.ToLower() switch + { + "anthropic" => await _sender.Send(new AnthropicRequest(prompt, provider, baseAddress)), + "googleai" => await _sender.Send(new GoogleAiRequest(prompt, provider, baseAddress)), + "llamafile" => await _sender.Send(new LlamafileRequest(prompt)), + "openai" => await _sender.Send(new OpenAiRequest(prompt, provider, baseAddress)), + _ => throw new ArgumentException($"Unsupported client type: {provider}") + }; + + return response.Prompt; } catch (HttpRequestException ex) { diff --git a/src/Cellm/Models/ClientFactory.cs b/src/Cellm/Models/ClientFactory.cs deleted file mode 100644 index acd5964..0000000 --- a/src/Cellm/Models/ClientFactory.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Cellm.Models.Anthropic; -using Cellm.Models.GoogleAi; -using Cellm.Models.Llamafile; -using Cellm.Models.OpenAi; -using Cellm.Services; - -namespace Cellm.Models; - -internal class ClientFactory : IClientFactory -{ - public IClient GetClient(string modelProvider) - { - - return modelProvider.ToLower() switch - { - "anthropic" => ServiceLocator.Get(), - "googleai" => ServiceLocator.Get(), - "openai" => ServiceLocator.Get(), - "llamafile" => ServiceLocator.Get(), - _ => throw new ArgumentException($"Unsupported client type: {modelProvider}") - }; - } -} diff --git a/src/Cellm/Models/GoogleAi/GoogleAiClient.cs b/src/Cellm/Models/GoogleAi/GoogleAiClient.cs deleted file mode 100644 index c6d769c..0000000 --- a/src/Cellm/Models/GoogleAi/GoogleAiClient.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System.Text; -using System.Text.Json.Serialization; -using Cellm.AddIn; -using Cellm.AddIn.Exceptions; -using Cellm.AddIn.Prompts; -using Microsoft.Extensions.Options; - -namespace Cellm.Models.GoogleAi; - -internal class GoogleAiClient : IClient -{ - private readonly GoogleAiConfiguration _googleAiConfiguration; - private readonly CellmConfiguration _cellmConfiguration; - private readonly HttpClient _httpClient; - private readonly ICache _cache; - private readonly ISerde _serde; - - public GoogleAiClient( - IOptions googleAiConfiguration, - IOptions cellmConfiguration, - HttpClient httpClient, - ICache cache, - ISerde serde) - { - _googleAiConfiguration = googleAiConfiguration.Value; - _cellmConfiguration = cellmConfiguration.Value; - _httpClient = httpClient; - _cache = cache; - _serde = serde; - } - - public async Task Send(Prompt prompt, string? provider, string? model, Uri? baseAddress) - { - var transaction = SentrySdk.StartTransaction(typeof(GoogleAiClient).Name, nameof(Send)); - SentrySdk.ConfigureScope(scope => scope.Transaction = transaction); - - var requestBody = new RequestBody - { - SystemInstruction = new Content - { - Parts = new List { new Part { Text = prompt.SystemMessage } } - }, - Contents = new List - { - new Content - { - Parts = prompt.Messages.Select(x => new Part { Text = x.Content }).ToList() - } - } - }; - - if (_cache.TryGetValue(requestBody, out object? value) && value is Prompt assistantPrompt) - { - return assistantPrompt; - } - - var json = _serde.Serialize(requestBody); - var jsonAsString = new StringContent(json, Encoding.UTF8, "application/json"); - - string path = $"/v1beta/models/{model ?? _googleAiConfiguration.DefaultModel}:generateContent?key={_googleAiConfiguration.ApiKey}"; - var address = baseAddress is null ? new Uri(path, UriKind.Relative) : new Uri(baseAddress, path); - - var response = await _httpClient.PostAsync(address, jsonAsString); - var responseBodyAsString = await response.Content.ReadAsStringAsync(); - - if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException(responseBodyAsString, null, response.StatusCode); - } - - var responseBody = _serde.Deserialize(responseBodyAsString); - var assistantMessage = responseBody?.Candidates?.SingleOrDefault()?.Content?.Parts?.SingleOrDefault()?.Text ?? throw new CellmException("#EMPTY_RESPONSE?"); - - if (assistantMessage.StartsWith("#INSTRUCTION_ERROR?")) - { - throw new CellmException(assistantMessage); - } - - assistantPrompt = new PromptBuilder(prompt) - .AddAssistantMessage(assistantMessage) - .Build(); - - _cache.Set(requestBody, assistantPrompt); - - var inputTokens = responseBody?.UsageMetadata?.PromptTokenCount ?? -1; - if (inputTokens > 0) - { - SentrySdk.Metrics.Distribution("InputTokens", - inputTokens, - unit: MeasurementUnit.Custom("token"), - tags: new Dictionary { - { nameof(provider), provider?.ToLower() ?? _cellmConfiguration.DefaultProvider }, - { nameof(model), model?.ToLower() ?? _googleAiConfiguration.DefaultModel }, - { nameof(_httpClient.BaseAddress), _httpClient.BaseAddress?.ToString() ?? string.Empty } - } - ); - } - - var outputTokens = responseBody?.UsageMetadata?.CandidatesTokenCount ?? -1; - if (outputTokens > 0) - { - SentrySdk.Metrics.Distribution("OutputTokens", - outputTokens, - unit: MeasurementUnit.Custom("token"), - tags: new Dictionary { - { nameof(provider), provider?.ToLower() ?? _cellmConfiguration.DefaultProvider }, - { nameof(model), model?.ToLower() ?? _googleAiConfiguration.DefaultModel }, - { nameof(_httpClient.BaseAddress), _httpClient.BaseAddress?.ToString() ?? string.Empty } - } - ); - } - - transaction.Finish(); - - return assistantPrompt; - } - - private class RequestBody - { - [JsonPropertyName("system_instruction")] - public Content? SystemInstruction { get; set; } - - public List? Contents { get; set; } - } - - public class ResponseBody - { - public List? Candidates { get; set; } - - public UsageMetadata? UsageMetadata { get; set; } - } - - public class Candidate - { - public Content? Content { get; set; } - - public string? FinishReason { get; set; } - - public int Index { get; set; } - - public List? SafetyRatings { get; set; } - } - - public class Content - { - public List? Parts { get; set; } - - public string? Role { get; set; } - } - - public class Part - { - public string? Text { get; set; } - } - - public class SafetyRating - { - public string? Category { get; set; } - - public string? Probability { get; set; } - } - - public class UsageMetadata - { - public int PromptTokenCount { get; set; } - - public int CandidatesTokenCount { get; set; } - - public int TotalTokenCount { get; set; } - } -} diff --git a/src/Cellm/Models/GoogleAi/GoogleAiRequest.cs b/src/Cellm/Models/GoogleAi/GoogleAiRequest.cs new file mode 100644 index 0000000..9596d0f --- /dev/null +++ b/src/Cellm/Models/GoogleAi/GoogleAiRequest.cs @@ -0,0 +1,5 @@ +using Cellm.Prompts; + +namespace Cellm.Models.GoogleAi; + +internal record GoogleAiRequest(Prompt Prompt, string? Provider, Uri? BaseAddress) : IModelRequest; \ No newline at end of file diff --git a/src/Cellm/Models/GoogleAi/GoogleAiRequestHandler.cs b/src/Cellm/Models/GoogleAi/GoogleAiRequestHandler.cs new file mode 100644 index 0000000..e515beb --- /dev/null +++ b/src/Cellm/Models/GoogleAi/GoogleAiRequestHandler.cs @@ -0,0 +1,116 @@ +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Cellm.AddIn; +using Cellm.AddIn.Exceptions; +using Cellm.Prompts; +using Microsoft.Extensions.Options; + +namespace Cellm.Models.GoogleAi; + +internal class GoogleAiRequestHandler : IModelRequestHandler +{ + private readonly GoogleAiConfiguration _googleAiConfiguration; + private readonly CellmConfiguration _cellmConfiguration; + private readonly HttpClient _httpClient; + private readonly ISerde _serde; + + public GoogleAiRequestHandler( + IOptions googleAiConfiguration, + IOptions cellmConfiguration, + HttpClient httpClient, + ISerde serde) + { + _googleAiConfiguration = googleAiConfiguration.Value; + _cellmConfiguration = cellmConfiguration.Value; + _httpClient = httpClient; + _serde = serde; + } + + public async Task Handle(GoogleAiRequest request, CancellationToken cancellationToken) + { + string path = $"/v1beta/models/{request.Prompt.Model ?? _googleAiConfiguration.DefaultModel}:generateContent?key={_googleAiConfiguration.ApiKey}"; + var address = request.BaseAddress is null ? new Uri(path, UriKind.Relative) : new Uri(request.BaseAddress, path); + + var json = Serialize(request); + var jsonAsStringContent = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync(address, jsonAsStringContent, cancellationToken); + var responseBodyAsString = await response.Content.ReadAsStringAsync(cancellationToken); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"{nameof(GoogleAiRequest)} failed: {responseBodyAsString}", null, response.StatusCode); + } + + return Deserialize(request, responseBodyAsString); + } + + public string Serialize(GoogleAiRequest request) + { + var requestBody = new GoogleAiRequestBody + { + SystemInstruction = new GoogleAiContent + { + Parts = new List { new GoogleAiPart { Text = request.Prompt.SystemMessage } } + }, + Contents = new List + { + new GoogleAiContent + { + Parts = request.Prompt.Messages.Select(x => new GoogleAiPart { Text = x.Content }).ToList() + } + } + }; + + return _serde.Serialize(requestBody, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + } + + public GoogleAiResponse Deserialize(GoogleAiRequest request, string response) + { + var responseBody = _serde.Deserialize(response, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + + var tags = new Dictionary { + { nameof(request.Provider), request.Provider?.ToLower() ?? _cellmConfiguration.DefaultProvider }, + { nameof(request.Prompt.Model), request.Prompt.Model?.ToLower() ?? _googleAiConfiguration.DefaultModel }, + { nameof(_httpClient.BaseAddress), _httpClient.BaseAddress?.ToString() ?? string.Empty } + }; + + var inputTokens = responseBody?.UsageMetadata?.PromptTokenCount ?? -1; + if (inputTokens > 0) + { + SentrySdk.Metrics.Distribution("InputTokens", + inputTokens, + unit: MeasurementUnit.Custom("token"), + tags); + } + + var outputTokens = responseBody?.UsageMetadata?.CandidatesTokenCount ?? -1; + if (outputTokens > 0) + { + SentrySdk.Metrics.Distribution("OutputTokens", + outputTokens, + unit: MeasurementUnit.Custom("token"), + tags); + } + + var assistantMessage = responseBody?.Candidates?.SingleOrDefault()?.Content?.Parts?.SingleOrDefault()?.Text ?? throw new CellmException("#EMPTY_RESPONSE?"); + + var assistantPrompt = new PromptBuilder(request.Prompt) + .AddAssistantMessage(assistantMessage) + .Build(); + + return new GoogleAiResponse(assistantPrompt); + } +} diff --git a/src/Cellm/Models/GoogleAi/GoogleAiResponse.cs b/src/Cellm/Models/GoogleAi/GoogleAiResponse.cs new file mode 100644 index 0000000..e8436ea --- /dev/null +++ b/src/Cellm/Models/GoogleAi/GoogleAiResponse.cs @@ -0,0 +1,5 @@ +using Cellm.Prompts; + +namespace Cellm.Models.GoogleAi; + +internal record GoogleAiResponse(Prompt Prompt) : IModelResponse; \ No newline at end of file diff --git a/src/Cellm/Models/GoogleAi/Models.cs b/src/Cellm/Models/GoogleAi/Models.cs new file mode 100644 index 0000000..730b3f6 --- /dev/null +++ b/src/Cellm/Models/GoogleAi/Models.cs @@ -0,0 +1,57 @@ +using System.Text.Json.Serialization; + +namespace Cellm.Models.GoogleAi; + +public class GoogleAiRequestBody +{ + [JsonPropertyName("system_instruction")] + public GoogleAiContent? SystemInstruction { get; set; } + + public List? Contents { get; set; } +} + +public class GoogleAiResponseBody +{ + public List? Candidates { get; set; } + + public GoogleAiUsageMetadata? UsageMetadata { get; set; } +} + +public class GoogleAiCandidate +{ + public GoogleAiContent? Content { get; set; } + + public string? FinishReason { get; set; } + + public int Index { get; set; } + + public List? SafetyRatings { get; set; } +} + +public class GoogleAiContent +{ + public List? Parts { get; set; } + + public string? Role { get; set; } +} + +public class GoogleAiPart +{ + public string? Text { get; set; } +} + +public class GoogleAiSafetyRating +{ + public string? Category { get; set; } + + public string? Probability { get; set; } +} + +public class GoogleAiUsageMetadata +{ + public int PromptTokenCount { get; set; } + + public int CandidatesTokenCount { get; set; } + + public int TotalTokenCount { get; set; } +} \ No newline at end of file diff --git a/src/Cellm/Models/IClient.cs b/src/Cellm/Models/IClient.cs index 0c02ed6..263496d 100644 --- a/src/Cellm/Models/IClient.cs +++ b/src/Cellm/Models/IClient.cs @@ -1,8 +1,8 @@ -using Cellm.AddIn.Prompts; +using Cellm.Prompts; namespace Cellm.Models; internal interface IClient { - public Task Send(Prompt prompt, string? provider, string? model, Uri? baseAddress); + public Task Send(Prompt prompt, string? provider, Uri? baseAddress); } \ No newline at end of file diff --git a/src/Cellm/Models/IClientFactory.cs b/src/Cellm/Models/IClientFactory.cs deleted file mode 100644 index f2c0e83..0000000 --- a/src/Cellm/Models/IClientFactory.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Cellm.Models; - -internal interface IClientFactory -{ - IClient GetClient(string clientName); -} diff --git a/src/Cellm/Models/IModelRequest.cs b/src/Cellm/Models/IModelRequest.cs new file mode 100644 index 0000000..d9d4310 --- /dev/null +++ b/src/Cellm/Models/IModelRequest.cs @@ -0,0 +1,9 @@ +using Cellm.Prompts; +using MediatR; + +namespace Cellm.Models; + +internal interface IModelRequest : IRequest +{ + Prompt Prompt { get; } +} \ No newline at end of file diff --git a/src/Cellm/Models/IModelRequestHandler.cs b/src/Cellm/Models/IModelRequestHandler.cs new file mode 100644 index 0000000..d4983a6 --- /dev/null +++ b/src/Cellm/Models/IModelRequestHandler.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace Cellm.Models; + +internal interface IModelRequestHandler : IRequestHandler + where TRequest : IRequest +{ + public string Serialize(TRequest request); + + public TResponse Deserialize(TRequest request, string response); +} diff --git a/src/Cellm/Models/IModelResponse.cs b/src/Cellm/Models/IModelResponse.cs new file mode 100644 index 0000000..1338c7c --- /dev/null +++ b/src/Cellm/Models/IModelResponse.cs @@ -0,0 +1,8 @@ +using Cellm.Prompts; + +namespace Cellm.Models; + +public interface IModelResponse +{ + Prompt Prompt { get; } +} \ No newline at end of file diff --git a/src/Cellm/Models/IProviderRequest.cs b/src/Cellm/Models/IProviderRequest.cs new file mode 100644 index 0000000..ed937bc --- /dev/null +++ b/src/Cellm/Models/IProviderRequest.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace Cellm.Models; + +internal interface IProviderRequest : IRequest +{ +} diff --git a/src/Cellm/Models/IProviderRequestHandler.cs b/src/Cellm/Models/IProviderRequestHandler.cs new file mode 100644 index 0000000..12dc800 --- /dev/null +++ b/src/Cellm/Models/IProviderRequestHandler.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Cellm.Models; + +internal interface IProviderRequestHandler : IRequestHandler + where TRequest : IRequest +{ +} diff --git a/src/Cellm/Models/IProviderResponse.cs b/src/Cellm/Models/IProviderResponse.cs new file mode 100644 index 0000000..e218401 --- /dev/null +++ b/src/Cellm/Models/IProviderResponse.cs @@ -0,0 +1,5 @@ +namespace Cellm.Models; + +public interface IProviderResponse : IModelResponse +{ +} \ No newline at end of file diff --git a/src/Cellm/Models/Llamafile/LlamafileRequest.cs b/src/Cellm/Models/Llamafile/LlamafileRequest.cs new file mode 100644 index 0000000..ae45bae --- /dev/null +++ b/src/Cellm/Models/Llamafile/LlamafileRequest.cs @@ -0,0 +1,5 @@ +using Cellm.Prompts; + +namespace Cellm.Models.Llamafile; + +internal record LlamafileRequest(Prompt Prompt) : IProviderRequest; \ No newline at end of file diff --git a/src/Cellm/Models/Llamafile/LlamafileClient.cs b/src/Cellm/Models/Llamafile/LlamafileRequestHandler.cs similarity index 85% rename from src/Cellm/Models/Llamafile/LlamafileClient.cs rename to src/Cellm/Models/Llamafile/LlamafileRequestHandler.cs index 09af2bf..8a6f5d7 100644 --- a/src/Cellm/Models/Llamafile/LlamafileClient.cs +++ b/src/Cellm/Models/Llamafile/LlamafileRequestHandler.cs @@ -2,17 +2,17 @@ using System.Net.NetworkInformation; using Cellm.AddIn; using Cellm.AddIn.Exceptions; -using Cellm.AddIn.Prompts; using Cellm.Models.OpenAi; +using MediatR; using Microsoft.Extensions.Options; namespace Cellm.Models.Llamafile; -internal class LlamafileClient : IClient +internal class LlamafileRequestHandler : IProviderRequestHandler { private record Llamafile(string ModelPath, Uri BaseAddress, Process Process); - private readonly AsyncLazy _llamafilePath; + private readonly AsyncLazy _llamafileExePath; private readonly Dictionary> _llamafiles; private readonly LLamafileProcessManager _llamafileProcessManager; @@ -20,26 +20,26 @@ private record Llamafile(string ModelPath, Uri BaseAddress, Process Process); private readonly LlamafileConfiguration _llamafileConfiguration; private readonly OpenAiConfiguration _openAiConfiguration; - private readonly IClient _openAiClient; + private readonly ISender _sender; private readonly HttpClient _httpClient; - public LlamafileClient(IOptions cellmConfiguration, + public LlamafileRequestHandler(IOptions cellmConfiguration, IOptions llamafileConfiguration, IOptions openAiConfiguration, - OpenAiClient openAiClient, + ISender sender, HttpClient httpClient, LLamafileProcessManager llamafileProcessManager) { _cellmConfiguration = cellmConfiguration.Value; _llamafileConfiguration = llamafileConfiguration.Value; _openAiConfiguration = openAiConfiguration.Value; - _openAiClient = openAiClient; + _sender = sender; _httpClient = httpClient; _llamafileProcessManager = llamafileProcessManager; - _llamafilePath = new AsyncLazy(async () => + _llamafileExePath = new AsyncLazy(async () => { - return await DownloadFile(_llamafileConfiguration.LlamafileUrl, "Llamafile.exe"); + return await DownloadFile(_llamafileConfiguration.LlamafileUrl, $"{nameof(Llamafile)}.exe"); }); _llamafiles = _llamafileConfiguration.Models.ToDictionary(x => x.Key, x => new AsyncLazy(async () => @@ -55,21 +55,19 @@ public LlamafileClient(IOptions cellmConfiguration, })); } - public async Task Send(Prompt prompt, string? provider, string? model, Uri? baseAddress) + public async Task Handle(LlamafileRequest request, CancellationToken cancellationToken) { // Download model and start Llamafile on first call - var llamafile = await _llamafiles[model ?? _llamafileConfiguration.DefaultModel]; + var llamafile = await _llamafiles[request.Prompt.Model ?? _llamafileConfiguration.DefaultModel]; - return await _openAiClient.Send( - prompt, - provider ?? "Llamafile", - model ?? _llamafileConfiguration.DefaultModel, - baseAddress ?? llamafile.BaseAddress); + var openAiResponse = await _sender.Send(new OpenAiRequest(request.Prompt, nameof(Llamafile), llamafile.BaseAddress), cancellationToken); + + return new LlamafileResponse(openAiResponse.Prompt); } private async Task StartProcess(string modelPath, Uri baseAddress) { - var processStartInfo = new ProcessStartInfo(await _llamafilePath); + var processStartInfo = new ProcessStartInfo(await _llamafileExePath); processStartInfo.Arguments += $"--server "; processStartInfo.Arguments += "--nobrowser "; @@ -149,7 +147,7 @@ private async Task WaitForLlamafile(Uri baseAddress, Process process) { if (process.HasExited) { - throw new CellmException($"Failed to run Llamafile. Exit code: {process.ExitCode}"); + throw new CellmException($"Failed to run Llamafile, process exited. Exit code: {process.ExitCode}"); } try @@ -175,7 +173,7 @@ private async Task WaitForLlamafile(Uri baseAddress, Process process) process.Kill(); - throw new CellmException("Timeout waiting for Llamafile server to start"); + throw new CellmException("Failed to run Llamafile, timeout waiting for Llamafile server to start"); } string CreateFilePath(string fileName) @@ -185,9 +183,9 @@ string CreateFilePath(string fileName) return filePath; } - private string CreateModelFileName(string modelName) + private static string CreateModelFileName(string modelName) { - return $"Llamafile-model-weights-{modelName}"; + return $"Llamafile-model-{modelName}"; } private Uri CreateBaseAddress() diff --git a/src/Cellm/Models/Llamafile/LlamafileResponse.cs b/src/Cellm/Models/Llamafile/LlamafileResponse.cs new file mode 100644 index 0000000..e90be81 --- /dev/null +++ b/src/Cellm/Models/Llamafile/LlamafileResponse.cs @@ -0,0 +1,5 @@ +using Cellm.Prompts; + +namespace Cellm.Models.Llamafile; + +internal record LlamafileResponse(Prompt Prompt) : IProviderResponse; \ No newline at end of file diff --git a/src/Cellm/Models/ModelRequestBehavior/CachingBehavior.cs b/src/Cellm/Models/ModelRequestBehavior/CachingBehavior.cs new file mode 100644 index 0000000..839633e --- /dev/null +++ b/src/Cellm/Models/ModelRequestBehavior/CachingBehavior.cs @@ -0,0 +1,32 @@ +using MediatR; + +namespace Cellm.Models.PipelineBehavior; + +internal class CachingBehavior : IPipelineBehavior + where TRequest : IModelRequest +{ + private readonly ICache _cache; + + public CachingBehavior(ICache cache) + { + _cache = cache; + } + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + if (_cache.TryGetValue(request, out object? value) && value is TResponse response) + { + return response; + } + + response = await next(); + + // Tool results depend on state external to prompt and should not be cached + if (!request.Prompt.Messages.Any(x => x.Role == Prompts.Roles.Tool)) + { + _cache.Set(request, response); + } + + return response; + } +} diff --git a/src/Cellm/Models/ModelRequestBehavior/SentryBehavior.cs b/src/Cellm/Models/ModelRequestBehavior/SentryBehavior.cs new file mode 100644 index 0000000..e4d93b4 --- /dev/null +++ b/src/Cellm/Models/ModelRequestBehavior/SentryBehavior.cs @@ -0,0 +1,22 @@ +using MediatR; + +namespace Cellm.Models.PipelineBehavior; + +internal class SentryBehavior : IPipelineBehavior + where TRequest : notnull +{ + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + var transaction = SentrySdk.StartTransaction(typeof(TRequest).Name, typeof(TRequest).Name); + SentrySdk.ConfigureScope(scope => scope.Transaction = transaction); + + try + { + return await next(); + } + finally + { + transaction.Finish(); + } + } +} diff --git a/src/Cellm/Models/ModelRequestBehavior/ToolBehavior.cs b/src/Cellm/Models/ModelRequestBehavior/ToolBehavior.cs new file mode 100644 index 0000000..6501263 --- /dev/null +++ b/src/Cellm/Models/ModelRequestBehavior/ToolBehavior.cs @@ -0,0 +1,46 @@ +using Cellm.Models.OpenAi; +using Cellm.Prompts; +using Cellm.Tools; +using MediatR; + +namespace Cellm.Models.PipelineBehavior; + +internal class ToolBehavior : IPipelineBehavior + where TRequest : IModelRequest + where TResponse : IModelResponse +{ + private readonly ISender _sender; + private readonly ITools _tools; + + public ToolBehavior(ISender sender, ITools tools) + { + _sender = sender; + _tools = tools; + } + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + var response = await next(); + + var toolCalls = response.Prompt.Messages.LastOrDefault()?.ToolCalls; + + if (toolCalls is not null) + { + // Model called tools, run tools and call model again + request.Prompt.Messages.Add(await RunTools(toolCalls)); + response = await _sender.Send(request, cancellationToken); + } + + return response; + } + + private async Task RunTools(List toolCalls) + { + var toolResults = await Task.WhenAll(toolCalls.Select(x => _tools.Run(x))); + var toolCallsWithResults = toolCalls + .Zip(toolResults, (toolCall, toolResult) => toolCall with { Result = toolResult }) + .ToList(); + + return new Message(string.Empty, Roles.Tool, toolCallsWithResults); + } +} diff --git a/src/Cellm/Models/OpenAi/Extensions.cs b/src/Cellm/Models/OpenAi/Extensions.cs new file mode 100644 index 0000000..9d4f9c6 --- /dev/null +++ b/src/Cellm/Models/OpenAi/Extensions.cs @@ -0,0 +1,50 @@ +using Cellm.AddIn.Exceptions; +using Cellm.Models.OpenAi.Models; +using Cellm.Prompts; +using Cellm.Tools; + +namespace Cellm.Models.OpenAi; + +internal static class Extensions +{ + public static List ToOpenAiMessages(this Prompt prompt) + { + return prompt.Messages.SelectMany(x => ToOpenAiMessage(x)).ToList(); + } + + private static List ToOpenAiMessage(Message message) + { + return message.Role switch + { + Roles.Tool => ToOpenAiToolResults(message), + _ => new List() + { + new OpenAiMessage( + message.Role.ToString().ToLower(), + message.Content, + message.ToolCalls? + .Select(x => new OpenAiToolCall(x.Id, "function", new OpenAiFunctionCall(x.Name, x.Arguments))) + .ToList()) + } + }; + } + + private static List ToOpenAiToolResults(Message message) + { + var toolCalls = message?.ToolCalls ?? throw new CellmException("No tool calls in tool message"); + return toolCalls + .Select(x => new OpenAiMessage( + Roles.Tool.ToString().ToLower(), + $"Tool: {x.Name}, Arguments: {x.Arguments}, Result: {x.Result}", + null, + x.Id) + ).ToList(); + } + + public static List ToOpenAiTools(this ITools tools) + { + return tools.GetTools() + .Select(x => new OpenAiTool("function", new OpenAiFunction(x.Name, x.Description, x.Parameters))) + .ToList(); + } +} diff --git a/src/Cellm/Models/OpenAi/Models.cs b/src/Cellm/Models/OpenAi/Models.cs new file mode 100644 index 0000000..fef02a8 --- /dev/null +++ b/src/Cellm/Models/OpenAi/Models.cs @@ -0,0 +1,53 @@ +using System.Text.Json; + +namespace Cellm.Models.OpenAi.Models; + +public record OpenAiChatCompletionRequest( + string Model, + List Messages, + int MaxTokens, + double Temperature, + List? Tools = null, + string? ToolChoice = null); + +public record OpenAiChatCompletionResponse( + string Id, + string Object, + long Created, + string Model, + List Choices, + OpenAiUsage? Usage = null); + +public record OpenAiMessage( + string Role, + string Content, + List? ToolCalls = null, + string? ToolCallId = null); + +public record OpenAiTool( + string Type, + OpenAiFunction Function); + +public record OpenAiFunction( + string Name, + string Description, + JsonDocument Parameters); + +public record OpenAiChoice( + int Index, + OpenAiMessage Message, + string FinishReason); + +public record OpenAiToolCall( + string Id, + string Type, + OpenAiFunctionCall Function); + +public record OpenAiFunctionCall( + string Name, + string Arguments); + +public record OpenAiUsage( + int PromptTokens, + int CompletionTokens, + int TotalTokens); diff --git a/src/Cellm/Models/OpenAi/OpenAiClient.cs b/src/Cellm/Models/OpenAi/OpenAiClient.cs deleted file mode 100644 index b1a4c88..0000000 --- a/src/Cellm/Models/OpenAi/OpenAiClient.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System.Net.Http; -using System.Text; -using System.Text.Json.Serialization; -using Cellm.AddIn; -using Cellm.AddIn.Exceptions; -using Cellm.AddIn.Prompts; -using Microsoft.Extensions.Options; - -namespace Cellm.Models.OpenAi; - -internal class OpenAiClient : IClient -{ - private readonly OpenAiConfiguration _openAiConfiguration; - private readonly CellmConfiguration _cellmConfiguration; - private readonly HttpClient _httpClient; - private readonly ICache _cache; - private readonly ISerde _serde; - - public OpenAiClient( - IOptions openAiConfiguration, - IOptions cellmConfiguration, - HttpClient httpClient, - ICache cache, - ISerde serde) - { - _openAiConfiguration = openAiConfiguration.Value; - _cellmConfiguration = cellmConfiguration.Value; - _httpClient = httpClient; - _cache = cache; - _serde = serde; - } - - public async Task Send(Prompt prompt, string? provider, string? model, Uri? baseAddress) - { - var transaction = SentrySdk.StartTransaction(typeof(OpenAiClient).Name, nameof(Send)); - SentrySdk.ConfigureScope(scope => scope.Transaction = transaction); - - var openAiPrompt = new PromptBuilder() - .SetSystemMessage(prompt.SystemMessage) - .AddSystemMessage() - .AddMessages(prompt.Messages) - .SetTemperature(prompt.Temperature) - .Build(); - - var requestBody = new RequestBody - { - Model = model ?? _openAiConfiguration.DefaultModel, - Messages = openAiPrompt.Messages.Select(x => new Message { Content = x.Content, Role = x.Role.ToString().ToLower() }).ToList(), - MaxTokens = _cellmConfiguration.MaxTokens, - Temperature = prompt.Temperature - }; - - if (_cache.TryGetValue(requestBody, out object? value) && value is Prompt assistantPrompt) - { - return assistantPrompt; - } - - var json = _serde.Serialize(requestBody); - var jsonAsString = new StringContent(json, Encoding.UTF8, "application/json"); - - const string path = "/v1/chat/completions"; - var address = baseAddress is null ? new Uri(path, UriKind.Relative) : new Uri(baseAddress, path); - - var response = await _httpClient.PostAsync(address, jsonAsString); - var responseBodyAsString = await response.Content.ReadAsStringAsync(); - - if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException(responseBodyAsString, null, response.StatusCode); - } - - var responseBody = _serde.Deserialize(responseBodyAsString); - var assistantMessage = responseBody?.Choices?.FirstOrDefault()?.Message?.Content ?? throw new CellmException("#EMPTY_RESPONSE?"); - - if (assistantMessage.StartsWith("#INSTRUCTION_ERROR?")) - { - throw new CellmException(assistantMessage); - } - - assistantPrompt = new PromptBuilder(prompt) - .AddAssistantMessage(assistantMessage) - .Build(); - - _cache.Set(requestBody, assistantPrompt); - - var inputTokens = responseBody?.Usage?.PromptTokens ?? -1; - if (inputTokens > 0) - { - SentrySdk.Metrics.Distribution("InputTokens", - inputTokens, - unit: MeasurementUnit.Custom("token"), - tags: new Dictionary { - { nameof(provider), provider?.ToLower() ?? _cellmConfiguration.DefaultProvider }, - { nameof(model), model?.ToLower() ?? _openAiConfiguration.DefaultModel }, - { nameof(_httpClient.BaseAddress), _httpClient.BaseAddress?.ToString() ?? string.Empty } - } - ); - } - - var outputTokens = responseBody?.Usage?.CompletionTokens ?? -1; - if (outputTokens > 0) - { - SentrySdk.Metrics.Distribution("OutputTokens", - outputTokens, - unit: MeasurementUnit.Custom("token"), - tags: new Dictionary - { - { nameof(provider), provider?.ToLower() ?? _cellmConfiguration.DefaultProvider }, - { nameof(model), model?.ToLower() ?? _openAiConfiguration.DefaultModel }, - { nameof(_httpClient.BaseAddress), _httpClient.BaseAddress?.ToString() ?? string.Empty } - } - ); - } - - transaction.Finish(); - - return assistantPrompt; - } - - private class RequestBody - { - public string? Model { get; set; } - - public List? Messages { get; set; } - - [JsonPropertyName("max_tokens")] - public int MaxTokens { get; set; } - - public double Temperature { get; set; } - } - - private class ResponseBody - { - public string? Id { get; set; } - - public string? Object { get; set; } - public long Created { get; set; } - public string? Model { get; set; } - - [JsonPropertyName("system_fingerprint")] - public string? SystemFingerprint { get; set; } - - public List? Choices { get; set; } - - public Usage? Usage { get; set; } - } - - private class Message - { - public string? Role { get; set; } - - public string? Content { get; set; } - } - - private class Choice - { - public int Index { get; set; } - - public Message? Message { get; set; } - - public object? Logprobs { get; set; } - - [JsonPropertyName("finish_reason")] - public string? FinishReason { get; set; } - } - - private class Usage - { - [JsonPropertyName("prompt_tokens")] - public int PromptTokens { get; set; } - - [JsonPropertyName("completion_tokens")] - public int CompletionTokens { get; set; } - - [JsonPropertyName("total_tokens")] - public int TotalTokens { get; set; } - } -} diff --git a/src/Cellm/Models/OpenAi/OpenAiConfiguration.cs b/src/Cellm/Models/OpenAi/OpenAiConfiguration.cs index e165191..5b04f6d 100644 --- a/src/Cellm/Models/OpenAi/OpenAiConfiguration.cs +++ b/src/Cellm/Models/OpenAi/OpenAiConfiguration.cs @@ -10,10 +10,13 @@ internal class OpenAiConfiguration : IProviderConfiguration public string ApiKey { get; init; } + public bool EnableTools { get; init; } + public OpenAiConfiguration() { BaseAddress = default!; DefaultModel = default!; ApiKey = default!; + EnableTools = default; } -} \ No newline at end of file +} diff --git a/src/Cellm/Models/OpenAi/OpenAiRequest.cs b/src/Cellm/Models/OpenAi/OpenAiRequest.cs new file mode 100644 index 0000000..ab4653c --- /dev/null +++ b/src/Cellm/Models/OpenAi/OpenAiRequest.cs @@ -0,0 +1,5 @@ +using Cellm.Prompts; + +namespace Cellm.Models.OpenAi; + +internal record OpenAiRequest(Prompt Prompt, string? Provider, Uri? BaseAddress) : IModelRequest; diff --git a/src/Cellm/Models/OpenAi/OpenAiRequestHandler.cs b/src/Cellm/Models/OpenAi/OpenAiRequestHandler.cs new file mode 100644 index 0000000..54f6ccc --- /dev/null +++ b/src/Cellm/Models/OpenAi/OpenAiRequestHandler.cs @@ -0,0 +1,124 @@ +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Cellm.AddIn; +using Cellm.AddIn.Exceptions; +using Cellm.Models.OpenAi.Models; +using Cellm.Prompts; +using Cellm.Tools; +using Microsoft.Extensions.Options; + +namespace Cellm.Models.OpenAi; + +internal class OpenAiRequestHandler : IModelRequestHandler +{ + private readonly OpenAiConfiguration _openAiConfiguration; + private readonly CellmConfiguration _cellmConfiguration; + private readonly HttpClient _httpClient; + private readonly ITools _tools; + private readonly ISerde _serde; + + public OpenAiRequestHandler( + IOptions openAiConfiguration, + IOptions cellmConfiguration, + HttpClient httpClient, + ITools tools, + ISerde serde) + { + _openAiConfiguration = openAiConfiguration.Value; + _cellmConfiguration = cellmConfiguration.Value; + _httpClient = httpClient; + _tools = tools; + _serde = serde; + } + + public async Task Handle(OpenAiRequest request, CancellationToken cancellationToken) + { + const string path = "/v1/chat/completions"; + var address = request.BaseAddress is null ? new Uri(path, UriKind.Relative) : new Uri(request.BaseAddress, path); + + var json = Serialize(request); + var jsonAsStringContent = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync(address, jsonAsStringContent, cancellationToken); + var responseBodyAsString = await response.Content.ReadAsStringAsync(cancellationToken); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"{nameof(OpenAiRequest)} failed: {responseBodyAsString}", null, response.StatusCode); + } + + return Deserialize(request, responseBodyAsString); + } + + public string Serialize(OpenAiRequest request) + { + var openAiPrompt = new PromptBuilder(request.Prompt) + .AddSystemMessage() + .Build(); + + var chatCompletionRequest = new OpenAiChatCompletionRequest( + openAiPrompt.Model, + openAiPrompt.ToOpenAiMessages(), + _cellmConfiguration.MaxOutputTokens, + openAiPrompt.Temperature, + _tools.ToOpenAiTools(), + "auto"); + + return _serde.Serialize(chatCompletionRequest, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + } + + public OpenAiResponse Deserialize(OpenAiRequest request, string responseBodyAsString) + { + var responseBody = _serde.Deserialize(responseBodyAsString, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + + var tags = new Dictionary { + { nameof(request.Provider), request.Provider?.ToLower() ?? _cellmConfiguration.DefaultProvider }, + { nameof(request.Prompt.Model), request.Prompt.Model ?.ToLower() ?? _openAiConfiguration.DefaultModel }, + { nameof(_httpClient.BaseAddress), _httpClient.BaseAddress?.ToString() ?? string.Empty } + }; + + var inputTokens = responseBody?.Usage?.PromptTokens ?? -1; + if (inputTokens > 0) + { + SentrySdk.Metrics.Distribution("InputTokens", + inputTokens, + unit: MeasurementUnit.Custom("token"), + tags); + } + + var outputTokens = responseBody?.Usage?.CompletionTokens ?? -1; + if (outputTokens > 0) + { + SentrySdk.Metrics.Distribution("OutputTokens", + outputTokens, + unit: MeasurementUnit.Custom("token"), + tags); + } + + var choice = responseBody?.Choices?.FirstOrDefault() ?? throw new CellmException("Empty response from OpenAI API"); + var toolCalls = choice.Message.ToolCalls? + .Select(x => new ToolCall(x.Id, x.Function.Name, x.Function.Arguments, null)) + .ToList(); + + var content = choice.Message.Content; + var message = new Message(content, Roles.Assistant, toolCalls); + + var prompt = new PromptBuilder(request.Prompt) + .AddMessage(message) + .Build(); + + return new OpenAiResponse(prompt); + } +} diff --git a/src/Cellm/Models/OpenAi/OpenAiResponse.cs b/src/Cellm/Models/OpenAi/OpenAiResponse.cs new file mode 100644 index 0000000..22ade00 --- /dev/null +++ b/src/Cellm/Models/OpenAi/OpenAiResponse.cs @@ -0,0 +1,5 @@ +using Cellm.Prompts; + +namespace Cellm.Models.OpenAi; + +internal record OpenAiResponse(Prompt Prompt) : IModelResponse; diff --git a/src/Cellm/Models/Serde.cs b/src/Cellm/Models/Serde.cs index ce7c6ed..aa95262 100644 --- a/src/Cellm/Models/Serde.cs +++ b/src/Cellm/Models/Serde.cs @@ -1,5 +1,6 @@ using System.Text.Encodings.Web; using System.Text.Json; +using System.Text.Json.Serialization; using Cellm.AddIn.Exceptions; namespace Cellm.Models; @@ -8,17 +9,17 @@ internal class Serde : ISerde { private readonly JsonSerializerOptions _defaultOptions = new() { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; - public string Serialize(TValue value, JsonSerializerOptions? options = null) + public string Serialize(TSerialize value, JsonSerializerOptions? options = null) { return JsonSerializer.Serialize(value, options ?? _defaultOptions); } - public TValue Deserialize(string value, JsonSerializerOptions? options = null) + public TDeserialize Deserialize(string value, JsonSerializerOptions? options = null) { - return JsonSerializer.Deserialize(value, options ?? _defaultOptions) ?? throw new CellmException("Failed to deserialize responds"); + return JsonSerializer.Deserialize(value, options ?? _defaultOptions) ?? throw new CellmException($"Failed to deserialize {value} to {typeof(TDeserialize).Name}"); } } diff --git a/src/Cellm/Prompts/Message.cs b/src/Cellm/Prompts/Message.cs new file mode 100644 index 0000000..3870e1b --- /dev/null +++ b/src/Cellm/Prompts/Message.cs @@ -0,0 +1,3 @@ +namespace Cellm.Prompts; + +public record Message(string Content, Roles Role, List? ToolCalls = null); \ No newline at end of file diff --git a/src/Cellm/Prompts/Prompt.cs b/src/Cellm/Prompts/Prompt.cs new file mode 100644 index 0000000..9868746 --- /dev/null +++ b/src/Cellm/Prompts/Prompt.cs @@ -0,0 +1,3 @@ +namespace Cellm.Prompts; + +public record Prompt(string Model, string SystemMessage, List Messages, double Temperature, List Tools); diff --git a/src/Cellm/AddIn/Prompts/PromptBuilder.cs b/src/Cellm/Prompts/PromptBuilder.cs similarity index 58% rename from src/Cellm/AddIn/Prompts/PromptBuilder.cs rename to src/Cellm/Prompts/PromptBuilder.cs index 68b2701..b676a05 100644 --- a/src/Cellm/AddIn/Prompts/PromptBuilder.cs +++ b/src/Cellm/Prompts/PromptBuilder.cs @@ -1,12 +1,14 @@ using Cellm.AddIn.Exceptions; -namespace Cellm.AddIn.Prompts; +namespace Cellm.Prompts; public class PromptBuilder { + private string? _model; private string? _systemMessage; - private readonly List _messages = new(); + private List _messages = new(); private double? _temperature; + private List _tools = new(); public PromptBuilder() { @@ -14,11 +16,18 @@ public PromptBuilder() public PromptBuilder(Prompt prompt) { + _model = prompt.Model; _systemMessage = prompt.SystemMessage; _messages = prompt.Messages; _temperature = prompt.Temperature; } + public PromptBuilder SetModel(string model) + { + _model = model; + return this; + } + public PromptBuilder SetSystemMessage(string systemMessage) { _systemMessage = systemMessage; @@ -38,40 +47,53 @@ public PromptBuilder AddSystemMessage() throw new CellmException("Cannot add empty system message"); } - _messages.Add(new Message(_systemMessage!, Role.System)); + _messages.Insert(0, new Message(_systemMessage!, Roles.System)); return this; } public PromptBuilder AddSystemMessage(string content) { - _messages.Add(new Message(content, Role.System)); + _messages.Add(new Message(content, Roles.System)); return this; } public PromptBuilder AddUserMessage(string content) { - _messages.Add(new Message(content, Role.User)); + _messages.Add(new Message(content, Roles.User)); return this; } - public PromptBuilder AddAssistantMessage(string content) + public PromptBuilder AddAssistantMessage(string content, List? toolCalls = null) { - _messages.Add(new Message(content, Role.Assistant)); + _messages.Add(new Message(content, Roles.Assistant, toolCalls)); return this; } + public PromptBuilder AddMessage(Message message) + { + return AddMessages(new List { message }); + } + public PromptBuilder AddMessages(List messages) { _messages.AddRange(messages); return this; } + public PromptBuilder AddTools(List tools) + { + _tools = tools; + return this; + } + public Prompt Build() { return new Prompt( + _model ?? throw new ArgumentNullException(nameof(_model)), _systemMessage ?? string.Empty, _messages, - _temperature ?? throw new ArgumentNullException(nameof(_temperature)) + _temperature ?? throw new ArgumentNullException(nameof(_temperature)), + _tools ); } } diff --git a/src/Cellm/Prompts/Roles.cs b/src/Cellm/Prompts/Roles.cs new file mode 100644 index 0000000..9746734 --- /dev/null +++ b/src/Cellm/Prompts/Roles.cs @@ -0,0 +1,9 @@ +namespace Cellm.Prompts; + +public enum Roles +{ + System, + User, + Assistant, + Tool +} diff --git a/src/Cellm/Prompts/Tool.cs b/src/Cellm/Prompts/Tool.cs new file mode 100644 index 0000000..04446ea --- /dev/null +++ b/src/Cellm/Prompts/Tool.cs @@ -0,0 +1,5 @@ +using System.Text.Json; + +namespace Cellm.Prompts; + +public record Tool(string Name, string Description, JsonDocument Parameters); diff --git a/src/Cellm/Prompts/ToolCall.cs b/src/Cellm/Prompts/ToolCall.cs new file mode 100644 index 0000000..c0ed06a --- /dev/null +++ b/src/Cellm/Prompts/ToolCall.cs @@ -0,0 +1,3 @@ +namespace Cellm.Prompts; + +public record ToolCall(string Id, string Name, string Arguments, string? Result); \ No newline at end of file diff --git a/src/Cellm/Services/ServiceLocator.cs b/src/Cellm/Services/ServiceLocator.cs index 4373ca8..4ffd3ee 100644 --- a/src/Cellm/Services/ServiceLocator.cs +++ b/src/Cellm/Services/ServiceLocator.cs @@ -1,12 +1,16 @@ -using Cellm.AddIn; +using System.Reflection; +using Cellm.AddIn; using Cellm.AddIn.Exceptions; using Cellm.Models; using Cellm.Models.Anthropic; using Cellm.Models.GoogleAi; using Cellm.Models.Llamafile; using Cellm.Models.OpenAi; +using Cellm.Models.PipelineBehavior; using Cellm.Services.Configuration; +using Cellm.Tools; using ExcelDna.Integration; +using MediatR; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -81,10 +85,12 @@ private static IServiceCollection ConfigureServices(IServiceCollection services) // Internals services .AddSingleton(configuration) + .AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) + .AddTransient(typeof(IPipelineBehavior<,>), typeof(ToolBehavior<,>)) .AddMemoryCache() .AddTransient() - .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(); @@ -105,31 +111,31 @@ private static IServiceCollection ConfigureServices(IServiceCollection services) var anthropicConfiguration = configuration.GetRequiredSection(nameof(AnthropicConfiguration)).Get() ?? throw new NullReferenceException(nameof(AnthropicConfiguration)); - services.AddHttpClient(anthropicHttpClient => + services.AddHttpClient, AnthropicRequestHandler>(anthropicHttpClient => { anthropicHttpClient.BaseAddress = anthropicConfiguration.BaseAddress; anthropicHttpClient.DefaultRequestHeaders.Add("x-api-key", anthropicConfiguration.ApiKey); anthropicHttpClient.DefaultRequestHeaders.Add("anthropic-version", anthropicConfiguration.Version); - }).AddResilienceHandler($"{nameof(AnthropicClient)}ResiliencePipeline", resiliencePipelineConfigurator.ConfigureResiliencePipeline); + }).AddResilienceHandler($"{nameof(AnthropicRequestHandler)}ResiliencePipeline", resiliencePipelineConfigurator.ConfigureResiliencePipeline); var googleAiConfiguration = configuration.GetRequiredSection(nameof(GoogleAiConfiguration)).Get() ?? throw new NullReferenceException(nameof(GoogleAiConfiguration)); - services.AddHttpClient(googleHttpClient => + services.AddHttpClient, GoogleAiRequestHandler>(googleHttpClient => { googleHttpClient.BaseAddress = googleAiConfiguration.BaseAddress; - }).AddResilienceHandler($"{nameof(GoogleAiClient)}ResiliencePipeline", resiliencePipelineConfigurator.ConfigureResiliencePipeline); + }).AddResilienceHandler($"{nameof(GoogleAiRequestHandler)}ResiliencePipeline", resiliencePipelineConfigurator.ConfigureResiliencePipeline); var openAiConfiguration = configuration.GetRequiredSection(nameof(OpenAiConfiguration)).Get() ?? throw new NullReferenceException(nameof(OpenAiConfiguration)); - services.AddHttpClient(openAiHttpClient => + services.AddHttpClient, OpenAiRequestHandler>(openAiHttpClient => { openAiHttpClient.BaseAddress = openAiConfiguration.BaseAddress; openAiHttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {openAiConfiguration.ApiKey}"); - }).AddResilienceHandler($"{nameof(OpenAiClient)}ResiliencePipeline", resiliencePipelineConfigurator.ConfigureResiliencePipeline); + }).AddResilienceHandler($"{nameof(OpenAiRequestHandler)}ResiliencePipeline", resiliencePipelineConfigurator.ConfigureResiliencePipeline); - services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Cellm/Tools/Glob.cs b/src/Cellm/Tools/Glob.cs new file mode 100644 index 0000000..ea835d4 --- /dev/null +++ b/src/Cellm/Tools/Glob.cs @@ -0,0 +1,48 @@ +using System.ComponentModel; +using MediatR; +using Microsoft.Extensions.FileSystemGlobbing; + +namespace Cellm.Tools; + +//internal record GlobRequest( +// [Description("The root directory to start the glob search from")] string RootPath, +// [Description("List of patterns to include in the search")] List IncludePatterns, +// [Description("Optional list of patterns to exclude from the search")] List? ExcludePatterns) : IRequest; + +internal record GlobRequest : IRequest +{ + public GlobRequest(string rootPath, List includePatterns, List? excludePatterns = null) + { + RootPath = rootPath; + IncludePatterns = includePatterns; + ExcludePatterns = excludePatterns; + } + + [Description("The root directory to start the glob search from")] + public string RootPath { get; set; } + + [Description("List of patterns to include in the search")] + public List IncludePatterns { get; set; } + + [Description("Optional list of patterns to exclude from the search")] + public List? ExcludePatterns { get; set; } +} + +internal record GlobResponse( + [Description("List of file paths matching the glob patterns")] List FilePaths); + +[Description("Search for files on the user's disk using glob patterns. Useful when user asks you to find files.")] +internal class Glob : IRequestHandler +{ + public Task Handle(GlobRequest request, CancellationToken cancellationToken) + { + var matcher = new Matcher(); + matcher.AddIncludePatterns(request.IncludePatterns); + matcher.AddExcludePatterns(request.ExcludePatterns ?? new List()); + var fileNames = matcher.GetResultsInFullPath(request.RootPath); + + return Task.FromResult(new GlobResponse(fileNames.ToList())); + } +} + +// https://medium.com/@kmorpex/quick-guide-mediatr-in-net-8-e3e2730bcc08 diff --git a/src/Cellm/Tools/ITools.cs b/src/Cellm/Tools/ITools.cs new file mode 100644 index 0000000..d7c8e94 --- /dev/null +++ b/src/Cellm/Tools/ITools.cs @@ -0,0 +1,10 @@ +using Cellm.Prompts; + +namespace Cellm.Tools; + +internal interface ITools +{ + public List GetTools(); + + public Task Run(ToolCall toolRequest); +} diff --git a/src/Cellm/Tools/Tools.cs b/src/Cellm/Tools/Tools.cs new file mode 100644 index 0000000..ea6248b --- /dev/null +++ b/src/Cellm/Tools/Tools.cs @@ -0,0 +1,73 @@ +using System.ComponentModel; +using System.Reflection; +using System.Text.Json; +using Cellm.AddIn.Exceptions; +using Cellm.Models; +using Cellm.Prompts; +using Json.More; +using Json.Patch; +using Json.Pointer; +using Json.Schema; +using Json.Schema.Generation; +using MediatR; + +namespace Cellm.Tools; + +internal class Tools : ITools +{ + private readonly ISender _sender; + private readonly ISerde _serde; + + public Tools(ISender sender, ISerde serde) + { + _sender = sender; + _serde = serde; + } + + public async Task Run(ToolCall toolCall) + { + var globRequest = _serde.Deserialize(toolCall.Arguments); + + return toolCall.Name switch + { + "Glob" => await RunGlob(globRequest), + _ => throw new ArgumentException($"Unsupported tool: {toolCall.Name}") + }; + } + + private async Task RunGlob(GlobRequest request) + { + var response = await _sender.Send(request); + return _serde.Serialize(response); + } + + public List GetTools() + { + // https://til.cazzulino.com/dotnet/how-to-emit-descriptions-for-exported-json-schema-using-jsonschemaexporter + var builder = new JsonSchemaBuilder() + .FromType() + .Required("RootPath", "IncludePatterns"); + + var schema = builder.Build(); + var jsonDocument = schema.ToJsonDocument(); + + var patchOperations = typeof(GlobRequest) + .GetProperties() + .Select(x => PatchOperation.Add(JsonPointer.Parse($"/properties/{x.Name}/description"), x.GetCustomAttribute()?.Description)) + .ToList(); + + var patchDescriptions = new JsonPatch(patchOperations); + var jsonDocumentWithPatchedDescriptions = patchDescriptions.Apply(jsonDocument) ?? throw new CellmException(); + + var classDescription = typeof(Glob).GetCustomAttribute()?.Description ?? throw new CellmException(); + + return new List() + { + new Tool( + nameof(Glob), + classDescription, + jsonDocumentWithPatchedDescriptions + ) + }; + } +} diff --git a/src/Cellm/appsettings.Local.Anthropic.json b/src/Cellm/appsettings.Local.Anthropic.json index 3b394a2..c1acab4 100644 --- a/src/Cellm/appsettings.Local.Anthropic.json +++ b/src/Cellm/appsettings.Local.Anthropic.json @@ -1,6 +1,6 @@ { "AnthropicConfiguration": { - "DefaultModel": "claude-3-haiku-20240307", + "DefaultModel": "claude-3-5-sonnet-20240620", "ApiKey": "YOUR_ANTHROPIC_APIKEY" }, "CellmConfiguration": { diff --git a/src/Cellm/appsettings.json b/src/Cellm/appsettings.json index ccf1cbd..066e5f4 100644 --- a/src/Cellm/appsettings.json +++ b/src/Cellm/appsettings.json @@ -13,9 +13,9 @@ "DefaultModel": "gpt-4o-mini" }, "CellmConfiguration": { - "DefaultProvider": "Anthropic", + "DefaultProvider": "OpenAI", "DefaultTemperature": 0, - "MaxTokens": 256, + "MaxOutputTokens": 256, "CacheTimeoutInSeconds": 300 }, "RateLimiterConfiguration": { diff --git a/src/Cellm/packages.lock.json b/src/Cellm/packages.lock.json index 6c39668..eb8092a 100644 --- a/src/Cellm/packages.lock.json +++ b/src/Cellm/packages.lock.json @@ -2,495 +2,560 @@ "version": 1, "dependencies": { "net6.0-windows7.0": { - "ExcelDna.AddIn": { - "type": "Direct", - "requested": "[1.8.0, )", - "resolved": "1.8.0", - "contentHash": "Q+NcNeuzIkE6d21Sc0NGBpfKClKO8QU2ozR/EZ9RE+WjCxwwOUWXPRLev8+DpOCwkUjXtBIKjE7+xxHTac3PHA==", - "dependencies": { - "ExcelDna.Integration": "[1.8.0]" - } - }, - "ExcelDna.Interop": { - "type": "Direct", - "requested": "[15.0.1, )", - "resolved": "15.0.1", - "contentHash": "CIKPXlw+6C6v2Em8DVFgkTsrMZoz978CtSG722+dwHi3Tt7JTxQ2BmmBH4yJuzcA9EaOC5AMEmioLkLsvhESQw==" - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Configuration": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Configuration.Json": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==", - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "System.Text.Json": "8.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.Http": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" - } - }, - "Microsoft.Extensions.Http.Resilience": { - "type": "Direct", - "requested": "[8.8.0, )", - "resolved": "8.8.0", - "contentHash": "A1endJCDOjxF095DrWQhchzwNBFLPiNcerh2x8sbiFbUXQfLaOTeaUfgp9iTMFsSpwhAD3qcSEMMsgFlJ0tDxg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "8.0.2", - "Microsoft.Extensions.Http.Diagnostics": "8.8.0", - "Microsoft.Extensions.ObjectPool": "8.0.8", - "Microsoft.Extensions.Resilience": "8.8.0" - } - }, - "Microsoft.Extensions.Logging.Console": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "e+48o7DztoYog+PY430lPxrM4mm3PbA6qucvQtUDDwVo4MO+ejMw7YGc/o2rnxbxj4isPxdfKFzTxvXMwAz83A==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Configuration": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Json": "8.0.0" - } - }, - "Microsoft.Extensions.Logging.Debug": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "dt0x21qBdudHLW/bjMJpkixv858RRr8eSomgVbU8qljOyfrfDGi1JQvpF9w8S7ziRPtRKisuWaOwFxJM82GxeA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.Options": { - "type": "Direct", - "requested": "[8.0.2, )", - "resolved": "8.0.2", - "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Sentry.Extensions.Logging": { - "type": "Direct", - "requested": "[4.10.2, )", - "resolved": "4.10.2", - "contentHash": "1F86+Ly1+9I7WSCdwAWg14GCuBwYgCGOf4x2BiDMxEsYw7FCAmCjfjA+AVEEim4efVtdMuldvH7Ux1K9tRvH8w==", - "dependencies": { - "Microsoft.Extensions.Http": "6.0.0", - "Microsoft.Extensions.Logging.Configuration": "6.0.0", - "Sentry": "4.10.2" - } - }, - "Sentry.Profiling": { - "type": "Direct", - "requested": "[4.10.2, )", - "resolved": "4.10.2", - "contentHash": "OgFfG7nnF6iENkq9AqDPn5T1H1/aPbo4zj990YdSddfrE/A5PCpU8cDKZ/JT2oWokb6igtQgpJHjFxAeZM9NBQ==", - "dependencies": { - "Microsoft.Diagnostics.NETCore.Client": "0.2.510501", - "Sentry": "4.10.2" - } - }, - "ExcelDna.Integration": { - "type": "Transitive", - "resolved": "1.8.0", - "contentHash": "Z7UYuY291cTxc8lEESlUT73iI28yZ9LeJnvhPIRpZHBlrjbCN8lnEWvSmUqmUn1lBNGk6p2n0GlPuBhopJuksA==" - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==" - }, - "Microsoft.Bcl.TimeProvider": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "C7kWHJnMRY7EvJev2S8+yJHZ1y7A4ZlLbA4NE+O23BDIAN5mHeqND1m+SKv1ChRS5YlCDW7yAMUe7lttRsJaAA==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "8.0.0" - } - }, - "Microsoft.Diagnostics.NETCore.Client": { - "type": "Transitive", - "resolved": "0.2.510501", - "contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==", - "dependencies": { - "Microsoft.Extensions.Logging": "6.0.0" - } - }, - "Microsoft.Extensions.AmbientMetadata.Application": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "R/MVBReInWAHzUiuj+JCzwOryxIuLYLEFRSU9LX+R7hcFg7WySiqmAIj9aa4JSsNm6EkEpxTXCxTB4HeIT0m8A==", - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" - } - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Compliance.Abstractions": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "v5vsjDXh/GceR9k8qglsvEJeW4YC7E9Cd7nokr8FcJ6CG5APKxDhEn4zeUsNEsFWRKOSoi96r2tUnf5nGbScFg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", - "Microsoft.Extensions.ObjectPool": "8.0.8" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "8.0.2", - "contentHash": "7IQhGK+wjyGrNsPBjJcZwWAr+Wf6D4+TwOptUt77bWtgNkiV8tDEbhFS+dDamtQFZ2X7kWG9m71iZQRj2x3zgQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.Configuration.FileExtensions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==", - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Physical": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" - }, - "Microsoft.Extensions.DependencyInjection.AutoActivation": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "DBuKYUr3lvl1jFYPoNIIiXS9dcs3Xxp0HXhPj1xLYp1XFueCZMeiN/RDC1vWdQCK15aFAMZxuxEBBJQD7LxY+g==", - "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.Diagnostics": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" - } - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "System.Diagnostics.DiagnosticSource": "8.0.0" - } - }, - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "K9jlnvIbkYvHRAwxl8WjFkIU7D/5PYAeVmDMAN6Y4IQyWyBUT/xMotYwYGRFAO2kiPPJhIIj7a9VI2p7ydakkg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", - "System.Collections.Immutable": "8.0.0" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.FileProviders.Physical": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.FileSystemGlobbing": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.FileSystemGlobbing": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.Http.Diagnostics": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "N2o2E+SDU3Lu7RD8U2Nw3G2WPEPGx2nBNAueqm3nQPnWWGzFlkE6eRqGp1Lwvey8aiFnPbJ0rHoiM1m+RCt/Bg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.AutoActivation": "8.8.0", - "Microsoft.Extensions.Http": "8.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", - "Microsoft.Extensions.Telemetry": "8.8.0", - "Microsoft.IO.RecyclableMemoryStream": "3.0.0" - } - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1" - } - }, - "Microsoft.Extensions.Logging.Configuration": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==", - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "8.0.8", - "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "Microsoft.Extensions.Resilience": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "Lzd/CKTv8KrudJCD7yD0tEW0D05073YbLHINdCYsWelcksdLSwu1IwjwTYru+w/PAeawu8qOU2dtnIgi+ssFSQ==", - "dependencies": { - "Microsoft.Extensions.Diagnostics": "8.0.0", - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "8.8.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", - "Microsoft.Extensions.Telemetry.Abstractions": "8.8.0", - "Polly.Extensions": "8.4.1", - "Polly.RateLimiting": "8.4.1" - } - }, - "Microsoft.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "3LMXqE65Sv/u160bsG7/meI55peTi68DZ5eAlOb+0FSHdg1rBwn7sNDh6arW135sOoIp/28CsZIigIX5ZPvXTA==", - "dependencies": { - "Microsoft.Bcl.TimeProvider": "8.0.1", - "Microsoft.Extensions.AmbientMetadata.Application": "8.8.0", - "Microsoft.Extensions.DependencyInjection.AutoActivation": "8.8.0", - "Microsoft.Extensions.Logging.Configuration": "8.0.0", - "Microsoft.Extensions.ObjectPool": "8.0.8", - "Microsoft.Extensions.Telemetry.Abstractions": "8.8.0", - "System.Collections.Immutable": "8.0.0" - } - }, - "Microsoft.Extensions.Telemetry.Abstractions": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "6d4p22MCcWbHJtSN2vtLU35T/qDaYKmR2ZLJwtDv5FzOCF65QZN1bMsMd1KCGZte4QLn+OCs/8q0Sz9LpMCG4g==", - "dependencies": { - "Microsoft.Extensions.Compliance.Abstractions": "8.8.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.1", - "Microsoft.Extensions.ObjectPool": "8.0.8", - "Microsoft.Extensions.Options": "8.0.2" - } - }, - "Microsoft.IO.RecyclableMemoryStream": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "irv0HuqoH8Ig5i2fO+8dmDNdFdsrO+DoQcedwIlb810qpZHBNQHZLW7C/AHBQDgLLpw2T96vmMAy/aE4Yj55Sg==" - }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.1", - "contentHash": "bg4kE7mFwXc6FJ8NLknTgVgLAMlbToWC7vpdqAITv8lPzKpp9v7aWJPc04GRoZQaJhVY/tdr8K2/VW2aTmaA1Q==", - "dependencies": { - "Microsoft.Bcl.TimeProvider": "8.0.0" - } - }, - "Polly.Extensions": { - "type": "Transitive", - "resolved": "8.4.1", - "contentHash": "NaRu+mopzJLoDm3qhklrUENIwkhmJbtzLRXK+oMb0c4bGwT84co+BM+TIwqApUfZrcz+BvA/vpB1vk6hB4XtAA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Polly.Core": "8.4.1" - } - }, - "Polly.RateLimiting": { - "type": "Transitive", - "resolved": "8.4.1", - "contentHash": "YF9/pUUd3VZchjJ7+KWAINv5xtHlaWUvrhpGGC73He/zz0mRHzV7gKVDzqwAZrdDk09CdunA+Gt/a37Bl/rMwQ==", - "dependencies": { - "Polly.Core": "8.4.1", - "System.Threading.RateLimiting": "8.0.0" - } - }, - "Sentry": { - "type": "Transitive", - "resolved": "4.10.2", - "contentHash": "B5amIE3VXi4BdERxExlmaRWTfUNmv7uiznMdyVZBAbT9pq/uS8rabQ2/K3qNCpew9hzGvHeA7oRLaumS84COEA==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "OdrZO2WjkiEG6ajEFRABTRCi/wuXQPxeV6g8xvUJqdxMvvuCCEk86zPla8UiIQJz3durtUEbNyY/3lIhS0yZvQ==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encodings.Web": "8.0.0" - } - }, - "System.Threading.RateLimiting": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + "ExcelDna.AddIn": { + "type": "Direct", + "requested": "[1.8.0, )", + "resolved": "1.8.0", + "contentHash": "Q+NcNeuzIkE6d21Sc0NGBpfKClKO8QU2ozR/EZ9RE+WjCxwwOUWXPRLev8+DpOCwkUjXtBIKjE7+xxHTac3PHA==", + "dependencies": { + "ExcelDna.Integration": "[1.8.0]" } + }, + "ExcelDna.Interop": { + "type": "Direct", + "requested": "[15.0.1, )", + "resolved": "15.0.1", + "contentHash": "CIKPXlw+6C6v2Em8DVFgkTsrMZoz978CtSG722+dwHi3Tt7JTxQ2BmmBH4yJuzcA9EaOC5AMEmioLkLsvhESQw==" + }, + "JsonPatch.Net": { + "type": "Direct", + "requested": "[3.1.1, )", + "resolved": "3.1.1", + "contentHash": "dLAUhmL7RgezL8lkBpzf+O4U4sEtbGE9DDF858MiQdNmGK8LYBfLqO73n5N288e5H8jVvwypQG/DUJunWvaJyQ==", + "dependencies": { + "JsonPointer.Net": "5.0.2" + } + }, + "JsonSchema.Net.Generation": { + "type": "Direct", + "requested": "[4.5.1, )", + "resolved": "4.5.1", + "contentHash": "Fr6PxLs4aq/FTigJ1tQPhvZ28aBe9op1pieN6RqJBYPMa0NcZJuqeZSkBeqc1TfkckUwJwfWUQzr9uCkqe80fw==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "JsonSchema.Net": "7.2.3" + } + }, + "MediatR": { + "type": "Direct", + "requested": "[12.4.1, )", + "resolved": "12.4.1", + "contentHash": "0tLxCgEC5+r1OCuumR3sWyiVa+BMv3AgiU4+pz8xqTc+2q1WbUEXFOr7Orm96oZ9r9FsldgUtWvB2o7b9jDOaw==", + "dependencies": { + "MediatR.Contracts": "[2.0.1, 3.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "System.Text.Json": "8.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" + }, + "Microsoft.Extensions.Http": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.Http.Resilience": { + "type": "Direct", + "requested": "[8.8.0, )", + "resolved": "8.8.0", + "contentHash": "A1endJCDOjxF095DrWQhchzwNBFLPiNcerh2x8sbiFbUXQfLaOTeaUfgp9iTMFsSpwhAD3qcSEMMsgFlJ0tDxg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "8.0.2", + "Microsoft.Extensions.Http.Diagnostics": "8.8.0", + "Microsoft.Extensions.ObjectPool": "8.0.8", + "Microsoft.Extensions.Resilience": "8.8.0" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "e+48o7DztoYog+PY430lPxrM4mm3PbA6qucvQtUDDwVo4MO+ejMw7YGc/o2rnxbxj4isPxdfKFzTxvXMwAz83A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Configuration": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Json": "8.0.0" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "dt0x21qBdudHLW/bjMJpkixv858RRr8eSomgVbU8qljOyfrfDGi1JQvpF9w8S7ziRPtRKisuWaOwFxJM82GxeA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.Options": { + "type": "Direct", + "requested": "[8.0.2, )", + "resolved": "8.0.2", + "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Sentry.Extensions.Logging": { + "type": "Direct", + "requested": "[4.10.2, )", + "resolved": "4.10.2", + "contentHash": "1F86+Ly1+9I7WSCdwAWg14GCuBwYgCGOf4x2BiDMxEsYw7FCAmCjfjA+AVEEim4efVtdMuldvH7Ux1K9tRvH8w==", + "dependencies": { + "Microsoft.Extensions.Http": "6.0.0", + "Microsoft.Extensions.Logging.Configuration": "6.0.0", + "Sentry": "4.10.2" + } + }, + "Sentry.Profiling": { + "type": "Direct", + "requested": "[4.10.2, )", + "resolved": "4.10.2", + "contentHash": "OgFfG7nnF6iENkq9AqDPn5T1H1/aPbo4zj990YdSddfrE/A5PCpU8cDKZ/JT2oWokb6igtQgpJHjFxAeZM9NBQ==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.510501", + "Sentry": "4.10.2" + } + }, + "ExcelDna.Integration": { + "type": "Transitive", + "resolved": "1.8.0", + "contentHash": "Z7UYuY291cTxc8lEESlUT73iI28yZ9LeJnvhPIRpZHBlrjbCN8lnEWvSmUqmUn1lBNGk6p2n0GlPuBhopJuksA==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Json.More.Net": { + "type": "Transitive", + "resolved": "2.0.1.2", + "contentHash": "uF3QeiaXEfH92emz0/BWUiNtMSfxIIvgynuB0Bf1vF4s8eWTcZitBx9l+g/FDaJk5XxqBv9buQXizXKQcXFG1w==", + "dependencies": { + "System.Text.Json": "8.0.0" + } + }, + "JsonPointer.Net": { + "type": "Transitive", + "resolved": "5.0.2", + "contentHash": "H/OtixKadr+ja1j7Fru3WG56V9zP0AKT1Bd0O7RWN/zH1bl8ZIwW9aCa4+xvzuVvt4SPmrvBu3G6NpAkNOwNAA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Json.More.Net": "2.0.1.2" + } + }, + "JsonSchema.Net": { + "type": "Transitive", + "resolved": "7.2.3", + "contentHash": "O3KclMcPVFYTZsTeZBpwtKd/lYrNc3AFR+xi9j3Q4CfhDufOUx25TMMWJOcFRrqVklvKQ4Kl+0UhlNX1iDGoRw==", + "dependencies": { + "JsonPointer.Net": "5.0.0" + } + }, + "MediatR.Contracts": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==" + }, + "Microsoft.Bcl.TimeProvider": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "C7kWHJnMRY7EvJev2S8+yJHZ1y7A4ZlLbA4NE+O23BDIAN5mHeqND1m+SKv1ChRS5YlCDW7yAMUe7lttRsJaAA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "8.0.0" + } + }, + "Microsoft.Diagnostics.NETCore.Client": { + "type": "Transitive", + "resolved": "0.2.510501", + "contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==", + "dependencies": { + "Microsoft.Extensions.Logging": "6.0.0" + } + }, + "Microsoft.Extensions.AmbientMetadata.Application": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "R/MVBReInWAHzUiuj+JCzwOryxIuLYLEFRSU9LX+R7hcFg7WySiqmAIj9aa4JSsNm6EkEpxTXCxTB4HeIT0m8A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "v5vsjDXh/GceR9k8qglsvEJeW4YC7E9Cd7nokr8FcJ6CG5APKxDhEn4zeUsNEsFWRKOSoi96r2tUnf5nGbScFg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", + "Microsoft.Extensions.ObjectPool": "8.0.8" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "7IQhGK+wjyGrNsPBjJcZwWAr+Wf6D4+TwOptUt77bWtgNkiV8tDEbhFS+dDamtQFZ2X7kWG9m71iZQRj2x3zgQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.FileProviders.Physical": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + }, + "Microsoft.Extensions.DependencyInjection.AutoActivation": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "DBuKYUr3lvl1jFYPoNIIiXS9dcs3Xxp0HXhPj1xLYp1XFueCZMeiN/RDC1vWdQCK15aFAMZxuxEBBJQD7LxY+g==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "System.Diagnostics.DiagnosticSource": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "K9jlnvIbkYvHRAwxl8WjFkIU7D/5PYAeVmDMAN6Y4IQyWyBUT/xMotYwYGRFAO2kiPPJhIIj7a9VI2p7ydakkg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", + "System.Collections.Immutable": "8.0.0" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.FileSystemGlobbing": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.Http.Diagnostics": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "N2o2E+SDU3Lu7RD8U2Nw3G2WPEPGx2nBNAueqm3nQPnWWGzFlkE6eRqGp1Lwvey8aiFnPbJ0rHoiM1m+RCt/Bg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.AutoActivation": "8.8.0", + "Microsoft.Extensions.Http": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", + "Microsoft.Extensions.Telemetry": "8.8.0", + "Microsoft.IO.RecyclableMemoryStream": "3.0.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "8.0.8", + "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.Extensions.Resilience": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "Lzd/CKTv8KrudJCD7yD0tEW0D05073YbLHINdCYsWelcksdLSwu1IwjwTYru+w/PAeawu8qOU2dtnIgi+ssFSQ==", + "dependencies": { + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "8.8.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", + "Microsoft.Extensions.Telemetry.Abstractions": "8.8.0", + "Polly.Extensions": "8.4.1", + "Polly.RateLimiting": "8.4.1" + } + }, + "Microsoft.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "3LMXqE65Sv/u160bsG7/meI55peTi68DZ5eAlOb+0FSHdg1rBwn7sNDh6arW135sOoIp/28CsZIigIX5ZPvXTA==", + "dependencies": { + "Microsoft.Bcl.TimeProvider": "8.0.1", + "Microsoft.Extensions.AmbientMetadata.Application": "8.8.0", + "Microsoft.Extensions.DependencyInjection.AutoActivation": "8.8.0", + "Microsoft.Extensions.Logging.Configuration": "8.0.0", + "Microsoft.Extensions.ObjectPool": "8.0.8", + "Microsoft.Extensions.Telemetry.Abstractions": "8.8.0", + "System.Collections.Immutable": "8.0.0" + } + }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "6d4p22MCcWbHJtSN2vtLU35T/qDaYKmR2ZLJwtDv5FzOCF65QZN1bMsMd1KCGZte4QLn+OCs/8q0Sz9LpMCG4g==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "8.8.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.1", + "Microsoft.Extensions.ObjectPool": "8.0.8", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "irv0HuqoH8Ig5i2fO+8dmDNdFdsrO+DoQcedwIlb810qpZHBNQHZLW7C/AHBQDgLLpw2T96vmMAy/aE4Yj55Sg==" + }, + "Polly.Core": { + "type": "Transitive", + "resolved": "8.4.1", + "contentHash": "bg4kE7mFwXc6FJ8NLknTgVgLAMlbToWC7vpdqAITv8lPzKpp9v7aWJPc04GRoZQaJhVY/tdr8K2/VW2aTmaA1Q==", + "dependencies": { + "Microsoft.Bcl.TimeProvider": "8.0.0" + } + }, + "Polly.Extensions": { + "type": "Transitive", + "resolved": "8.4.1", + "contentHash": "NaRu+mopzJLoDm3qhklrUENIwkhmJbtzLRXK+oMb0c4bGwT84co+BM+TIwqApUfZrcz+BvA/vpB1vk6hB4XtAA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Polly.Core": "8.4.1" + } + }, + "Polly.RateLimiting": { + "type": "Transitive", + "resolved": "8.4.1", + "contentHash": "YF9/pUUd3VZchjJ7+KWAINv5xtHlaWUvrhpGGC73He/zz0mRHzV7gKVDzqwAZrdDk09CdunA+Gt/a37Bl/rMwQ==", + "dependencies": { + "Polly.Core": "8.4.1", + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Sentry": { + "type": "Transitive", + "resolved": "4.10.2", + "contentHash": "B5amIE3VXi4BdERxExlmaRWTfUNmv7uiznMdyVZBAbT9pq/uS8rabQ2/K3qNCpew9hzGvHeA7oRLaumS84COEA==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "OdrZO2WjkiEG6ajEFRABTRCi/wuXQPxeV6g8xvUJqdxMvvuCCEk86zPla8UiIQJz3durtUEbNyY/3lIhS0yZvQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "8.0.0" + } + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + } } } } \ No newline at end of file