From 7524c8b550d3a2df31c3c1413e5cbb50e0ab9246 Mon Sep 17 00:00:00 2001 From: Mehdi Hadeli Date: Fri, 29 Nov 2024 20:15:07 +0330 Subject: [PATCH 1/4] refactor: :recycle: enhance markdown block rendering in spectre console --- .github/workflows/publish.yml | 8 +- AIAssistant.sln | 7 + README.md | 26 +- src/AIAssist/Commands/CodeAssistCommand.cs | 29 +- src/AIAssist/Commands/GenerateCommand.cs | 23 + .../InternalCommands/AddFileCommand.cs | 4 +- .../InternalCommands/ClearHistoryCommand.cs | 2 +- .../Commands/InternalCommands/QuitCommand.cs | 2 +- .../Commands/InternalCommands/RunCommand.cs | 3 +- src/AIAssist/Diff/CodeDiffUpdater.cs | 30 +- .../EmbeddingCodeAssist.cs | 1 + src/AIAssist/aiassist-config.json | 8 +- .../MarkdigMarkdown/MarkdownParser.cs | 8 +- .../Contracts/ISpectreUtilities.cs | 11 +- .../Markdown/SpectreCompositeRenderable.cs | 11 - .../Markdown/SpectreMarkdownBlockRendering.cs | 284 +++++------- .../SpectreMarkdownInlineRendering.cs | 5 +- .../SpectreVerticalCompositeRenderable.cs | 23 + .../SpectreConsole/SpectreUtilities.cs | 66 ++- .../StyleElements/ConsoleStyle.cs | 2 + .../SpectreConsole/Themes/dracula.json | 6 + src/Clients/AnthropicClient.cs | 7 +- src/Clients/AzureClient.cs | 13 +- src/Clients/CacheModels.cs | 84 ++-- src/Clients/OllamaClient.cs | 10 +- src/Clients/OpenAiClient.cs | 10 +- src/Clients/Options/LLMOptions.cs | 8 +- .../BuildingBlocks.UnitTests.csproj | 6 - .../Markdown/SpectreMarkdownTests.cs | 34 ++ .../Utilities/FilesUtilitiesTests.cs | 412 +++++++++--------- version.json | 2 +- 31 files changed, 645 insertions(+), 500 deletions(-) create mode 100644 src/AIAssist/Commands/GenerateCommand.cs delete mode 100644 src/BuildingBlocks/SpectreConsole/Markdown/SpectreCompositeRenderable.cs create mode 100644 src/BuildingBlocks/SpectreConsole/Markdown/SpectreVerticalCompositeRenderable.cs create mode 100644 tests/UnitTests/BuildingBlocks.UnitTests/SpectreConsole/Markdown/SpectreMarkdownTests.cs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d30576a..2e89884 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -131,8 +131,10 @@ jobs: # https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-pack - name: Pack NuGet Package Version ${{ steps.get_version.outputs.nuget_version }} run: dotnet pack src/AIAssist/AIAssist.csproj -o ${{ env.NuGetDirectory }} -c Release --no-restore --no-build + # Publish the NuGet package as an artifact, so they can be used in the following jobs - - uses: actions/upload-artifact@v4 + - name: Upload Package Version ${{ steps.get_version.outputs.nuget_version }} + uses: actions/upload-artifact@v4 with: name: nuget if-no-files-found: error @@ -149,8 +151,10 @@ jobs: with: # https://github.com/dotnet/Nerdbank.GitVersioning/blob/main/doc/cloudbuild.md#github-actions fetch-depth: 0 # doing deep clone and avoid shallow clone so nbgv can do its work. + # Download the NuGet package created in the previous job and copy in the root - - uses: actions/download-artifact@v4 + - name: Download Nuget + uses: actions/download-artifact@v4 with: name: nuget ## Optional. Default is $GITHUB_WORKSPACE diff --git a/AIAssistant.sln b/AIAssistant.sln index 25f73fc..f027357 100644 --- a/AIAssistant.sln +++ b/AIAssistant.sln @@ -82,6 +82,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AIAssist", "AIAssist", "{4F EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIAssist", "src\AIAssist\AIAssist.csproj", "{A4801AE4-5836-47CF-8AA4-DF99918BE2CC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildingBlocks.UnitTests", "tests\UnitTests\BuildingBlocks.UnitTests\BuildingBlocks.UnitTests.csproj", "{496F9F39-89E5-4F9F-9DF4-8D5A86236C1D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -105,6 +107,7 @@ Global {FA8BD80F-036B-4D0C-BE2D-62EA5FAF9C8C} = {6456834B-EA6C-48EA-9434-A4185D70F65F} {4FAC7598-86FC-495F-B310-C641F424A904} = {97D741A1-DEFC-4241-99BC-7123A2D981E4} {A4801AE4-5836-47CF-8AA4-DF99918BE2CC} = {4FAC7598-86FC-495F-B310-C641F424A904} + {496F9F39-89E5-4F9F-9DF4-8D5A86236C1D} = {CF49F264-D5C4-4A9F-BF9C-8706559CFC53} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {4973B5D3-67CE-47CC-A0C7-55EC268A2268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -123,5 +126,9 @@ Global {A4801AE4-5836-47CF-8AA4-DF99918BE2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4801AE4-5836-47CF-8AA4-DF99918BE2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4801AE4-5836-47CF-8AA4-DF99918BE2CC}.Release|Any CPU.Build.0 = Release|Any CPU + {496F9F39-89E5-4F9F-9DF4-8D5A86236C1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {496F9F39-89E5-4F9F-9DF4-8D5A86236C1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {496F9F39-89E5-4F9F-9DF4-8D5A86236C1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {496F9F39-89E5-4F9F-9DF4-8D5A86236C1D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 82ed1da..c33c639 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,13 @@ # AI Assist -> `Context Aware` AI assistant for coding, chat, code explanation, review with supporting local and online language models. +> `Context Aware` AI coding assistant inside terminal to help in code development, code explanation, code refactor and review, bug fix and chat with supporting local and online language models -`AIAssist` is compatible with [OpenAI](https://platform.openai.com/docs/api-reference/introduction) and [Azure AI Services](https://azure.microsoft.com/en-us/products/ai-services) through apis and [Ollama models](https://ollama.com/search) through [ollama engine](https://ollama.com/) locally. +`AIAssist` is compatible with bellow AI Services: +- [x] [OpenAI](https://platform.openai.com/docs/api-reference/introduction) through apis +- [x] [Azure AI Services](https://azure.microsoft.com/en-us/products/ai-services) through apis +- [x] [Ollama](https://ollama.com/) with using [ollama models](https://ollama.com/search) locally +- [ ] [Anthropic](https://docs.anthropic.com/en/api/getting-started) through apis +- [ ] [OpenRouter](https://openrouter.ai/docs/quick-start) through apis > [!TIP] > You can use ollama and its models that are more compatible with code like [deepseek-v2.5](https://ollama.com/library/deepseek-v2.5) or [qwen2.5-coder](https://ollama.com/library/qwen2.5-coder) locally. To use local models, you will need to run [Ollama](https://github.com/ollama/ollama) process first. For running ollama you can use [ollama docker](https://ollama.com/blog/ollama-is-now-available-as-an-official-docker-image) container. @@ -30,16 +35,17 @@ AIAssist uses [Azure AI Services](https://azure.microsoft.com/en-us/products/ai-services) or [OpenAI](https://platform.openai.com/docs/api-reference/introduction) apis by default. For using `OpenAI` or `Azure AI` apis we need to have a `ApiKey`. -- Install `aiassist` with `dotnet tool install ` and bellow command: +- To access `dotnet tool`, we need to install [latest .net sdk](https://dotnet.microsoft.com/en-us/download) first. +- Install `aiassist` with `dotnet tool install` and bellow command: ```bash -TODO: Add Nuget Soon +dotnet tool install --global AIAssist ``` -- For OpenAI If you don't have a API key you can [sign up](https://platform.openai.com/signup) in OpenAI and get a ApiKey. -- For Azure AI service you can [signup](https://azure.microsoft.com/en-us/products/ai-services) a azure account and get a AI model API key. -- After getting Api key we should set API key for chat and embedding models through environment variable or command options. -- Now got to `project directory` with `cd` command in terminal, For running `aiassist` and setting api key. +- For OpenAI If you don't have a API key you can [sign up](https://platform.openai.com/signup) in OpenAI and get a ApiKey. +- For Azure AI service you can [signup](https://azure.microsoft.com/en-us/products/ai-services) a azure account and get a AI model API key. +- After getting Api key we should set API key for chat and embedding models through environment variable or command options. +- Now got to `project directory` with `cd` command in terminal, For running `aiassist` and setting api key. ```bash # Go to project directory @@ -49,14 +55,12 @@ cd /to/project/directory - Set `Api Key` through `environment variable`: Linux terminal: - ```bash export CHAT_MODEL_API_KEY=your-chat-api-key-here export EMBEDDINGS_MODEL_API_KEY=your-embedding-api-key-here ``` Windows Powershell Terminal: - ```powershell $env:CHAT_MODEL_API_KEY=your-chat-api-key-here $env:EMBEDDINGS_MODEL_API_KEY=your-embedding-api-key-here @@ -72,7 +76,6 @@ aiassist code --chat-api-key your-chat-api-key-here --embeddings-api-key your-e - Set `ApiVersion`, `DeploymentId` and `BaseAddress` through`environment variable`: Linux terminal: - ```bash export CHAT_BASE_ADDRESS=your-chat-base-address-here export CHAT_API_VERSION=your-chat-api-version-here @@ -83,7 +86,6 @@ export EMBEDDINGS_DEPLOYMENT_ID=your-embedding-deployment-id-here ``` Windows Powershell Terminal: - ```powershell $env:CHAT_BASE_ADDRESS=your-chat-base-address-here $env:CHAT_API_VERSION=your-chat-api-version-here diff --git a/src/AIAssist/Commands/CodeAssistCommand.cs b/src/AIAssist/Commands/CodeAssistCommand.cs index 7d2c683..fe30c6e 100644 --- a/src/AIAssist/Commands/CodeAssistCommand.cs +++ b/src/AIAssist/Commands/CodeAssistCommand.cs @@ -121,17 +121,11 @@ public override async Task ExecuteAsync(CommandContext context, Settings se SetupOptions(settings); - spectreUtilities.InformationText("Code assist mode is activated!"); - spectreUtilities.InformationText($"Chat model: {_chatModel.Name}"); - - if (_embeddingModel is not null) - { - spectreUtilities.InformationText($"Embedding model: {_embeddingModel.Name}"); - } - - spectreUtilities.InformationText($"CodeAssistType: {_chatModel.ModelOption.CodeAssistType}"); - spectreUtilities.InformationText($"CodeDiffType: {_chatModel.ModelOption.CodeDiffType}"); - spectreUtilities.InformationText("Please 'Ctrl+H' to see all available commands in the code assist mode."); + spectreUtilities.SummaryTextLine("Code assist mode is activated!"); + spectreUtilities.SummaryTextLine( + $"Chat model: {_chatModel.Name} | Embedding model: {_embeddingModel?.Name ?? "-"} | CodeAssistType: {_chatModel.ModelOption.CodeAssistType} | CodeDiffType: {_chatModel.ModelOption.CodeDiffType}" + ); + spectreUtilities.SummaryTextLine("Please 'Ctrl+H' to see all available commands in the code assist mode."); spectreUtilities.WriteRule(); await AnsiConsole @@ -166,7 +160,7 @@ await AnsiConsole if (string.IsNullOrEmpty(userInput)) { - spectreUtilities.ErrorText("Input can't be null or empty string."); + spectreUtilities.ErrorTextLine("Input can't be null or empty string."); continue; } @@ -251,25 +245,22 @@ private void SetupOptions(Settings settings) if (settings.CodeDiffType is not null) { - _chatModel.ModelOption.CodeDiffType = settings.CodeDiffType.Value; + _llmOptions.CodeDiffType = settings.CodeDiffType.Value; } if (settings.CodeAssistType is not null) { - _chatModel.ModelOption.CodeAssistType = settings.CodeAssistType.Value; + _llmOptions.CodeAssistType = settings.CodeAssistType.Value; } if (settings.Threshold is not null && _embeddingModel is not null) { - _embeddingModel.ModelOption.Threshold = settings.Threshold.Value; + _llmOptions.Threshold = settings.Threshold.Value; } if (settings.Temperature is not null) { - _chatModel.ModelOption.Temperature = settings.Temperature.Value; - - if (_embeddingModel is not null) - _embeddingModel.ModelOption.Temperature = settings.Temperature.Value; + _llmOptions.Temperature = settings.Temperature.Value; } } } diff --git a/src/AIAssist/Commands/GenerateCommand.cs b/src/AIAssist/Commands/GenerateCommand.cs new file mode 100644 index 0000000..49f4672 --- /dev/null +++ b/src/AIAssist/Commands/GenerateCommand.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace AIAssist.Commands; + +[Description("Generate some settings and configs for the AI Assist.")] +public class GenerateCommand : AsyncCommand +{ + [CommandOption("-c|--config")] + [Description("[grey] generate base config file for the AIAssist.[/].")] + public bool GenerateConfig { get; set; } + + [CommandOption("-i|--ignore")] + [Description("[grey] generate AIAssist ignore file.[/].")] + public bool GenerateIgnore { get; set; } + + public sealed class Settings : CommandSettings { } + + public override Task ExecuteAsync(CommandContext context, Settings settings) + { + return null; + } +} diff --git a/src/AIAssist/Commands/InternalCommands/AddFileCommand.cs b/src/AIAssist/Commands/InternalCommands/AddFileCommand.cs index f8b84ec..cd003d9 100644 --- a/src/AIAssist/Commands/InternalCommands/AddFileCommand.cs +++ b/src/AIAssist/Commands/InternalCommands/AddFileCommand.cs @@ -38,7 +38,7 @@ public Task ExecuteAsync(IServiceScope scope, string? input) } else { - spectreUtilities.ErrorText($"The specified path does not exist: {path}"); + spectreUtilities.ErrorTextLine($"The specified path does not exist: {path}"); } } @@ -56,7 +56,7 @@ public Task ExecuteAsync(IServiceScope scope, string? input) } } - spectreUtilities.InformationText( + spectreUtilities.InformationTextLine( filesToAdd.Count != 0 ? $"Files added: {string.Join(", ", filesToAdd)}" : "No files were added." ); diff --git a/src/AIAssist/Commands/InternalCommands/ClearHistoryCommand.cs b/src/AIAssist/Commands/InternalCommands/ClearHistoryCommand.cs index af8623b..ae75174 100644 --- a/src/AIAssist/Commands/InternalCommands/ClearHistoryCommand.cs +++ b/src/AIAssist/Commands/InternalCommands/ClearHistoryCommand.cs @@ -16,7 +16,7 @@ public class ClearHistoryCommand(ISpectreUtilities spectreUtilities, IOptions ExecuteAsync(IServiceScope scope, string? input) { - spectreUtilities.InformationText("History cleared."); + spectreUtilities.InformationTextLine("History cleared."); return Task.FromResult(true); } diff --git a/src/AIAssist/Commands/InternalCommands/QuitCommand.cs b/src/AIAssist/Commands/InternalCommands/QuitCommand.cs index 0196872..3b19b2c 100644 --- a/src/AIAssist/Commands/InternalCommands/QuitCommand.cs +++ b/src/AIAssist/Commands/InternalCommands/QuitCommand.cs @@ -15,7 +15,7 @@ public class QuitCommand(ISpectreUtilities spectreUtilities, IOptions ExecuteAsync(IServiceScope scope, string? input) { - spectreUtilities.ErrorText("Process interrupted. Exiting..."); + spectreUtilities.ErrorTextLine("Process interrupted. Exiting..."); // stop running commands return Task.FromResult(false); diff --git a/src/AIAssist/Commands/InternalCommands/RunCommand.cs b/src/AIAssist/Commands/InternalCommands/RunCommand.cs index 76c8810..5e729b8 100644 --- a/src/AIAssist/Commands/InternalCommands/RunCommand.cs +++ b/src/AIAssist/Commands/InternalCommands/RunCommand.cs @@ -50,7 +50,7 @@ public async Task ExecuteAsync(IServiceScope scope, string? input) var fullFilesContentForContext = await codeAssistantManager.GetCodeTreeContentsFromCache(requiredFiles); var newQueryWithAddedFiles = promptManager.FilesAddedToChat(fullFilesContentForContext); - spectreUtilities.SuccessText( + spectreUtilities.SuccessTextLine( $"{string.Join(",", requiredFiles.Select(file => $"'{file}'"))} added to the context." ); @@ -84,5 +84,6 @@ private void PrintChatCost(ChatHistoryItem lastChatHistoryItem) return; spectreUtilities.WriteRule(); spectreUtilities.InformationText(message: lastChatHistoryItem.ChatCost.ToString(), justify: Justify.Right); + spectreUtilities.WriteRule(); } } diff --git a/src/AIAssist/Diff/CodeDiffUpdater.cs b/src/AIAssist/Diff/CodeDiffUpdater.cs index 6e1ea3c..d601267 100644 --- a/src/AIAssist/Diff/CodeDiffUpdater.cs +++ b/src/AIAssist/Diff/CodeDiffUpdater.cs @@ -12,7 +12,7 @@ public void ApplyChanges(IList diffResults, string contextWorkingDir if (string.IsNullOrWhiteSpace(contextWorkingDirectory)) { - spectreUtilities.ErrorText("Working directory cannot be null or whitespace."); + spectreUtilities.ErrorTextLine("Working directory cannot be null or whitespace."); } foreach (var diffResult in diffResults) @@ -53,11 +53,11 @@ private void HandleReplacementFile(DiffResult diffResult, string contextWorkingD { var updatedLines = ApplyReplacements(new List(), diffResult.Replacements); File.WriteAllText(modifiedFilePath, string.Join("\n", updatedLines)); - spectreUtilities.SuccessText($"File created: {modifiedFilePath}"); + spectreUtilities.SuccessTextLine($"File created: {modifiedFilePath}"); } else { - spectreUtilities.ErrorText("No modified lines provided for new file creation."); + spectreUtilities.ErrorTextLine("No modified lines provided for new file creation."); } } else if (diffResult.ModifiedPath == noneExistPath && diffResult.OriginalPath != noneExistPath) @@ -68,11 +68,11 @@ private void HandleReplacementFile(DiffResult diffResult, string contextWorkingD if (File.Exists(originalFilePath) && diffResult.Replacements is not null && diffResult.Replacements.Any()) { File.Delete(originalFilePath); - spectreUtilities.SuccessText($"File deleted: {originalFilePath}"); + spectreUtilities.SuccessTextLine($"File deleted: {originalFilePath}"); } else { - spectreUtilities.ErrorText($"File not found for deletion: {originalFilePath}"); + spectreUtilities.ErrorTextLine($"File not found for deletion: {originalFilePath}"); } } else if (diffResult.OriginalPath != diffResult.ModifiedPath) @@ -94,7 +94,7 @@ private void HandleReplacementFile(DiffResult diffResult, string contextWorkingD } else { - spectreUtilities.ErrorText($"Original file not found for rename/move: {originalFilePath}"); + spectreUtilities.ErrorTextLine($"Original file not found for rename/move: {originalFilePath}"); } } else @@ -108,7 +108,7 @@ private void HandleReplacementFile(DiffResult diffResult, string contextWorkingD if (!File.Exists(originalFilePath)) { - spectreUtilities.ErrorText($"Original file not found: {originalFilePath}"); + spectreUtilities.ErrorTextLine($"Original file not found: {originalFilePath}"); } var originalLines = File.ReadAllLines(originalFilePath).ToList(); @@ -117,7 +117,7 @@ private void HandleReplacementFile(DiffResult diffResult, string contextWorkingD Directory.CreateDirectory(Path.GetDirectoryName(modifiedFilePath)!); File.WriteAllText(modifiedFilePath, string.Join("\n", updatedLines)); - spectreUtilities.SuccessText($"File updated: {modifiedFilePath}"); + spectreUtilities.SuccessTextLine($"File updated: {modifiedFilePath}"); } } @@ -167,7 +167,7 @@ string contextWorkingDirectory Directory.CreateDirectory(Path.GetDirectoryName(modifiedFullPath)!); // Normalize and write lines to prevent extra blank lines because WriteAllLines File.WriteAllText(modifiedFullPath, string.Join("\n", modifiedLines)); - spectreUtilities.SuccessText($"File created: {modifiedPath}"); + spectreUtilities.SuccessTextLine($"File created: {modifiedPath}"); break; } @@ -180,11 +180,11 @@ string contextWorkingDirectory { // Normalize and write lines to prevent blank lines File.WriteAllText(modifiedFullPath, string.Join("\n", modifiedLines)); - spectreUtilities.SuccessText($"File updated: {modifiedPath}"); + spectreUtilities.SuccessTextLine($"File updated: {modifiedPath}"); } else { - spectreUtilities.ErrorText($"File {modifiedPath} does not exist to modify."); + spectreUtilities.ErrorTextLine($"File {modifiedPath} does not exist to modify."); } break; } @@ -195,23 +195,23 @@ string contextWorkingDirectory if (File.Exists(originalFullPath)) { File.Delete(originalFullPath); - spectreUtilities.SuccessText($"File deleted: {originalPath}"); + spectreUtilities.SuccessTextLine($"File deleted: {originalPath}"); } else { - spectreUtilities.ErrorText($"File {originalPath} not found for deletion."); + spectreUtilities.ErrorTextLine($"File {originalPath} not found for deletion."); } break; } default: - spectreUtilities.ErrorText($"Unsupported action type: {actionType}"); + spectreUtilities.ErrorTextLine($"Unsupported action type: {actionType}"); break; } } catch (Exception ex) { - spectreUtilities.ErrorText($"Failed to update file {modifiedPath} \n {ex.Message}"); + spectreUtilities.ErrorTextLine($"Failed to update file {modifiedPath} \n {ex.Message}"); } } } diff --git a/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs b/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs index 7deb13c..469f063 100644 --- a/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs +++ b/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs @@ -121,5 +121,6 @@ private void PrintEmbeddingCost(int totalCount, decimal totalCost) message: $"Total Embedding Tokens: {totalCount.FormatCommas()} | Total Embedding Cost: ${totalCost.FormatCommas()}", justify: Justify.Right ); + spectreUtilities.WriteRule(); } } diff --git a/src/AIAssist/aiassist-config.json b/src/AIAssist/aiassist-config.json index 77b9baf..cf2dda3 100644 --- a/src/AIAssist/aiassist-config.json +++ b/src/AIAssist/aiassist-config.json @@ -1,12 +1,14 @@ { - "Test":"Test1", "AppOptions": { "ThemeName": "dracula", "PrintCostEnabled": true }, "LLMOptions": { - "ChatModel": "ollama/llama3", - "EmbeddingsModel": "ollama/nomic-embed-text" + "ChatModel": "azure/gpt-4o", + "EmbeddingsModel": "azure/text-embedding-3-large", + "CodeDiffType": "CodeBlockDiff", + "CodeAssistType": "Embedding", + "Temperature": 0.2 }, "Serilog": { "MinimumLevel": { diff --git a/src/BuildingBlocks/MarkdigMarkdown/MarkdownParser.cs b/src/BuildingBlocks/MarkdigMarkdown/MarkdownParser.cs index 9873dd8..17a770d 100644 --- a/src/BuildingBlocks/MarkdigMarkdown/MarkdownParser.cs +++ b/src/BuildingBlocks/MarkdigMarkdown/MarkdownParser.cs @@ -7,10 +7,14 @@ public class MarkdownParser { public MarkdownDocument ToMarkdownDocument(string markdown, string theme = "dracula") { - var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseColorCodeBlock(theme).Build(); + var pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .UseSoftlineBreakAsHardlineBreak() + .UseColorCodeBlock(theme) + .Build(); // parser Markdown text to markdig documents object - var markdownDocument = global::Markdig.Markdown.Parse(markdown, pipeline); + var markdownDocument = Markdown.Parse(markdown, pipeline); return markdownDocument; } diff --git a/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs b/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs index 5893977..a4b33a4 100644 --- a/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs +++ b/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs @@ -6,11 +6,18 @@ public interface ISpectreUtilities { bool ConfirmationPrompt(string message); string? UserPrompt(string? promptMessage = null); + void InformationTextLine(string message, Justify? justify = null, Overflow? overflow = null); void InformationText(string message, Justify? justify = null, Overflow? overflow = null); + public void SummaryTextLine(string message, Justify? justify = null, Overflow? overflow = null); + public void SummaryText(string message, Justify? justify = null, Overflow? overflow = null); + public void HighlightTextLine(string message, Justify? justify = null, Overflow? overflow = null); + public void HighlightText(string message, Justify? justify = null, Overflow? overflow = null); + void NormalTextLine(string message, Justify? justify = null, Overflow? overflow = null); void NormalText(string message, Justify? justify = null, Overflow? overflow = null); + void WarningTextLine(string message, Justify? justify = null, Overflow? overflow = null); void WarningText(string message, Justify? justify = null, Overflow? overflow = null); - void ErrorText(string message, Justify? justify = null, Overflow? overflow = null); - void SuccessText(string message, Justify? justify = null, Overflow? overflow = null); + void ErrorTextLine(string message, Justify? justify = null, Overflow? overflow = null); + void SuccessTextLine(string message, Justify? justify = null, Overflow? overflow = null); void WriteCursor(); void WriteRule(); void Exception(string errorMessage, Exception ex); diff --git a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreCompositeRenderable.cs b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreCompositeRenderable.cs deleted file mode 100644 index 4cdb6e8..0000000 --- a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreCompositeRenderable.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Spectre.Console.Rendering; - -namespace BuildingBlocks.SpectreConsole.Markdown; - -internal class SpectreCompositeRenderable(IEnumerable renderables) : Renderable -{ - protected override IEnumerable Render(RenderOptions options, int maxWidth) - { - return renderables.SelectMany(x => x.Render(options, maxWidth)); - } -} diff --git a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs index 4b36dc9..6cdce6c 100644 --- a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs +++ b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs @@ -36,84 +36,53 @@ public IRenderable RenderBlock( bool suppressNewLine = false ) { - IRenderable? result = null; - switch (markdigBlock) + return markdigBlock switch { - case CustomContainer: - case MathBlock: - case Footnote: - case FootnoteGroup: - case FootnoteLinkReferenceDefinition: - case Figure: - case FigureCaption: - case YamlFrontMatterBlock: - case Abbreviation: - case FooterBlock: - case HtmlBlock: - case DefinitionItem: - case DefinitionList: - case DefinitionTerm: - break; - - // No point rendering these as the definitions are already reconciled by the parser. - case HeadingLinkReferenceDefinition: - case LinkReferenceDefinitionGroup: - case LinkReferenceDefinition: - break; - - case BlankLineBlock: - return new Text(Environment.NewLine); - case EmptyBlock: - return new Text(Environment.NewLine); - case Table table: - result = RenderTableBlock(table); - break; - case FencedCodeBlock fencedCodeBlock: - return RenderFenceBlock(fencedCodeBlock); - case CodeBlock codeBlock: - var blockContents = codeBlock.Lines.ToString(); - result = new Panel(blockContents) - { - Header = new PanelHeader("code"), - Expand = true, - Border = BoxBorder.Rounded, - }; - break; - case ListBlock listBlock: - result = RenderListBlock(listBlock, CreateStyle(_colorTheme.ListStyle, style)); - break; - case ListItemBlock listItemBlock: - result = RenderListItemBlock(listItemBlock, style); - break; - case QuoteBlock quoteBlock: - result = RenderQuoteBlock(quoteBlock, CreateStyle(_colorTheme.BlockQuoteStyle, style)); - break; - case HeadingBlock headingBlock: - result = RenderHeadingBlock(headingBlock, CreateStyle(_colorTheme.HeadStyle, style)); - break; - case ParagraphBlock paragraphBlock: - if (suppressNewLine) - return RenderParagraphBlock( - paragraphBlock, - alignment, - CreateStyle(_colorTheme.ParagraphStyle, style), - suppressNewLine - ); - result = RenderParagraphBlock( - paragraphBlock, - alignment, - CreateStyle(_colorTheme.ParagraphStyle, style), - suppressNewLine - ); - break; - case ThematicBreakBlock thematicBreakBlock: - return new Text(thematicBreakBlock.Content.Text); - } + BlankLineBlock => new Text(Environment.NewLine), + EmptyBlock => new Text(string.Empty), + Table table => AppendBreakAfter(RenderTableBlock(table, style)), + FencedCodeBlock fencedCodeBlock => AppendBreakAfter(RenderFenceBlock(fencedCodeBlock)), + CodeBlock codeBlock => AppendBreakAfter(RenderCodeBlock(codeBlock)), + ListBlock listBlock => AppendBreakBeforeAfter( + RenderListBlock(listBlock, CreateStyle(_colorTheme.ListStyle, style)) + ), + ListItemBlock listItemBlock => RenderListItemBlock(listItemBlock, style), + QuoteBlock quoteBlock => AppendBreakAfter( + RenderQuoteBlock(quoteBlock, CreateStyle(_colorTheme.BlockQuoteStyle, style)) + ), + HeadingBlock headingBlock => AppendBreakAfter( + RenderHeadingBlock(headingBlock, CreateStyle(_colorTheme.HeadStyle, style)) + ), + ParagraphBlock paragraphBlock => RenderParagraphBlock( + paragraphBlock, + alignment, + CreateStyle(_colorTheme.ParagraphStyle, style), + suppressNewLine + ), + ThematicBreakBlock thematicBreakBlock => new Text(thematicBreakBlock.Content.Text), + _ => Text.Empty, + }; + } - if (result is not null) - return new SpectreCompositeRenderable(new List { result, new Text(Environment.NewLine) }); + private IRenderable AppendBreakAfter(IRenderable renderable) + { + return new SpectreVerticalCompositeRenderable( + new List { renderable, new Text(Environment.NewLine) } + ); + } - return Text.Empty; + private IRenderable AppendBreakBefore(IRenderable renderable) + { + return new SpectreVerticalCompositeRenderable( + new List { new Text(Environment.NewLine), renderable } + ); + } + + private IRenderable AppendBreakBeforeAfter(IRenderable renderable) + { + return new SpectreVerticalCompositeRenderable( + new List { new Text(Environment.NewLine), renderable, new Text(Environment.NewLine) } + ); } private IRenderable RenderFenceBlock(FencedCodeBlock fencedCodeBlock) @@ -131,11 +100,22 @@ private IRenderable RenderFenceBlock(FencedCodeBlock fencedCodeBlock) ).PadLeft(_colorTheme.CodeBlockStyle.Margin); } + private IRenderable RenderCodeBlock(CodeBlock codeBlock) + { + var blockContents = codeBlock.Lines.ToString(); + return new Panel(blockContents) + { + Header = new PanelHeader("code"), + Expand = true, + Border = BoxBorder.Rounded, + }; + } + private IRenderable RenderQuoteBlock(QuoteBlock quoteBlock, Style style) { foreach (var subBlock in quoteBlock) if (subBlock is ParagraphBlock paragraph) - return new SpectreCompositeRenderable( + return new SpectreVerticalCompositeRenderable( new List { new Markup( @@ -146,41 +126,57 @@ private IRenderable RenderQuoteBlock(QuoteBlock quoteBlock, Style style) } ); - return new Text(""); + return Text.Empty; } - private IRenderable RenderListBlock(ListBlock listBlock, Style style) + private IRenderable RenderListBlock(ListBlock listBlock, Style style, int indentLevel = 0) { - IEnumerable? itemPrefixes; - if (listBlock.IsOrdered) - { - var startNum = int.Parse(listBlock.OrderedStart); - var orderedDelimiter = listBlock.OrderedDelimiter; - itemPrefixes = Enumerable.Range(startNum, listBlock.Count).Select(num => $"{num}{orderedDelimiter}"); - } - else - { - itemPrefixes = Enumerable.Repeat( - _colorTheme.ListStyle.BlockPrefix ?? CharacterSet.ListBullet, - listBlock.Count - ); - } + int startNumber = int.TryParse(listBlock.OrderedStart, out var parsedNumber) ? parsedNumber : 1; - var paddedItemPrefixes = itemPrefixes.Select(x => new Text( - $" {x} ", - style: Style.Parse(_colorTheme.ListStyle.PrefixForeground ?? "default") - )); + // Generate item prefixes + var itemPrefixes = listBlock.IsOrdered + ? Enumerable.Range(startNumber, listBlock.Count).Select(num => $"{num}{listBlock.OrderedDelimiter}") + : Enumerable.Repeat(_colorTheme.ListStyle.BlockPrefix ?? CharacterSet.ListBullet, listBlock.Count); - return new SpectreCompositeRenderable( - [.. Interleave(paddedItemPrefixes, listBlock.Select(x => RenderBlock(x, style: style)))] - ); + var renderedItems = listBlock + .OfType() + .Select( + (itemBlock, index) => + { + // Generate the prefix for the current list item + var prefix = new Text( + $"{new string(' ', indentLevel * 4)}{itemPrefixes.ElementAt(index)} ", + style: Style.Parse(_colorTheme.ListStyle.PrefixForeground ?? "default") + ); + + var itemContent = RenderListItemBlock(itemBlock, style, indentLevel); + + // Combine the prefix and content + return new SpectreVerticalCompositeRenderable([prefix, itemContent]); + } + ); + + // Combine all rendered items without introducing extra line break + return new SpectreHorizontalCompositeRenderable(renderedItems); } - private IRenderable RenderListItemBlock(ListItemBlock listItemBlock, Style? style = null) + private IRenderable RenderListItemBlock(ListItemBlock listItemBlock, Style? style = null, int indentLevel = 0) { - return new SpectreCompositeRenderable( - listItemBlock.Select(x => RenderBlock(x, suppressNewLine: true, style: style)) - ); + // Render children blocks for the list item + var renderedChildren = listItemBlock.Select(child => + { + if (child is ListBlock nestedList) + { + // Indent nested lists + return RenderListBlock(nestedList, style ?? Style.Plain, indentLevel + 1); + } + + // Maintain indentation for other blocks + return RenderBlock(child, suppressNewLine: true, style: style); + }); + + // Combine all child blocks without unnecessary line break + return new SpectreHorizontalCompositeRenderable(renderedChildren); } private IRenderable RenderTableBlock(Table table, Style? style = null) @@ -189,7 +185,6 @@ private IRenderable RenderTableBlock(Table table, Style? style = null) { var renderedTable = new Spectre.Console.Table(); - // Safe to unconditionally cast to TableRow as IsValid() ensures this is the case under the hood foreach (var tableRow in table.Cast()) if (tableRow.IsHeader) AddColumnsToTable(tableRow, table.ColumnDefinitions, renderedTable, style); @@ -209,7 +204,6 @@ private void AddColumnsToTable( Style? style = null ) { - // Safe to unconditionally cast to TableCell as IsValid() ensures this is the case under the hood foreach (var (cell, def) in tableRow.Cast().Zip(columnDefinitions)) renderedTable.AddColumn(new TableColumn(RenderTableCell(cell, def.Alignment, style))); } @@ -221,11 +215,11 @@ private void AddRowToTable( Style? style = null ) { - var renderedRow = new List(); - - // Safe to unconditionally cast to TableCell as IsValid() ensures this is the case under the hood - foreach (var (cell, def) in tableRow.Cast().Zip(columnDefinitions)) - renderedRow.Add(RenderTableCell(cell, def.Alignment, style)); + var renderedRow = tableRow + .Cast() + .Zip(columnDefinitions) + .Select(cellDef => RenderTableCell(cellDef.First, cellDef.Second.Alignment, style)) + .ToList(); renderedTable.AddRow(renderedRow); } @@ -238,68 +232,37 @@ private IRenderable RenderTableCell(TableCell tableCell, TableColumnAlign? markd TableColumnAlign.Center => Justify.Center, TableColumnAlign.Right => Justify.Right, null => Justify.Left, - _ => throw new ArgumentOutOfRangeException( - nameof(markdownAlignment), - markdownAlignment, - "Unable to convert between Markdig alignment and Spectre.Console alignment" - ), + _ => throw new ArgumentOutOfRangeException(), }; - return new SpectreCompositeRenderable( - tableCell.Select(x => RenderBlock(x, consoleAlignment, style: style, true)) + return new SpectreVerticalCompositeRenderable( + tableCell.Select(x => RenderBlock(x, consoleAlignment, style, true)) ); } - private IRenderable RenderParagraphBlock( - ParagraphBlock paragraphBlock, - Justify alignment, - Style style, - bool suppressNewLine = false - ) - { - var text = _inlineRendering.RenderContainerInline(paragraphBlock.Inline, style, alignment: alignment); - - if (!suppressNewLine) - { - return new SpectreCompositeRenderable(new List { text, new Text(Environment.NewLine) }); - } - - return new SpectreCompositeRenderable(new List { text }); - } - private IRenderable RenderHeadingBlock(HeadingBlock headingBlock, Style style) { var headingText = headingBlock.Inline?.GetInlineContent() ?? string.Empty; - var prefix = new string('#', headingBlock.Level); - return new SpectreCompositeRenderable( - new List { new Markup($" {prefix} {headingText} ", style), new Text(Environment.NewLine) } - ); + return new Markup($" {prefix} {headingText} ", style); } - // write items for each bullet - private static IEnumerable Interleave(IEnumerable seqA, IEnumerable seqB) + private IRenderable RenderParagraphBlock( + ParagraphBlock paragraphBlock, + Justify alignment, + Style style, + bool suppressNewLine = false + ) { - using var enumeratorA = seqA.GetEnumerator(); - using var enumeratorB = seqB.GetEnumerator(); - - while (enumeratorA.MoveNext()) - { - yield return enumeratorA.Current; - - if (enumeratorB.MoveNext()) - yield return enumeratorB.Current; - } + var text = _inlineRendering.RenderContainerInline(paragraphBlock.Inline, style, alignment: alignment); - while (enumeratorB.MoveNext()) - yield return enumeratorB.Current; + return suppressNewLine ? text : AppendBreakAfter(text); } private Style CreateStyle(StyleBase styleBase, Style? style = null) { style ??= Style.Parse(CreateStringStyle(styleBase)); - return style; } @@ -309,20 +272,7 @@ private string CreateStringStyle(StyleBase styleBase) var bold = styleBase.Bold ? "bold" : "default"; var underline = styleBase.Underline ? "underline" : "default"; - var style = - $"{ - styleBase.Foreground ?? "default" - } on { - styleBase.Background ?? "default" - } { - italic - } { - bold - } { - underline - }"; - - return style; + return $"{styleBase.Foreground ?? "default"} on {styleBase.Background ?? "default"} {italic} {bold} {underline}"; } public void Dispose() diff --git a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs index de8c92d..5e3eee1 100644 --- a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs +++ b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs @@ -21,7 +21,6 @@ private IRenderable RenderInline(Inline inline, Style style, Justify alignment) { switch (inline) { - // TODO: These features are less adopted in practice and the MarkdownPipeline isn't configured to generate them - feel free to add support! case JiraLink jiraLink: case SmartyPant smartyPant: case MathInline mathInline: @@ -90,7 +89,9 @@ public IRenderable RenderContainerInline( Justify alignment = Justify.Left ) { - return new SpectreCompositeRenderable(inline.Select(x => RenderInline(x, style ?? Style.Plain, alignment))); + return new SpectreVerticalCompositeRenderable( + inline.Select(x => RenderInline(x, style ?? Style.Plain, alignment)) + ); } private Markup WriteCodeInline(CodeInline code, Style style) diff --git a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreVerticalCompositeRenderable.cs b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreVerticalCompositeRenderable.cs new file mode 100644 index 0000000..812229f --- /dev/null +++ b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreVerticalCompositeRenderable.cs @@ -0,0 +1,23 @@ +using Spectre.Console; +using Spectre.Console.Rendering; + +namespace BuildingBlocks.SpectreConsole.Markdown; + +internal class SpectreVerticalCompositeRenderable(IEnumerable renderables) : Renderable +{ + protected override IEnumerable Render(RenderOptions options, int maxWidth) + { + return renderables.SelectMany(x => x.Render(options, maxWidth)); + } +} + +// Stacking elements vertically or adding multiple horizontal rows without extra space +internal class SpectreHorizontalCompositeRenderable(IEnumerable renderablesElements) : Renderable +{ + protected override IEnumerable Render(RenderOptions options, int maxWidth) + { + // Use Rows to combine renderables items in multiple horizontal rows without extra space + IRenderable rows = new Rows(renderablesElements); + return rows.Render(options, maxWidth); + } +} diff --git a/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs b/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs index 27607e5..ade21e9 100644 --- a/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs +++ b/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs @@ -29,10 +29,16 @@ public bool ConfirmationPrompt(string message) return input; } + public void InformationTextLine(string message, Justify? justify = null, Overflow? overflow = null) + { + InformationText(message, justify: justify, overflow: overflow); + console.WriteLine(); + } + public void InformationText(string message, Justify? justify = null, Overflow? overflow = null) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Information)}]{message}[/]" + Environment.NewLine) + new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Information)}]{message}[/]") { Overflow = overflow, Justification = justify, @@ -40,10 +46,16 @@ public void InformationText(string message, Justify? justify = null, Overflow? o ); } - public void WarningText(string message, Justify? justify = null, Overflow? overflow = null) + public void SummaryTextLine(string message, Justify? justify = null, Overflow? overflow = null) + { + SummaryText(message, justify: justify, overflow: overflow); + console.WriteLine(); + } + + public void SummaryText(string message, Justify? justify = null, Overflow? overflow = null) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Warning)}]{message}[/]" + Environment.NewLine) + new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Summary)}]{message}[/]") { Overflow = overflow, Justification = justify, @@ -51,10 +63,50 @@ public void WarningText(string message, Justify? justify = null, Overflow? overf ); } + public void HighlightTextLine(string message, Justify? justify = null, Overflow? overflow = null) + { + HighlightText(message, justify: justify, overflow: overflow); + console.WriteLine(); + } + + public void HighlightText(string message, Justify? justify = null, Overflow? overflow = null) + { + console.Write( + new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Highlight)}]{message}[/]") + { + Overflow = overflow, + Justification = justify, + } + ); + } + + public void NormalTextLine(string message, Justify? justify = null, Overflow? overflow = null) + { + NormalText(message, justify: justify, overflow: overflow); + console.WriteLine(); + } + public void NormalText(string message, Justify? justify = null, Overflow? overflow = null) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Text)}]{message}[/]" + Environment.NewLine) + new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Text)}]{message}[/]") + { + Overflow = overflow, + Justification = justify, + } + ); + } + + public void WarningTextLine(string message, Justify? justify = null, Overflow? overflow = null) + { + WarningText(message, justify: justify, overflow: overflow); + console.WriteLine(); + } + + public void WarningText(string message, Justify? justify = null, Overflow? overflow = null) + { + console.Write( + new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Warning)}]{message}[/]") { Overflow = overflow, Justification = justify, @@ -62,7 +114,7 @@ public void NormalText(string message, Justify? justify = null, Overflow? overfl ); } - public void ErrorText(string message, Justify? justify = null, Overflow? overflow = null) + public void ErrorTextLine(string message, Justify? justify = null, Overflow? overflow = null) { console.Write( new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Error)}]{message}[/]" + Environment.NewLine) @@ -73,7 +125,7 @@ public void ErrorText(string message, Justify? justify = null, Overflow? overflo ); } - public void SuccessText(string message, Justify? justify = null, Overflow? overflow = null) + public void SuccessTextLine(string message, Justify? justify = null, Overflow? overflow = null) { console.Write( new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Success)}]{message}[/]" + Environment.NewLine) @@ -96,7 +148,7 @@ public void WriteRule() public void Exception(string errorMessage, Exception ex) { - ErrorText(errorMessage); + ErrorTextLine(errorMessage); console.WriteException(ex, ExceptionFormats.ShortenEverything); } diff --git a/src/BuildingBlocks/SpectreConsole/StyleElements/ConsoleStyle.cs b/src/BuildingBlocks/SpectreConsole/StyleElements/ConsoleStyle.cs index 4d10e57..1f0eef2 100644 --- a/src/BuildingBlocks/SpectreConsole/StyleElements/ConsoleStyle.cs +++ b/src/BuildingBlocks/SpectreConsole/StyleElements/ConsoleStyle.cs @@ -9,6 +9,8 @@ public class ConsoleStyle public StyleBase Success { get; set; } = default!; public StyleBase Tree { get; set; } = default!; public StyleBase Information { get; set; } = default!; + public StyleBase Summary { get; set; } = default!; + public StyleBase Highlight { get; set; } = default!; public StyleBase Warning { get; set; } = default!; public StyleBase Confirmation { get; set; } = default!; } diff --git a/src/BuildingBlocks/SpectreConsole/Themes/dracula.json b/src/BuildingBlocks/SpectreConsole/Themes/dracula.json index 14fccde..c891b13 100644 --- a/src/BuildingBlocks/SpectreConsole/Themes/dracula.json +++ b/src/BuildingBlocks/SpectreConsole/Themes/dracula.json @@ -17,6 +17,12 @@ "confirmation": { "foreground": "#8be9fd" }, + "summary": { + "foreground": "#8be9fd" + }, + "highlight": { + "foreground": "#ff79c6" + }, "error": { "foreground": "#ff5555" }, diff --git a/src/Clients/AnthropicClient.cs b/src/Clients/AnthropicClient.cs index c2ea97d..a6c6581 100644 --- a/src/Clients/AnthropicClient.cs +++ b/src/Clients/AnthropicClient.cs @@ -23,13 +23,16 @@ namespace Clients; public class AnthropicClient( IHttpClientFactory httpClientFactory, - IOptions options, + IOptions llmOptions, ICacheModels cacheModels, ITokenizer tokenizer, AsyncPolicyWrap combinedPolicy ) : ILLMClient { - private readonly Model _chatModel = cacheModels.GetModel(options.Value.ChatModel); + private readonly Model _chatModel = + cacheModels.GetModel(llmOptions.Value.ChatModel) + ?? throw new KeyNotFoundException($"Model '{llmOptions.Value.ChatModel}' not found in the ModelCache."); + private readonly Model? _embeddingModel = cacheModels.GetModel(llmOptions.Value.EmbeddingsModel); private const int MaxRequestSizeInBytes = 100 * 1024; // 100KB public async Task GetCompletionAsync( diff --git a/src/Clients/AzureClient.cs b/src/Clients/AzureClient.cs index f333796..313cf6d 100644 --- a/src/Clients/AzureClient.cs +++ b/src/Clients/AzureClient.cs @@ -24,14 +24,16 @@ namespace Clients; public class AzureClient( IHttpClientFactory httpClientFactory, - IOptions options, + IOptions llmOptions, ICacheModels cacheModels, ITokenizer tokenizer, AsyncPolicyWrap combinedPolicy ) : ILLMClient { - private readonly Model _chatModel = cacheModels.GetModel(options.Value.ChatModel); - private readonly Model _embeddingModel = cacheModels.GetModel(options.Value.EmbeddingsModel); + private readonly Model _chatModel = + cacheModels.GetModel(llmOptions.Value.ChatModel) + ?? throw new KeyNotFoundException($"Model '{llmOptions.Value.ChatModel}' not found in the ModelCache."); + private readonly Model? _embeddingModel = cacheModels.GetModel(llmOptions.Value.EmbeddingsModel); private const int MaxRequestSizeInBytes = 100 * 1024; // 100KB public async Task GetCompletionAsync( @@ -247,6 +249,7 @@ AsyncPolicyWrap combinedPolicy await ValidateEmbeddingMaxInputToken(string.Concat(inputs)); ValidateRequestSizeAndContent(string.Concat(inputs)); + ArgumentNullException.ThrowIfNull(_embeddingModel); var requestBody = new { input = inputs, @@ -258,11 +261,11 @@ AsyncPolicyWrap combinedPolicy var apiVersion = Environment.GetEnvironmentVariable(ClientsConstants.Environments.EmbeddingsApiVersion) - ?? _chatModel.ModelOption.ApiVersion; + ?? _embeddingModel.ModelOption.ApiVersion; var deploymentId = Environment.GetEnvironmentVariable(ClientsConstants.Environments.EmbeddingsDeploymentId) - ?? _chatModel.ModelOption.DeploymentId; + ?? _embeddingModel.ModelOption.DeploymentId; ArgumentException.ThrowIfNullOrEmpty(apiVersion); ArgumentException.ThrowIfNullOrEmpty(deploymentId); diff --git a/src/Clients/CacheModels.cs b/src/Clients/CacheModels.cs index c188f34..3540703 100644 --- a/src/Clients/CacheModels.cs +++ b/src/Clients/CacheModels.cs @@ -14,12 +14,18 @@ public class CacheModels : ICacheModels { private readonly ModelsInformationOptions _modelsInformation; private readonly ModelsOptions _modelOptions; + private readonly LLMOptions _llmOptions; private readonly Dictionary _models = new(); - public CacheModels(IOptions modelOptions, IOptions modelsInformation) + public CacheModels( + IOptions modelOptions, + IOptions modelsInformation, + IOptions llmOptions + ) { _modelsInformation = modelsInformation.Value; _modelOptions = modelOptions.Value; + _llmOptions = llmOptions.Value; InitCache(); } @@ -85,9 +91,12 @@ private void InitCache() options )!; - foreach (var (originalName, information) in predefinedModelsInformation.Where(x => x.Value.Enabled)) + foreach ( + var (originalName, predefinedModelInformation) in predefinedModelsInformation.Where(x => x.Value.Enabled) + ) { - var modelOption = predefinedModelOptions.GetValueOrDefault(originalName); + var predefinedModelOption = predefinedModelOptions.GetValueOrDefault(originalName); + var overrideModelOption = _modelOptions.GetValueOrDefault(originalName); var overrideModelInformation = _modelsInformation.GetValueOrDefault(originalName); @@ -98,39 +107,62 @@ private void InitCache() ModelOption = new ModelOption { CodeAssistType = - overrideModelOption?.CodeAssistType ?? modelOption?.CodeAssistType ?? CodeAssistType.Embedding, + overrideModelOption?.CodeAssistType + ?? _llmOptions.CodeAssistType + ?? predefinedModelOption?.CodeAssistType + ?? CodeAssistType.Embedding, CodeDiffType = - overrideModelOption?.CodeDiffType ?? modelOption?.CodeDiffType ?? CodeDiffType.CodeBlockDiff, - Threshold = overrideModelOption?.Threshold ?? modelOption?.Threshold ?? 0.4m, - Temperature = overrideModelOption?.Temperature ?? modelOption?.Temperature ?? 0.2m, - ApiVersion = overrideModelOption?.ApiVersion ?? modelOption?.ApiVersion, - BaseAddress = overrideModelOption?.BaseAddress ?? modelOption?.BaseAddress, - DeploymentId = overrideModelOption?.DeploymentId ?? modelOption?.DeploymentId, + overrideModelOption?.CodeDiffType + ?? _llmOptions.CodeDiffType + ?? predefinedModelOption?.CodeDiffType + ?? CodeDiffType.CodeBlockDiff, + Threshold = + overrideModelOption?.Threshold + ?? _llmOptions.Threshold + ?? predefinedModelOption?.Threshold + ?? 0.4m, + Temperature = + overrideModelOption?.Temperature + ?? _llmOptions.Temperature + ?? predefinedModelOption?.Temperature + ?? 0.2m, + ApiVersion = overrideModelOption?.ApiVersion ?? predefinedModelOption?.ApiVersion, + BaseAddress = overrideModelOption?.BaseAddress ?? predefinedModelOption?.BaseAddress, + DeploymentId = overrideModelOption?.DeploymentId ?? predefinedModelOption?.DeploymentId, }, ModelInformation = new ModelInformation { - AIProvider = overrideModelInformation?.AIProvider ?? information.AIProvider, - ModelType = overrideModelInformation?.ModelType ?? information.ModelType, - MaxTokens = overrideModelInformation?.MaxTokens ?? information.MaxTokens, - MaxInputTokens = overrideModelInformation?.MaxInputTokens ?? information.MaxInputTokens, - MaxOutputTokens = overrideModelInformation?.MaxOutputTokens ?? information.MaxOutputTokens, - InputCostPerToken = overrideModelInformation?.InputCostPerToken ?? information.InputCostPerToken, - OutputCostPerToken = overrideModelInformation?.OutputCostPerToken ?? information.OutputCostPerToken, - OutputVectorSize = overrideModelInformation?.OutputVectorSize ?? information.OutputVectorSize, - Enabled = overrideModelInformation?.Enabled ?? information.Enabled, + AIProvider = overrideModelInformation?.AIProvider ?? predefinedModelInformation.AIProvider, + ModelType = overrideModelInformation?.ModelType ?? predefinedModelInformation.ModelType, + MaxTokens = overrideModelInformation?.MaxTokens ?? predefinedModelInformation.MaxTokens, + MaxInputTokens = + overrideModelInformation?.MaxInputTokens ?? predefinedModelInformation.MaxInputTokens, + MaxOutputTokens = + overrideModelInformation?.MaxOutputTokens ?? predefinedModelInformation.MaxOutputTokens, + InputCostPerToken = + overrideModelInformation?.InputCostPerToken ?? predefinedModelInformation.InputCostPerToken, + OutputCostPerToken = + overrideModelInformation?.OutputCostPerToken ?? predefinedModelInformation.OutputCostPerToken, + OutputVectorSize = + overrideModelInformation?.OutputVectorSize ?? predefinedModelInformation.OutputVectorSize, + Enabled = overrideModelInformation?.Enabled ?? predefinedModelInformation.Enabled, SupportsFunctionCalling = - overrideModelInformation?.SupportsFunctionCalling ?? information.SupportsFunctionCalling, + overrideModelInformation?.SupportsFunctionCalling + ?? predefinedModelInformation.SupportsFunctionCalling, SupportsParallelFunctionCalling = overrideModelInformation?.SupportsParallelFunctionCalling - ?? information.SupportsParallelFunctionCalling, - SupportsVision = overrideModelInformation?.SupportsVision ?? information.SupportsVision, + ?? predefinedModelInformation.SupportsParallelFunctionCalling, + SupportsVision = + overrideModelInformation?.SupportsVision ?? predefinedModelInformation.SupportsVision, EmbeddingDimensions = - overrideModelInformation?.EmbeddingDimensions ?? information.EmbeddingDimensions, - SupportsAudioInput = overrideModelInformation?.SupportsAudioInput ?? information.SupportsAudioInput, + overrideModelInformation?.EmbeddingDimensions ?? predefinedModelInformation.EmbeddingDimensions, + SupportsAudioInput = + overrideModelInformation?.SupportsAudioInput ?? predefinedModelInformation.SupportsAudioInput, SupportsAudioOutput = - overrideModelInformation?.SupportsAudioOutput ?? information.SupportsAudioOutput, + overrideModelInformation?.SupportsAudioOutput ?? predefinedModelInformation.SupportsAudioOutput, SupportsPromptCaching = - overrideModelInformation?.SupportsPromptCaching ?? information.SupportsPromptCaching, + overrideModelInformation?.SupportsPromptCaching + ?? predefinedModelInformation.SupportsPromptCaching, }, }; diff --git a/src/Clients/OllamaClient.cs b/src/Clients/OllamaClient.cs index 0796659..ae5c354 100644 --- a/src/Clients/OllamaClient.cs +++ b/src/Clients/OllamaClient.cs @@ -24,14 +24,16 @@ namespace Clients; public class OllamaClient( IHttpClientFactory httpClientFactory, - IOptions options, + IOptions llmOptions, ICacheModels cacheModels, ITokenizer tokenizer, AsyncPolicyWrap combinedPolicy ) : ILLMClient { - private readonly Model _chatModel = cacheModels.GetModel(options.Value.ChatModel); - private readonly Model _embeddingModel = cacheModels.GetModel(options.Value.EmbeddingsModel); + private readonly Model _chatModel = + cacheModels.GetModel(llmOptions.Value.ChatModel) + ?? throw new KeyNotFoundException($"Model '{llmOptions.Value.ChatModel}' not found in the ModelCache."); + private readonly Model? _embeddingModel = cacheModels.GetModel(llmOptions.Value.EmbeddingsModel); private const int MaxRequestSizeInBytes = 100 * 1024; // 100KB public async Task GetCompletionAsync( @@ -190,6 +192,8 @@ AsyncPolicyWrap combinedPolicy await ValidateEmbeddingMaxInputToken(string.Concat(inputs), path); ValidateRequestSizeAndContent(string.Concat(inputs)); + ArgumentNullException.ThrowIfNull(_embeddingModel); + // https://github.com/ollama/ollama/blob/main/docs/api.md#generate-embeddings var requestBody = new { diff --git a/src/Clients/OpenAiClient.cs b/src/Clients/OpenAiClient.cs index 3ffabde..710bf1a 100644 --- a/src/Clients/OpenAiClient.cs +++ b/src/Clients/OpenAiClient.cs @@ -23,14 +23,16 @@ namespace Clients; public class OpenAiClient( IHttpClientFactory httpClientFactory, - IOptions options, + IOptions llmOptions, ICacheModels cacheModels, ITokenizer tokenizer, AsyncPolicyWrap combinedPolicy ) : ILLMClient { - private readonly Model _chatModel = cacheModels.GetModel(options.Value.ChatModel); - private readonly Model _embeddingModel = cacheModels.GetModel(options.Value.EmbeddingsModel); + private readonly Model _chatModel = + cacheModels.GetModel(llmOptions.Value.ChatModel) + ?? throw new KeyNotFoundException($"Model '{llmOptions.Value.ChatModel}' not found in the ModelCache."); + private readonly Model? _embeddingModel = cacheModels.GetModel(llmOptions.Value.EmbeddingsModel); private const int MaxRequestSizeInBytes = 100 * 1024; // 100KB public async Task GetCompletionAsync( @@ -212,6 +214,8 @@ AsyncPolicyWrap combinedPolicy await ValidateEmbeddingMaxInputToken(string.Concat(inputs), path); ValidateRequestSizeAndContent(string.Concat(inputs)); + ArgumentNullException.ThrowIfNull(_embeddingModel); + var requestBody = new { input = inputs, diff --git a/src/Clients/Options/LLMOptions.cs b/src/Clients/Options/LLMOptions.cs index 57bafe4..632494e 100644 --- a/src/Clients/Options/LLMOptions.cs +++ b/src/Clients/Options/LLMOptions.cs @@ -1,7 +1,13 @@ +using Clients.Models; + namespace Clients.Options; public class LLMOptions { public string ChatModel { get; set; } = default!; - public string EmbeddingsModel { get; set; } = default!; + public string? EmbeddingsModel { get; set; } + public CodeAssistType? CodeAssistType { get; set; } + public CodeDiffType? CodeDiffType { get; set; } + public decimal? Temperature { get; set; } + public decimal? Threshold { get; set; } } diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj b/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj index afeb675..14aeb72 100644 --- a/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj +++ b/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj @@ -19,9 +19,6 @@ Always - - Always - Always @@ -62,9 +59,6 @@ Always - - Always - Always diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/SpectreConsole/Markdown/SpectreMarkdownTests.cs b/tests/UnitTests/BuildingBlocks.UnitTests/SpectreConsole/Markdown/SpectreMarkdownTests.cs new file mode 100644 index 0000000..3b2b8ec --- /dev/null +++ b/tests/UnitTests/BuildingBlocks.UnitTests/SpectreConsole/Markdown/SpectreMarkdownTests.cs @@ -0,0 +1,34 @@ +using BuildingBlocks.SpectreConsole.Markdown; +using Spectre.Console; +using Xunit; + +namespace BuildingBlocks.UnitTests.SpectreConsole.Markdown; + +public class SpectreMarkdownTests +{ + [Fact] + public void Test() + { + var s = new SpectreMarkdown( + @"1. **Calculate()**: + - **Description**: Calculates the sum of two numbers and updates the result field. + - **Returns**: The result of the addition as a double. +2. **AddNumbers(double first, double second)**: + - **Description**: A private method that adds two numbers. + - **Returns**: The sum of the two numbers." + ); + AnsiConsole.Write(s); + } + + [Fact] + public void Test2() + { + var s = new SpectreMarkdown( + @"Here are the method names inside the `Add` class: + +- `Calculate()` +- `AddNumbers(double first, double second)`" + ); + AnsiConsole.Write(s); + } +} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/Utilities/FilesUtilitiesTests.cs b/tests/UnitTests/BuildingBlocks.UnitTests/Utilities/FilesUtilitiesTests.cs index 4385ef8..22cb506 100644 --- a/tests/UnitTests/BuildingBlocks.UnitTests/Utilities/FilesUtilitiesTests.cs +++ b/tests/UnitTests/BuildingBlocks.UnitTests/Utilities/FilesUtilitiesTests.cs @@ -43,210 +43,210 @@ public Task DisposeAsync() return Task.CompletedTask; } - [Theory] - [InlineData("bin/test.txt", true)] // Should be ignored - [InlineData("bin", true)] // Should be ignored - [InlineData("obj/test.txt", true)] // Should be ignored - [InlineData("src/main.cs", false)] // Should not be ignored - [InlineData("docs/readme.md", false)] // Should not be ignored - public void Test_IgnoredDirectoriesAndFiles(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("bin", true)] // Normal case - [InlineData("BIN", true)] // Uppercase - [InlineData("Bin", true)] // Mixed case - [InlineData("BiN/test.dll", true)] // Mixed case with file - [InlineData("bin/test.dll", true)] // Normal case with file - [InlineData("src/main.cs", false)] // Source directory, not ignored - public void Test_IgnoredBinWithDifferentCase(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("obj", true)] // The obj directory itself should be ignored - [InlineData("obj/Debug/test.dll", true)] // Files inside obj directory should be ignored - [InlineData("obj/Release/test.dll", true)] // Files inside obj directory should be ignored - [InlineData("src/main.cs", false)] // Should not be ignored (valid source folder) - [InlineData("docs/readme.md", false)] // Should not be ignored (documentation folder) - public void Test_IgnoredObjDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("obj", true)] // Normal case - [InlineData("OBJ", true)] // Uppercase - [InlineData("Obj", true)] // Mixed case - [InlineData("oBj/Debug/test.dll", true)] // Mixed case with a file path inside obj - [InlineData("obj/test.dll", true)] // Normal case with a file inside obj - [InlineData("src/main.cs", false)] // Source directory, should not be ignored - [InlineData("docs/readme.md", false)] // Documentation file, should not be ignored - public void Test_IgnoredObjWithDifferentCase(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("bin", true)] // Root bin directory - [InlineData("bin/", true)] // Explicit directory pattern with a trailing slash - [InlineData("bin/test.dll", true)] // File inside the bin directory - [InlineData("bin/Debug/test.dll", true)] // File inside a subdirectory of bin - [InlineData("src/bin/test.dll", false)] // A bin directory inside another path, should not be ignored - [InlineData("binExtra/test.dll", false)] // Similar name but different, should not be ignored - public void Test_IgnoredBinPatternFromGitignore(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("src/main.cs", false)] // Source file outside of bin - [InlineData("docs/readme.md", false)] // Documentation file outside of bin - [InlineData("assets/image.png", false)] // Image file outside of bin - [InlineData("config/settings.json", false)] // Configuration file outside of bin - [InlineData("binTest/test.dll", false)] // Directory similar to bin but should not be ignored - [InlineData("README.md", false)] // Project root file - public void Test_NotIgnoredFileOutsideBinDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("node_modules", true)] // The node_modules directory itself should be ignored - [InlineData("node_modules/", true)] // Explicit directory pattern with a trailing slash - [InlineData("node_modules/package.json", true)] // A file inside the node_modules directory - [InlineData("node_modules/react/index.js", true)] // A file in a subdirectory of node_modules - [InlineData("src/node_modules/test.js", false)] // A node_modules directory inside another path, should not be ignored - [InlineData("README.md", false)] // A file in the root that should not be ignored - public void Test_IgnoredNodeModulesDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("temp", true)] // The temp directory itself should be ignored - [InlineData("temp/", true)] // Explicit directory pattern with a trailing slash - [InlineData("temp/file.tmp", true)] // A file inside the temp directory - [InlineData("temp/session/anotherfile.tmp", true)] // A file in a subdirectory of temp - [InlineData("src/temp/test.tmp", false)] // A temp directory inside another path, should not be ignored - [InlineData("tempFiles/somefile.txt", false)] // A similar but different directory, should not be ignored - [InlineData("README.md", false)] // A file in the root that should not be ignored - public void Test_IgnoredTempDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("Models/Add.cs", false)] // Checking Add.cs file in Models folder - [InlineData("Models/Subtract.cs", false)] // Checking Subtract.cs file in Models folder - [InlineData("Models/Multiply.cs", false)] // Checking Multiply.cs file in Models folder - [InlineData("Models/Divide.cs", false)] // Checking Divide.cs file in Models folder - [InlineData("Program.cs", false)] // Checking Program.cs file in the root - [InlineData("Calculator.csproj", false)] // Checking Calculator.csproj in the root - public void Test_NotIgnoredFileInValidDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("logs", true)] // The logs directory itself should be ignored - [InlineData("logs/", true)] // Explicit directory pattern with a trailing slash - [InlineData("logs/application.log", true)] // A log file inside the logs directory - [InlineData("logs/2024-10-09.log", true)] // Another log file with a date pattern - [InlineData("src/logs/debug.log", false)] // A logs directory inside another path (should not be ignored) - [InlineData("temporaryLogs/test.log", false)] // A different directory that should not be ignored - [InlineData("README.md", false)] // A file that should not be ignored - public void Test_IgnoredLogsDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData(".env", true)] // The root .env file should be ignored - [InlineData(".env.example", false)] // An example env file, should not be ignored - [InlineData("config/.env", true)] // A .env file inside a config directory should be ignored - [InlineData("src/.env", true)] // Another .env file in a source folder - [InlineData("data/.env.backup", false)] // A backup file that should not be ignored - [InlineData("temp/.env.tmp", true)] // A temporary .env file that should be ignored - [InlineData("README.md", false)] // A file that is not a .env file and should not be ignored - public void Test_IgnoredDotEnvFile(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData(".keep", false)] - [InlineData("src/.keep", false)] - public void Test_NotIgnoredFileWithAllowedPrefix(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("cache", true)] // The cache directory itself should be ignored - [InlineData("cache/", true)] // Explicit directory pattern with a trailing slash - [InlineData("cache/temp.txt", true)] // A file inside the cache directory should be ignored - [InlineData("cache/images/image1.png", true)] // An image file inside a cache subdirectory should be ignored - [InlineData("src/cache/temp.log", false)] // A cache directory inside src should not be ignored - [InlineData("temporaryCache/test.txt", false)] // A different directory that should not be ignored - [InlineData("README.md", false)] // A file that is not in any cache directory and should not be ignored - public void Test_IgnoredCacheDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } + // [Theory] + // [InlineData("bin/test.txt", true)] // Should be ignored + // [InlineData("bin", true)] // Should be ignored + // [InlineData("obj/test.txt", true)] // Should be ignored + // [InlineData("src/main.cs", false)] // Should not be ignored + // [InlineData("docs/readme.md", false)] // Should not be ignored + // public void Test_IgnoredDirectoriesAndFiles(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("bin", true)] // Normal case + // [InlineData("BIN", true)] // Uppercase + // [InlineData("Bin", true)] // Mixed case + // [InlineData("BiN/test.dll", true)] // Mixed case with file + // [InlineData("bin/test.dll", true)] // Normal case with file + // [InlineData("src/main.cs", false)] // Source directory, not ignored + // public void Test_IgnoredBinWithDifferentCase(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("obj", true)] // The obj directory itself should be ignored + // [InlineData("obj/Debug/test.dll", true)] // Files inside obj directory should be ignored + // [InlineData("obj/Release/test.dll", true)] // Files inside obj directory should be ignored + // [InlineData("src/main.cs", false)] // Should not be ignored (valid source folder) + // [InlineData("docs/readme.md", false)] // Should not be ignored (documentation folder) + // public void Test_IgnoredObjDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("obj", true)] // Normal case + // [InlineData("OBJ", true)] // Uppercase + // [InlineData("Obj", true)] // Mixed case + // [InlineData("oBj/Debug/test.dll", true)] // Mixed case with a file path inside obj + // [InlineData("obj/test.dll", true)] // Normal case with a file inside obj + // [InlineData("src/main.cs", false)] // Source directory, should not be ignored + // [InlineData("docs/readme.md", false)] // Documentation file, should not be ignored + // public void Test_IgnoredObjWithDifferentCase(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("bin", true)] // Root bin directory + // [InlineData("bin/", true)] // Explicit directory pattern with a trailing slash + // [InlineData("bin/test.dll", true)] // File inside the bin directory + // [InlineData("bin/Debug/test.dll", true)] // File inside a subdirectory of bin + // [InlineData("src/bin/test.dll", false)] // A bin directory inside another path, should not be ignored + // [InlineData("binExtra/test.dll", false)] // Similar name but different, should not be ignored + // public void Test_IgnoredBinPatternFromGitignore(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("src/main.cs", false)] // Source file outside of bin + // [InlineData("docs/readme.md", false)] // Documentation file outside of bin + // [InlineData("assets/image.png", false)] // Image file outside of bin + // [InlineData("config/settings.json", false)] // Configuration file outside of bin + // [InlineData("binTest/test.dll", false)] // Directory similar to bin but should not be ignored + // [InlineData("README.md", false)] // Project root file + // public void Test_NotIgnoredFileOutsideBinDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("node_modules", true)] // The node_modules directory itself should be ignored + // [InlineData("node_modules/", true)] // Explicit directory pattern with a trailing slash + // [InlineData("node_modules/package.json", true)] // A file inside the node_modules directory + // [InlineData("node_modules/react/index.js", true)] // A file in a subdirectory of node_modules + // [InlineData("src/node_modules/test.js", false)] // A node_modules directory inside another path, should not be ignored + // [InlineData("README.md", false)] // A file in the root that should not be ignored + // public void Test_IgnoredNodeModulesDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("temp", true)] // The temp directory itself should be ignored + // [InlineData("temp/", true)] // Explicit directory pattern with a trailing slash + // [InlineData("temp/file.tmp", true)] // A file inside the temp directory + // [InlineData("temp/session/anotherfile.tmp", true)] // A file in a subdirectory of temp + // [InlineData("src/temp/test.tmp", false)] // A temp directory inside another path, should not be ignored + // [InlineData("tempFiles/somefile.txt", false)] // A similar but different directory, should not be ignored + // [InlineData("README.md", false)] // A file in the root that should not be ignored + // public void Test_IgnoredTempDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("Models/Add.cs", false)] // Checking Add.cs file in Models folder + // [InlineData("Models/Subtract.cs", false)] // Checking Subtract.cs file in Models folder + // [InlineData("Models/Multiply.cs", false)] // Checking Multiply.cs file in Models folder + // [InlineData("Models/Divide.cs", false)] // Checking Divide.cs file in Models folder + // [InlineData("Program.cs", false)] // Checking Program.cs file in the root + // [InlineData("Calculator.csproj", false)] // Checking Calculator.csproj in the root + // public void Test_NotIgnoredFileInValidDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("logs", true)] // The logs directory itself should be ignored + // [InlineData("logs/", true)] // Explicit directory pattern with a trailing slash + // [InlineData("logs/application.log", true)] // A log file inside the logs directory + // [InlineData("logs/2024-10-09.log", true)] // Another log file with a date pattern + // [InlineData("src/logs/debug.log", false)] // A logs directory inside another path (should not be ignored) + // [InlineData("temporaryLogs/test.log", false)] // A different directory that should not be ignored + // [InlineData("README.md", false)] // A file that should not be ignored + // public void Test_IgnoredLogsDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData(".env", true)] // The root .env file should be ignored + // [InlineData(".env.example", false)] // An example env file, should not be ignored + // [InlineData("config/.env", true)] // A .env file inside a config directory should be ignored + // [InlineData("src/.env", true)] // Another .env file in a source folder + // [InlineData("data/.env.backup", false)] // A backup file that should not be ignored + // [InlineData("temp/.env.tmp", true)] // A temporary .env file that should be ignored + // [InlineData("README.md", false)] // A file that is not a .env file and should not be ignored + // public void Test_IgnoredDotEnvFile(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData(".keep", false)] + // [InlineData("src/.keep", false)] + // public void Test_NotIgnoredFileWithAllowedPrefix(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("cache", true)] // The cache directory itself should be ignored + // [InlineData("cache/", true)] // Explicit directory pattern with a trailing slash + // [InlineData("cache/temp.txt", true)] // A file inside the cache directory should be ignored + // [InlineData("cache/images/image1.png", true)] // An image file inside a cache subdirectory should be ignored + // [InlineData("src/cache/temp.log", false)] // A cache directory inside src should not be ignored + // [InlineData("temporaryCache/test.txt", false)] // A different directory that should not be ignored + // [InlineData("README.md", false)] // A file that is not in any cache directory and should not be ignored + // public void Test_IgnoredCacheDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } } diff --git a/version.json b/version.json index 33f7ba2..9c870d9 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.0.0-preview", + "version": "1.0.0-preview2", "gitCommitIdShortAutoMinimum": 7, "nugetPackageVersion": { "semVer": 2 From 034a7a0e3223ab215b732ec709c04f898d154c45 Mon Sep 17 00:00:00 2001 From: Mehdi Hadeli Date: Fri, 29 Nov 2024 20:31:46 +0330 Subject: [PATCH 2/4] fix: ci bug fix --- .../BuildingBlocks.UnitTests.csproj | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj b/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj index 14aeb72..96cddbf 100644 --- a/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj +++ b/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj @@ -7,14 +7,12 @@ + + - - TestData\Calculator\.gitignore - Always - Always @@ -58,14 +56,6 @@ Always - - - - Always - - - - From fb9ef6b457b28896f316740dd0d98b664cd168c6 Mon Sep 17 00:00:00 2001 From: Mehdi Hadeli Date: Fri, 29 Nov 2024 20:46:43 +0330 Subject: [PATCH 3/4] fix: fix ci bug --- src/AIAssist/AIAssist.nuspec | 3 -- .../BuildingBlocks.UnitTests.csproj | 46 ------------------- .../TestData/Calculator/Calculator.csproj | 10 ---- .../TestData/Calculator/IOperation.cs | 6 --- .../TestData/Calculator/Models/Add.cs | 19 -------- .../TestData/Calculator/Models/Divide.cs | 23 ---------- .../TestData/Calculator/Models/Multiply.cs | 14 ------ .../TestData/Calculator/Models/Subtract.cs | 14 ------ .../TestData/Calculator/Program.cs | 28 ----------- 9 files changed, 163 deletions(-) delete mode 100644 tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Calculator.csproj delete mode 100644 tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/IOperation.cs delete mode 100644 tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Add.cs delete mode 100644 tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Divide.cs delete mode 100644 tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Multiply.cs delete mode 100644 tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Subtract.cs delete mode 100644 tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Program.cs diff --git a/src/AIAssist/AIAssist.nuspec b/src/AIAssist/AIAssist.nuspec index 6089a18..6ce5b96 100644 --- a/src/AIAssist/AIAssist.nuspec +++ b/src/AIAssist/AIAssist.nuspec @@ -7,9 +7,6 @@ - - - AIAssist AIAssist diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj b/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj index 96cddbf..8b9e4bb 100644 --- a/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj +++ b/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj @@ -12,50 +12,4 @@ - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Calculator.csproj b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Calculator.csproj deleted file mode 100644 index 2150e37..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Calculator.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/IOperation.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/IOperation.cs deleted file mode 100644 index 81e3384..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/IOperation.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BuildingBlocks.UnitTests.TestData.Calculator; - -public interface IOperation -{ - double Calculate(); -} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Add.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Add.cs deleted file mode 100644 index 568b0f4..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Add.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace BuildingBlocks.UnitTests.TestData.Calculator.Models; - -/// -/// Add two value -/// -/// -/// -public class Add(double number1, double number2) : IOperation -{ - public double Calculate() - { - return AddNumbers(); - } - - private double AddNumbers() - { - return number1 / number2; - } -} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Divide.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Divide.cs deleted file mode 100644 index 1f2f8c1..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Divide.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace BuildingBlocks.UnitTests.TestData.Calculator.Models; - -/// -/// Divide to values -/// -/// -/// -public class Divide(double number1, double number2) : IOperation -{ - public double Calculate() - { - return DivideNumbers(); - } - - private double DivideNumbers() - { - if (number1 == 0) - { - throw new DivideByZeroException("Cannot divide by zero."); - } - return number1 / number2; - } -} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Multiply.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Multiply.cs deleted file mode 100644 index 1795772..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Multiply.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace BuildingBlocks.UnitTests.TestData.Calculator.Models; - -/// -/// Multiply two values -/// -/// -/// -public class Multiply(double number1, double number2) : IOperation -{ - public double Calculate() - { - return number1 * number2; - } -} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Subtract.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Subtract.cs deleted file mode 100644 index 871d99e..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Subtract.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace BuildingBlocks.UnitTests.TestData.Calculator.Models; - -/// -/// Subtract two values -/// -/// -/// -public class Subtract(double number1, double number2) : IOperation -{ - public double Calculate() - { - return number1 - number2; - } -} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Program.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Program.cs deleted file mode 100644 index 23bfea6..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Program.cs +++ /dev/null @@ -1,28 +0,0 @@ -using BuildingBlocks.UnitTests.TestData.Calculator; -using BuildingBlocks.UnitTests.TestData.Calculator.Models; - -Console.WriteLine("Simple Calculator\n"); - -// Input first number -Console.Write("Enter the first number: "); -double num1 = Convert.ToDouble(Console.ReadLine()); - -// Input operator -Console.Write("Enter an operation (+, -, *, /): "); -char operation = Convert.ToChar(Console.ReadLine()); - -// Input second number -Console.Write("Enter the second number: "); -double num2 = Convert.ToDouble(Console.ReadLine()); - -IOperation calculation = operation switch -{ - '+' => new Add(num1, num2), - '-' => new Subtract(num1, num2), - '*' => new Multiply(num1, num2), - '/' => new Divide(num1, num2), - _ => throw new InvalidOperationException("Invalid operation"), -}; - -double result = calculation.Calculate(); -Console.WriteLine($"Result: {result}"); From 4b903653d3ea9c090e5cb92e1586949cb454a581 Mon Sep 17 00:00:00 2001 From: Mehdi Hadeli Date: Sat, 30 Nov 2024 01:33:56 +0330 Subject: [PATCH 4/4] refactor: :recycle: enhance cache-models and model-options loading --- src/AIAssist/Commands/CodeAssistCommand.cs | 54 ++++++-- .../DependencyInjectionExtensions.cs | 34 +++-- .../EmbeddingCodeAssist.cs | 4 +- .../TreeSitterCodeAssistSummary.cs | 4 +- src/AIAssist/Services/LLMClientManager.cs | 15 ++- .../SpectreConsole/ColorTheme.cs | 2 + .../Contracts/ISpectreUtilities.cs | 74 ++++++++-- .../Markdown/SpectreMarkdownBlockRendering.cs | 2 +- .../SpectreMarkdownInlineRendering.cs | 2 +- .../SpectreConsole/SpectreUtilities.cs | 126 ++++++++++++++---- .../SpectreConsole/Themes/dracula.json | 1 + src/Clients/AnthropicClient.cs | 23 ++-- src/Clients/AzureClient.cs | 59 ++++---- src/Clients/CacheModels.cs | 125 ++++++++--------- .../Converters/AIProviderTypeConverter.cs | 53 -------- .../Converters/CodeAssistTypeConverter.cs | 43 ------ .../Converters/CodeDiffTypeConverter.cs | 49 ------- src/Clients/Converters/ModelTypeConverter.cs | 46 ------- src/Clients/Converters/RoleTypeConverter.cs | 1 + src/Clients/LLMs/models_information_list.json | 8 +- src/Clients/Models/Model.cs | 24 +++- src/Clients/Models/ModelInformation.cs | 13 +- src/Clients/Models/ModelOption.cs | 8 +- src/Clients/OllamaClient.cs | 45 +++---- src/Clients/OpenAiClient.cs | 45 +++---- 25 files changed, 420 insertions(+), 440 deletions(-) delete mode 100644 src/Clients/Converters/AIProviderTypeConverter.cs delete mode 100644 src/Clients/Converters/CodeAssistTypeConverter.cs delete mode 100644 src/Clients/Converters/CodeDiffTypeConverter.cs delete mode 100644 src/Clients/Converters/ModelTypeConverter.cs diff --git a/src/AIAssist/Commands/CodeAssistCommand.cs b/src/AIAssist/Commands/CodeAssistCommand.cs index fe30c6e..c1be460 100644 --- a/src/AIAssist/Commands/CodeAssistCommand.cs +++ b/src/AIAssist/Commands/CodeAssistCommand.cs @@ -30,7 +30,7 @@ IOptions appOptions private readonly AppOptions _appOptions = appOptions.Value; private readonly Model _chatModel = cacheModels.GetModel(llmOptions.Value.ChatModel) - ?? throw new KeyNotFoundException($"Model '{llmOptions.Value.ChatModel}' not found in the ModelCache."); + ?? throw new ArgumentNullException($"Model '{llmOptions.Value.ChatModel}' not found in the ModelCache."); private readonly Model? _embeddingModel = cacheModels.GetModel(llmOptions.Value.EmbeddingsModel); private static bool _running = true; @@ -122,10 +122,22 @@ public override async Task ExecuteAsync(CommandContext context, Settings se SetupOptions(settings); spectreUtilities.SummaryTextLine("Code assist mode is activated!"); - spectreUtilities.SummaryTextLine( - $"Chat model: {_chatModel.Name} | Embedding model: {_embeddingModel?.Name ?? "-"} | CodeAssistType: {_chatModel.ModelOption.CodeAssistType} | CodeDiffType: {_chatModel.ModelOption.CodeDiffType}" + spectreUtilities.NormalText("Chat model: "); + spectreUtilities.HighlightTextLine(_chatModel.Name); + + spectreUtilities.NormalText("Embedding model: "); + spectreUtilities.HighlightTextLine(_embeddingModel?.Name ?? "-"); + + spectreUtilities.NormalText("CodeAssistType: "); + spectreUtilities.HighlightTextLine(_chatModel.CodeAssistType.ToString()); + + spectreUtilities.NormalText("CodeDiffType: "); + spectreUtilities.HighlightTextLine(_chatModel.CodeDiffType.ToString()); + + spectreUtilities.NormalTextLine( + "Please 'Ctrl+H' to see all available commands in the code assist mode.", + decoration: Decoration.Bold ); - spectreUtilities.SummaryTextLine("Please 'Ctrl+H' to see all available commands in the code assist mode."); spectreUtilities.WriteRule(); await AnsiConsole @@ -189,42 +201,42 @@ private void SetupOptions(Settings settings) if (!string.IsNullOrEmpty(settings.ChatModelApiKey)) { - _chatModel.ModelOption.ApiKey = settings.ChatModelApiKey.Trim(); + _chatModel.ApiKey = settings.ChatModelApiKey.Trim(); } if (!string.IsNullOrEmpty(settings.ChatApiVersion)) { - _chatModel.ModelOption.ApiVersion = settings.ChatApiVersion.Trim(); + _chatModel.ApiVersion = settings.ChatApiVersion.Trim(); } if (!string.IsNullOrEmpty(settings.ChatDeploymentId)) { - _chatModel.ModelOption.DeploymentId = settings.ChatDeploymentId.Trim(); + _chatModel.DeploymentId = settings.ChatDeploymentId.Trim(); } if (!string.IsNullOrEmpty(settings.ChatBaseAddress)) { - _chatModel.ModelOption.BaseAddress = settings.ChatBaseAddress.Trim(); + _chatModel.BaseAddress = settings.ChatBaseAddress.Trim(); } if (!string.IsNullOrEmpty(settings.EmbeddingsModelApiKey) && _embeddingModel is not null) { - _embeddingModel.ModelOption.ApiKey = settings.EmbeddingsModelApiKey.Trim(); + _embeddingModel.ApiKey = settings.EmbeddingsModelApiKey.Trim(); } if (!string.IsNullOrEmpty(settings.EmbeddingsApiVersion) && _embeddingModel is not null) { - _embeddingModel.ModelOption.ApiVersion = settings.EmbeddingsApiVersion.Trim(); + _embeddingModel.ApiVersion = settings.EmbeddingsApiVersion.Trim(); } if (!string.IsNullOrEmpty(settings.EmbeddingsDeploymentId) && _embeddingModel is not null) { - _embeddingModel.ModelOption.DeploymentId = settings.EmbeddingsDeploymentId.Trim(); + _embeddingModel.DeploymentId = settings.EmbeddingsDeploymentId.Trim(); } if (!string.IsNullOrEmpty(settings.EmbeddingsBaseAddress) && _embeddingModel is not null) { - _embeddingModel.ModelOption.BaseAddress = settings.EmbeddingsBaseAddress.Trim(); + _embeddingModel.BaseAddress = settings.EmbeddingsBaseAddress.Trim(); } _appOptions.ContextWorkingDirectory = !string.IsNullOrEmpty(settings.ContextWorkingDirectory) @@ -246,21 +258,37 @@ private void SetupOptions(Settings settings) if (settings.CodeDiffType is not null) { _llmOptions.CodeDiffType = settings.CodeDiffType.Value; + _chatModel.CodeDiffType = settings.CodeDiffType.Value; + + if (_embeddingModel != null) + _embeddingModel.CodeDiffType = settings.CodeDiffType.Value; } if (settings.CodeAssistType is not null) { _llmOptions.CodeAssistType = settings.CodeAssistType.Value; + _chatModel.CodeAssistType = settings.CodeAssistType.Value; + + if (_embeddingModel != null) + _embeddingModel.CodeAssistType = settings.CodeAssistType.Value; } - if (settings.Threshold is not null && _embeddingModel is not null) + if (settings.Threshold is not null) { _llmOptions.Threshold = settings.Threshold.Value; + _chatModel.Threshold = settings.Threshold.Value; + + if (_embeddingModel != null) + _embeddingModel.Threshold = settings.Threshold.Value; } if (settings.Temperature is not null) { _llmOptions.Temperature = settings.Temperature.Value; + _chatModel.Temperature = settings.Temperature.Value; + + if (_embeddingModel != null) + _embeddingModel.Temperature = settings.Temperature.Value; } } } diff --git a/src/AIAssist/Extensions/DependencyInjectionExtensions.cs b/src/AIAssist/Extensions/DependencyInjectionExtensions.cs index 0a70674..ee07906 100644 --- a/src/AIAssist/Extensions/DependencyInjectionExtensions.cs +++ b/src/AIAssist/Extensions/DependencyInjectionExtensions.cs @@ -212,7 +212,9 @@ private static void AddCodeAssistDependencies(HostApplicationBuilder builder) var chatModel = cacheModels.GetModel(llmOptions.Value.ChatModel); - ICodeAssist codeAssist = factory.Create(chatModel.ModelOption.CodeAssistType); + ArgumentNullException.ThrowIfNull(chatModel); + + ICodeAssist codeAssist = factory.Create(chatModel.CodeAssistType); return new CodeAssistantManager(codeAssist, codeDiffManager); }); @@ -284,17 +286,19 @@ private static void AddClientDependencies(HostApplicationBuilder builder) var options = sp.GetRequiredService>().Value; var policyOptions = sp.GetRequiredService>().Value; - var cacheModels = sp.GetRequiredService(); ArgumentException.ThrowIfNullOrEmpty(options.ChatModel); + + var cacheModels = sp.GetRequiredService(); var chatModel = cacheModels.GetModel(options.ChatModel); + ArgumentNullException.ThrowIfNull(chatModel); client.Timeout = TimeSpan.FromSeconds(policyOptions.TimeoutSeconds); var chatApiKey = Environment.GetEnvironmentVariable(ClientsConstants.Environments.ChatModelApiKey) - ?? chatModel.ModelOption.ApiKey; + ?? chatModel.ApiKey; - switch (chatModel.ModelInformation.AIProvider) + switch (chatModel.AIProvider) { case AIProvider.Openai: { @@ -303,7 +307,7 @@ private static void AddClientDependencies(HostApplicationBuilder builder) var baseAddress = Environment.GetEnvironmentVariable(ClientsConstants.Environments.ChatBaseAddress) - ?? chatModel.ModelOption.BaseAddress + ?? chatModel.BaseAddress ?? "https://api.openai.com"; client.BaseAddress = new Uri(baseAddress.Trim()); @@ -320,7 +324,7 @@ private static void AddClientDependencies(HostApplicationBuilder builder) var baseAddress = Environment.GetEnvironmentVariable(ClientsConstants.Environments.ChatBaseAddress) - ?? chatModel.ModelOption.BaseAddress; + ?? chatModel.BaseAddress; ArgumentException.ThrowIfNullOrEmpty(baseAddress); client.BaseAddress = new Uri(baseAddress.Trim()); @@ -332,7 +336,7 @@ private static void AddClientDependencies(HostApplicationBuilder builder) { var baseAddress = Environment.GetEnvironmentVariable(ClientsConstants.Environments.ChatBaseAddress) - ?? chatModel.ModelOption.BaseAddress + ?? chatModel.BaseAddress ?? "http://localhost:11434"; // https://github.com/ollama/ollama/blob/main/docs/api.md @@ -359,15 +363,17 @@ private static void AddClientDependencies(HostApplicationBuilder builder) var cacheModels = sp.GetRequiredService(); ArgumentException.ThrowIfNullOrEmpty(options.EmbeddingsModel); + var embeddingModel = cacheModels.GetModel(options.EmbeddingsModel); + ArgumentNullException.ThrowIfNull(embeddingModel); client.Timeout = TimeSpan.FromSeconds(policyOptions.TimeoutSeconds); var embeddingsApiKey = Environment.GetEnvironmentVariable(ClientsConstants.Environments.EmbeddingsModelApiKey) - ?? embeddingModel.ModelOption.ApiKey; + ?? embeddingModel.ApiKey; - switch (embeddingModel.ModelInformation.AIProvider) + switch (embeddingModel.AIProvider) { case AIProvider.Openai: { @@ -376,7 +382,7 @@ private static void AddClientDependencies(HostApplicationBuilder builder) var baseAddress = Environment.GetEnvironmentVariable(ClientsConstants.Environments.EmbeddingsBaseAddress) - ?? embeddingModel.ModelOption.BaseAddress + ?? embeddingModel.BaseAddress ?? "https://api.openai.com"; client.BaseAddress = new Uri(baseAddress.Trim()); @@ -393,7 +399,7 @@ private static void AddClientDependencies(HostApplicationBuilder builder) var baseAddress = Environment.GetEnvironmentVariable(ClientsConstants.Environments.EmbeddingsBaseAddress) - ?? embeddingModel.ModelOption.BaseAddress; + ?? embeddingModel.BaseAddress; ArgumentException.ThrowIfNullOrEmpty(baseAddress); client.BaseAddress = new Uri(baseAddress.Trim()); @@ -405,7 +411,7 @@ private static void AddClientDependencies(HostApplicationBuilder builder) { var baseAddress = Environment.GetEnvironmentVariable(ClientsConstants.Environments.EmbeddingsBaseAddress) - ?? embeddingModel.ModelOption.BaseAddress + ?? embeddingModel.BaseAddress ?? "http://localhost:11434"; // https://github.com/ollama/ollama/blob/main/docs/api.md @@ -505,7 +511,9 @@ private static void AddCodeDiffDependency(HostApplicationBuilder builder) var cacheModels = sp.GetRequiredService(); var chatModel = cacheModels.GetModel(options.Value.ChatModel); - var codeDiffParser = factory.Create(chatModel.ModelOption.CodeDiffType); + ArgumentNullException.ThrowIfNull(chatModel); + + var codeDiffParser = factory.Create(chatModel.CodeDiffType); var codeDiffUpdater = sp.GetRequiredService(); diff --git a/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs b/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs index 469f063..d3fde7f 100644 --- a/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs +++ b/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs @@ -85,8 +85,8 @@ public Task> GetCodeTreeContents(IList? codeFiles) var systemPrompt = promptManager.GetSystemPrompt( embeddingOriginalTreeCodes, - llmClientManager.ChatModel.ModelOption.CodeAssistType, - llmClientManager.ChatModel.ModelOption.CodeDiffType + llmClientManager.ChatModel.CodeAssistType, + llmClientManager.ChatModel.CodeDiffType ); // Generate a response from the language model (e.g., OpenAI or Llama) diff --git a/src/AIAssist/Services/CodeAssistStrategies/TreeSitterCodeAssistSummary.cs b/src/AIAssist/Services/CodeAssistStrategies/TreeSitterCodeAssistSummary.cs index debe278..0abdd49 100644 --- a/src/AIAssist/Services/CodeAssistStrategies/TreeSitterCodeAssistSummary.cs +++ b/src/AIAssist/Services/CodeAssistStrategies/TreeSitterCodeAssistSummary.cs @@ -50,8 +50,8 @@ public Task> GetCodeTreeContents(IList? codeFiles) var systemPrompt = promptManager.GetSystemPrompt( summaryTreeCodes, - llmClientManager.ChatModel.ModelOption.CodeAssistType, - llmClientManager.ChatModel.ModelOption.CodeDiffType + llmClientManager.ChatModel.CodeAssistType, + llmClientManager.ChatModel.CodeDiffType ); // Generate a response from the language model (e.g., OpenAI or Llama) diff --git a/src/AIAssist/Services/LLMClientManager.cs b/src/AIAssist/Services/LLMClientManager.cs index 4782d0e..9b9e8c8 100644 --- a/src/AIAssist/Services/LLMClientManager.cs +++ b/src/AIAssist/Services/LLMClientManager.cs @@ -29,12 +29,14 @@ ICacheModels cacheModels _tokenizer = tokenizer; EmbeddingModel = cacheModels.GetModel(llmOptions.Value.EmbeddingsModel); - ChatModel = cacheModels.GetModel(llmOptions.Value.ChatModel); - EmbeddingThreshold = EmbeddingModel.ModelOption.Threshold; + ChatModel = + cacheModels.GetModel(llmOptions.Value.ChatModel) + ?? throw new ArgumentNullException($"Model '{llmOptions.Value.ChatModel}' not found in the CacheModels."); + EmbeddingThreshold = EmbeddingModel?.Threshold ?? 0.2m; } public Model ChatModel { get; } - public Model EmbeddingModel { get; } + public Model? EmbeddingModel { get; } public decimal EmbeddingThreshold { get; } public async IAsyncEnumerable GetCompletionStreamAsync( @@ -50,7 +52,7 @@ ICacheModels cacheModels var chatItems = chatSession.GetChatItemsFromHistory(); - var llmClientStratgey = _clientFactory.CreateClient(ChatModel.ModelInformation.AIProvider); + var llmClientStratgey = _clientFactory.CreateClient(ChatModel.AIProvider); var chatCompletionResponseStreams = llmClientStratgey.GetCompletionStreamAsync( new ChatCompletionRequest(chatItems.Select(x => new ChatCompletionRequestItem(x.Role, x.Prompt))), @@ -94,14 +96,15 @@ public async Task GetEmbeddingAsync( CancellationToken cancellationToken = default ) { - var llmClientStratgey = _clientFactory.CreateClient(EmbeddingModel.ModelInformation.AIProvider); + ArgumentNullException.ThrowIfNull(EmbeddingModel); + var llmClientStratgey = _clientFactory.CreateClient(EmbeddingModel.AIProvider); var embeddingResponse = await llmClientStratgey.GetEmbeddingAsync(inputs, path, cancellationToken); // in embedding output tokens and its cost is 0 var inputTokens = embeddingResponse?.TokenUsage?.InputTokens ?? await _tokenizer.GetTokenCount(string.Concat(inputs)); - var cost = inputTokens * EmbeddingModel.ModelInformation.InputCostPerToken; + var cost = inputTokens * EmbeddingModel.InputCostPerToken; return new GetEmbeddingResult(embeddingResponse?.Embeddings ?? new List>(), inputTokens, cost); } diff --git a/src/BuildingBlocks/SpectreConsole/ColorTheme.cs b/src/BuildingBlocks/SpectreConsole/ColorTheme.cs index 4095602..779c420 100644 --- a/src/BuildingBlocks/SpectreConsole/ColorTheme.cs +++ b/src/BuildingBlocks/SpectreConsole/ColorTheme.cs @@ -7,6 +7,8 @@ public class ColorTheme { public string Name { get; set; } = default!; + public string? Foreground { get; set; } = default!; + [JsonPropertyName("console")] public ConsoleStyle ConsoleStyle { get; set; } = default!; diff --git a/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs b/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs index a4b33a4..fb5f4bd 100644 --- a/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs +++ b/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs @@ -6,18 +6,68 @@ public interface ISpectreUtilities { bool ConfirmationPrompt(string message); string? UserPrompt(string? promptMessage = null); - void InformationTextLine(string message, Justify? justify = null, Overflow? overflow = null); - void InformationText(string message, Justify? justify = null, Overflow? overflow = null); - public void SummaryTextLine(string message, Justify? justify = null, Overflow? overflow = null); - public void SummaryText(string message, Justify? justify = null, Overflow? overflow = null); - public void HighlightTextLine(string message, Justify? justify = null, Overflow? overflow = null); - public void HighlightText(string message, Justify? justify = null, Overflow? overflow = null); - void NormalTextLine(string message, Justify? justify = null, Overflow? overflow = null); - void NormalText(string message, Justify? justify = null, Overflow? overflow = null); - void WarningTextLine(string message, Justify? justify = null, Overflow? overflow = null); - void WarningText(string message, Justify? justify = null, Overflow? overflow = null); - void ErrorTextLine(string message, Justify? justify = null, Overflow? overflow = null); - void SuccessTextLine(string message, Justify? justify = null, Overflow? overflow = null); + void InformationTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ); + void InformationText( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ); + public void SummaryTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ); + public void SummaryText( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ); + public void HighlightTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ); + public void HighlightText( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ); + void NormalTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ); + void NormalText(string message, Justify? justify = null, Overflow? overflow = null, Decoration? decoration = null); + void WarningTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ); + void WarningText(string message, Justify? justify = null, Overflow? overflow = null, Decoration? decoration = null); + void ErrorTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ); + void SuccessTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ); void WriteCursor(); void WriteRule(); void Exception(string errorMessage, Exception ex); diff --git a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs index 6cdce6c..3808203 100644 --- a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs +++ b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs @@ -272,7 +272,7 @@ private string CreateStringStyle(StyleBase styleBase) var bold = styleBase.Bold ? "bold" : "default"; var underline = styleBase.Underline ? "underline" : "default"; - return $"{styleBase.Foreground ?? "default"} on {styleBase.Background ?? "default"} {italic} {bold} {underline}"; + return $"{styleBase.Foreground ?? _colorTheme.Foreground ?? "default"} on {styleBase.Background ?? "default"} {italic} {bold} {underline}"; } public void Dispose() diff --git a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs index 5e3eee1..0ef9b38 100644 --- a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs +++ b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs @@ -120,7 +120,7 @@ private string CreateStringStyle(StyleBase styleBase) var style = $"{ - styleBase.Foreground ?? "default" + styleBase.Foreground ?? colorTheme.Foreground ?? "default" } on { styleBase.Background ?? "default" } { diff --git a/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs b/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs index ade21e9..fab3c2d 100644 --- a/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs +++ b/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs @@ -29,16 +29,29 @@ public bool ConfirmationPrompt(string message) return input; } - public void InformationTextLine(string message, Justify? justify = null, Overflow? overflow = null) + public void InformationTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { - InformationText(message, justify: justify, overflow: overflow); + InformationText(message, justify: justify, overflow: overflow, decoration: decoration); console.WriteLine(); } - public void InformationText(string message, Justify? justify = null, Overflow? overflow = null) + public void InformationText( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Information)}]{message}[/]") + new Markup( + $"[{CreateStringStyle(theme.ConsoleStyle.Information)}]{message}[/]", + new Style(decoration: decoration) + ) { Overflow = overflow, Justification = justify, @@ -46,16 +59,29 @@ public void InformationText(string message, Justify? justify = null, Overflow? o ); } - public void SummaryTextLine(string message, Justify? justify = null, Overflow? overflow = null) + public void SummaryTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { - SummaryText(message, justify: justify, overflow: overflow); + SummaryText(message, justify: justify, overflow: overflow, decoration: decoration); console.WriteLine(); } - public void SummaryText(string message, Justify? justify = null, Overflow? overflow = null) + public void SummaryText( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Summary)}]{message}[/]") + new Markup( + $"[{CreateStringStyle(theme.ConsoleStyle.Summary)}]{message}[/]", + new Style(decoration: decoration) + ) { Overflow = overflow, Justification = justify, @@ -63,16 +89,29 @@ public void SummaryText(string message, Justify? justify = null, Overflow? overf ); } - public void HighlightTextLine(string message, Justify? justify = null, Overflow? overflow = null) + public void HighlightTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { - HighlightText(message, justify: justify, overflow: overflow); + HighlightText(message, justify: justify, overflow: overflow, decoration: decoration); console.WriteLine(); } - public void HighlightText(string message, Justify? justify = null, Overflow? overflow = null) + public void HighlightText( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Highlight)}]{message}[/]") + new Markup( + $"[{CreateStringStyle(theme.ConsoleStyle.Highlight)}]{message}[/]", + new Style(decoration: decoration) + ) { Overflow = overflow, Justification = justify, @@ -80,16 +119,26 @@ public void HighlightText(string message, Justify? justify = null, Overflow? ove ); } - public void NormalTextLine(string message, Justify? justify = null, Overflow? overflow = null) + public void NormalTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { - NormalText(message, justify: justify, overflow: overflow); + NormalText(message, justify: justify, overflow: overflow, decoration: decoration); console.WriteLine(); } - public void NormalText(string message, Justify? justify = null, Overflow? overflow = null) + public void NormalText( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Text)}]{message}[/]") + new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Text)}]{message}[/]", new Style(decoration: decoration)) { Overflow = overflow, Justification = justify, @@ -97,16 +146,29 @@ public void NormalText(string message, Justify? justify = null, Overflow? overfl ); } - public void WarningTextLine(string message, Justify? justify = null, Overflow? overflow = null) + public void WarningTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { - WarningText(message, justify: justify, overflow: overflow); + WarningText(message, justify: justify, overflow: overflow, decoration: decoration); console.WriteLine(); } - public void WarningText(string message, Justify? justify = null, Overflow? overflow = null) + public void WarningText( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Warning)}]{message}[/]") + new Markup( + $"[{CreateStringStyle(theme.ConsoleStyle.Warning)}]{message}[/]", + new Style(decoration: decoration) + ) { Overflow = overflow, Justification = justify, @@ -114,10 +176,18 @@ public void WarningText(string message, Justify? justify = null, Overflow? overf ); } - public void ErrorTextLine(string message, Justify? justify = null, Overflow? overflow = null) + public void ErrorTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Error)}]{message}[/]" + Environment.NewLine) + new Markup( + $"[{CreateStringStyle(theme.ConsoleStyle.Error)}]{message}[/]" + Environment.NewLine, + new Style(decoration: decoration) + ) { Overflow = overflow, Justification = justify, @@ -125,10 +195,18 @@ public void ErrorTextLine(string message, Justify? justify = null, Overflow? ove ); } - public void SuccessTextLine(string message, Justify? justify = null, Overflow? overflow = null) + public void SuccessTextLine( + string message, + Justify? justify = null, + Overflow? overflow = null, + Decoration? decoration = null + ) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Success)}]{message}[/]" + Environment.NewLine) + new Markup( + $"[{CreateStringStyle(theme.ConsoleStyle.Success)}]{message}[/]" + Environment.NewLine, + new Style(decoration: decoration) + ) { Overflow = overflow, Justification = justify, diff --git a/src/BuildingBlocks/SpectreConsole/Themes/dracula.json b/src/BuildingBlocks/SpectreConsole/Themes/dracula.json index c891b13..f7147fd 100644 --- a/src/BuildingBlocks/SpectreConsole/Themes/dracula.json +++ b/src/BuildingBlocks/SpectreConsole/Themes/dracula.json @@ -1,6 +1,7 @@ { "$schema": "./spectre_console_schema.json", "name": "dracula", + "foreground": "#f8f8f2", "console": { "prompt": { "foreground": "#bd93f9" diff --git a/src/Clients/AnthropicClient.cs b/src/Clients/AnthropicClient.cs index a6c6581..ccdbc21 100644 --- a/src/Clients/AnthropicClient.cs +++ b/src/Clients/AnthropicClient.cs @@ -51,7 +51,7 @@ AsyncPolicyWrap combinedPolicy role = x.Role.Humanize(LetterCasing.LowerCase), content = x.Prompt, }), - temperature = _chatModel.ModelOption.Temperature, + temperature = _chatModel.Temperature, }; var client = httpClientFactory.CreateClient("llm_chat_client"); @@ -80,8 +80,8 @@ AsyncPolicyWrap combinedPolicy var inputTokens = completionResponse.Usage?.InputTokens ?? 0; var outTokens = completionResponse.Usage?.OutputTokens ?? 0; - var inputCostPerToken = _chatModel.ModelInformation.InputCostPerToken; - var outputCostPerToken = _chatModel.ModelInformation.OutputCostPerToken; + var inputCostPerToken = _chatModel.InputCostPerToken; + var outputCostPerToken = _chatModel.OutputCostPerToken; ValidateChatMaxToken(inputTokens + outTokens); @@ -107,7 +107,7 @@ AsyncPolicyWrap combinedPolicy role = x.Role.Humanize(LetterCasing.LowerCase), content = x.Prompt, }), - temperature = _chatModel.ModelOption.Temperature, + temperature = _chatModel.Temperature, stream = true, }; @@ -165,8 +165,8 @@ AsyncPolicyWrap combinedPolicy // we have the usage in the last chunk and done state var inputTokens = completionStreamResponse.Usage?.InputTokens ?? 0; var outTokens = completionStreamResponse.Usage?.OutputTokens ?? 0; - var inputCostPerToken = _chatModel.ModelInformation.InputCostPerToken; - var outputCostPerToken = _chatModel.ModelInformation.OutputCostPerToken; + var inputCostPerToken = _chatModel.InputCostPerToken; + var outputCostPerToken = _chatModel.OutputCostPerToken; ValidateChatMaxToken(inputTokens + outTokens); @@ -241,17 +241,14 @@ private async Task ValidateChatMaxInputToken(ChatCompletionRequest chatCompletio string.Concat(chatCompletionRequest.Items.Select(x => x.Prompt)) ); - if ( - _chatModel.ModelInformation.MaxInputTokens > 0 - && inputTokenCount > _chatModel.ModelInformation.MaxInputTokens - ) + if (_chatModel.MaxInputTokens > 0 && inputTokenCount > _chatModel.MaxInputTokens) { throw new AnthropicException( new AnthropicError { StatusCode = (int)HttpStatusCode.BadRequest, Message = - $"current chat 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_chatModel.ModelInformation.MaxInputTokens.FormatCommas()}", + $"current chat 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_chatModel.MaxInputTokens.FormatCommas()}", }, HttpStatusCode.BadRequest ); @@ -260,14 +257,14 @@ private async Task ValidateChatMaxInputToken(ChatCompletionRequest chatCompletio private void ValidateChatMaxToken(int maxTokenCount) { - if (_chatModel.ModelInformation.MaxTokens > 0 && maxTokenCount > _chatModel.ModelInformation.MaxTokens) + if (_chatModel.MaxTokens > 0 && maxTokenCount > _chatModel.MaxTokens) { throw new AnthropicException( new AnthropicError { StatusCode = (int)HttpStatusCode.BadRequest, Message = - $"current chat 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_chatModel.ModelInformation.MaxTokens.FormatCommas()}.", + $"current chat 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_chatModel.MaxTokens.FormatCommas()}.", }, HttpStatusCode.BadRequest ); diff --git a/src/Clients/AzureClient.cs b/src/Clients/AzureClient.cs index 313cf6d..94cbd39 100644 --- a/src/Clients/AzureClient.cs +++ b/src/Clients/AzureClient.cs @@ -53,18 +53,17 @@ AsyncPolicyWrap combinedPolicy role = x.Role.Humanize(LetterCasing.LowerCase), content = x.Prompt, }), - temperature = _chatModel.ModelOption.Temperature, + temperature = _chatModel.Temperature, }; var client = httpClientFactory.CreateClient("llm_chat_client"); var apiVersion = - Environment.GetEnvironmentVariable(ClientsConstants.Environments.ChatApiVersion) - ?? _chatModel.ModelOption.ApiVersion; + Environment.GetEnvironmentVariable(ClientsConstants.Environments.ChatApiVersion) ?? _chatModel.ApiVersion; var deploymentId = Environment.GetEnvironmentVariable(ClientsConstants.Environments.ChatDeploymentId) - ?? _chatModel.ModelOption.DeploymentId; + ?? _chatModel.DeploymentId; ArgumentException.ThrowIfNullOrEmpty(apiVersion); ArgumentException.ThrowIfNullOrEmpty(deploymentId); @@ -101,8 +100,8 @@ AsyncPolicyWrap combinedPolicy var inputTokens = completionResponse.Usage?.PromptTokens ?? 0; var outTokens = completionResponse.Usage?.CompletionTokens ?? 0; - var inputCostPerToken = _chatModel.ModelInformation.InputCostPerToken; - var outputCostPerToken = _chatModel.ModelInformation.OutputCostPerToken; + var inputCostPerToken = _chatModel.InputCostPerToken; + var outputCostPerToken = _chatModel.OutputCostPerToken; ValidateChatMaxToken(inputTokens + outTokens); @@ -128,7 +127,7 @@ AsyncPolicyWrap combinedPolicy role = x.Role.Humanize(LetterCasing.LowerCase), content = x.Prompt, }), - temperature = _chatModel.ModelOption.Temperature, + temperature = _chatModel.Temperature, stream = true, // https://cookbook.openai.com/examples/how_to_stream_completions#4-how-to-get-token-usage-data-for-streamed-chat-completion-response stream_options = new { include_usage = true }, @@ -137,12 +136,11 @@ AsyncPolicyWrap combinedPolicy var client = httpClientFactory.CreateClient("llm_chat_client"); var apiVersion = - Environment.GetEnvironmentVariable(ClientsConstants.Environments.ChatApiVersion) - ?? _chatModel.ModelOption.ApiVersion; + Environment.GetEnvironmentVariable(ClientsConstants.Environments.ChatApiVersion) ?? _chatModel.ApiVersion; var deploymentId = Environment.GetEnvironmentVariable(ClientsConstants.Environments.ChatDeploymentId) - ?? _chatModel.ModelOption.DeploymentId; + ?? _chatModel.DeploymentId; ArgumentException.ThrowIfNullOrEmpty(apiVersion); ArgumentException.ThrowIfNullOrEmpty(deploymentId); @@ -218,8 +216,8 @@ AsyncPolicyWrap combinedPolicy // Capture the `usage` data from the final chunk and after done var inputTokens = completionStreamResponse.Usage?.PromptTokens ?? 0; var outTokens = completionStreamResponse.Usage?.CompletionTokens ?? 0; - var inputCostPerToken = _chatModel.ModelInformation.InputCostPerToken; - var outputCostPerToken = _chatModel.ModelInformation.OutputCostPerToken; + var inputCostPerToken = _chatModel.InputCostPerToken; + var outputCostPerToken = _chatModel.OutputCostPerToken; ValidateChatMaxToken(inputTokens + outTokens); @@ -254,18 +252,18 @@ AsyncPolicyWrap combinedPolicy { input = inputs, model = _embeddingModel.Name.Trim(), - dimensions = _embeddingModel.ModelInformation.EmbeddingDimensions, + dimensions = _embeddingModel.EmbeddingDimensions, }; var client = httpClientFactory.CreateClient("llm_embeddings_client"); var apiVersion = Environment.GetEnvironmentVariable(ClientsConstants.Environments.EmbeddingsApiVersion) - ?? _embeddingModel.ModelOption.ApiVersion; + ?? _embeddingModel.ApiVersion; var deploymentId = Environment.GetEnvironmentVariable(ClientsConstants.Environments.EmbeddingsDeploymentId) - ?? _embeddingModel.ModelOption.DeploymentId; + ?? _embeddingModel.DeploymentId; ArgumentException.ThrowIfNullOrEmpty(apiVersion); ArgumentException.ThrowIfNullOrEmpty(deploymentId); @@ -298,8 +296,8 @@ AsyncPolicyWrap combinedPolicy var inputTokens = embeddingResponse.Usage?.PromptTokens ?? 0; var outTokens = embeddingResponse.Usage?.CompletionTokens ?? 0; - var inputCostPerToken = _embeddingModel.ModelInformation.InputCostPerToken; - var outputCostPerToken = _embeddingModel.ModelInformation.OutputCostPerToken; + var inputCostPerToken = _embeddingModel.InputCostPerToken; + var outputCostPerToken = _embeddingModel.OutputCostPerToken; ValidateEmbeddingMaxToken(inputTokens + outTokens, path); @@ -344,17 +342,14 @@ private async Task ValidateChatMaxInputToken(ChatCompletionRequest chatCompletio string.Concat(chatCompletionRequest.Items.Select(x => x.Prompt), false) ); - if ( - _chatModel.ModelInformation.MaxInputTokens > 0 - && inputTokenCount > _chatModel.ModelInformation.MaxInputTokens - ) + if (_chatModel.MaxInputTokens > 0 && inputTokenCount > _chatModel.MaxInputTokens) { throw new OpenAIException( new OpenAIError { StatusCode = (int)HttpStatusCode.BadRequest, Message = - $"current chat 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_chatModel.ModelInformation.MaxInputTokens.FormatCommas()}.", + $"current chat 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_chatModel.MaxInputTokens.FormatCommas()}.", }, HttpStatusCode.BadRequest ); @@ -365,10 +360,8 @@ private async Task ValidateEmbeddingMaxInputToken(string input, string? path = n { var inputTokenCount = await tokenizer.GetTokenCount(input); - if ( - _embeddingModel.ModelInformation.MaxInputTokens > 0 - && inputTokenCount > _embeddingModel.ModelInformation.MaxInputTokens - ) + ArgumentNullException.ThrowIfNull(_embeddingModel); + if (_embeddingModel.MaxInputTokens > 0 && inputTokenCount > _embeddingModel.MaxInputTokens) { var moreInfo = path is not null ? $"if file '{ @@ -381,7 +374,7 @@ private async Task ValidateEmbeddingMaxInputToken(string input, string? path = n { StatusCode = (int)HttpStatusCode.BadRequest, Message = - $"embedding {path} 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_embeddingModel.ModelInformation.MaxInputTokens.FormatCommas()}. {moreInfo}", + $"embedding {path} 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_embeddingModel.MaxInputTokens.FormatCommas()}. {moreInfo}", }, HttpStatusCode.BadRequest ); @@ -390,14 +383,14 @@ private async Task ValidateEmbeddingMaxInputToken(string input, string? path = n private void ValidateChatMaxToken(int maxTokenCount) { - if (_chatModel.ModelInformation.MaxTokens > 0 && maxTokenCount > _chatModel.ModelInformation.MaxTokens) + if (_chatModel.MaxTokens > 0 && maxTokenCount > _chatModel.MaxTokens) { throw new OpenAIException( new OpenAIError { StatusCode = (int)HttpStatusCode.BadRequest, Message = - $"current chat 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_chatModel.ModelInformation.MaxTokens.FormatCommas()}.", + $"current chat 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_chatModel.MaxTokens.FormatCommas()}.", }, HttpStatusCode.BadRequest ); @@ -406,17 +399,15 @@ private void ValidateChatMaxToken(int maxTokenCount) private void ValidateEmbeddingMaxToken(int maxTokenCount, string? path) { - if ( - _embeddingModel.ModelInformation.MaxTokens > 0 - && maxTokenCount > _embeddingModel.ModelInformation.MaxTokens - ) + ArgumentNullException.ThrowIfNull(_embeddingModel); + if (_embeddingModel.MaxTokens > 0 && maxTokenCount > _embeddingModel.MaxTokens) { throw new OpenAIException( new OpenAIError { StatusCode = (int)HttpStatusCode.BadRequest, Message = - $"embedding {path} 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_embeddingModel.ModelInformation.MaxTokens.FormatCommas()}.", + $"embedding {path} 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_embeddingModel.MaxTokens.FormatCommas()}.", }, HttpStatusCode.BadRequest ); diff --git a/src/Clients/CacheModels.cs b/src/Clients/CacheModels.cs index 3540703..683ed39 100644 --- a/src/Clients/CacheModels.cs +++ b/src/Clients/CacheModels.cs @@ -51,10 +51,11 @@ IOptions llmOptions private bool TryGetModelWithFallback(string modelName, out Model model) { var parts = modelName.Split('/'); + if ( parts.Length == 2 && _models.TryGetValue(parts[1], out var fallbackModel) - && fallbackModel.ModelInformation.AIProvider.ToString() == parts[0] + && fallbackModel.AIProvider.ToString() == parts[0] ) { model = fallbackModel; @@ -104,66 +105,68 @@ private void InitCache() { Name = GetName(originalName), OriginalName = originalName, - ModelOption = new ModelOption - { - CodeAssistType = - overrideModelOption?.CodeAssistType - ?? _llmOptions.CodeAssistType - ?? predefinedModelOption?.CodeAssistType - ?? CodeAssistType.Embedding, - CodeDiffType = - overrideModelOption?.CodeDiffType - ?? _llmOptions.CodeDiffType - ?? predefinedModelOption?.CodeDiffType - ?? CodeDiffType.CodeBlockDiff, - Threshold = - overrideModelOption?.Threshold - ?? _llmOptions.Threshold - ?? predefinedModelOption?.Threshold - ?? 0.4m, - Temperature = - overrideModelOption?.Temperature - ?? _llmOptions.Temperature - ?? predefinedModelOption?.Temperature - ?? 0.2m, - ApiVersion = overrideModelOption?.ApiVersion ?? predefinedModelOption?.ApiVersion, - BaseAddress = overrideModelOption?.BaseAddress ?? predefinedModelOption?.BaseAddress, - DeploymentId = overrideModelOption?.DeploymentId ?? predefinedModelOption?.DeploymentId, - }, - ModelInformation = new ModelInformation - { - AIProvider = overrideModelInformation?.AIProvider ?? predefinedModelInformation.AIProvider, - ModelType = overrideModelInformation?.ModelType ?? predefinedModelInformation.ModelType, - MaxTokens = overrideModelInformation?.MaxTokens ?? predefinedModelInformation.MaxTokens, - MaxInputTokens = - overrideModelInformation?.MaxInputTokens ?? predefinedModelInformation.MaxInputTokens, - MaxOutputTokens = - overrideModelInformation?.MaxOutputTokens ?? predefinedModelInformation.MaxOutputTokens, - InputCostPerToken = - overrideModelInformation?.InputCostPerToken ?? predefinedModelInformation.InputCostPerToken, - OutputCostPerToken = - overrideModelInformation?.OutputCostPerToken ?? predefinedModelInformation.OutputCostPerToken, - OutputVectorSize = - overrideModelInformation?.OutputVectorSize ?? predefinedModelInformation.OutputVectorSize, - Enabled = overrideModelInformation?.Enabled ?? predefinedModelInformation.Enabled, - SupportsFunctionCalling = - overrideModelInformation?.SupportsFunctionCalling - ?? predefinedModelInformation.SupportsFunctionCalling, - SupportsParallelFunctionCalling = - overrideModelInformation?.SupportsParallelFunctionCalling - ?? predefinedModelInformation.SupportsParallelFunctionCalling, - SupportsVision = - overrideModelInformation?.SupportsVision ?? predefinedModelInformation.SupportsVision, - EmbeddingDimensions = - overrideModelInformation?.EmbeddingDimensions ?? predefinedModelInformation.EmbeddingDimensions, - SupportsAudioInput = - overrideModelInformation?.SupportsAudioInput ?? predefinedModelInformation.SupportsAudioInput, - SupportsAudioOutput = - overrideModelInformation?.SupportsAudioOutput ?? predefinedModelInformation.SupportsAudioOutput, - SupportsPromptCaching = - overrideModelInformation?.SupportsPromptCaching - ?? predefinedModelInformation.SupportsPromptCaching, - }, + + // Model Options + CodeAssistType = + overrideModelOption?.CodeAssistType + ?? _llmOptions.CodeAssistType + ?? predefinedModelOption?.CodeAssistType + ?? CodeAssistType.Embedding, + CodeDiffType = + overrideModelOption?.CodeDiffType + ?? _llmOptions.CodeDiffType + ?? predefinedModelOption?.CodeDiffType + ?? CodeDiffType.CodeBlockDiff, + Threshold = + overrideModelOption?.Threshold ?? _llmOptions.Threshold ?? predefinedModelOption?.Threshold ?? 0.4m, + Temperature = + overrideModelOption?.Temperature + ?? _llmOptions.Temperature + ?? predefinedModelOption?.Temperature + ?? 0.2m, + ApiVersion = overrideModelOption?.ApiVersion ?? predefinedModelOption?.ApiVersion, + BaseAddress = overrideModelOption?.BaseAddress ?? predefinedModelOption?.BaseAddress, + DeploymentId = overrideModelOption?.DeploymentId ?? predefinedModelOption?.DeploymentId, + + // Model Information + AIProvider = + overrideModelInformation?.AIProvider + ?? predefinedModelInformation.AIProvider + ?? throw new ArgumentException($"AI Provider not set for model {originalName}."), + ModelType = + overrideModelInformation?.ModelType + ?? predefinedModelInformation.ModelType + ?? throw new ArgumentException($"Model Type not set for model {originalName}."), + MaxTokens = + overrideModelInformation?.MaxTokens + ?? predefinedModelInformation.MaxTokens + ?? throw new ArgumentException($"Max tokens not set for model {originalName}."), + MaxInputTokens = + overrideModelInformation?.MaxInputTokens + ?? predefinedModelInformation.MaxInputTokens + ?? throw new ArgumentException($"Max input tokens not set for model {originalName}."), + MaxOutputTokens = + overrideModelInformation?.MaxOutputTokens + ?? predefinedModelInformation.MaxOutputTokens + ?? throw new ArgumentException($"Max output tokens not set for model {originalName}."), + InputCostPerToken = + overrideModelInformation?.InputCostPerToken ?? predefinedModelInformation.InputCostPerToken, + OutputCostPerToken = + overrideModelInformation?.OutputCostPerToken ?? predefinedModelInformation.OutputCostPerToken, + OutputVectorSize = + overrideModelInformation?.OutputVectorSize ?? predefinedModelInformation.OutputVectorSize, + Enabled = overrideModelInformation?.Enabled ?? predefinedModelInformation.Enabled, + SupportsFunctionCalling = + overrideModelInformation?.SupportsFunctionCalling + ?? predefinedModelInformation.SupportsFunctionCalling, + SupportsParallelFunctionCalling = + overrideModelInformation?.SupportsParallelFunctionCalling + ?? predefinedModelInformation.SupportsParallelFunctionCalling, + SupportsVision = overrideModelInformation?.SupportsVision ?? predefinedModelInformation.SupportsVision, + EmbeddingDimensions = + overrideModelInformation?.EmbeddingDimensions ?? predefinedModelInformation.EmbeddingDimensions, + SupportsPromptCaching = + overrideModelInformation?.SupportsPromptCaching ?? predefinedModelInformation.SupportsPromptCaching, }; _models[originalName] = model; diff --git a/src/Clients/Converters/AIProviderTypeConverter.cs b/src/Clients/Converters/AIProviderTypeConverter.cs deleted file mode 100644 index 6feeda6..0000000 --- a/src/Clients/Converters/AIProviderTypeConverter.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Clients.Models; -using Humanizer; - -namespace Clients.Converters; - -public class AIProviderTypeConverter : JsonConverter -{ - public override AIProvider Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - // Read the string value from JSON and convert it to snake_case - string aiProviderValue = reader.GetString() ?? string.Empty; - var snakeCaseValue = aiProviderValue.Underscore(); - - // Define snake_case mappings for each enum value - var openAI = AIProvider.Openai.ToString().Underscore(); - var ollama = AIProvider.Ollama.ToString().Underscore(); - var azure = AIProvider.Azure.ToString().Underscore(); - var anthropic = AIProvider.Anthropic.ToString().Underscore(); - - // Convert snake_case string to AIProvider enum value - return snakeCaseValue switch - { - var type when type == openAI => AIProvider.Openai, - var type when type == ollama => AIProvider.Ollama, - var type when type == azure => AIProvider.Azure, - var type when type == anthropic => AIProvider.Anthropic, - _ => throw new JsonException($"Unknown AIProvider type: {aiProviderValue}"), - }; - } - - public override void Write(Utf8JsonWriter writer, AIProvider value, JsonSerializerOptions options) - { - // Define snake_case strings for each enum value - string openAI = AIProvider.Openai.ToString().Underscore(); - string ollama = AIProvider.Ollama.ToString().Underscore(); - string azure = AIProvider.Azure.ToString().Underscore(); - string anthropic = AIProvider.Anthropic.ToString().Underscore(); - - // Convert AIProvider enum to corresponding snake_case string - string aiProviderString = value switch - { - AIProvider.Openai => openAI, - AIProvider.Ollama => ollama, - AIProvider.Azure => azure, - AIProvider.Anthropic => anthropic, - _ => throw new JsonException($"Unknown AIProvider type: {value}"), - }; - - writer.WriteStringValue(aiProviderString); - } -} diff --git a/src/Clients/Converters/CodeAssistTypeConverter.cs b/src/Clients/Converters/CodeAssistTypeConverter.cs deleted file mode 100644 index 139f38b..0000000 --- a/src/Clients/Converters/CodeAssistTypeConverter.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Clients.Models; -using Humanizer; - -namespace Clients.Converters; - - -// public class CodeAssistTypeConverter : JsonConverter -// { -// public override CodeAssistType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) -// { -// string modelType = reader.GetString() ?? string.Empty; -// -// var snakeModelType = modelType.Underscore(); -// var embedding = CodeAssistType.Embedding.ToString().Underscore(); -// var summary = CodeAssistType.Summary.ToString().Underscore(); -// -// // Convert snake_case string to CodeDiff enum -// return snakeModelType switch -// { -// var type when type == embedding => CodeAssistType.Embedding, -// var type when type == summary => CodeAssistType.Summary, -// _ => throw new JsonException($"Unknown CodeAssistType: {modelType}"), -// }; -// } -// -// public override void Write(Utf8JsonWriter writer, CodeAssistType value, JsonSerializerOptions options) -// { -// string embedding = CodeAssistType.Embedding.ToString().Underscore(); -// string summary = CodeAssistType.Summary.ToString().Underscore(); -// -// // Convert CodeDiffType enum back to snake_case string -// string modelTypeString = value switch -// { -// CodeAssistType.Embedding => embedding, -// CodeAssistType.Summary => summary, -// _ => throw new JsonException($"Unknown CodeAssistType value: {value}"), -// }; -// -// writer.WriteStringValue(modelTypeString); -// } -// } diff --git a/src/Clients/Converters/CodeDiffTypeConverter.cs b/src/Clients/Converters/CodeDiffTypeConverter.cs deleted file mode 100644 index 6455263..0000000 --- a/src/Clients/Converters/CodeDiffTypeConverter.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Clients.Models; -using Humanizer; - -namespace Clients.Converters; - -// -// public class CodeDiffTypeConverter : JsonConverter -// { -// public CodeDiffTypeConverter() { } -// -// public override CodeDiffType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) -// { -// string modelType = reader.GetString() ?? string.Empty; -// -// var snakeModelType = modelType.Underscore(); -// var codeBlock = CodeDiffType.CodeBlockDiff.ToString().Underscore(); -// var unifiedDiff = CodeDiffType.UnifiedDiff.ToString().Underscore(); -// var mergeConflict = CodeDiffType.MergeConflictDiff.ToString().Underscore(); -// -// // Convert snake_case string to CodeDiff enum -// return snakeModelType switch -// { -// var type when type == codeBlock => CodeDiffType.CodeBlockDiff, -// var type when type == unifiedDiff => CodeDiffType.UnifiedDiff, -// var type when type == mergeConflict => CodeDiffType.MergeConflictDiff, -// _ => throw new JsonException($"Unknown CodeDiffType: {modelType}"), -// }; -// } -// -// public override void Write(Utf8JsonWriter writer, CodeDiffType value, JsonSerializerOptions options) -// { -// string codeBlock = CodeDiffType.CodeBlockDiff.ToString().Underscore(); -// string unifiedDiff = CodeDiffType.UnifiedDiff.ToString().Underscore(); -// string mergeConflict = CodeDiffType.MergeConflictDiff.ToString().Underscore(); -// -// // Convert CodeDiffType enum back to snake_case string -// string modelTypeString = value switch -// { -// CodeDiffType.CodeBlockDiff => codeBlock, -// CodeDiffType.UnifiedDiff => unifiedDiff, -// CodeDiffType.MergeConflictDiff => mergeConflict, -// _ => throw new JsonException($"Unknown CodeDiffType value: {value}"), -// }; -// -// writer.WriteStringValue(modelTypeString); -// } -// } diff --git a/src/Clients/Converters/ModelTypeConverter.cs b/src/Clients/Converters/ModelTypeConverter.cs deleted file mode 100644 index 546dcc7..0000000 --- a/src/Clients/Converters/ModelTypeConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; -using Clients.Models; -using Humanizer; - -namespace Clients.Converters; - -public class ModelTypeConverter : JsonConverter -{ - public override ModelType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - // Read the string value from JSON and convert it to snake_case - string modelType = reader.GetString() ?? string.Empty; - var snakeCaseType = modelType.Underscore(); - - // Define snake_case mappings for each enum value - var chat = ModelType.Chat.ToString().Underscore(); - var embedding = ModelType.Embedding.ToString().Underscore(); - - // Convert snake_case string to ModelType enum - return snakeCaseType switch - { - var type when type == chat => ModelType.Chat, - var type when type == embedding => ModelType.Embedding, - _ => throw new JsonException($"Unknown model type: {modelType}"), - }; - } - - public override void Write(Utf8JsonWriter writer, ModelType value, JsonSerializerOptions options) - { - // Define snake_case strings for each enum value - string chat = ModelType.Chat.ToString().Underscore(); - string embedding = ModelType.Embedding.ToString().Underscore(); - - // Convert ModelType enum to corresponding snake_case string - string modelTypeString = value switch - { - ModelType.Chat => chat, - ModelType.Embedding => embedding, - _ => throw new JsonException($"Unknown model type: {value}"), - }; - - writer.WriteStringValue(modelTypeString); - } -} diff --git a/src/Clients/Converters/RoleTypeConverter.cs b/src/Clients/Converters/RoleTypeConverter.cs index b0a218d..6692094 100644 --- a/src/Clients/Converters/RoleTypeConverter.cs +++ b/src/Clients/Converters/RoleTypeConverter.cs @@ -5,6 +5,7 @@ namespace Clients.Converters; +// we use RoleTypeConverter when Role type particpate in a model and we use the model inside of serialization mechanism nor binding configuration because they are not serialization based public class RoleTypeConverter : JsonConverter { public override RoleType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/src/Clients/LLMs/models_information_list.json b/src/Clients/LLMs/models_information_list.json index eafa543..a8cd5ef 100644 --- a/src/Clients/LLMs/models_information_list.json +++ b/src/Clients/LLMs/models_information_list.json @@ -14,6 +14,7 @@ "text-embedding-3-large": { "MaxTokens": 8191, "MaxInputTokens": 8191, + "MaxOutputTokens": 8191, "OutputVectorSize": 3072, "InputCostPerToken": 0.00000013, "OutputCostPerToken": 0.000000, @@ -24,6 +25,7 @@ "text-embedding-3-small": { "MaxTokens": 8191, "MaxInputTokens": 8191, + "MaxOutputTokens": 8191, "OutputVectorSize": 1536, "InputCostPerToken": 0.00000002, "OutputCostPerToken": 0.000000, @@ -33,8 +35,8 @@ }, "azure/gpt-4o": { "MaxTokens": 8192, - "MaxInputTokens": 128000, "MaxOutputTokens": 16384, + "MaxInputTokens": 128000, "InputCostPerToken": 0.000005, "OutputCostPerToken": 0.000015, "AIProvider": "Azure", @@ -45,6 +47,7 @@ "azure/text-embedding-3-large": { "MaxTokens": 8191, "MaxInputTokens": 8191, + "MaxOutputTokens": 8191, "InputCostPerToken": 0.00000013, "OutputCostPerToken": 0.000000, "AIProvider": "Azure", @@ -54,6 +57,7 @@ "azure/text-embedding-3-small": { "MaxTokens": 8191, "MaxInputTokens": 8191, + "MaxOutputTokens": 8191, "InputCostPerToken": 0.00000002, "OutputCostPerToken": 0.000000, "AIProvider": "Azure", @@ -159,6 +163,7 @@ "ollama/nomic-embed-text": { "MaxTokens": 8192, "MaxInputTokens": 8192, + "MaxOutputTokens": 8192, "InputCostPerToken": 0.0, "OutputCostPerToken": 0.0, "AIProvider": "Ollama", @@ -168,6 +173,7 @@ "ollama/mxbai-embed-large": { "MaxTokens": 8192, "MaxInputTokens": 8192, + "MaxOutputTokens": 8192, "InputCostPerToken": 0.0, "OutputCostPerToken": 0.0, "AIProvider": "Ollama", diff --git a/src/Clients/Models/Model.cs b/src/Clients/Models/Model.cs index f9a2329..5adcaa0 100644 --- a/src/Clients/Models/Model.cs +++ b/src/Clients/Models/Model.cs @@ -11,6 +11,26 @@ public class Model /// Model name with an AI provider type with '/' prefix /// public string OriginalName { get; set; } = default!; - public ModelInformation ModelInformation { get; set; } = default!; - public ModelOption ModelOption { get; set; } = default!; + public AIProvider AIProvider { get; set; } + public ModelType ModelType { get; set; } + public CodeDiffType CodeDiffType { get; set; } + public CodeAssistType CodeAssistType { get; set; } + public decimal Threshold { get; set; } + public decimal Temperature { get; set; } + public string? BaseAddress { get; set; } + public string? ApiVersion { get; set; } + public string? DeploymentId { get; set; } + public string? ApiKey { get; set; } + public int MaxTokens { get; set; } + public int MaxInputTokens { get; set; } + public int MaxOutputTokens { get; set; } + public decimal InputCostPerToken { get; set; } + public decimal OutputCostPerToken { get; set; } + public int? OutputVectorSize { get; set; } + public bool SupportsFunctionCalling { get; set; } + public bool SupportsParallelFunctionCalling { get; set; } + public bool SupportsVision { get; set; } + public int? EmbeddingDimensions { get; set; } + public bool SupportsPromptCaching { get; set; } + public bool Enabled { get; set; } = true; } diff --git a/src/Clients/Models/ModelInformation.cs b/src/Clients/Models/ModelInformation.cs index 349b9f9..fa57e91 100644 --- a/src/Clients/Models/ModelInformation.cs +++ b/src/Clients/Models/ModelInformation.cs @@ -6,11 +6,11 @@ namespace Clients.Models; public class ModelInformation { - public AIProvider AIProvider { get; set; } - public ModelType ModelType { get; set; } - public int MaxTokens { get; set; } - public int MaxInputTokens { get; set; } - public int MaxOutputTokens { get; set; } + public AIProvider? AIProvider { get; set; } + public ModelType? ModelType { get; set; } + public int? MaxTokens { get; set; } + public int? MaxInputTokens { get; set; } + public int? MaxOutputTokens { get; set; } public decimal InputCostPerToken { get; set; } public decimal OutputCostPerToken { get; set; } public int? OutputVectorSize { get; set; } @@ -18,9 +18,6 @@ public class ModelInformation public bool SupportsParallelFunctionCalling { get; set; } public bool SupportsVision { get; set; } public int? EmbeddingDimensions { get; set; } - public bool SupportsAudioInput { get; set; } - public bool SupportsAudioOutput { get; set; } - public bool SupportsPromptCaching { get; set; } public bool Enabled { get; set; } = true; } diff --git a/src/Clients/Models/ModelOption.cs b/src/Clients/Models/ModelOption.cs index e38786b..7867299 100644 --- a/src/Clients/Models/ModelOption.cs +++ b/src/Clients/Models/ModelOption.cs @@ -2,10 +2,10 @@ namespace Clients.Models; public class ModelOption { - public CodeDiffType CodeDiffType { get; set; } - public CodeAssistType CodeAssistType { get; set; } - public decimal Threshold { get; set; } - public decimal Temperature { get; set; } + public CodeDiffType? CodeDiffType { get; set; } + public CodeAssistType? CodeAssistType { get; set; } + public decimal? Threshold { get; set; } + public decimal? Temperature { get; set; } public string? BaseAddress { get; set; } public string? ApiVersion { get; set; } public string? DeploymentId { get; set; } diff --git a/src/Clients/OllamaClient.cs b/src/Clients/OllamaClient.cs index ae5c354..da71b55 100644 --- a/src/Clients/OllamaClient.cs +++ b/src/Clients/OllamaClient.cs @@ -53,7 +53,7 @@ AsyncPolicyWrap combinedPolicy role = x.Role.Humanize(LetterCasing.LowerCase), content = x.Prompt, }), - options = new { temperature = _chatModel.ModelOption.Temperature }, + options = new { temperature = _chatModel.Temperature }, keep_alive = "30m", stream = false, }; @@ -79,8 +79,8 @@ AsyncPolicyWrap combinedPolicy var inputTokens = completionResponse.PromptEvalCount; var outTokens = completionResponse.EvalCount; - var inputCostPerToken = _chatModel.ModelInformation.InputCostPerToken; - var outputCostPerToken = _chatModel.ModelInformation.OutputCostPerToken; + var inputCostPerToken = _chatModel.InputCostPerToken; + var outputCostPerToken = _chatModel.OutputCostPerToken; ValidateChatMaxToken(inputTokens + outTokens); @@ -109,7 +109,7 @@ AsyncPolicyWrap combinedPolicy role = x.Role.Humanize(LetterCasing.LowerCase), content = x.Prompt, }), - options = new { temperature = _chatModel.ModelOption.Temperature }, + options = new { temperature = _chatModel.Temperature }, stream = true, keep_alive = "30m", }; @@ -158,8 +158,8 @@ AsyncPolicyWrap combinedPolicy // https://github.com/ollama/ollama/blob/main/docs/api.md#response-9 var inputTokens = completionStreamResponse.PromptEvalCount; var outTokens = completionStreamResponse.EvalCount; - var inputCostPerToken = _chatModel.ModelInformation.InputCostPerToken; - var outputCostPerToken = _chatModel.ModelInformation.OutputCostPerToken; + var inputCostPerToken = _chatModel.InputCostPerToken; + var outputCostPerToken = _chatModel.OutputCostPerToken; ValidateChatMaxToken(inputTokens + outTokens); @@ -199,7 +199,7 @@ AsyncPolicyWrap combinedPolicy { input = inputs, model = _embeddingModel.Name, - options = new { temperature = _embeddingModel.ModelOption.Temperature }, + options = new { temperature = _embeddingModel.Temperature }, keep_alive = "30m", }; @@ -225,8 +225,8 @@ AsyncPolicyWrap combinedPolicy var inputTokens = embeddingResponse.PromptEvalCount; var outTokens = embeddingResponse.EvalCount; - var inputCostPerToken = _embeddingModel.ModelInformation.InputCostPerToken; - var outputCostPerToken = _embeddingModel.ModelInformation.OutputCostPerToken; + var inputCostPerToken = _embeddingModel.InputCostPerToken; + var outputCostPerToken = _embeddingModel.OutputCostPerToken; ValidateEmbeddingMaxToken(inputTokens + outTokens, path); @@ -262,13 +262,10 @@ private async Task ValidateChatMaxInputToken(ChatCompletionRequest chatCompletio string.Concat(chatCompletionRequest.Items.Select(x => x.Prompt)) ); - if ( - _chatModel.ModelInformation.MaxInputTokens > 0 - && inputTokenCount > _chatModel.ModelInformation.MaxInputTokens - ) + if (_chatModel.MaxInputTokens > 0 && inputTokenCount > _chatModel.MaxInputTokens) { throw new OllamaException( - $"current chat 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_chatModel.ModelInformation.MaxInputTokens.FormatCommas()}.", + $"current chat 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_chatModel.MaxInputTokens.FormatCommas()}.", HttpStatusCode.BadRequest ); } @@ -278,10 +275,8 @@ private async Task ValidateEmbeddingMaxInputToken(string input, string? path = n { var inputTokenCount = await tokenizer.GetTokenCount(input); - if ( - _embeddingModel.ModelInformation.MaxInputTokens > 0 - && inputTokenCount > _embeddingModel.ModelInformation.MaxInputTokens - ) + ArgumentNullException.ThrowIfNull(_embeddingModel); + if (_embeddingModel.MaxInputTokens > 0 && inputTokenCount > _embeddingModel.MaxInputTokens) { var moreInfo = path is not null ? $"if file '{ @@ -290,7 +285,7 @@ private async Task ValidateEmbeddingMaxInputToken(string input, string? path = n : ""; throw new OllamaException( - $"embedding {path} 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_embeddingModel.ModelInformation.MaxInputTokens.FormatCommas()}. {moreInfo}", + $"embedding {path} 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_embeddingModel.MaxInputTokens.FormatCommas()}. {moreInfo}", HttpStatusCode.BadRequest ); } @@ -298,10 +293,10 @@ private async Task ValidateEmbeddingMaxInputToken(string input, string? path = n private void ValidateChatMaxToken(int maxTokenCount) { - if (_chatModel.ModelInformation.MaxTokens > 0 && maxTokenCount > _chatModel.ModelInformation.MaxTokens) + if (_chatModel.MaxTokens > 0 && maxTokenCount > _chatModel.MaxTokens) { throw new OllamaException( - $"current chat 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_chatModel.ModelInformation.MaxTokens.FormatCommas()}.", + $"current chat 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_chatModel.MaxTokens.FormatCommas()}.", HttpStatusCode.BadRequest ); } @@ -309,13 +304,11 @@ private void ValidateChatMaxToken(int maxTokenCount) private void ValidateEmbeddingMaxToken(int maxTokenCount, string? path) { - if ( - _embeddingModel.ModelInformation.MaxTokens > 0 - && maxTokenCount > _embeddingModel.ModelInformation.MaxTokens - ) + ArgumentNullException.ThrowIfNull(_embeddingModel); + if (_embeddingModel.MaxTokens > 0 && maxTokenCount > _embeddingModel.MaxTokens) { throw new OllamaException( - $"embedding {path} 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_embeddingModel.ModelInformation.MaxTokens.FormatCommas()}.", + $"embedding {path} 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_embeddingModel.MaxTokens.FormatCommas()}.", HttpStatusCode.BadRequest ); } diff --git a/src/Clients/OpenAiClient.cs b/src/Clients/OpenAiClient.cs index 710bf1a..6313887 100644 --- a/src/Clients/OpenAiClient.cs +++ b/src/Clients/OpenAiClient.cs @@ -52,7 +52,7 @@ AsyncPolicyWrap combinedPolicy role = x.Role.Humanize(LetterCasing.LowerCase), content = x.Prompt, }), - temperature = _chatModel.ModelOption.Temperature, + temperature = _chatModel.Temperature, }; var client = httpClientFactory.CreateClient("llm_chat_client"); @@ -83,8 +83,8 @@ AsyncPolicyWrap combinedPolicy var inputTokens = completionResponse.Usage?.PromptTokens ?? 0; var outTokens = completionResponse.Usage?.CompletionTokens ?? 0; - var inputCostPerToken = _chatModel.ModelInformation.InputCostPerToken; - var outputCostPerToken = _chatModel.ModelInformation.OutputCostPerToken; + var inputCostPerToken = _chatModel.InputCostPerToken; + var outputCostPerToken = _chatModel.OutputCostPerToken; ValidateChatMaxToken(inputTokens + outTokens); @@ -110,7 +110,7 @@ AsyncPolicyWrap combinedPolicy role = x.Role.Humanize(LetterCasing.LowerCase), content = x.Prompt, }), - temperature = _chatModel.ModelOption.Temperature, + temperature = _chatModel.Temperature, stream = true, // https://cookbook.openai.com/examples/how_to_stream_completions#4-how-to-get-token-usage-data-for-streamed-chat-completion-response stream_options = new { include_usage = true }, @@ -183,8 +183,8 @@ AsyncPolicyWrap combinedPolicy // Capture the `usage` data from the final chunk and after done var inputTokens = completionStreamResponse.Usage?.PromptTokens ?? 0; var outTokens = completionStreamResponse.Usage?.CompletionTokens ?? 0; - var inputCostPerToken = _chatModel.ModelInformation.InputCostPerToken; - var outputCostPerToken = _chatModel.ModelInformation.OutputCostPerToken; + var inputCostPerToken = _chatModel.InputCostPerToken; + var outputCostPerToken = _chatModel.OutputCostPerToken; ValidateChatMaxToken(inputTokens + outTokens); @@ -220,7 +220,7 @@ AsyncPolicyWrap combinedPolicy { input = inputs, model = _embeddingModel.Name.Trim(), - dimensions = _embeddingModel.ModelInformation.EmbeddingDimensions, + dimensions = _embeddingModel.EmbeddingDimensions, }; var client = httpClientFactory.CreateClient("llm_embeddings_client"); @@ -247,8 +247,8 @@ AsyncPolicyWrap combinedPolicy var inputTokens = embeddingResponse.Usage?.PromptTokens ?? 0; var outTokens = embeddingResponse.Usage?.CompletionTokens ?? 0; - var inputCostPerToken = _embeddingModel.ModelInformation.InputCostPerToken; - var outputCostPerToken = _embeddingModel.ModelInformation.OutputCostPerToken; + var inputCostPerToken = _embeddingModel.InputCostPerToken; + var outputCostPerToken = _embeddingModel.OutputCostPerToken; ValidateEmbeddingMaxToken(inputTokens + outTokens, path); @@ -293,17 +293,14 @@ private async Task ValidateChatMaxInputToken(ChatCompletionRequest chatCompletio string.Concat(chatCompletionRequest.Items.Select(x => x.Prompt)) ); - if ( - _chatModel.ModelInformation.MaxInputTokens > 0 - && inputTokenCount > _chatModel.ModelInformation.MaxInputTokens - ) + if (_chatModel.MaxInputTokens > 0 && inputTokenCount > _chatModel.MaxInputTokens) { throw new OpenAIException( new OpenAIError { StatusCode = (int)HttpStatusCode.BadRequest, Message = - $"current chat 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_chatModel.ModelInformation.MaxInputTokens.FormatCommas()}", + $"current chat 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_chatModel.MaxInputTokens.FormatCommas()}", }, HttpStatusCode.BadRequest ); @@ -314,10 +311,8 @@ private async Task ValidateEmbeddingMaxInputToken(string input, string? path = n { var inputTokenCount = await tokenizer.GetTokenCount(input); - if ( - _embeddingModel.ModelInformation.MaxInputTokens > 0 - && inputTokenCount > _embeddingModel.ModelInformation.MaxInputTokens - ) + ArgumentNullException.ThrowIfNull(_embeddingModel); + if (_embeddingModel.MaxInputTokens > 0 && inputTokenCount > _embeddingModel.MaxInputTokens) { var moreInfo = path is not null ? $"if file '{ @@ -330,7 +325,7 @@ private async Task ValidateEmbeddingMaxInputToken(string input, string? path = n { StatusCode = (int)HttpStatusCode.BadRequest, Message = - $"embedding {path} 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_embeddingModel.ModelInformation.MaxInputTokens.FormatCommas()}. {moreInfo}", + $"embedding {path} 'max_input_token' count: {inputTokenCount.FormatCommas()} is larger than configured 'max_input_token' count: {_embeddingModel.MaxInputTokens.FormatCommas()}. {moreInfo}", }, HttpStatusCode.BadRequest ); @@ -339,14 +334,14 @@ private async Task ValidateEmbeddingMaxInputToken(string input, string? path = n private void ValidateChatMaxToken(int maxTokenCount) { - if (_chatModel.ModelInformation.MaxTokens > 0 && maxTokenCount > _chatModel.ModelInformation.MaxTokens) + if (_chatModel.MaxTokens > 0 && maxTokenCount > _chatModel.MaxTokens) { throw new OpenAIException( new OpenAIError { StatusCode = (int)HttpStatusCode.BadRequest, Message = - $"current chat 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_chatModel.ModelInformation.MaxTokens.FormatCommas()}.", + $"current chat 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_chatModel.MaxTokens.FormatCommas()}.", }, HttpStatusCode.BadRequest ); @@ -355,17 +350,15 @@ private void ValidateChatMaxToken(int maxTokenCount) private void ValidateEmbeddingMaxToken(int maxTokenCount, string? path) { - if ( - _embeddingModel.ModelInformation.MaxTokens > 0 - && maxTokenCount > _embeddingModel.ModelInformation.MaxTokens - ) + ArgumentNullException.ThrowIfNull(_embeddingModel); + if (_embeddingModel.MaxTokens > 0 && maxTokenCount > _embeddingModel.MaxTokens) { throw new OpenAIException( new OpenAIError { StatusCode = (int)HttpStatusCode.BadRequest, Message = - $"embedding {path} 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_chatModel.ModelInformation.MaxTokens.FormatCommas()}.", + $"embedding {path} 'max_token' count: {maxTokenCount.FormatCommas()} is larger than configured 'max_token' count: {_chatModel.MaxTokens.FormatCommas()}.", }, HttpStatusCode.BadRequest );