From 5b4303125112b9c36d78a57d1a7f07a7df721663 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Wed, 6 Nov 2024 17:20:05 +0100 Subject: [PATCH 01/11] Add TokenizerService and update settings configuration Updated Program.cs to use ConfigureAndGet method for settings, changed ChatService to singleton, and added TokenizerService singleton. Modified ChatService to use TokenizerService for token counting. Updated AppSettings and AzureOpenAISettings with new properties. Added new package references in SqlDatabaseVectorSearch.csproj. Updated appsettings.json with new properties. Added TokenizerService class for token counting. --- SqlDatabaseVectorSearch/Program.cs | 5 +- .../Services/ChatService.cs | 53 +++++++++++++------ .../Services/TokenizerService.cs | 13 +++++ .../Settings/AppSettings.cs | 4 ++ .../Settings/AzureOpenAISettings.cs | 8 +-- .../SqlDatabaseVectorSearch.csproj | 4 +- SqlDatabaseVectorSearch/appsettings.json | 6 ++- 7 files changed, 69 insertions(+), 24 deletions(-) create mode 100644 SqlDatabaseVectorSearch/Services/TokenizerService.cs diff --git a/SqlDatabaseVectorSearch/Program.cs b/SqlDatabaseVectorSearch/Program.cs index a798f73..6381ef6 100644 --- a/SqlDatabaseVectorSearch/Program.cs +++ b/SqlDatabaseVectorSearch/Program.cs @@ -14,7 +14,7 @@ var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true); // Add services to the container. -var aiSettings = builder.Configuration.GetSection("AzureOpenAI")!; +var aiSettings = builder.Services.ConfigureAndGet(builder.Configuration, "AzureOpenAI")!; var appSettings = builder.Services.ConfigureAndGet(builder.Configuration, nameof(AppSettings))!; builder.Services.AddSingleton(TimeProvider.System); @@ -35,7 +35,8 @@ builder.Services.AddKernel() .AddAzureOpenAITextEmbeddingGeneration(aiSettings.Embedding.Deployment, aiSettings.Embedding.Endpoint, aiSettings.Embedding.ApiKey, dimensions: aiSettings.Embedding.Dimensions) .AddAzureOpenAIChatCompletion(aiSettings.ChatCompletion.Deployment, aiSettings.ChatCompletion.Endpoint, aiSettings.ChatCompletion.ApiKey); -builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddEndpointsApiExplorer(); diff --git a/SqlDatabaseVectorSearch/Services/ChatService.cs b/SqlDatabaseVectorSearch/Services/ChatService.cs index ecb9ec6..80fb965 100644 --- a/SqlDatabaseVectorSearch/Services/ChatService.cs +++ b/SqlDatabaseVectorSearch/Services/ChatService.cs @@ -2,11 +2,12 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using SqlDatabaseVectorSearch.Settings; namespace SqlDatabaseVectorSearch.Services; -public class ChatService(IMemoryCache cache, IChatCompletionService chatCompletionService, IOptions appSettingsOptions) +public class ChatService(IMemoryCache cache, IChatCompletionService chatCompletionService, TokenizerService tokenizerService, IOptions appSettingsOptions) { private readonly AppSettings appSettings = appSettingsOptions.Value; @@ -35,36 +36,54 @@ public class ChatService(IMemoryCache cache, IChatCompletionService chatCompleti public async Task AskQuestionAsync(Guid conversationId, IEnumerable chunks, string question) { - var chat = new ChatHistory("""" - """ + var chat = new ChatHistory(""" You can use only the information provided in this chat to answer questions. If you don't know the answer, reply suggesting to refine the question. For example, if the user asks "What is the capital of France?" and in this chat there isn't information about France, you should reply something like "This information isn't available in the given context". Never answer to questions that are not related to this chat. You must answer in the same language of the user's question. - """"); + """); - var prompt = new StringBuilder(""" + var prompt = new StringBuilder($""" + Answer the following question: + --- + {question} + --- Using the following information: --- """); - // TODO: Ensure that chunks are not too long, according to the model max token. - foreach (var result in chunks) - { - prompt.AppendLine(result); - prompt.AppendLine("---"); - } + var tokensAvailable = appSettings.MaxInputTokens + - tokenizerService.CountTokens(chat[0].ToString()) - tokenizerService.CountTokens(prompt.ToString()) + - appSettings.MaxOutputTokens; // To ensure there is enough space for the answer. - prompt.AppendLine($""" - Answer the following question: - --- - {question} - """); + foreach (var chunk in chunks) + { + var text = $"{chunk}---"; + + var tokenCount = tokenizerService.CountTokens(text); + if (tokenCount > tokensAvailable) + { + // There isn't enough space to add the text. + break; + } + + prompt.AppendLine(text); + + tokensAvailable -= tokenCount; + if (tokensAvailable <= 0) + { + // There isn't enough space to add more chunks. + break; + } + } chat.AddUserMessage(prompt.ToString()); - var answer = await chatCompletionService.GetChatMessageContentAsync(chat)!; + var answer = await chatCompletionService.GetChatMessageContentAsync(chat, new AzureOpenAIPromptExecutionSettings + { + MaxTokens = appSettings.MaxOutputTokens + }); // Add question and answer to the chat history. var history = new ChatHistory(cache.Get(conversationId) ?? []); diff --git a/SqlDatabaseVectorSearch/Services/TokenizerService.cs b/SqlDatabaseVectorSearch/Services/TokenizerService.cs new file mode 100644 index 0000000..9115b3d --- /dev/null +++ b/SqlDatabaseVectorSearch/Services/TokenizerService.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Options; +using Microsoft.ML.Tokenizers; +using SqlDatabaseVectorSearch.Settings; + +namespace SqlDatabaseVectorSearch.Services; + +public class TokenizerService(IOptions settingsOptions) +{ + private readonly TiktokenTokenizer tokenizer = TiktokenTokenizer.CreateForModel(settingsOptions.Value.ChatCompletion.ModelId); + + public int CountTokens(string input) + => tokenizer.CountTokens(input); +} diff --git a/SqlDatabaseVectorSearch/Settings/AppSettings.cs b/SqlDatabaseVectorSearch/Settings/AppSettings.cs index 4466e1a..56300a0 100644 --- a/SqlDatabaseVectorSearch/Settings/AppSettings.cs +++ b/SqlDatabaseVectorSearch/Settings/AppSettings.cs @@ -10,6 +10,10 @@ public class AppSettings public int MaxRelevantChunks { get; init; } = 5; + public int MaxInputTokens { get; init; } = 16385; + + public int MaxOutputTokens { get; init; } = 800; + public int MessageLimit { get; init; } public TimeSpan MessageExpiration { get; init; } diff --git a/SqlDatabaseVectorSearch/Settings/AzureOpenAISettings.cs b/SqlDatabaseVectorSearch/Settings/AzureOpenAISettings.cs index e85d51d..2faac44 100644 --- a/SqlDatabaseVectorSearch/Settings/AzureOpenAISettings.cs +++ b/SqlDatabaseVectorSearch/Settings/AzureOpenAISettings.cs @@ -4,7 +4,7 @@ public class AzureOpenAISettings { public required ServiceSettings ChatCompletion { get; init; } - public required EmbeddingServiceSettings Embedding { get; init; } + public required EmbeddingSettings Embedding { get; init; } } public class ServiceSettings @@ -13,10 +13,12 @@ public class ServiceSettings public required string Deployment { get; init; } + public required string ModelId { get; init; } + public required string ApiKey { get; init; } } -public class EmbeddingServiceSettings : ServiceSettings +public class EmbeddingSettings : ServiceSettings { public int? Dimensions { get; set; } -} +} \ No newline at end of file diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj index 7fc93f7..9773a95 100644 --- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj +++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj @@ -12,7 +12,9 @@ - + + + diff --git a/SqlDatabaseVectorSearch/appsettings.json b/SqlDatabaseVectorSearch/appsettings.json index 79648fe..d3834ee 100644 --- a/SqlDatabaseVectorSearch/appsettings.json +++ b/SqlDatabaseVectorSearch/appsettings.json @@ -6,12 +6,14 @@ "ChatCompletion": { "Endpoint": "", "Deployment": "", - "ApiKey": "" + "ApiKey": "", + "ModelId": "" }, "Embedding": { "Endpoint": "", "Deployment": "", "ApiKey": "", + "ModelId": "", // Set this value only if you're using a model that allows to specify the dimensions of the embeddings // (e.g. text-embedding-3-small or text-embedding-3-large). Currently, a maximum value of 1998 is supported. "Dimensions": null @@ -22,6 +24,8 @@ "MaxTokensPerParagraph": 1024, "OverlapTokens": 100, "MaxRelevantChunks": 10, + "MaxInputTokens": 16385, + "MaxOutputTokens": 800, "MessageLimit": 20, "MessageExpiration": "00:05:00" }, From bcd085e49de166be504f467e1747e9e15f43686b Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Thu, 7 Nov 2024 10:03:58 +0100 Subject: [PATCH 02/11] Update README, adjust prompt formatting, add new package Updated README.md to include a note on Vector support in Azure SQL Database/Managed Instance (EAP) and corrected "Dimension" to "Dimensions". Adjusted prompt formatting in ChatService.cs by replacing "---" with "=====" and improved text chunk appending. Added new package reference for `Microsoft.ML.Tokenizers.Data.Cl100kBase` in SqlDatabaseVectorSearch.csproj. --- README.md | 5 +---- SqlDatabaseVectorSearch/Services/ChatService.cs | 7 +++---- SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj | 1 + 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 94ca5cc..044cf77 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # SQL Database Vector Search Sample A repository that showcases the native VECTOR type in Azure SQL Database to perform embeddings and RAG with Azure OpenAI. -> [!IMPORTANT] -> Usage of this application requires the Vector support feature in Azure SQL Database or Managed Instance, currently in EAP. [See this blog post](https://devblogs.microsoft.com/azure-sql/announcing-eap-native-vector-support-in-azure-sql-database/) for more details. - The application is a Minimal API that exposes endpoints to load documents, generate embeddings and save them into the database as Vectors, and perform searches using Vector Search and RAG. Currently, only PDF files are supported. Vectors are saved and retrieved with Entity Framework Core using the [EFCore.SqlServer.VectorSearch](https://github.com/efcore/EfCore.SqlServer.VectorSearch) library. Embedding and Chat Completion are integrated with [Semantic Kernel](https://github.com/microsoft/semantic-kernel). > [!NOTE] @@ -17,5 +14,5 @@ The application is a Minimal API that exposes endpoints to load documents, gener - Execute the [Scripts.sql](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/Scripts.sql) file to create the tables needed by the application - You may need to update the size of the [`VECTOR`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/Scripts.sql#L17) column to match the size of the embedding model. Currently, the maximum allowed value is 1998. - Open the [appsettings.json](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/SqlDatabaseVectorSearch/appsettings.json) file and set the connection string to the database and the other settings required by Azure OpenAI - - If your embedding model supports shortening, like **text-embedding-3-small** and **text-embedding-3-large**, and you want to use this feature, you need to set the [`Dimension`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/SqlDatabaseVectorSearch/appsettings.json#L17) property to match the value you have used in the SQL script. If your model doesn't provide this feature, or do you want to use the default size, just leave the [`Dimension`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/SqlDatabaseVectorSearch/appsettings.json#L17) property to NULL. Keep in mind that **text-embedding-3-small** has a dimension of 1536, while **text-embedding-3-large** uses vectors with 3072 elements, so with this latter model it is mandatory to specify a value (that, as said, must be less or equal to 1998). + - If your embedding model supports shortening, like **text-embedding-3-small** and **text-embedding-3-large**, and you want to use this feature, you need to set the [`Dimensions`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/SqlDatabaseVectorSearch/appsettings.json#L17) property to match the value you have used in the SQL script. If your model doesn't provide this feature, or do you want to use the default size, just leave the [`Dimensions`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/SqlDatabaseVectorSearch/appsettings.json#L17) property to NULL. Keep in mind that **text-embedding-3-small** has a dimension of 1536, while **text-embedding-3-large** uses vectors with 3072 elements, so with this latter model it is mandatory to specify a value (that, as said, must be less or equal to 1998). - Run the application and start importing your PDF documents. diff --git a/SqlDatabaseVectorSearch/Services/ChatService.cs b/SqlDatabaseVectorSearch/Services/ChatService.cs index 80fb965..c3cea37 100644 --- a/SqlDatabaseVectorSearch/Services/ChatService.cs +++ b/SqlDatabaseVectorSearch/Services/ChatService.cs @@ -47,9 +47,8 @@ public class ChatService(IMemoryCache cache, IChatCompletionService chatCompleti Answer the following question: --- {question} - --- + ===== Using the following information: - --- """); @@ -59,7 +58,7 @@ public class ChatService(IMemoryCache cache, IChatCompletionService chatCompleti foreach (var chunk in chunks) { - var text = $"{chunk}---"; + var text = $"---{Environment.NewLine}{chunk}"; var tokenCount = tokenizerService.CountTokens(text); if (tokenCount > tokensAvailable) @@ -68,7 +67,7 @@ public class ChatService(IMemoryCache cache, IChatCompletionService chatCompleti break; } - prompt.AppendLine(text); + prompt.Append(text); tokensAvailable -= tokenCount; if (tokensAvailable <= 0) diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj index 9773a95..2aea4e1 100644 --- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj +++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj @@ -13,6 +13,7 @@ + From 9a7ea2f5b020dd9344473487f01916c7618ad10e Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Thu, 21 Nov 2024 17:57:10 +0100 Subject: [PATCH 03/11] Update .editorconfig, VectorSearchService, and .csproj - Updated .editorconfig with new C# style preferences. - Modified using directives in VectorSearchService.cs. - Changed tuple element order in VectorSearchService.cs foreach loop. - Updated SqlDatabaseVectorSearch.csproj to target .NET 9.0. - Updated package references and removed TinyHelpers package. Update .editorconfig, fix VectorSearchService, upgrade packages Updated .editorconfig with new C# style preferences. Removed `TinyHelpers.Extensions` using directive and corrected variable order in `VectorSearchService.cs`. Upgraded target framework to `net9.0` and updated several package references in `SqlDatabaseVectorSearch.csproj`. --- .editorconfig | 1 + .../Services/VectorSearchService.cs | 3 +-- .../SqlDatabaseVectorSearch.csproj | 17 ++++++++--------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.editorconfig b/.editorconfig index def8856..263f5c7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -131,6 +131,7 @@ csharp_prefer_braces = true:silent csharp_prefer_simple_using_statement = true:suggestion csharp_style_namespace_declarations = file_scoped:suggestion csharp_style_prefer_method_group_conversion = true:silent +csharp_prefer_system_threading_lock = true:suggestion # Expression-level preferences csharp_prefer_simple_default_expression = true:suggestion diff --git a/SqlDatabaseVectorSearch/Services/VectorSearchService.cs b/SqlDatabaseVectorSearch/Services/VectorSearchService.cs index c725568..1417ce0 100644 --- a/SqlDatabaseVectorSearch/Services/VectorSearchService.cs +++ b/SqlDatabaseVectorSearch/Services/VectorSearchService.cs @@ -7,7 +7,6 @@ using Microsoft.SemanticKernel.Text; using SqlDatabaseVectorSearch.DataAccessLayer; using SqlDatabaseVectorSearch.Models; using SqlDatabaseVectorSearch.Settings; -using TinyHelpers.Extensions; using UglyToad.PdfPig; using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor; using Entities = SqlDatabaseVectorSearch.DataAccessLayer.Entities; @@ -39,7 +38,7 @@ public class VectorSearchService(ApplicationDbContext dbContext, ITextEmbeddingG var embeddings = await textEmbeddingGenerationService.GenerateEmbeddingsAsync(paragraphs); // Save the document chunks and the corresponding embedding in the database. - foreach (var (paragraph, index) in paragraphs.WithIndex()) + foreach (var (index, paragraph) in paragraphs.Index()) { var documentChunk = new Entities.DocumentChunk { Document = document, Index = index, Content = paragraph!, Embedding = embeddings[index].ToArray() }; dbContext.DocumentChunks.Add(documentChunk); diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj index 2aea4e1..e5153ac 100644 --- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj +++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable $(NoWarn);SKEXP0001;SKEXP0010;SKEXP0050; @@ -10,16 +10,15 @@ - - - - - - + + + + + + - - + From a358567c0ef8c55901112e8396b70a099d94483b Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Wed, 4 Dec 2024 10:55:14 +0100 Subject: [PATCH 04/11] Update package versions and add new package reference Updated Microsoft.SemanticKernel from 1.30.0 to 1.31.0. Updated Swashbuckle.AspNetCore from 7.0.0 to 7.1.0. Updated TinyHelpers.AspNetCore from 3.1.19 to 4.0.2. Added TinyHelpers.AspNetCore.Swashbuckle version 4.0.3. --- SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj index e5153ac..990c83f 100644 --- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj +++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj @@ -15,11 +15,12 @@ - + - - + + + From f5d5fe151f0712b14fcc39f0a1f9130da3661a6c Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Wed, 4 Dec 2024 10:59:56 +0100 Subject: [PATCH 05/11] Update TinyHelpers.AspNetCore package versions Updated TinyHelpers.AspNetCore from 4.0.2 to 4.0.4 and TinyHelpers.AspNetCore.Swashbuckle from 4.0.3 to 4.0.5 in SqlDatabaseVectorSearch.csproj. --- SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj index 990c83f..e53cf70 100644 --- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj +++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj @@ -19,8 +19,8 @@ - - + + From 810b25c23354f9fb3e053d0309d248e567c7e766 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Wed, 4 Dec 2024 15:35:36 +0100 Subject: [PATCH 06/11] Update TinyHelpers.AspNetCore to version 4.0.5 Updated the TinyHelpers.AspNetCore package from version 4.0.4 to 4.0.5 to incorporate the latest fixes and improvements. --- SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj index e53cf70..c54b8c4 100644 --- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj +++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj @@ -19,7 +19,7 @@ - + From 0575482bff4d1398817aa7f7f97cc858ff6c80ac Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Thu, 5 Dec 2024 18:00:01 +0100 Subject: [PATCH 07/11] Add EnableRetryOnFailure to SQL Server config Added EnableRetryOnFailure option to the AddSqlServer method in Program.cs. This change configures the SQL Server connection to automatically retry on failure, improving the application's resilience to transient faults. --- SqlDatabaseVectorSearch/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/SqlDatabaseVectorSearch/Program.cs b/SqlDatabaseVectorSearch/Program.cs index 6381ef6..88495b9 100644 --- a/SqlDatabaseVectorSearch/Program.cs +++ b/SqlDatabaseVectorSearch/Program.cs @@ -22,6 +22,7 @@ builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSqlServer(builder.Configuration.GetConnectionString("SqlConnection"), options => { options.UseVectorSearch(); + options.EnableRetryOnFailure(); }, options => { options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); From 62d596ea98888770ca39405c6e6f8321d4d66b71 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Tue, 10 Dec 2024 11:53:58 +0100 Subject: [PATCH 08/11] Refactor caching and OpenAPI integration Updated Program.cs to replace Swagger with OpenApi and MemoryCache with HybridCache. Refactored ChatService.cs to use HybridCache asynchronously. Removed MessageLimit from AppSettings.cs and appsettings.json. Updated SqlDatabaseVectorSearch.csproj to include HybridCache package and update dependencies. --- SqlDatabaseVectorSearch/Program.cs | 83 +++++++------------ .../Services/ChatService.cs | 42 ++++++---- .../Settings/AppSettings.cs | 2 - .../SqlDatabaseVectorSearch.csproj | 18 ++-- SqlDatabaseVectorSearch/appsettings.json | 1 - 5 files changed, 63 insertions(+), 83 deletions(-) diff --git a/SqlDatabaseVectorSearch/Program.cs b/SqlDatabaseVectorSearch/Program.cs index 88495b9..bdfd360 100644 --- a/SqlDatabaseVectorSearch/Program.cs +++ b/SqlDatabaseVectorSearch/Program.cs @@ -1,14 +1,13 @@ +using System.ComponentModel; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; -using Microsoft.OpenApi.Models; using Microsoft.SemanticKernel; -using MinimalHelpers.OpenApi; using SqlDatabaseVectorSearch.DataAccessLayer; using SqlDatabaseVectorSearch.Models; using SqlDatabaseVectorSearch.Services; using SqlDatabaseVectorSearch.Settings; using TinyHelpers.AspNetCore.Extensions; -using TinyHelpers.AspNetCore.Swagger; +using TinyHelpers.AspNetCore.OpenApi; var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true); @@ -22,13 +21,18 @@ builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSqlServer(builder.Configuration.GetConnectionString("SqlConnection"), options => { options.UseVectorSearch(); - options.EnableRetryOnFailure(); }, options => { options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); }); -builder.Services.AddMemoryCache(); +builder.Services.AddHybridCache(options => +{ + options.DefaultEntryOptions = new() + { + LocalCacheExpiration = appSettings.MessageExpiration + }; +}); // Semantic Kernel is used to generate embeddings and to reformulate questions taking into account all the previous interactions, // so that embeddings themselves can be generated more accurately. @@ -40,11 +44,8 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => +builder.Services.AddOpenApi(options => { - options.SwaggerDoc("v1", new OpenApiInfo { Title = "SQL Database Vector Search API", Version = "v1" }); - options.AddDefaultResponse(); }); @@ -61,11 +62,11 @@ app.UseStatusCodePages(); if (app.Environment.IsDevelopment()) { - app.UseSwagger(); + app.MapOpenApi(); app.UseSwaggerUI(options => { options.RoutePrefix = string.Empty; - options.SwaggerEndpoint("/swagger/v1/swagger.json", "SQL Database Vector Search API v1"); + options.SwaggerEndpoint("/openapi/v1.json", builder.Environment.ApplicationName); }); } @@ -76,24 +77,15 @@ documentsApiGroup.MapGet(string.Empty, async (VectorSearchService vectorSearchSe var documents = await vectorSearchService.GetDocumentsAsync(); return TypedResults.Ok(documents); }) -.WithOpenApi(operation => -{ - operation.Summary = "Gets the list of documents"; - return operation; -}); +.WithSummary("Gets the list of documents"); documentsApiGroup.MapGet("{documentId:guid}/chunks", async (Guid documentId, VectorSearchService vectorSearchService) => { var documents = await vectorSearchService.GetDocumentChunksAsync(documentId); return TypedResults.Ok(documents); }) -.WithOpenApi(operation => -{ - operation.Summary = "Gets the list of chunks of a given document"; - operation.Description = "The list does not contain embedding. Use '/api/documents/{documentId}/chunks/{documentChunkId}' to get the embedding for a given chunk."; - - return operation; -}); +.WithSummary("Gets the list of chunks of a given document") +.WithDescription("The list does not contain embedding. Use '/api/documents/{documentId}/chunks/{documentChunkId}' to get the embedding for a given chunk."); documentsApiGroup.MapGet("{documentId:guid}/chunks/{documentChunkId:guid}", async Task, NotFound>> (Guid documentId, Guid documentChunkId, VectorSearchService vectorSearchService) => { @@ -105,13 +97,11 @@ documentsApiGroup.MapGet("{documentId:guid}/chunks/{documentChunkId:guid}", asyn return TypedResults.Ok(chunk); }) -.WithOpenApi(operation => -{ - operation.Summary = "Gets the details of a given chunk, includings its embedding"; - return operation; -}); +.ProducesProblem(StatusCodes.Status404NotFound) +.WithSummary("Gets the details of a given chunk, includings its embedding"); -documentsApiGroup.MapPost(string.Empty, async (IFormFile file, VectorSearchService vectorSearchService, Guid? documentId = null) => +documentsApiGroup.MapPost(string.Empty, async (IFormFile file, VectorSearchService vectorSearchService, + [Description("The unique identifier of the document. If not provided, a new one will be generated. If you specify an existing documentId, the corresponding document will be overwritten.")] Guid? documentId = null) => { using var stream = file.OpenReadStream(); documentId = await vectorSearchService.ImportAsync(stream, file.FileName, documentId); @@ -119,43 +109,26 @@ documentsApiGroup.MapPost(string.Empty, async (IFormFile file, VectorSearchServi return TypedResults.Ok(new UploadDocumentResponse(documentId.Value)); }) .DisableAntiforgery() -.WithOpenApi(operation => -{ - operation.Summary = "Uploads a document"; - operation.Description = "Uploads a document to SQL Database and saves its embedding using the new native Vector type. The document will be indexed and used to answer questions. Currently, only PDF files are supported."; - - operation.Parameter("documentId").Description = "The unique identifier of the document. If not provided, a new one will be generated. If you specify an existing documentId, the corresponding document will be overwritten."; - - return operation; -}); +.ProducesProblem(StatusCodes.Status400BadRequest) +.WithSummary("Uploads a document") +.WithDescription("Uploads a document to SQL Database and saves its embedding using the new native Vector type. The document will be indexed and used to answer questions. Currently, only PDF files are supported."); documentsApiGroup.MapDelete("{documentId:guid}", async (Guid documentId, VectorSearchService vectorSearchService) => { await vectorSearchService.DeleteDocumentAsync(documentId); return TypedResults.NoContent(); }) -.WithOpenApi(operation => -{ - operation.Summary = "Deletes a document"; - operation.Description = "This endpoint deletes the document and all its chunks."; +.WithSummary("Deletes a document") +.WithDescription("This endpoint deletes the document and all its chunks."); - return operation; -}); - -app.MapPost("/api/ask", async (Question question, VectorSearchService vectorSearchService, bool reformulate = true) => +app.MapPost("/api/ask", async (Question question, VectorSearchService vectorSearchService, + [Description("If true, the question will be reformulated taking into account the context of the chat identified by the given ConversationId.")] bool reformulate = true) => { var response = await vectorSearchService.AskQuestionAsync(question, reformulate); return TypedResults.Ok(response); }) -.WithOpenApi(operation => -{ - operation.Summary = "Asks a question"; - operation.Description = "The question will be reformulated taking into account the context of the chat identified by the given ConversationId."; - - operation.Parameter("reformulate").Description = "If true, the question will be reformulated taking into account the context of the chat identified by the given ConversationId."; - - return operation; -}) +.WithSummary("Asks a question") +.WithDescription("The question will be reformulated taking into account the context of the chat identified by the given ConversationId.") .WithTags("Ask"); app.Run(); \ No newline at end of file diff --git a/SqlDatabaseVectorSearch/Services/ChatService.cs b/SqlDatabaseVectorSearch/Services/ChatService.cs index c3cea37..dce855c 100644 --- a/SqlDatabaseVectorSearch/Services/ChatService.cs +++ b/SqlDatabaseVectorSearch/Services/ChatService.cs @@ -1,5 +1,5 @@ using System.Text; -using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Options; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -7,13 +7,13 @@ using SqlDatabaseVectorSearch.Settings; namespace SqlDatabaseVectorSearch.Services; -public class ChatService(IMemoryCache cache, IChatCompletionService chatCompletionService, TokenizerService tokenizerService, IOptions appSettingsOptions) +public class ChatService(IChatCompletionService chatCompletionService, TokenizerService tokenizerService, HybridCache cache, IOptions appSettingsOptions) { private readonly AppSettings appSettings = appSettingsOptions.Value; public async Task CreateQuestionAsync(Guid conversationId, string question) { - var chat = new ChatHistory(cache.Get(conversationId) ?? []); + var chat = await GetChatHistoryAsync(conversationId); var embeddingQuestion = $""" Reformulate the following question taking into account the context of the chat to perform embeddings search: @@ -85,23 +85,33 @@ public class ChatService(IMemoryCache cache, IChatCompletionService chatCompleti }); // Add question and answer to the chat history. - var history = new ChatHistory(cache.Get(conversationId) ?? []); - history.AddUserMessage(question); - history.AddAssistantMessage(answer.Content!); - - await UpdateCacheAsync(conversationId, history); + await SetChatHistoryAsync(conversationId, question, answer.Content!); return answer.Content!; } - private Task UpdateCacheAsync(Guid conversationId, ChatHistory chat) - { - if (chat.Count > appSettings.MessageLimit) - { - chat = new ChatHistory(chat.TakeLast(appSettings.MessageLimit)); - } + private async Task UpdateCacheAsync(Guid conversationId, ChatHistory chat) + => await cache.SetAsync(conversationId.ToString(), chat); - cache.Set(conversationId, chat, appSettings.MessageExpiration); - return Task.CompletedTask; + private async Task GetChatHistoryAsync(Guid conversationId) + { + var historyCache = await cache.GetOrCreateAsync(conversationId.ToString(), + (cancellationToken) => + { + return ValueTask.FromResult([]); + }); + + var chat = new ChatHistory(historyCache); + return chat; + } + + private async Task SetChatHistoryAsync(Guid conversationId, string question, string answer) + { + var history = await GetChatHistoryAsync(conversationId); + + history.AddUserMessage(question); + history.AddAssistantMessage(answer); + + await UpdateCacheAsync(conversationId, history); } } diff --git a/SqlDatabaseVectorSearch/Settings/AppSettings.cs b/SqlDatabaseVectorSearch/Settings/AppSettings.cs index 56300a0..74564bc 100644 --- a/SqlDatabaseVectorSearch/Settings/AppSettings.cs +++ b/SqlDatabaseVectorSearch/Settings/AppSettings.cs @@ -14,7 +14,5 @@ public class AppSettings public int MaxOutputTokens { get; init; } = 800; - public int MessageLimit { get; init; } - public TimeSpan MessageExpiration { get; init; } } diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj index c54b8c4..0362729 100644 --- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj +++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj @@ -1,10 +1,10 @@  - net9.0 - enable + net9.0 + enable enable - $(NoWarn);SKEXP0001;SKEXP0010;SKEXP0050; + $(NoWarn);SKEXP0001;SKEXP0010;SKEXP0050;EXTEXP0018 @@ -12,15 +12,15 @@ + - - - - - - + + + + + diff --git a/SqlDatabaseVectorSearch/appsettings.json b/SqlDatabaseVectorSearch/appsettings.json index d3834ee..1b3beb0 100644 --- a/SqlDatabaseVectorSearch/appsettings.json +++ b/SqlDatabaseVectorSearch/appsettings.json @@ -26,7 +26,6 @@ "MaxRelevantChunks": 10, "MaxInputTokens": 16385, "MaxOutputTokens": 800, - "MessageLimit": 20, "MessageExpiration": "00:05:00" }, "Logging": { From 54b50e97595d06db3974fdbe7f9948407a05a447 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Tue, 10 Dec 2024 11:58:30 +0100 Subject: [PATCH 09/11] Update MinimalHelpers.OpenApi to version 2.1.2 Upgraded the MinimalHelpers.OpenApi package from version 2.0.17 to 2.1.2 to incorporate the latest features and bug fixes. --- SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj index 0362729..c9e5984 100644 --- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj +++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj @@ -17,7 +17,7 @@ - + From e0d4ee63cee907797f266b1a6b899099bbefd1aa Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Wed, 11 Dec 2024 10:02:12 +0100 Subject: [PATCH 10/11] Update package versions in SqlDatabaseVectorSearch.csproj Updated Swashbuckle.AspNetCore.SwaggerUI from 7.1.0 to 7.2.0. Updated TinyHelpers.AspNetCore from 4.0.5 to 4.0.6. --- SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj index c9e5984..eece150 100644 --- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj +++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj @@ -19,8 +19,8 @@ - - + + From 80071e263eefdcc683a6c844aa2f33819e5a2fd6 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Fri, 10 Jan 2025 10:43:14 +0100 Subject: [PATCH 11/11] Update comments and package version Enhanced comments in ChatService for token counting clarity. Updated Microsoft.SemanticKernel to 1.33.0 in SqlDatabaseVectorSearch.csproj. Added possible values comment for ModelId in appsettings.json. --- SqlDatabaseVectorSearch/Services/ChatService.cs | 3 ++- SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj | 2 +- SqlDatabaseVectorSearch/appsettings.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/SqlDatabaseVectorSearch/Services/ChatService.cs b/SqlDatabaseVectorSearch/Services/ChatService.cs index dce855c..fc5cd36 100644 --- a/SqlDatabaseVectorSearch/Services/ChatService.cs +++ b/SqlDatabaseVectorSearch/Services/ChatService.cs @@ -53,7 +53,8 @@ public class ChatService(IChatCompletionService chatCompletionService, Tokenizer """); var tokensAvailable = appSettings.MaxInputTokens - - tokenizerService.CountTokens(chat[0].ToString()) - tokenizerService.CountTokens(prompt.ToString()) + - tokenizerService.CountTokens(chat[0].ToString()) // System prompt. + - tokenizerService.CountTokens(prompt.ToString()) // Initial user prompt. - appSettings.MaxOutputTokens; // To ensure there is enough space for the answer. foreach (var chunk in chunks) diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj index eece150..b4a910f 100644 --- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj +++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj @@ -16,7 +16,7 @@ - + diff --git a/SqlDatabaseVectorSearch/appsettings.json b/SqlDatabaseVectorSearch/appsettings.json index 1b3beb0..7c0435b 100644 --- a/SqlDatabaseVectorSearch/appsettings.json +++ b/SqlDatabaseVectorSearch/appsettings.json @@ -7,7 +7,7 @@ "Endpoint": "", "Deployment": "", "ApiKey": "", - "ModelId": "" + "ModelId": "" // o1, gpt-4o, gpt-4, gpt-3.5 }, "Embedding": { "Endpoint": "",