222 Commits

Author SHA1 Message Date
Marco Minerva 4dcd7c9fdc Update NuGet package versions in project file
Updated several NuGet package references in SqlDatabaseVectorSearch.csproj to their latest versions, including EntityFrameworkCore.Exceptions.SqlServer, Microsoft.AspNetCore.OpenApi, Microsoft.EntityFrameworkCore.SqlServer, Microsoft.EntityFrameworkCore.Tools, Microsoft.Extensions.Caching.Hybrid, Microsoft.Extensions.Http.Resilience, Microsoft.SemanticKernel, Swashbuckle.AspNetCore.SwaggerUI, and TinyHelpers.AspNetCore. No other changes were made.
2026-06-15 17:41:53 +02:00
Marco Minerva 69759d595f Update NuGet package versions to latest releases
Updated package references in SqlDatabaseVectorSearch.csproj:
- Microsoft.AspNetCore.OpenApi to 10.0.7
- Microsoft.EntityFrameworkCore.SqlServer to 10.0.7
- Microsoft.EntityFrameworkCore.Tools to 10.0.7
- Microsoft.SemanticKernel to 1.76.0
2026-05-11 16:14:26 +02:00
Marco Minerva b3ffddeaca Update guidance and upgrade NuGet package versions
Clarified ConfigureAwait(false) usage in copilot-instructions.md for library vs ASP.NET Core scenarios. Upgraded multiple NuGet dependencies in SqlDatabaseVectorSearch.csproj to their latest versions for improved compatibility and features.
2026-04-15 12:10:05 +02:00
Marco Minerva 2b5188aa7c Update NuGet package dependencies to latest versions
Upgraded several NuGet packages in SqlDatabaseVectorSearch.csproj, including Microsoft.AspNetCore.OpenApi, EntityFrameworkCore.SqlServer/Tools, Microsoft.Extensions.Caching.Hybrid, Microsoft.Extensions.Http.Resilience, Microsoft.SemanticKernel, Swashbuckle.AspNetCore.SwaggerUI, and TinyHelpers.AspNetCore to their latest patch releases. No other code or configuration changes were made.
2026-02-27 10:46:54 +01:00
Marco Minerva cc1e62f1d5 Update NuGet packages and solution items
Removed copilot instructions from solution items. Upgraded MimeMapping to v4.0.0 and Swashbuckle.AspNetCore.SwaggerUI to v10.1.1 in the project file.
2026-02-03 09:42:41 +01:00
Marco Minerva e1323573d0 Update Microsoft.SemanticKernel to version 1.70.0
Upgraded the Microsoft.SemanticKernel NuGet package in SqlDatabaseVectorSearch.csproj from version 1.69.0 to 1.70.0 to incorporate the latest features and improvements.
2026-01-29 10:06:08 +01:00
Marco Minerva c94b117eb0 Update NuGet package dependencies to latest versions
Updated several NuGet packages in SqlDatabaseVectorSearch.csproj:
- Microsoft.AspNetCore.OpenApi, Microsoft.EntityFrameworkCore.SqlServer, and Tools to 10.0.2
- Microsoft.Extensions.Caching.Hybrid and Http.Resilience to 10.2.0
- Microsoft.SemanticKernel to 1.69.0
- MimeMapping to 3.2.0
- TinyHelpers.AspNetCore to 4.1.15
2026-01-21 16:45:10 +01:00
Marco Minerva 04d777c9d5 Improve docx parsing, chunk ordering, and DB config
- Add null check for Document in DocxContentDecoder to prevent exceptions.
- Set DocumentChunk.Id to auto-generate in ApplicationDbContext.
- Order vector search results by cosine similarity for relevance.
2026-01-13 14:43:14 +01:00
Marco Minerva 1ae1db2628 Improve vector search relevance and update NuGet packages
- Order document chunks by cosine vector distance for better relevance in VectorSearchService; limit results to MaxRelevantChunks.
- Update multiple NuGet dependencies to latest versions for improved stability and features.
2026-01-09 10:54:58 +01:00
Marco Minerva 89ff8e9c6e Update NuGet packages: FluentValidation, SemanticKernel
Updated the following NuGet package dependencies in the
`SqlDatabaseVectorSearch.csproj` file:

- `FluentValidation.DependencyInjectionExtensions` updated
  from version `12.1.0` to `12.1.1` to incorporate potential
  bug fixes and improvements.
- `Microsoft.SemanticKernel` updated from version `1.67.1`
  to `1.68.0` to include enhancements and new functionality.

These updates aim to improve stability, performance, and
compatibility of the project.
2025-12-05 17:00:44 +01:00
Marco Minerva b0fe3cb827 Update CSS and upgrade Blazor.Bootstrap package
Reduced `.card-body` height in `Ask.razor.css` for screens with a minimum width of 768px. Updated `Blazor.Bootstrap` package in `SqlDatabaseVectorSearch.csproj` from version 3.4.0 to 3.5.0.
2025-11-25 09:46:18 +01:00
Marco Minerva e4700a4e28 Update README with screenshots and .NET 10 prerequisite
Added screenshots for the Web App and Web API to README for better visualization of the application. Updated the .NET SDK prerequisite from version 9 to version 10. Fixed a minor formatting issue in the description of Semantic Kernel and EFCore.SqlServer.VectorSearch.
2025-11-19 16:13:11 +01:00
Marco Minerva 4242bcc0c7 Update README: .NET 10 badge
Updated the README.md to replace the .NET 9 badge with a .NET 10 badge, reflecting compatibility with the newer version.
2025-11-19 14:51:54 +01:00
Marco Minerva d6c0e6630f Rename and enhance ChatService methods
Renamed `CreateQuestionAsync` to `CreateReformulateQuestionAsync` for clearer intent. Updated `CreateChatAsync` to include a new `ChatSystemPrompt` property in `AzureOpenAIPromptExecutionSettings` and modified `ChatHistory` initialization to simplify setup. Removed redundant `prompt` string construction and added user messages directly to `ChatHistory`.

Updated `VectorSearchService` to use the renamed `CreateReformulateQuestionAsync` method for consistency. These changes improve code clarity, maintainability, and functionality.
2025-11-17 15:49:42 +01:00
Marco Minerva dba2fb1c41 Update to stable version and enhance OpenAPI configuration
Updated `ProductVersion` in `ApplicationDbContextModelSnapshot.cs`
to target the stable release version `10.0.0`.

Added `TinyHelpers.AspNetCore.OpenApi` to `Program.cs` and
enabled OpenAPI options by uncommenting `RemoveServerList`
and `AddDefaultProblemDetailsResponse`. These changes improve
API documentation and error handling.
2025-11-17 10:20:36 +01:00
Marco Minerva 502c8756b1 Update C# version in guidelines to C# 14
Updated the copilot-instructions.md file to reflect the use of the latest version of C#, changing the reference from "C# 13" to "C# 14". This ensures the guidelines remain up-to-date with the latest language features.
2025-11-13 18:05:10 +01:00
Marco Minerva 522da4d5d0 Refactor embeddings and add reconnect modal
Enhanced support for SQL Server's `SqlVector<float>` for embeddings, replacing `float[]` to improve vector search compatibility. Added a reconnect modal for better handling of server disconnections, including UI, styles, and JavaScript logic.

Upgraded to .NET 10.0 and EF Core 10.0, updated dependencies, and improved token counting logic for embeddings. Updated documentation and configuration files to reflect these changes. Migrated database schema to support `SqlVector<float>`.
2025-11-13 17:59:06 +01:00
Marco Minerva d6458892f7 Update package dependencies to stable and latest versions
Updated multiple NuGet package dependencies to their latest stable versions, including:
- `Microsoft.AspNetCore.OpenApi` to `10.0.0`
- `Microsoft.EntityFrameworkCore.SqlServer` to `10.0.0`
- `Microsoft.Extensions.Caching.Hybrid` to `10.0.0`
- `Microsoft.ML.Tokenizers` to `2.0.0`
- `Microsoft.SemanticKernel` to `1.67.1`
- `Swashbuckle.AspNetCore.SwaggerUI` to `10.0.1`
- Other minor updates to `PdfPig`, `TinyHelpers.AspNetCore`, and more.

These updates ensure compatibility with the latest features and improvements.
2025-11-13 17:57:27 +01:00
Marco Minerva 5ac8175080 Update package dependencies to latest versions
Updated the following NuGet packages to their latest versions:
- `FluentValidation.DependencyInjectionExtensions` to `12.1.0`
- `Microsoft.ML.Tokenizers` to `1.0.3`
- `Microsoft.ML.Tokenizers.Data.Cl100kBase` to `1.0.3`
- `Microsoft.ML.Tokenizers.Data.O200kBase` to `1.0.3`
- `Microsoft.SemanticKernel` to `1.67.0`

Removed older versions of the above packages and replaced them with the updated versions.
2025-11-04 09:57:41 +01:00
Marco Minerva bef955831d Update package versions for FluentValidation and SemanticKernel
Updated the `FluentValidation.DependencyInjectionExtensions` package
from version 12.0.0 to 12.1.0 to incorporate potential bug fixes
and improvements.

Updated the `Microsoft.SemanticKernel` package from version 1.66.0
to 1.67.0, which may include enhancements, bug fixes, or new
functionality.
2025-11-04 09:57:03 +01:00
Marco Minerva b4e47ef552 Update Microsoft.ML.Tokenizers to version 1.0.3
Updated the `<PackageReference>` for `Microsoft.ML.Tokenizers` and its related data packages (`Data.Cl100kBase` and `Data.O200kBase`) from version `1.0.2` to `1.0.3`. These updates may include bug fixes, performance improvements, or new features introduced in the latest version.
2025-10-31 17:37:56 +01:00
Marco Minerva 59877ac6c2 Merge branch 'master' of https://github.com/marcominerva/SqlDatabaseVectorSearch 2025-10-22 12:46:50 +02:00
Marco Minerva 5fcfd11326 Update package versions for various dependencies
Updated the following package versions:
- Microsoft.AspNetCore.OpenApi to 9.0.10
- Microsoft.EntityFrameworkCore.SqlServer to 9.0.10
- Microsoft.EntityFrameworkCore.Tools to 9.0.10
- Microsoft.Extensions.Caching.Hybrid to 9.10.0
- Microsoft.Extensions.Http.Resilience to 9.10.0
- Microsoft.SemanticKernel to 1.66.0
- TinyHelpers.AspNetCore to 4.1.8

These updates ensure compatibility and leverage the latest features and fixes.
2025-10-22 12:46:46 +02:00
Marco Minerva 0cc969c164 Switch to CountEmbeddingTokens in chunkers
Updated the token counting method from CountChatCompletionTokens to CountEmbeddingTokens in VectorSearchService, DefaultTextChunker, and MarkdownTextChunker to align with embedding token counting. Added a new logging configuration for Microsoft.AspNetCore.Watch.BrowserRefresh in appsettings.Development.json to manage log verbosity during development.
2025-10-16 12:32:48 +02:00
Marco Minerva 6361bfd8d1 Add ReconnectModal component and optimize resource loading
Introduced a new `<ReconnectModal />` component in `App.razor` to handle reconnection scenarios with a user interface. Added a `<ResourcePreloader />` to optimize resource loading. Updated `_Imports.razor` with a new namespace for layout components. Enhanced error handling in `Program.cs` by modifying `UseExceptionHandler` to create a new scope for errors. Added `ReconnectModal.razor`, `ReconnectModal.razor.css`, and `ReconnectModal.razor.js` to implement the modal's HTML, CSS, and JavaScript logic.
2025-10-16 12:15:00 +02:00
Marco Minerva f0873bc9bf Update package dependencies to latest versions
Updated several package dependencies in the SqlDatabaseVectorSearch.csproj file to their latest versions:
- Microsoft.AspNetCore.OpenApi, Microsoft.EntityFrameworkCore.SqlServer, and Microsoft.EntityFrameworkCore.Tools to 10.0.0-rc.2.25502.107
- Microsoft.Extensions.Caching.Hybrid and Microsoft.Extensions.Http.Resilience to 9.10.0
- Microsoft.SemanticKernel to 1.66.0
- TinyHelpers.AspNetCore to 4.1.8

These updates may include bug fixes, performance improvements, or new features.
2025-10-15 11:33:14 +02:00
Marco Minerva ab66bce35a Update package versions for stability and features
Updated the following package versions to ensure compatibility, stability, and access to the latest features:
- Microsoft.AspNetCore.OpenApi: 9.0.9 to 9.0.10
- Microsoft.EntityFrameworkCore.SqlServer: 9.0.9 to 9.0.10
- Microsoft.EntityFrameworkCore.Tools: 9.0.9 to 9.0.10
- Microsoft.Extensions.Caching.Hybrid: 9.9.0 to 9.10.0
- Microsoft.Extensions.Http.Resilience: 9.9.0 to 9.10.0
- TinyHelpers.AspNetCore: 4.1.6 to 4.1.8
2025-10-15 11:31:38 +02:00
Marco Minerva d53df6b526 Merge branch 'master' of https://github.com/marcominerva/SqlDatabaseVectorSearch 2025-10-09 10:05:14 +02:00
Marco Minerva eb7813c277 Update package versions in csproj
Updated the `Microsoft.SemanticKernel` package from version 1.65.0 to 1.66.0, indicating potential new features or improvements. Also updated the `Swashbuckle.AspNetCore.SwaggerUI` package from version 9.0.5 to 9.0.6, likely addressing bug fixes or minor enhancements.
2025-10-09 10:05:08 +02:00
Marco Minerva edae0c35b9 Update ModelId guidance for gpt-4.1 and gpt-5
Updated README.md to specify that for gpt-4.1 and gpt-5 models,
the ModelId should be set to gpt-4o for proper token counting.
Aligned appsettings.json comments with this guidance to ensure
consistency and clarity for users configuring Azure OpenAI settings.
2025-10-07 10:13:36 +02:00
Marco Minerva 8788a013e6 Update README with ModelId and VECTOR size guidelines
Enhanced documentation to clarify the configuration of `ModelId` values for `ChatCompletion` and `Embedding` in `appsettings.json`, ensuring compatibility with `Microsoft.ML.Tokenizers`. Added guidance on setting `Dimensions` for embedding models and provided instructions for updating `ApplicationDbContext` and migrations when modifying VECTOR size.
2025-10-06 11:54:06 +02:00
Marco Minerva 85ffe575ca Merge branch 'master' into net10 2025-10-06 11:36:05 +02:00
Marco Minerva 9a31ad1400 Update SwaggerUI package to version 9.0.6
Upgraded the `Swashbuckle.AspNetCore.SwaggerUI` package in the `SqlDatabaseVectorSearch.csproj` file from version 9.0.5 to 9.0.6. This minor version update likely includes bug fixes, enhancements, or other improvements while maintaining backward compatibility.
2025-10-06 11:35:15 +02:00
Marco Minerva d730b1f760 Update Swashbuckle.AspNetCore.SwaggerUI to v9.0.5
Upgraded the `Swashbuckle.AspNetCore.SwaggerUI` package in the
`SqlDatabaseVectorSearch.csproj` file from version 9.0.4 to 9.0.5.
This update may include bug fixes, new features, or other
improvements provided in the latest version.
2025-09-30 10:49:49 +02:00
Marco Minerva 13c08c1752 Update .editorconfig with new style and diagnostic rules
Added `csharp_style_prefer_simple_property_accessors` to promote
simple property accessors with suggestion severity. Disabled
CA1873 warnings for potentially expensive logging. Retained
IDE0305 rule for simplifying collection initialization for
clarity. These changes enhance code style consistency and
suppress irrelevant diagnostics.
2025-09-19 10:26:37 +02:00
Marco Minerva a80e132f8f Update Microsoft.SemanticKernel to version 1.65.0
Upgraded the `Microsoft.SemanticKernel` package reference in
`SqlDatabaseVectorSearch.csproj` from version 1.64.0 to 1.65.0.
This update may include new features, bug fixes, or performance
improvements.
2025-09-11 09:25:32 +02:00
Marco Minerva d4753ca665 Update Microsoft.SemanticKernel to version 1.65.0
Upgraded the `Microsoft.SemanticKernel` package reference in
`SqlDatabaseVectorSearch.csproj` from version 1.64.0 to 1.65.0.
This update may include bug fixes, new features, or performance
improvements introduced in the newer version.
2025-09-11 09:24:52 +02:00
Marco Minerva 404cd7565a Switch to SqlVector<float> for embeddings
Updated the application to use SQL Server's native vector data type (`SqlVector<float>`) for embeddings, replacing the previous `float[]` or `string` representations.

- Updated `.editorconfig` with new code style preferences and diagnostic rule severities.
- Modified `DocumentChunk.cs` to use `SqlVector<float>` for the `Embedding` property.
- Updated migrations and `ApplicationDbContextModelSnapshot` to reflect the new `SqlVector<float>` type.
- Replaced `AddAzureSql` with `AddSqlServer` in `Program.cs` and removed `UseVectorSearch`.
- Adjusted `DocumentService` and `VectorSearchService` to handle `SqlVector<float>` and updated vector search logic.
- Removed the `EFCore.SqlServer.VectorSearch` package and upgraded EF Core to `10.0.0-rc.1`.
- Made minor adjustments to OpenAPI configuration and dependency management.
2025-09-10 16:45:16 +02:00
Marco Minerva f5011bc44b Upgrade to .NET 10.0 and update NuGet dependencies
Updated the project to target .NET 10.0, replacing the previous .NET 9.0 target framework. Upgraded several NuGet packages to their latest pre-release versions:
- `Microsoft.AspNetCore.OpenApi` to `10.0.0-rc.1.25451.107`
- `Microsoft.EntityFrameworkCore.SqlServer` to `10.0.0-rc.1.25451.107`
- `Microsoft.EntityFrameworkCore.Tools` to `10.0.0-rc.1.25451.107`

Retained `<PrivateAssets>` and `<IncludeAssets>` settings for `Microsoft.EntityFrameworkCore.Tools`. These changes ensure compatibility with the updated framework and leverage new features and improvements.
2025-09-10 16:05:10 +02:00
Marco Minerva e0220da84e Update NuGet package versions to latest releases
Updated the following NuGet packages to their latest versions:
- `Microsoft.AspNetCore.OpenApi` to `9.0.9`
- `Microsoft.EntityFrameworkCore.SqlServer` to `9.0.9`
- `Microsoft.EntityFrameworkCore.Tools` to `9.0.9`
- `Microsoft.Extensions.Caching.Hybrid` to `9.9.0`
- `Microsoft.Extensions.Http.Resilience` to `9.9.0`
- `TinyHelpers.AspNetCore` to `4.1.6`
2025-09-10 16:02:10 +02:00
Marco Minerva ed90a22888 Update copilot instructions and package version
Updated EFCore.SqlServer.VectorSearch package version from 9.0.0-preview.2 to 9.0.0 for stability.
2025-09-08 12:57:16 +02:00
Marco Minerva 98c18139f4 Update documentation and package versions
- Clarified test practices in `copilot-instructions.md`.
- Revised Azure rules for best practices and tool usage.
- Added instruction in `ChatService.cs` for asking clarifying questions.
- Updated package versions in `SqlDatabaseVectorSearch.csproj` for several dependencies.
2025-09-01 11:58:11 +02:00
Marco Minerva 261853e4a7 Fix typos and update dependencies and configurations
Updated the version of
`TinyHelpers.AspNetCore` in `SqlDatabaseVectorSearch.csproj`
from `4.1.2` to `4.1.3`. Clarified comments in
`appsettings.json` for the `ModelId` field under
`ChatCompletion` to specify requirements for `gpt-4.1` models.
2025-08-01 16:11:06 +02:00
Marco Minerva 0ba84a35ba Update package versions for dependencies
Updated the following packages:
- `MinimalHelpers.Routing.Analyzers` to version `1.2.2`
- `PdfPig` to version `0.1.11`
- `TinyHelpers.AspNetCore` to version `4.1.2`
2025-07-28 12:41:46 +02:00
Marco Minerva b20491f247 Refactor solution structure and update package version
Updated the Visual Studio solution file to remove obsolete project configuration lines, indicating a cleanup.
The `Microsoft.SemanticKernel` package version has been upgraded from `1.60.0` to `1.61.0`.
Introduced a new solution file format (`SqlDatabaseVectorSearch.slnx`) to better organize solution items in a structured XML format.
2025-07-25 12:24:05 +02:00
Marco Minerva f6fa60247a Enhance language handling and formatting in ChatService
Updated system prompts in ChatService to emphasize that responses must match the user's question language. Added formatting requirements for answers, including the need for a period before citations. Clarified citation format and adjusted prompt construction for improved readability.
2025-07-24 15:08:43 +02:00
Marco Minerva caa2ed4542 Improve streaming content layout and spinner positioning
Updated `Ask.razor` to conditionally apply a CSS class for the `streaming-text` div based on message status, enabling a spinner display during streaming. Adjusted spinner position from bottom right to bottom left and ensured proper layout with a minimum height for `streaming-content`. Modified a comment for clarity regarding the `Citations` property.

In `Ask.razor.css`, added padding to `streaming-text` for spinner accommodation and adjusted spinner styling to maintain layout integrity.
2025-07-22 10:49:12 +02:00
Marco Minerva d8343b16e5 Refactor message handling and UI in Ask.razor
- Introduced a new `MessageStatus` enum to replace the `IsCompleted` boolean in the `Message` class, allowing for better tracking of message states (New, Streaming, Completed).
- Updated UI rendering to enhance message display with new div elements and conditional spinner for streaming messages.
- Modified the `AskQuestion` method to set the status of user and assistant messages appropriately.
- Added new CSS styles for improved layout and alignment of message components.
- Overall improvements enhance functionality and user experience in the messaging system.
2025-07-22 10:30:16 +02:00
Marco Minerva 505ccff8b7 Enhance ChatService with new prompts and simplifications
Updated ChatService to include static readonly fields for
reformulation and answering prompts. Replaced the existing
ChatSystemPrompt in CreateQuestionAsync. Simplified
GetTokenUsage using expression-bodied members. Modified
SetChatHistoryAsync to ensure correct chat history updates
in the cache.
2025-07-16 16:33:52 +02:00
Marco Minerva 595d6f974f Enhance ChatService with new prompt settings
Updated ChatService to use AzureOpenAIPromptExecutionSettings for improved chat message generation.
The CreateChatAsync method now returns both ChatHistory and settings, allowing for better configuration.
Refined question reformulation prompts to ensure context relevance and language consistency.
These changes enhance the overall functionality and interaction of the chat service.
2025-07-16 15:45:09 +02:00
Marco Minerva 7ca51232fb Add copilot-instructions.md file to the Solution 2025-07-15 12:23:10 +02:00
Marco Minerva 1065dbbf83 Update package references in SqlDatabaseVectorSearch
Updated several package references in the `SqlDatabaseVectorSearch.csproj` file to newer versions, including:
- `Microsoft.AspNetCore.OpenApi` to `9.0.7`
- `Microsoft.EntityFrameworkCore.SqlServer` to `9.0.7`
- `Microsoft.EntityFrameworkCore.Tools` to `9.0.7`
- `Microsoft.Extensions.Caching.Hybrid` to `9.7.0`
- `Microsoft.Extensions.Http.Resilience` to `9.7.0`
- `Microsoft.SemanticKernel` to `1.60.0`
- `MinimalHelpers.FluentValidation` to `1.1.4`
- `Swashbuckle.AspNetCore.SwaggerUI` to `9.0.3`
2025-07-10 10:15:57 +02:00
Marco Minerva f8a48930f3 Enhance UI and improve code documentation
Updated button styles in Documents.razor for better appearance and usability. Improved the document table layout for responsiveness, integrating checkboxes into rows. Corrected comments in VectorSearchService.cs for clarity. Added new styles for content-type badges in app.css.
2025-07-03 11:54:16 +02:00
Marco Minerva b849c78594 Sort citations by filename and page number
Updated the return statement in the `VectorSearchService` class to return citations sorted by `FileName` and `PageNumber`, improving the organization of the output.
2025-07-03 09:31:06 +02:00
Marco Minerva 0a2aaa9e6e Update package versions in SqlDatabaseVectorSearch.csproj
- Updated `Blazor.Bootstrap` from `3.3.1` to `3.4.0`.
- Updated `Microsoft.SemanticKernel` from `1.58.0` to `1.59.0`.
2025-07-02 10:11:29 +02:00
Marco Minerva ddc8ab8791 Enhance documentation and update DocumentChunk model
Updated `copilot-instructions.md` to include new guidelines for Markdown documentation and testing practices. Modified the `DocumentChunk` record in `DocumentChunk.cs` to add `PageNumber` and `IndexOnPage` properties. Adjusted `GetChunksAsync` and `GetChunkEmbeddingAsync` methods in `DocumentService.cs` to accommodate the new properties.
2025-07-01 16:21:25 +02:00
Marco Minerva b971b6bf05 Update copilot-instructions with new guidelines
Enhance `copilot-instructions.md` to include updated best practices for code suggestions, style, architecture, documentation, and testing. Key changes emphasize high confidence suggestions, the latest C# features, specific formatting preferences, and comprehensive documentation requirements. Additionally, new testing practices are introduced to streamline unit test additions and ensure code quality.
2025-06-27 11:41:29 +02:00
Marco Minerva b473511fb6 Update Microsoft.SemanticKernel to version 1.58.0
Updated the `Microsoft.SemanticKernel` package from version 1.57.0 to 1.58.0 in the `SqlDatabaseVectorSearch.csproj` file.
2025-06-26 09:45:26 +02:00
Marco Minerva 523fac2743 Enhance response formatting in ChatService
Updated response guidelines in ChatService.cs to clarify
requirements for answer formatting, including citation rules
and examples of correct and incorrect responses.
Binary differences noted in SqlDatabaseVectorSearch_WebApp.png,
indicating modifications to the image file.
2025-06-24 15:37:07 +02:00
Marco Minerva 13c0beeee6 Merge pull request #12 from marcominerva/decoder_updates
Add citation supports
2025-06-24 14:54:53 +02:00
Marco Minerva 06c1741f14 Enforce response formatting in ChatService
Updated the ChatService class to require that all responses end with a period and a space. Added a condition to include citations in a specified XML format when the answer is known, and to omit citations when the answer is unknown.
2025-06-24 14:53:14 +02:00
Marco Minerva 9eaed9176c Enhance application description and user guidance
Updated `Home.razor` to provide a more detailed overview of the application's capabilities, including document loading, embedding generation, and semantic search. Improved clarity in the supported features section and added a new feature for citations. Included new paragraphs to encourage user interaction and referenced the README for API usage details.
2025-06-24 14:33:43 +02:00
Marco Minerva 476a7734ef Update README.md to add Minimal API badge
Added a badge for "Minimal API" availability to the README.md file, enhancing the visibility of project features. Existing badges for .NET 9 and Blazor remain unchanged.
2025-06-24 12:57:37 +02:00
Marco Minerva 02861a27c5 Update README.md for clarity and organization
Enhanced the README.md file with the following changes:
- Added badges for .NET 9 and Blazor.
- Rephrased the application overview.
- Structured the Table of Contents for easier navigation.
- Reformatted setup instructions for better readability.
- Provided additional details for configuring the database and OpenAI settings.
- Expanded the supported features section.
- Included examples of API requests and responses.
- Clarified limitations and FAQ sections.
- Added a note about using straight SQL.
2025-06-24 12:55:28 +02:00
Marco Minerva 4e01ec81be Improve README.md for clarity and consistency
Updated the README.md file to enhance readability and grammatical structure. Key changes include improved phrasing, added missing commas, consistent formatting of section headers and bullet points, and correction of typographical errors. The description of the response streaming feature was also clarified to provide a better understanding of the application's functionality.
2025-06-24 12:25:42 +02:00
Marco Minerva 30fba5cfe0 Add citations feature and update streaming responses
- Updated README.md to include a new **Citations** feature, detailing how users can access source information.
- Modified JSON response examples to include a `citations` field and updated token usage details.
- Enhanced streaming response section to clarify the end of the stream includes citations.
- Adjusted `VectorSearchService.cs` to return `StreamState.End` and improved citation handling in streaming.
- Updated `appsettings.json` with new model IDs for Azure OpenAI configuration.
2025-06-24 12:16:48 +02:00
Marco Minerva c6ad2ca3ea Update package versions for dependencies
- Updated `Microsoft.SemanticKernel` to version `1.57.0`.
- Updated `Swashbuckle.AspNetCore.SwaggerUI` to version `9.0.1`.
- Updated `TinyHelpers.AspNetCore` to version `4.0.29`.
2025-06-19 09:27:24 +02:00
Marco Minerva 765daa8544 Refactor citation and token usage handling
Updated comments for clarity and streamlined logic for managing tokenUsageResponse. Removed explicit null checks in favor of a null-coalescing assignment. Ensured citations are always extracted and returned at the end of the streaming process.
2025-06-18 14:51:26 +02:00
Marco Minerva e0cf824dd6 Refactor document processing and embedding generation
- Updated `DocxContentDecoder` to process Word documents as chunks of text, removing page tracking and enhancing content handling.
- Modified `VectorSearchService.ImportAsync` to work with chunks, implementing batching for embedding generation.
- Added `EmbeddingBatchSize` property to `AppSettings` for configurable batch processing.
- Updated `appsettings.json` to include the new `EmbeddingBatchSize` setting for improved control over embedding processes.
2025-06-18 14:45:08 +02:00
Marco Minerva 1975d63189 Improve layout and readability in Documents.razor
- Wrapped checkbox input in a div for better alignment.
- Changed documents initialization from an empty array to a list.
- Updated document addition code for improved readability.
- Modified ConfirmDialogOptions and ToastMessage initializations to use object initializer syntax.
- Translated comment in DocxContentDecoder.cs from Italian to English.
2025-06-11 17:47:44 +02:00
Marco Minerva cdbe2e3a91 Enhance content decoders and update dependencies
- Modified `DocxContentDecoder` to use `IServiceProvider` for text chunking and improved paragraph processing with page break handling.
- Updated `PdfContentDecoder` and `TextContentDecoder` to trim whitespace from text before splitting into paragraphs.
- Reordered service registrations in `Program.cs` while retaining existing functionality.
- Updated `SqlDatabaseVectorSearch.csproj` with new package versions for several dependencies, including `Microsoft.AspNetCore.OpenApi` and `Microsoft.EntityFrameworkCore`.
2025-06-11 17:20:56 +02:00
Marco Minerva c9c5b74e75 Update response guidelines in ChatService class
Modified response examples and citation rules in ChatService.cs.
Changed context from France to Italy, added conditions for including citations, and emphasized formatting requirements. Updated examples to ensure compliance with new guidelines.
2025-06-10 12:19:37 +02:00
Marco Minerva 3f5f44145f Enforce strict citation formatting in services
Updated `ChatService` to ensure citations are enclosed in XML tags with a consistent format. Modified `VectorSearchService` to implement a new regex pattern for citation matching and improved text cleaning by removing citation content more efficiently.
2025-06-10 12:07:35 +02:00
Marco Minerva 4571478787 Enhance citation handling and formatting
Updated citation management in the application by removing the `RemoveCitations` and `ExtractCitations` methods in `Ask.razor`, and directly processing citations from the `delta` object. The `Response` class now includes a `Citations` property for better data handling.

Modified `VectorSearchService.cs` to extract citations from the full answer in `AskQuestionAsync` and return them at the end of the streaming process in `AskStreamingAsync`.

Introduced a new `Citation` class in `Citation.cs` to encapsulate citation properties, ensuring structured management of citation data.

Updated citation formatting rules to enforce a specific XML format, ensuring citations are presented at the end of responses rather than within the answer text.
2025-06-10 11:50:51 +02:00
Marco Minerva cdf8356e11 Enhance citation handling and document chunk structure
- Updated `Ask.razor` to change `PageNumber` to a nullable integer and added `IndexOnPage` to the `Citation` class. Adjusted regex for citation parsing.
- Introduced `PageNumber` and `IndexOnPage` properties in `DocumentChunk.cs`, marking `Content` as required.
- Modified migration files to reflect changes in `DocumentChunk` and `Document` entities.
- Updated citation format in `ChatService.cs` to include `index-on-page` and adjusted document chunk text formatting.
- Changed embedding generation method in `VectorSearchService.cs` and updated document chunk creation to include new properties.
2025-06-06 11:26:27 +02:00
Marco Minerva dc6bbfde91 Update content decoding and validation logic
- Changed `PageNumber` in `Chunk` to nullable `int?` in `IContentDecoder` and updated related logic in `TextContentDecoder`.
- Revised citation rules in `ChatService` for stricter placement and formatting.
- Introduced `QuestionValidator` class with validation rules for `Question` model's `Text` property.
2025-06-06 10:50:03 +02:00
Marco Minerva 5530a84d82 Update citation handling and formatting in Ask.razor
Refactor regex pattern in `Ask.razor` to capture
`document-id` and `chunk-id`. Update `Citation` class
to include new properties and make `PageNumber` nullable.
Adjust citation addition logic and citation format rules.
Modify chunk text formatting in `ChatService.cs` to
include page number.
2025-06-05 16:25:16 +02:00
Marco Minerva 9f5bd02f78 Refactor citation handling in Ask.razor and ChatService.cs
Updated the assistant message construction in `Ask.razor` to manage citations more effectively by introducing a `RawText` property and a new `RemoveCitations` method. The `ExtractCitations` method now processes raw input for citation extraction.

Removed outdated comments in `ChatService.cs` regarding citation formatting rules, indicating a potential shift in how citation handling is enforced.
2025-06-05 11:48:18 +02:00
Marco Minerva aae42a1658 Enhance .editorconfig with new rules and adjustments
- Added suggestion for collection expressions with loose type matches.
- Introduced diagnostic rule IDE0305 for simplified collection initialization.
2025-06-04 12:48:31 +02:00
Marco Minerva 1c24250a42 Add citation handling and styling in Ask.razor
Updated Ask.razor to include regex for citation extraction and display.
Introduced a new method to extract citations and updated the Message
class to store them. Added a Citation class for individual citation
representation. Enhanced app.css with styles for citation display.
2025-06-04 12:34:02 +02:00
Marco Minerva 0766103b9a Refactor ChatService and VectorSearchService parameters
Updated parameter types in ChatService and VectorSearchService from IEnumerable<string> to IEnumerable<Entities.DocumentChunk> for better structure. Enhanced citation formatting rules in ChatService. Increased MaxRelevantChunks and MaxInputTokens in appsettings.json to improve processing capabilities.
2025-06-04 11:42:24 +02:00
Marco Minerva 2fc070d0aa Refactor response handling and content decoding
- Updated `TextContentDecoder` to use `ITextChunker` for paragraph splitting and return a list of `Chunk` objects.
- Changed return type of `Stream` method in `AskEndpoints.cs` from `IAsyncEnumerable<QuestionResponse>` to `IAsyncEnumerable<Response>`.
- Removed `QuestionResponse` class and introduced `Response` class to better handle streaming responses.
- Modified `AskQuestionAsync` and `AskStreamingAsync` methods in `VectorSearchService` to return `Response` instead of `QuestionResponse`, and adjusted token count calculation.
- Added namespace declaration in `Response.cs` and defined properties to align with new response structure.
2025-06-04 10:22:15 +02:00
Marco Minerva a7fef36b66 Merge branch 'master' into decoder_updates 2025-06-04 10:08:35 +02:00
Marco Minerva 12e8a042db Update Microsoft.SemanticKernel to version 1.55.0
Updated the `Microsoft.SemanticKernel` package version from `1.54.0` to `1.55.0` in the `SqlDatabaseVectorSearch.csproj` file.
2025-06-04 10:08:21 +02:00
Marco Minerva c0051dbeb7 Update Swashbuckle.AspNetCore.SwaggerUI version
Updated the `Swashbuckle.AspNetCore.SwaggerUI` package from version `8.1.2` to `8.1.3` in the `SqlDatabaseVectorSearch.csproj` file.
2025-06-03 11:28:32 +02:00
Marco Minerva 1e531e5ad6 Filter out empty paragraphs in PdfContentDecoder
Updated the paragraph processing to exclude empty or whitespace-only entries before creating Chunk objects, ensuring only meaningful text is included.
2025-05-27 17:19:25 +02:00
Marco Minerva fa81f01c27 Refactor content decoders and restructure data layer
Updated `DocxContentDecoder`, `PdfContentDecoder`, and `TextContentDecoder` to return `Task<IEnumerable<Chunk>>` instead of `Task<string>`, introducing a new `Chunk` record for structured output.

Restructured the `ApplicationDbContext`, `Document`, and `DocumentChunk` classes by moving them to the `SqlDatabaseVectorSearch.Data` namespace for better organization.

Updated database migration files to align with the new entity structure and modified references in `Program.cs`, `DocumentService.cs`, and `VectorSearchService.cs` to use the new namespace.
2025-05-27 17:10:17 +02:00
Marco Minerva 599cc84928 Update Swashbuckle.AspNetCore.SwaggerUI version
Updated the `Swashbuckle.AspNetCore.SwaggerUI` package from version `8.1.1` to `8.1.2` in the `SqlDatabaseVectorSearch.csproj` file.
2025-05-26 10:32:49 +02:00
Marco Minerva 505b74ad63 Update Microsoft.SemanticKernel to version 1.54.0
Updated the `Microsoft.SemanticKernel` package from version
1.53.1 to 1.54.0 in the `SqlDatabaseVectorSearch.csproj` file.
2025-05-23 10:08:16 +02:00
Marco Minerva db0ec7cec6 Enhance copy confirmation in Ask.razor and update package
Updated the copy confirmation feature in `Ask.razor` by replacing the `isCopied` variable with `showCopyConfirmation`, improving user feedback with a longer display duration for the checkmark icon.

Also, updated the `Microsoft.SemanticKernel` package version from `1.52.1` to `1.53.1` for potential bug fixes and improvements.
2025-05-22 09:36:09 +02:00
Marco Minerva e4a0a53e53 Add tooltip feedback for clipboard copy action
Introduce a tooltip feature in `Ask.razor` that provides user feedback when copying text to the clipboard. Added `isCopied` to track the copy state and `toolTipText` to manage the tooltip message. The tooltip title updates from "Copy to Clipboard" to "Copied!" after the action, with a delay to reset the message for subsequent copies.
2025-05-20 11:42:39 +02:00
Marco Minerva e7c4c45434 Add clipboard copy feature for messages
This update introduces a new feature that allows users to copy messages to the clipboard. A boolean variable `isCopied` tracks the copy action status, and the button now shows a check icon upon successful copy, reverting after 1.5 seconds. A null check is also added in the `CopyToClipboardAsync` method to enhance error handling.
2025-05-20 10:48:31 +02:00
Marco Minerva 72ce93c563 Update Microsoft.SemanticKernel to version 1.52.1
Updated the `Microsoft.SemanticKernel` package from version
1.51.0 to 1.52.1 in the `SqlDatabaseVectorSearch.csproj` file.
2025-05-20 10:36:41 +02:00
Marco Minerva 2d979fd8f0 Update Azure OpenAI services and Semantic Kernel version
- Updated `VectorSearchService.cs` to replace the text embedding generation service with an embedding generator and adjusted embedding generation and vector search logic.
- Changed `Microsoft.SemanticKernel` package version from 1.50.0 to 1.51.0 in `SqlDatabaseVectorSearch.csproj` and modified `NoWarn` property settings.
2025-05-16 17:42:38 +02:00
Marco Minerva 2a511a8836 Update Microsoft.SemanticKernel to version 1.50.0
Updated the `Microsoft.SemanticKernel` package version from `1.49.0` to `1.50.0` in the `SqlDatabaseVectorSearch.csproj` file.
2025-05-15 11:46:32 +02:00
Marco Minerva aabedf049e Update NuGet package references in project file
Updated several NuGet package versions in `SqlDatabaseVectorSearch.csproj` to improve functionality and compatibility. Key upgrades include:
- `Microsoft.AspNetCore.OpenApi` to version `9.0.5`
- `Microsoft.EntityFrameworkCore.SqlServer` to version `9.0.5`
- `Microsoft.EntityFrameworkCore.Tools` to version `9.0.5`
- `Microsoft.Extensions.Caching.Hybrid` to version `9.5.0`
- `Microsoft.Extensions.Http.Resilience` to version `9.5.0`
- `TinyHelpers.AspNetCore` to version `4.0.26`
2025-05-14 10:47:28 +02:00
Marco Minerva 3ad751888c Merge branch 'master' of https://github.com/marcominerva/SqlDatabaseVectorSearch 2025-05-12 10:32:31 +02:00
Marco Minerva f0e07fc96b Update Microsoft.SemanticKernel to version 1.49.0
Updated the `Microsoft.SemanticKernel` package version from `1.48.0` to `1.49.0` in the `SqlDatabaseVectorSearch.csproj` file.
2025-05-12 10:32:21 +02:00
Marco Minerva 3197637019 Update README.md 2025-05-07 17:50:34 +02:00
Marco Minerva 42bfac1648 Update documentation for supported features
Enhanced descriptions in `README.md` for features:
**Conversation History with Question Reformulation**,
**Information about Token Usage**, and
**Response Streaming**.

Reformatted feature list in `Home.razor` to include bold headings
and detailed explanations for improved clarity.
2025-05-07 17:25:14 +02:00
Marco Minerva 32fce98b63 Add FluentValidation for Question model validation
- Updated `AskEndpoints.cs` to include `MinimalHelpers.FluentValidation` and standardize endpoint descriptions.
- Integrated FluentValidation in `Program.cs` and registered validators.
- Modified `SqlDatabaseVectorSearch.csproj` to add necessary package references for FluentValidation.
- Created `QuestionValidator` class to enforce validation rules on the `Question` model.
2025-05-05 15:06:22 +02:00
Marco Minerva a2ae9c05af Update Microsoft.SemanticKernel to version 1.48.0
Updated the `Microsoft.SemanticKernel` package version from 1.47.0 to 1.48.0 in the `SqlDatabaseVectorSearch.csproj` file.
2025-04-30 10:32:12 +02:00
Marco Minerva 711d4a314f Enhance chat responses and update package versions
Updated `ChatService.cs` to improve user guidance for unavailable information responses. Added multiple phrases to ensure clarity and context relevance.

Modified `SqlDatabaseVectorSearch.csproj` to upgrade `Microsoft.SemanticKernel` from `1.45.0` to `1.47.0`, and incremented versions for `Swashbuckle.AspNetCore.SwaggerUI` and `TinyHelpers.AspNetCore` to `8.1.1` and `4.0.25`, respectively, for bug fixes and improvements.
2025-04-22 11:19:32 +02:00
Marco Minerva d8e699730d Update package references in SqlDatabaseVectorSearch
Updated versions of several package references in the
`SqlDatabaseVectorSearch.csproj` file, including:
- `Microsoft.AspNetCore.OpenApi`, `Microsoft.EntityFrameworkCore.SqlServer`,
  and `Microsoft.EntityFrameworkCore.Tools` from `9.0.3` to `9.0.4`.
- `Microsoft.Extensions.Caching.Hybrid` and
  `Microsoft.Extensions.Http.Resilience` from `9.3.0` to `9.4.0`.
- `TinyHelpers.AspNetCore` from `4.0.22` to `4.0.23`.

These updates aim to incorporate bug fixes, new features,
and improvements from the latest versions.
2025-04-09 11:09:05 +02:00
Marco Minerva bfdb96368f Update README.md for embedding model requirements
Clarified the `Dimensions` property requirements for embedding models that support shortening. Added information about the maximum supported size for the `VECTOR` type (1998) and the need to update database migrations accordingly.
2025-04-07 10:04:40 +02:00
Marco Minerva 467115f1a5 Update Microsoft.SemanticKernel to version 1.45.0
Updated the `Microsoft.SemanticKernel` package version from `1.44.0` to `1.45.0` in the `SqlDatabaseVectorSearch.csproj` file.
2025-04-04 11:55:02 +02:00
Marco Minerva cb0e534d17 Update package versions for SwaggerUI and TinyHelpers
Updated `Swashbuckle.AspNetCore.SwaggerUI` from `8.0.0` to `8.1.0` and `TinyHelpers.AspNetCore` from `4.0.21` to `4.0.22`. Removed previous versions in favor of the latest updates.
2025-04-01 12:49:20 +02:00
Marco Minerva b45af8dd9c Update Microsoft.SemanticKernel to version 1.44.0
Updated the `Microsoft.SemanticKernel` package version from
1.43.0 to 1.44.0 in the `SqlDatabaseVectorSearch.csproj` file.
2025-03-27 09:12:08 +01:00
Marco Minerva 25152f8872 Add timeout configurations
Introduce `AttemptTimeout.Timeout` of 15 seconds
2025-03-25 10:25:36 +01:00
Marco Minerva 5d4ef9dcf3 Update Microsoft.SemanticKernel to version 1.43.0
Updated the `Microsoft.SemanticKernel` package from version 1.42.0 to 1.43.0 in the `SqlDatabaseVectorSearch.csproj` file.
2025-03-25 10:08:16 +01:00
Marco Minerva e7eba25bc4 Refactor token usage structure in API responses
Updated token count keys from `inputTokenCount`, `outputTokenCount`, and `totalTokenCount` to `promptTokens`, `completionTokens`, and `totalTokens` for improved clarity. Modified `question` and `answer` fields to align with the new structure, with some values set to `null` to indicate streaming state.
2025-03-24 18:01:38 +01:00
Marco Minerva 817658d539 Rename tokensAvailable to availableTokens for clarity
Updated variable names and references for improved readability and consistency in token calculations and checks.
2025-03-24 17:49:21 +01:00
Marco Minerva f563cacfb1 Update middleware configuration in Program.cs
- Added Swagger endpoint for OpenAPI specification.
- Commented out unused middleware: UseRateLimiter and UseCors.
- Other middleware configurations remain unchanged.
2025-03-24 12:16:30 +01:00
Marco Minerva 406618527c Refactor token usage and enhance logging capabilities
- Updated `TokenUsage` class properties to `PromptTokens` and `CompletionTokens`.
- Modified `Ask.razor` to display new token counts.
- Added logger to `ChatService` constructor for improved logging.
- Implemented logging for token usage in `ChatService` methods.
- Changed logging level to `Debug` for paragraph storage in `VectorSearchService`.
- Updated logging configuration in `appsettings.Development.json` for better visibility.
2025-03-24 10:12:04 +01:00
Marco Minerva a0d1126d15 Enhance file upload and notification features
- Updated `Documents.razor` to change form submission handler to `UploadFile` and added `id` to `InputFile`.
- Implemented `UploadFile` method to handle file uploads and reset input after successful upload.
- Upgraded package references in `SqlDatabaseVectorSearch.csproj` for `Microsoft.SemanticKernel` and `Swashbuckle.AspNetCore.SwaggerUI`.
- Introduced `resetFileInput` function in `functions.js` to clear file input selections.
2025-03-19 10:38:42 +01:00
Marco Minerva d20b1395e0 Update package references in project file
Incremented versions for several packages in `SqlDatabaseVectorSearch.csproj`, including:
- `Microsoft.AspNetCore.OpenApi` to `9.0.3`
- `Microsoft.EntityFrameworkCore.SqlServer` to `9.0.3`
- `Microsoft.EntityFrameworkCore.Tools` to `9.0.3`
- `Microsoft.Extensions.Caching.Hybrid` to `9.3.0` (removed preview version)
- `Microsoft.Extensions.Http.Resilience` to `9.3.0`
- `Microsoft.SemanticKernel` to `1.41.0`
- `TinyHelpers.AspNetCore` to `4.0.21`
2025-03-12 11:16:35 +01:00
Marco Minerva a7fedc6c40 Merge branch 'master' of https://github.com/marcominerva/SqlDatabaseVectorSearch 2025-03-11 17:40:14 +01:00
Marco Minerva 7fab00037a Update package versions for DocumentFormat and PdfPig
Updated the `DocumentFormat.OpenXml` package from `3.2.0` to `3.3.0` and the `PdfPig` package from `0.1.9` to `0.1.10`.
2025-03-11 17:37:31 +01:00
Marco Minerva 0b31aaccbf Enhance user feedback in Ask.razor streaming response
Added functionality to update userMessage.Text with the
reformulated question when the streaming response starts.
This improvement provides immediate feedback to users,
enhancing the overall user experience.
2025-03-11 17:34:31 +01:00
Marco Minerva 0a38de6497 Refactor document upload handling
Replaced `UploadDocumentRequest` with `UploadDocument` class to streamline data management. Updated `EditForm` to bind directly to the new model, removing the `EditContext` and `HandleFileSelected` method. Adjusted validation and submission logic to work with the new structure, enhancing clarity and maintainability.
2025-03-11 17:27:07 +01:00
Marco Minerva 261caffb6d Update README.md 2025-03-10 16:50:07 +01:00
Marco Minerva e3dcf95da7 Update HTTP client configuration in Program.cs
- Modify `AddHybridCache` to include local cache expiration.
- Change resilience handler addition to use configure method.
- Set total request timeout to 2 minutes.
2025-03-07 17:55:25 +01:00
Marco Minerva b37ad46605 Update HTTP client resilience handler configuration
Modified the `AddStandardResilienceHandler` method in
`Program.cs` to accept an `options` parameter. This allows
for setting a `TotalRequestTimeout` of 2 minutes, enhancing
the resilience handling of HTTP requests.
2025-03-04 11:00:45 +01:00
Marco Minerva 90b69f05d8 Refactor endpoint definitions and update dependencies
- Separated endpoint definitions into `AskEndpoints` and `DocumentEndpoints` classes for better organization and maintainability.
- Removed inline endpoint definitions in `Program.cs` and replaced with `app.MapEndpoints();`.
- Updated `Microsoft.SemanticKernel` package from version `1.40.0` to `1.40.1`.
- Replaced `MinimalHelpers.OpenApi` with `MinimalHelpers.Routing.Analyzers` in project dependencies.
- Defined new POST endpoints for asking questions and document management with detailed API documentation.
- Enhanced code structure for easier management and future extensibility.
2025-03-04 10:46:35 +01:00
Marco Minerva 3683d16955 Update README.md 2025-02-28 11:59:26 +01:00
Marco Minerva 69991c076d Merge pull request #10 from marcominerva/ui
Add Web application
2025-02-28 11:55:00 +01:00
Marco Minerva fba3c742dd Merge commit 2025-02-28 11:53:41 +01:00
Marco Minerva 86b8e611ea Documentation update 2025-02-28 11:52:36 +01:00
Marco Minerva c662d34a2a Refactor Ask.razor and update dependencies
- Renamed `CopyToClipboard` to `CopyToClipboardAsync` in `Ask.razor` for clarity on asynchronous operation.
- Added `ElementReference` for `chat` and introduced `EnsureMessageIsVisibleAsync` to improve message visibility.
- Modified streaming state handling for better readability.
- Made `FormatTokenUsage` and `FormatTokenUsageDetails` methods static and adjusted their implementations.
- Enhanced styling in `Home.razor` with a new paragraph class.
- Updated `SqlDatabaseVectorSearch.csproj` to upgrade `Microsoft.SemanticKernel` and other package versions.
- Added a new `scrollTo` function in `functions.js` to improve user experience by ensuring elements are visible.
2025-02-28 11:24:57 +01:00
Marco Minerva 9d2c4e2e0c Update Swashbuckle.AspNetCore.SwaggerUI version
Updated the `Swashbuckle.AspNetCore.SwaggerUI` package from version `7.2.0` to `7.3.0` in the `SqlDatabaseVectorSearch.csproj` file.
2025-02-26 16:09:04 +01:00
Marco Minerva ec5bf2acb2 Update Home.razor and project dependencies
Refactor Home.razor to replace markdown loading with static HTML content for the SQL Database Vector Search application, including a main heading and feature list. Remove the IWebHostEnvironment dependency.

Upgrade package references in SqlDatabaseVectorSearch.csproj for Microsoft.ML.Tokenizers and Microsoft.SemanticKernel.

Revise docs.md to focus on supported features instead of setup instructions.

Add new SVG files (openai.svg and sqldatabase.svg) for application icons.
2025-02-26 10:36:50 +01:00
Marco Minerva e1324115f8 Remove footer and update card layout styles
- Removed the `FooterSection` from `MainLayout.razor` and its associated styles in `MainLayout.razor.css`.
- Added top margin to the card in `Ask.razor` for improved spacing.
- Increased height of `.card-body` in both default and media queries for better content display.
- Added CSS rules in `app.css` to reset margin and padding for `body` and `html`.
- Modified binary file `SqlDatabaseVectorSearch_WebApp.png`.
2025-02-25 15:52:58 +01:00
Marco Minerva 9071e130de Enhance documentation and refactor request handling
Updated README.md to clarify functionality and added a link to OpenAPI documentation in MainLayout.razor. Removed unnecessary methods from RequestExtensions.cs for simplification. Streamlined docs.md by removing detailed JSON response examples while retaining key information about response streaming.
2025-02-25 15:17:06 +01:00
Marco Minerva eb368470e8 Update README and image assets for SQL Database Vector Search
- Enhanced README.md with new sections and images for the web app and API.
- Clarified setup instructions and reorganized content.
- Removed SqlDatabaseVectorSearch.png; modified favicon.png and SqlDatabaseVectorSearch_API.png.
- Added new image file: SqlDatabaseVectorSearch_WebApp.png.
2025-02-25 14:56:46 +01:00
Marco Minerva 22522a2d50 Update documentation for VECTOR column migration
Clarified the need to update the Database Migration when changing the size of the `VECTOR` column to match the embedding model. This replaces the previous mention that lacked a specific link to the Database Migration.
2025-02-24 12:02:17 +01:00
Marco Minerva a0a6df9cb3 Add loading state and database migration support
- Updated `Documents.razor` to show a loading spinner when documents are being fetched, introducing an `isLoading` state variable.
- Added `ConfigureDatabaseAsync` method in `Program.cs` for handling database migrations at startup.
- Modified `SqlDatabaseVectorSearch.csproj` to include `Microsoft.EntityFrameworkCore.Tools` for migration management.
- Enhanced documentation in `docs.md` regarding the `Dimensions` property and `VECTOR` column size requirements.
- Created initial migration files to define the database schema for `Documents` and `DocumentChunks` tables.
- Defined `Document` and `DocumentChunk` entities in migration files for data storage and retrieval.
2025-02-24 12:01:22 +01:00
Marco Minerva 2ce3d23e73 Downgrade Microsoft.SemanticKernel package version
The version of the `Microsoft.SemanticKernel` package was changed from `1.38.0` to `1.37.0` in the `SqlDatabaseVectorSearch.csproj` file.
2025-02-21 16:27:22 +01:00
Marco Minerva 08ebc517c8 Refactor navigation and update styles and dependencies
- Rearranged navigation items in `MainLayout.razor`.
- Removed `ToastService` injection and modified key handling in `Ask.razor`.
- Downgraded `Microsoft.SemanticKernel` version in project file.
- Updated CSS styles for sidebar brand icon and navigation item states.
- Fixed error message content in `.blazor-error-boundary` class.
2025-02-21 14:13:25 +01:00
Marco Minerva ae88408823 Merge branch 'master' into ui 2025-02-21 12:19:05 +01:00
Marco Minerva 486d73d662 Update Microsoft.SemanticKernel to version 1.38.0
Updated the `Microsoft.SemanticKernel` package version from
1.37.0 to 1.38.0 in the `SqlDatabaseVectorSearch.csproj` file.
2025-02-21 11:05:39 +01:00
Marco Minerva c22b4100fb Refactor Ask component and improve sidebar styles
- Removed clipboard icon reference and added ElementReference for input field in Ask.razor.
- Introduced OnAfterRenderAsync method to manage input focus.
- Updated app.css with new CSS variables for enhanced sidebar styling.
- Added setFocus function in functions.js to manage element focus.
2025-02-20 15:01:25 +01:00
Marco Minerva b7c8cfff76 Update button types in Ask.razor and improve CSS focus style
- Specified `Type` attributes for buttons in `Ask.razor` to clarify their intended behavior.
- Added `h1:focus` style in `app.css` to remove default outline for focused headings.
2025-02-20 12:19:20 +01:00
Marco Minerva eba0d4c272 Add footer to layout and enhance document handling
- Updated `MainLayout.razor` to include a footer displaying the framework description, styled with new CSS.
- Modified `MainLayout.razor.css` to add styles for the footer.
- Enhanced `Ask.razor` with a new `ToastService` for user notifications and improved message handling.
- Updated `Documents.razor` to restrict file uploads to specific formats and improved error handling with notifications for uploads and deletions.
2025-02-20 11:56:48 +01:00
Marco Minerva 402bf1e570 Merge branch 'master' into ui 2025-02-20 10:42:51 +01:00
Marco Minerva 7922fff402 Enhance question reformulation instructions
Added a line to the `embeddingQuestion` string to emphasize that the reformulated question must explicitly contain the subject. This change clarifies the requirements for the chat service's functionality.
2025-02-20 10:36:58 +01:00
Marco Minerva 8472775333 Add message limit to chat history management
Updated `ChatService` to enforce a message limit based on the new `MessageLimit` property in `AppSettings`. Excess messages are removed before updating the cache to optimize performance. Adjusted `appsettings.json` to reflect the new configuration, changing `MaxInputTokens` from 16385 to 16384 and adding `MessageLimit` with a default value of 20.
2025-02-20 10:30:55 +01:00
Marco Minerva 596aa7cf6f Merge branch 'master' of https://github.com/marcominerva/SqlDatabaseVectorSearch 2025-02-20 10:06:29 +01:00
Marco Minerva b1aa81e4ec Enhance question reformulation in ChatService
Updated the `embeddingQuestion` string to require that the reformulated question explicitly contains the subject of the original question. The reformulation must also be in the same language as the user's question and should not include phrases like "in this chat" or "search for."
2025-02-20 10:06:11 +01:00
Marco Minerva 5262f9f794 Enhance UI and notification handling; add documentation
- Updated `MainLayout.razor` to include a `Toasts` component and removed the `FooterSection`.
- Replaced `Toasts` in `Documents.razor` with a new `ToastService` for notifications, enhancing document upload and deletion processes.
- Modified `Home.razor` to read markdown content from a local file using `IWebHostEnvironment`.
- Removed HTTP client service registration in `Program.cs` and adjusted resilience handling configuration.
- Updated `ChatService.cs` with guidelines for question reformulation.
- Added `docs.md` with setup instructions and details on supported features for the SQL Database Vector Search application.
2025-02-20 10:05:44 +01:00
Marco Minerva e1d83f1051 Delete docs.md 2025-02-20 09:09:05 +01:00
Marco Minerva fd025ce45e Improve async handling in Ask.razor and update markdown URL
- Added `await Task.Yield();` to enhance responsiveness in `Ask.razor`.
- Updated the markdown content source from README to `docs.md` in `Home.razor` for more relevant information.
2025-02-19 17:37:47 +01:00
Marco Minerva a9028929eb Update README.md 2025-02-19 17:37:17 +01:00
Marco Minerva 79e7ddf8b1 Create docs.md 2025-02-19 17:36:46 +01:00
Marco Minerva 5382795529 Enhance Ask.razor UI and functionality
Updated clipboard button to use Button component with Icon.
Added HandleKeyDown event for submitting questions with Enter key.
Modified question handling to support streaming responses.
Refactored CopyToClipboard method to be asynchronous.
2025-02-19 17:33:30 +01:00
Marco Minerva 9f6ac67b26 Enhance chat interface and update styles
Significantly updated `Ask.razor` to improve the chat interface with a new layout for user and assistant messages, added input area for questions, and buttons for submission and reset. Removed the previous count display and introduced asynchronous message handling and a new `Message` class.

Minor change in `Documents.razor` by removing a 2000 ms delay before loading documents.

Updated `Ask.razor.css` with new styles for tooltips, avatars, input fields, card bodies, and progress indicators.

Added or updated `assistant.png` and `user.png` for new avatar images in the chat interface.
2025-02-19 16:48:02 +01:00
Marco Minerva ca51b19ea3 Change page route and update titles in Ask.razor
Updated the route from "/counter" to "/ask". Changed the page title from "Counter" to "Chat with your data" and modified the heading accordingly. The functionality for displaying the current count and the increment button remains intact.
2025-02-18 17:56:02 +01:00
Marco Minerva 9c19b4ec73 Refactor layout and remove unused components
Updated `MainLayout.razor` to include a GitHub link and modified sidebar navigation. Removed `Counter.razor` and integrated its functionality into `Ask.razor`. Enhanced error handling in `Error.razor` and removed request ID display. Deleted `Weather.razor` and its associated content.
2025-02-18 17:54:34 +01:00
Marco Minerva f0cccb00b9 Enhance error handling UI and middleware in ASP.NET
Updated `Error.razor` to provide distinct 404 and 500 error messages with a user-friendly layout, including a home button. Conditionally rendered Request ID display and removed development mode information for a cleaner user experience.

Modified `Program.cs` to change error handling middleware from re-executing the error page to redirecting with error codes, improving error information passed to the error page.
2025-02-18 14:41:16 +01:00
Marco Minerva d1ce7111c3 Enhance document loading and update UI in Blazor app
- Added `StateHasChanged()` call after loading documents in `Documents.razor`.
- Introduced `LoadDocumentsAsync` method for document retrieval.
- Updated page title in `Home.razor` and injected `IHttpClientFactory`.
- Implemented loading spinner and markdown fetching in `OnAfterRenderAsync`.
- Configured HTTP client with resilience handler in `Program.cs`.
2025-02-17 13:02:32 +01:00
Marco Minerva a0c777c138 Update README.md 2025-02-17 12:45:41 +01:00
Marco Minerva b155f8eb2e Update README.md 2025-02-17 12:40:06 +01:00
Marco Minerva be3f0dbf09 Update document handling and styling improvements
- Changed `app.css` path in `App.razor`.
- Refactored `Documents.razor` to improve form handling:
  - Removed `VectorSearchService` injection; added `IServiceProvider` and `IJSRuntime`.
  - Updated header from `<h2>` to `<h4>`.
  - Introduced `uploadDocumentRequest` for form state management.
  - Modified document ID input for optional GUID with validation.
  - Disabled upload button when no file is selected.
  - Enhanced document loading logic with scoped service provider.
  - Updated deletion logic to handle multiple document IDs.

- Added method in `DocumentService.cs` for bulk document deletion.
- Restructured `app.css` for improved styling and new validation/error message styles.
2025-02-17 12:32:12 +01:00
Marco Minerva f9a2bf0bf9 Enhance document upload and sidebar functionality
- Updated `Sidebar2` in `MainLayout.razor` for SQL Vector Search.
- Improved document upload interface in `Documents.razor` with icons and tooltips.
- Adjusted layout to include Document ID input and changed checkbox to `CheckboxInput`.
- Added `documentId` property for handling document uploads.
- Enhanced `SelectableDocument` class with `ContentType` property for better document info.
2025-02-14 18:01:59 +01:00
Marco Minerva 83e8f8ff23 Update document management UI and functionality
- Changed the "Documents" icon in `MainLayout.razor`.
- Added a `ConfirmDialog` component in `Documents.razor` for user confirmations.
- Adjusted the capitalization of the creation date column header.
- Restructured the delete button layout for better responsiveness and changed its icon.
- Updated lifecycle method from `OnInitializedAsync` to `OnAfterRenderAsync` for improved document loading.
- Introduced a new asynchronous method for obtaining local date strings.
- Updated toast messages to use the new date formatting method.
- Added a new method for converting UTC dates to local time.
- Introduced a `SelectableDocument` record class to enhance document management.
2025-02-14 17:38:03 +01:00
Marco Minerva 09f15a9cb7 Enhance application structure and functionality
- Updated README.md for clarity on application features.
- Added using directives and improved service configuration in Program.cs.
- Enhanced error handling and status code management in Program.cs.
- Changed application URL port in launchSettings.json.
- Added package references for Blazor Bootstrap and other libraries.
- Created new HTML structure in App.razor and implemented routing in Routes.razor.
- Updated MainLayout.razor for Blazor Bootstrap layout and sidebar navigation.
- Developed new components: Counter.razor, Documents.razor, Error.razor, Home.razor, and Weather.razor.
- Added utility classes: RequestExtensions.cs and StreamExtensions.cs.
- Updated app.css for custom styles and added favicon.png.
- Created functions.js for local time conversion utility.
2025-02-14 16:58:51 +01:00
Marco Minerva 5a507e972c Add Markdown support and refactor text chunking
Updated README.md to include Markdown file support.
Introduced new endpoint for uploading Markdown documents with MIME type handling.
Removed TextChunkerService and created DefaultTextChunker and MarkdownTextChunker classes implementing ITextChunker.
Updated VectorSearchService to utilize the new chunking interface.
Added MimeMapping package reference in the project file.
2025-02-14 12:06:52 +01:00
Marco Minerva e228d0bdbc Update package versions for dependencies
- Updated `Microsoft.SemanticKernel` to version `1.37.0`.
- Updated `MinimalHelpers.OpenApi` to version `2.1.4`.
- Updated `TinyHelpers.AspNetCore` to version `4.0.19`.
2025-02-14 09:51:16 +01:00
Marco Minerva 4d3172ed05 Update NuGet package versions in project file
Upgraded several NuGet packages in `SqlDatabaseVectorSearch.csproj`:
- `Microsoft.AspNetCore.OpenApi` to version `9.0.2`
- `Microsoft.EntityFrameworkCore.SqlServer` to version `9.0.2`
- `Microsoft.Extensions.Caching.Hybrid` to version `9.2.0-preview.1.25105.6`
- `Microsoft.Extensions.Http.Resilience` to version `9.2.0`

These updates may provide bug fixes, new features, and performance improvements.
2025-02-12 10:06:37 +01:00
Marco Minerva 9e844f8a0f Update service registrations and rename DocumentService methods 2025-02-10 16:27:27 +01:00
Marco Minerva a5f8425e61 Update VectorSearchService and package reference
- Removed `System.Threading` directive from `VectorSearchService.cs`
- Updated `BeginTransactionAsync` call to include `cancellationToken`
- Upgraded `Microsoft.SemanticKernel` package to version `1.36.1`
2025-02-10 16:22:03 +01:00
Marco Minerva a0f1755c85 Add CancellationToken support to async methods #9
Introduce support for `CancellationToken` across various methods to allow for task cancellation and improve responsiveness.
- Update `DecodeAsync` method in `DocxContentDecoder.cs`, `PdfContentDecoder.cs`, `TextContentDecoder.cs`, and `IContentDecoder.cs` to include an optional `CancellationToken` parameter.
- Modify endpoint handlers in `Program.cs` to accept and pass `CancellationToken` parameters.
- Update methods in `ChatService.cs` to include `CancellationToken` parameters.
- Update methods in `DocumentService.cs` to include `CancellationToken` parameters.
- Update methods in `VectorSearchService.cs` to include `CancellationToken` parameters.
These changes ensure that long-running operations can be canceled if needed, improving the application's ability to handle cancellation requests gracefully.
2025-02-10 16:20:35 +01:00
Marco Minerva d0fce6ffd2 Simplify /api/ask-streaming response and update README
The response format for the `/api/ask-streaming` endpoint has been simplified by removing multiple individual elements that contained parts of the answer.

The README.md file was updated to correct the terminology from "tag" to "property" for the *streamState* attribute.
2025-02-07 12:08:03 +01:00
Marco Minerva eeb13e9096 Merge pull request #8 from marcominerva/token_usage
Token usage
2025-02-07 12:04:13 +01:00
Marco Minerva 9312bf35cb Add new endpoints and update README with details
Updated README.md to document new `/api/documents`, `/api/ask`, and `/api/ask-streaming` endpoints, including features like conversation history, token usage, and response streaming. Enhanced Program.cs by adding new endpoints for asking questions and streaming responses, with additional metadata. Updated `documentsApiGroup` to include new document management endpoints.
2025-02-07 12:02:48 +01:00
Marco Minerva f02a1c9b69 Refactor document operations into DocumentService
Refactored Program.cs to use AddAzureSql with new options. Added VectorSearchService and DocumentService as scoped services. Updated documentsApiGroup to use DocumentService for document operations and added a delete document endpoint. Moved document-related methods from VectorSearchService to new DocumentService for better separation of concerns.
2025-02-07 11:30:14 +01:00
Marco Minerva cdd0199e8f Refactor services and update token handling
- Replace `TotalTokenCount` with `EmbeddingTokenCount` in `ImportDocumentResponse`.
- Add `OriginalQuestion` and `ReformulatedQuestion` fields to `QuestionResponse` and a new constructor.
- Add a new constructor to `TokenUsageResponse` to initialize `Question`.
- Add `TextChunkerService` to service collection in `Program.cs`.
- Clarify prompt and update token counting in `ChatService`.
- Differentiate token counting in `TokenizerService` with `CountChatCompletionTokens` and `CountEmbeddingTokens`.
- Update `VectorSearchService` to use `TextChunkerService` and new token counting methods.
- Introduce `TextChunkerService` for text splitting and tokenization.
2025-02-07 10:24:16 +01:00
Marco Minerva fd6c63c9c4 Update Microsoft.SemanticKernel to 1.36.0
Updated the Microsoft.SemanticKernel package version from 1.35.0 to 1.36.0 in the SqlDatabaseVectorSearch.csproj file.
2025-02-05 18:00:37 +01:00
Marco Minerva 8e06979993 Refactor response types and enhance token usage handling 2025-01-30 12:56:33 +01:00
Marco Minerva dd58c547d0 Merge pull request #7 from marcominerva/content_decoders
Add Content decoders
2025-01-29 10:03:40 +01:00
Marco Minerva b8aace05a5 Update Microsoft.SemanticKernel to v1.35.0
Upgraded Microsoft.SemanticKernel package from version 1.34.0 to 1.35.0 in SqlDatabaseVectorSearch.csproj. This update includes new features, bug fixes, and other improvements provided in the latest version.
2025-01-29 10:00:25 +01:00
Marco Minerva af9158873f Add support for DOCX and TXT files, update error handling
Updated README.md to reflect support for PDF, DOCX, and TXT files.
Removed commented-out code in DocxContentDecoder.cs.
Added TextContentDecoder service in Program.cs and updated exception handling middleware.
Updated document upload endpoint description in Program.cs.
Modified VectorSearchService to throw NotSupportedException for unsupported content types.
Added TextContentDecoder class in TextContentDecoder.cs.
2025-01-29 09:58:22 +01:00
Marco Minerva 110e21e1e0 Add content decoding for PDF and DOCX files
- Added `using` statements in `Program.cs` for new content decoding.
- Registered new content decoder services in `builder.Services`.
- Modified `documentsApiGroup.MapPost` to pass `file.ContentType`.
- Refactored `VectorSearchService` to use `IServiceProvider` and handle content types.
- Added `DocumentFormat.OpenXml` package reference.
- Created `DocxContentDecoder` and `PdfContentDecoder` classes.
- Created `IContentDecoder` interface.
2025-01-29 09:43:22 +01:00
Marco Minerva f15f387510 Update VectorSearchService and appsettings.json
- Clarified comment in `ChatService.cs`.
- Added `TokenizerService` and `ILogger` parameters to `VectorSearchService` constructor.
- Updated paragraph splitting to use `tokenizerService.CountTokens`.
- Added logging for token count of each paragraph in `VectorSearchService`.
- Updated `ModelId` comment in `appsettings.json` to include "gpt-4o-mini".
- Changed `MaxTokensPerParagraph` in `appsettings.json` from 1024 to 1000.
2025-01-28 16:09:34 +01:00
Marco Minerva d53330934e Merge pull request #4 from marcominerva/streaming
Add support for response streaming
2025-01-28 11:36:05 +01:00
Marco Minerva 0de2db4b5e Update SqlDatabaseVectorSearch.png binary file 2025-01-28 11:34:23 +01:00
Marco Minerva 86f161697a Remove delay in /api/ask-streaming endpoint response
Eliminated the 50ms delay in the asynchronous streaming response
logic of the /api/ask-streaming endpoint by removing the line
`await Task.Delay(50);`. This change improves the response time
by removing unnecessary latency after each delta in the response
stream.
2025-01-28 11:24:51 +01:00
Marco Minerva 1ef2d384ec Add streaming support and improve JSON serialization
- Updated `Response` record class to allow nullable `Question` and `Answer` properties; moved `StreamState` enum to a new file.
- Added `JsonStringEnumConverter` in `Program.cs` for better enum serialization.
- Corrected terminology in document upload endpoint description.
- Introduced `/api/ask-streaming` endpoint for streaming question responses.
- Added `AskStreamingAsync` method in `VectorSearchService` for handling streaming logic.
- Created `StreamState.cs` to define `StreamState` enum with `Start`, `Append`, and `End` values.
2025-01-28 11:00:45 +01:00
Marco Minerva 44c6193674 Add streaming and refactor chat/question handling
Updated `Response` record in `Response.cs` to include an optional `StreamState` property, which can be `Start`, `Append`, or `End`. Added a new `StreamState` enum to `Response.cs`.

In `ChatService.cs`, added new methods `AskQuestionAsync` and `AskStreamingAsync` to handle asking questions and streaming responses, respectively. Refactored `CreateChatAsync` to return a `ChatHistory` object.

In `VectorSearchService.cs`, added a new `AskQuestionAsync` method to handle questions using `ChatService`. Updated `CreateContextAsync` to return a tuple with the reformulated question and chunks. Removed the previous implementation of `AskQuestionAsync` and replaced it with the new method utilizing `ChatService`.
2025-01-28 10:14:47 +01:00
Marco Minerva 14bc1c131f Update Microsoft.SemanticKernel to v1.34.0
Upgraded the Microsoft.SemanticKernel package from version 1.33.0 to 1.34.0 in the SqlDatabaseVectorSearch.csproj file. This update may include bug fixes, new features, or other improvements.
2025-01-23 09:04:13 +01:00
Marco Minerva 1406e98304 Update SQL server service method in ApplicationDbContext
Replaced AddAzureSql with AddSqlServer in ApplicationDbContext.
Configured vector search and set query tracking to NoTracking.
2025-01-16 17:14:54 +01:00
Marco Minerva b581b3786c Switch to Azure SQL and update EFCore package
Modified Program.cs to use AddAzureSql for ApplicationDbContext with UseVectorSearch and NoTracking options. Updated EFCore.SqlServer.VectorSearch package to version 9.0.0-preview.2 in SqlDatabaseVectorSearch.csproj.
2025-01-16 15:16:56 +01:00
Marco Minerva 9342b8d1e9 Add HTTP client config, update settings, and add package
- Added HTTP client defaults configuration in Program.cs.
- Adjusted OpenAPI configuration order in Program.cs.
- Changed MaxTokensPerParagraph in AppSettings.cs to 1000.
- Added Microsoft.Extensions.Http.Resilience package in SqlDatabaseVectorSearch.csproj.
- Reordered and added ModelId properties in appsettings.json.
2025-01-16 15:06:31 +01:00
Marco Minerva 6dbecfbc63 Update OpenAPI setup and package versions
Updated `Program.cs` to always execute OpenAPI setup, including `app.MapOpenApi()` and `app.UseSwaggerUI`, and added `options.RemoveServerList()` to `builder.Services.AddOpenApi`. Simplified `app.UseSwaggerUI` call.

In `SqlDatabaseVectorSearch.csproj`, updated `Microsoft.ML.Tokenizers` to `1.0.1`, `Microsoft.ML.Tokenizers.Data.Cl100kBase` and `Microsoft.ML.Tokenizers.Data.O200kBase` to `1.0.1`, `MinimalHelpers.OpenApi` to `2.1.3`, and `TinyHelpers.AspNetCore` to `4.0.15`.
2025-01-16 14:38:33 +01:00
Marco Minerva 3902481735 Update package versions in SqlDatabaseVectorSearch.csproj
- Updated EFCore.SqlServer.VectorSearch from 0.2.0 to 9.0.0-preview.1
- Updated Microsoft.AspNetCore.OpenApi from 9.0.0 to 9.0.1
- Updated Microsoft.EntityFrameworkCore.SqlServer from [8.0.11,9.0.0) to 9.0.1
- Updated Microsoft.Extensions.Caching.Hybrid from 9.0.0-preview.9.24556.5 to 9.1.0-preview.1.25064.3
2025-01-15 11:17:14 +01:00
Marco Minerva 9700051942 Add appSettings field and CreateQuestionAsync method
The `ChatService` class now includes a private readonly field `appSettings`, initialized via the constructor. A new method `CreateQuestionAsync` has been added to handle question creation asynchronously, leveraging chat history. Additionally, a comment has been updated for clarity regarding chunk limitations.
2025-01-10 11:15:18 +01:00
Marco Minerva b5c7ea57c3 Add TokenizerService and update settings for token limits
Updated Program.cs to use ConfigureAndGet for settings and added TokenizerService singleton. Modified ChatService to include TokenizerService and updated prompt logic for token limits. Added TokenizerService class using TiktokenTokenizer. Updated AppSettings and AzureOpenAISettings with new token and model settings. Updated SqlDatabaseVectorSearch.csproj with new package references and version bump. Updated appsettings.json with new settings.
2025-01-10 11:08:59 +01:00
Marco Minerva 80071e263e 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.
2025-01-10 10:43:14 +01:00
Marco Minerva e0d4ee63ce 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.
2024-12-11 10:02:12 +01:00
Marco Minerva 69db8891b5 Update package versions in SqlDatabaseVectorSearch.csproj
Upgraded Swashbuckle.AspNetCore.SwaggerUI from 7.1.0 to 7.2.0.
Upgraded TinyHelpers.AspNetCore from 4.0.5 to 4.0.6.
These updates may include bug fixes, performance improvements,
or new features.
2024-12-11 10:00:05 +01:00
Marco Minerva 54b50e9759 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.
2024-12-10 11:58:30 +01:00
Marco Minerva 62d596ea98 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.
2024-12-10 11:53:58 +01:00
Marco Minerva 67c600e9d4 Remove MessageLimit and MessageExpiration properties
The `AppSettings` class in `AppSettings.cs` has been modified to
remove the `MessageLimit` and `MessageExpiration` properties.
Correspondingly, the `appsettings.json` file has been updated
to remove the `MessageLimit` and `MessageExpiration` settings
under the `AppSettings` section to maintain consistency.
2024-12-10 11:44:20 +01:00
Marco Minerva 7e632892c7 Refactor caching and update dependencies
Updated Program.cs to use HybridCache and improved Swagger setup.
Refactored ChatService to simplify chat history management.
Updated SqlDatabaseVectorSearch.csproj to target net9.0 and
updated package references.
2024-12-10 11:38:59 +01:00
Marco Minerva 0575482bff 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.
2024-12-05 18:00:01 +01:00
Marco Minerva 7989a1570f Add EnableRetryOnFailure to SQL Server configuration
Enhanced ApplicationDbContext configuration by adding EnableRetryOnFailure to improve resilience against transient SQL Server failures. Existing configurations for UseVectorSearch and NoTracking remain unchanged.
2024-12-05 17:57:41 +01:00
Marco Minerva 810b25c233 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.
2024-12-04 15:35:36 +01:00
Marco Minerva 3caae928ba 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.
2024-12-04 15:33:51 +01:00
Marco Minerva f5d5fe151f 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.
2024-12-04 10:59:56 +01:00
Marco Minerva 32d7a4da9b Update version range formatting in csproj file
Adjusted the version range specification for the `Microsoft.EntityFrameworkCore.SqlServer` package in `SqlDatabaseVectorSearch.csproj` by removing the space between the version numbers and the comma. This change improves the readability and uniformity of the code without altering the functional behavior of the version constraint.
2024-12-04 10:59:27 +01:00
Marco Minerva f4362f1e92 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.4.
Added TinyHelpers.AspNetCore.Swashbuckle version 4.0.5.
2024-12-04 10:57:56 +01:00
Marco Minerva a358567c0e 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.
2024-12-04 10:55:14 +01:00
Marco Minerva 9a7ea2f5b0 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`.
2024-11-21 17:57:10 +01:00
Marco Minerva 232be6f083 Update code style, prompt, and dependencies
.editorconfig: Add new code style preferences.
ChatService.cs: Add formatted question to prompt string.
VectorSearchService.cs: Remove TinyHelpers.Extensions using directive.
VectorSearchService.cs: Use paragraphs.Index() in foreach loop.
SqlDatabaseVectorSearch.csproj: Update target framework to net9.0.
SqlDatabaseVectorSearch.csproj: Update package references, remove TinyHelpers.
2024-11-21 17:46:50 +01:00
Marco Minerva bcd085e49d 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.
2024-11-07 10:03:58 +01:00
Marco Minerva b9bcb5c9fd Update README and refine prompt logic in ChatService
Updated README.md to include a note on Vector support in Azure SQL Database and corrected "Dimension" to "Dimensions". Modified ChatService.cs to append separator "---" only once at the end of the prompt.
2024-11-07 09:40:20 +01:00
Marco Minerva 084177346f Change ChatService to singleton, update package version
- Changed ChatService registration in Program.cs from scoped to singleton.
- Modified ChatHistory initialization in ChatService.cs by removing unnecessary line breaks.
- Renamed loop variable in ChatService.cs from 'result' to 'text' for better readability.
- Updated Microsoft.SemanticKernel package version from 1.26.0 to 1.27.0 in SqlDatabaseVectorSearch.csproj.
2024-11-06 17:26:09 +01:00
Marco Minerva 5b43031251 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.
2024-11-06 17:20:05 +01:00
Marco Minerva c18a6b4e03 Reorder method calls for Embedding property
Swapped the order of `IsRequired` and `HasColumnType("vector(1536)")` method calls for the `Embedding` property in the `ApplicationDbContext` class. This change improves readability and adheres to a preferred coding convention without altering functionality.
2024-11-05 17:18:48 +01:00
Marco Minerva dc0b557010 Update Microsoft.SemanticKernel to v1.26.0
Upgraded the Microsoft.SemanticKernel package from version 1.25.0 to 1.26.0 to incorporate the latest features and bug fixes.
2024-11-05 17:07:15 +01:00
Marco Minerva bb3e794a29 Update README: clarify EFCore usage and SQL note
The README.md file has been updated to specify that Vectors are saved and retrieved using the `EFCore.SqlServer.VectorSearch` library with Entity Framework Core. Additionally, the note about using straight SQL has been moved to a separate section and rephrased for better clarity.
2024-11-05 11:25:45 +01:00
Marco Minerva 7e8d1245b1 Update README.md with vector handling details
Added information on using EFCore.SqlServer.VectorSearch for vector management with Entity Framework Core. Included a link to the `sql` branch for users preferring direct SQL usage.
2024-10-31 15:22:44 +01:00
Marco Minerva 0435f042f1 Refactor to use EF Core for database operations
Refactored the codebase to replace raw SQL connections and Dapper with Entity Framework Core (EF Core). Modified `Program.cs` to configure EF Core services. Refactored `VectorSearchService` to use EF Core for all database operations. Updated project dependencies to remove Dapper and `Microsoft.Data.SqlClient`, and add EF Core packages. Added `ApplicationDbContext` for EF Core context and new `Document` and `DocumentChunk` classes for entity models.
2024-10-31 15:16:38 +01:00
67 changed files with 3050 additions and 324 deletions
+8
View File
@@ -22,6 +22,7 @@ dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_object_initializer = true:suggestion dotnet_style_object_initializer = true:suggestion
dotnet_style_coalesce_expression = true:suggestion dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = false:silent dotnet_style_prefer_conditional_expression_over_assignment = false:silent
dotnet_style_prefer_conditional_expression_over_return = false:silent dotnet_style_prefer_conditional_expression_over_return = false:silent
@@ -81,6 +82,7 @@ csharp_style_prefer_local_over_anonymous_function = true:silent
csharp_style_prefer_extended_property_pattern = true:suggestion csharp_style_prefer_extended_property_pattern = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:silent csharp_style_implicit_object_creation_when_type_is_apparent = true:silent
csharp_style_prefer_tuple_swap = true:silent csharp_style_prefer_tuple_swap = true:silent
csharp_style_prefer_simple_property_accessors = true:suggestion
# Field preferences # Field preferences
dotnet_style_readonly_field = true:suggestion dotnet_style_readonly_field = true:suggestion
@@ -296,3 +298,9 @@ dotnet_diagnostic.IDE0010.severity = none
# IDE0072: Add missing cases # IDE0072: Add missing cases
dotnet_diagnostic.IDE0072.severity = none dotnet_diagnostic.IDE0072.severity = none
# IDE0305: Simplify collection initialization
dotnet_diagnostic.IDE0305.severity = none
# CA1873: Avoid potentially expensive logging
dotnet_diagnostic.CA1873.severity = none
+84
View File
@@ -0,0 +1,84 @@
## General
- Make only high confidence suggestions when reviewing code changes.
- Always use the latest version C#, currently C# 14 features.
- Write code that is clean, maintainable, and easy to understand.
- Only add comments rarely to explain why a non-intuitive solution was used. The code should be self-explanatory otherwise.
- Don't add the UTF-8 BOM to files unless they have non-ASCII characters.
- Never change global.json unless explicitly asked to.
- Never change package.json or package-lock.json files unless explicitly asked to.
- Never change NuGet.config files unless explicitly asked to.
## Code Style
### Formatting
- Apply code-formatting style defined in `.editorconfig`.
- Use primary constructors where applicable.
- Prefer file-scoped namespace declarations and single-line using directives.
- Insert a newline before the opening curly brace of any code block (e.g., after `if`, `for`, `while`, `foreach`, `using`, `try`, etc.).
- Ensure that the final return statement of a method is on its own line.
- Use pattern matching and switch expressions wherever possible.
- Prefer using collection expressions when possible
- Use `is` pattern matching instead of `as` and null checks
- Use `nameof` instead of string literals when referring to member names.
- Prefer `?.` if applicable (e.g. `scope?.Dispose()`).
- Use `ObjectDisposedException.ThrowIf` where applicable.
- Use `ArgumentNullException.ThrowIfNull` to validate input parameters.
- If you add new code files, ensure they are listed in the csproj file (if other files in that folder are listed there) so they build.
### Nullable Reference Types
- Declare variables non-nullable, and check for `null` at entry points.
- Always use `is null` or `is not null` instead of `== null` or `!= null`.
- Trust the C# null annotations and don't add null checks when the type system says a value cannot be null.
## Architecture and Design Patterns
### Asynchronous Programming
- Provide both synchronous and asynchronous versions of methods where appropriate.
- Use the `Async` suffix for asynchronous methods.
- Return `Task` or `ValueTask` from asynchronous methods.
- Use `CancellationToken` parameters to support cancellation.
- Avoid async void methods except for event handlers.
- Use `ConfigureAwait(false)` only in library code that may be consumed by apps with a `SynchronizationContext` (e.g., classic ASP.NET, WPF, WinForms); it is generally unnecessary in ASP.NET Core.
### Error Handling
- Use appropriate exception types.
- Include helpful error messages.
- Avoid catching exceptions without rethrowing them.
### Performance Considerations
- Be mindful of performance implications, especially for database operations.
- Avoid unnecessary allocations.
- Consider using more efficient code that is expected to be on the hot path, even if it is less readable.
### Implementation Guidelines
- Write code that is secure by default. Avoid exposing potentially private or sensitive data.
- Make code NativeAOT compatible when possible. This means avoiding dynamic code generation, reflection, and other features that are not compatible with NativeAOT. If not possible, mark the code with an appropriate annotation or throw an exception.
## Documentation
- Include XML documentation for all public APIs. Mention the purpose, intent, and 'the why' of the code, so developers unfamiliar with the project can better understand it. If comments already exist, update them to meet the before mentioned criteria if needed. Use the full syntax of XML Doc Comments to make them as awesome as possible including references to types. Don't add any documentation that is obvious for even novice developers by reading the code.
- Add proper `<remarks>` tags with links to relevant documentation where helpful.
- For keywords like `null`, `true` or `false` use `<see langword="*" />` tags.
- Include code examples in documentation where appropriate.
- Overriding members should inherit the XML documentation from the base type via `/// <inheritdoc />`.
## Markdown
- Use Markdown for documentation files (e.g., README.md).
- Use triple backticks for code blocks, JSON snippets and bash commands, specifying the language (e.g., ```csharp, ```json and ```bash).
## Testing
- When adding new unit tests, strongly prefer to add them to existing test code files rather than creating new code files.
- We use xUnit SDK v3 for tests.
- Do not emit "Act", "Arrange" or "Assert" comments.
- Use NSubstitute for mocking in tests.
- Copy existing style in nearby files for test method names and capitalization.
- When running tests, if possible use filters and check test run counts, or look at test logs, to ensure they actually ran.
- Do not finish work with any tests commented out or disabled that were not previously commented out or disabled.
+336 -13
View File
@@ -1,18 +1,341 @@
# SQL Database Vector Search Sample # 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.
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 using direct SQL queries with [Dapper](https://github.com/DapperLib/Dapper). Embedding and Chat Completion are integrated with [Semantic Kernel](https://github.com/microsoft/semantic-kernel). [![.NET 10](https://img.shields.io/badge/.NET-10-blue)](https://dotnet.microsoft.com/en-us/download/dotnet/10.0)
[![Minimal API](https://img.shields.io/badge/Minimal%20API-Available-green)](https://dotnet.microsoft.com/apps/aspnet/apis)
[![Blazor](https://img.shields.io/badge/Blazor-WebApp-purple)](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor)
A Blazor Web App and Minimal API for performing RAG (Retrieval Augmented Generation) and vector search using the native VECTOR type in Azure SQL Database and Azure OpenAI.
## Table of Contents
- [Overview](#overview)
- [Screenshots](#screenshots)
- [Prerequisites](#prerequisites)
- [Project Structure](#project-structure)
- [Setup](#setup)
- [Supported Features](#supported-features)
- [How to Use](#how-to-use)
- [Limitations & FAQ](#limitations-faq)
- [Contributing](#contributing)
- [License](#license)
---
## Overview
This application allows you to:
- Load documents (PDF, DOCX, TXT, MD)
- Generate embeddings and save them as vectors in Azure SQL Database
- Perform semantic search and RAG using Azure OpenAI
- Interact via a Blazor Web App or programmatically via Minimal API
Embeddings and chat completion are powered by [Semantic Kernel](https://github.com/microsoft/semantic-kernel).
## Screenshots
### Web App
![SQL Database Vector Search Web App](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/assets/SqlDatabaseVectorSearch_WebApp.png)
### Web API
![SQL Database Vector Search API](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/assets/SqlDatabaseVectorSearch_API.png)
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0)
- [Azure SQL Database](https://learn.microsoft.com/en-us/azure/azure-sql/database/single-database-create-quickstart)
- Azure OpenAI resource and API keys
## Project Structure
- `SqlDatabaseVectorSearch/` - Main Blazor Web App and API
- `Components/` - Blazor UI components
- `Data/` - EF Core context, migrations, and entities
- `Endpoints/` - Minimal API endpoints
- `Services/` - Business logic and integration services
- `TextChunkers/` - Text splitting utilities
- `Settings/` - Configuration classes
## Setup
1. Clone the repository
```bash
git clone https://github.com/marcominerva/SqlDatabaseVectorSearch.git
```
2. Configure the database and OpenAI settings
- Edit `SqlDatabaseVectorSearch/appsettings.json` and set your Azure SQL connection string and OpenAI settings.
- **Important**: The `ModelId` values for both `ChatCompletion` and `Embedding` are used for token counting via `Microsoft.ML.Tokenizers`. These values must be valid model identifiers supported by the tokenizer library (e.g., `gpt-4o`, `gpt-4`, `gpt-3.5-turbo`, `text-embedding-3-small`, `text-embedding-3-large`, `text-embedding-ada-002`). The `ModelId` may differ from the actual deployment name you're using in Azure OpenAI. For example, for gpt-4.1 and gpt-5 models set the `ModelId` to `gpt-4o` for proper token counting.
- If using embedding models with shortening (e.g., `text-embedding-3-small` or `text-embedding-3-large`), set the `Dimensions` property accordingly. For `text-embedding-3-large`, you must specify a value <= 1998.
- If you change the VECTOR size, update both the [ApplicationDbContext](SqlDatabaseVectorSearch/Data/ApplicationDbContext.cs) and the [Initial Migration](SqlDatabaseVectorSearch/Data/Migrations/00000000000000_Initial.cs).
3. Run the application
```bash
dotnet run --project SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj
```
5. Access the Web App
- Navigate to `https://localhost:5001` (or the port shown in the console)
## Supported features
- **Conversation History with Question Reformulation**: This feature allows users to view the history of their conversations, including the ability to reformulate questions for better clarity and understanding. This ensures that users can track their interactions and refine their queries as needed.
- **Information about Token Usage**: Users can access detailed information about token usage, which helps in understanding the consumption of tokens during interactions. This feature provides transparency and helps users manage their token usage effectively.
- **Response Streaming**: This feature enables real-time streaming of responses, allowing users to receive information as it is being processed. This ensures a seamless and efficient flow of information, enhancing the overall user experience.
- **Citations**: The application provides citations for the sources used to justify each answer. This allows users to verify the information and understand the origin of the content provided by the system.
## How to Use
- **Web App**: Use the Blazor interface to upload documents, search, and chat with RAG.
- **API**: Import documents via `POST /api/documents` and ask questions via `POST /api/ask` or `POST /api/ask-streaming`.
#### Example API Request
```
POST /api/ask
Content-Type: application/json
{
"conversationId": "3d0bd178-499d-433a-b2bc-c35e488d9e2c"
"text": "Why is Mars called the red planet?"
}
```
#### Example API Response
```json
{
"originalQuestion": "why is mars called the red planet?",
"reformulatedQuestion": "Why is the planet Mars called the red planet?",
"answer": "Mars is called the Red Planet because its surface has an orange-red color due to being covered in iron(III) oxide dust, also known as rust. This iron oxide gives Mars its distinctive reddish appearance when observed from Earth and is the origin of its well-known nickname",
"streamState": "End",
"tokenUsage": {
"reformulation": {
"promptTokens": 812,
"completionTokens": 11,
"totalTokens": 823
},
"embeddingTokenCount": 10,
"question": {
"promptTokens": 31708,
"completionTokens": 227,
"totalTokens": 31935
}
},
"citations": [
{
"documentId": "b1870ad7-4685-42a3-576a-08ddb01159d5",
"chunkId": "749aba1e-0db5-4033-cfa6-08ddb0115da3",
"fileName": "Mars.pdf",
"quote": "surface of Mars is orange-red because it is covered in iron(III) oxide",
"pageNumber": 1,
"indexOnPage": 0
},
{
"documentId": "b1870ad7-4685-42a3-576a-08ddb01159d5",
"chunkId": "215e7197-513f-4fbe-cfa8-08ddb0115da3",
"fileName": "Mars.pdf",
"quote": "Martian surface is caused by ferric oxide, or rust",
"pageNumber": 3,
"indexOnPage": 0
}
]
}
```
### How response streaming works
When using the `/api/ask-streaming` endpoint, answers will be streamed as with the typical response from OpenAI. The format of the response is as follows:
```json
[
{
"originalQuestion": "why is mars called the red planet?",
"reformulatedQuestion": "Why is the planet Mars known as the red planet?",
"answer": null,
"streamState": "Start",
"tokenUsage": {
"reformulation": {
"promptTokens": 541,
"completionTokens": 12,
"totalTokens": 553
},
"embeddingTokenCount": 11,
"question": null
},
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": "Mars",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " is",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " known",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " as",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " the",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " red",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " planet",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " because",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " its",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " surface",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " is",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " covered",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " in",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": " iron",
"streamState": "Append",
"tokenUsage": null,
"citations": null
},
/// ...
{
"originalQuestion": null,
"reformulatedQuestion": null,
"answer": null,
"streamState": "End",
"tokenUsage": {
"reformulation": null,
"embeddingTokenCount": null,
"question": {
"promptTokens": 30949,
"completionTokens": 221,
"totalTokens": 31170
}
},
"citations": [
{
"documentId": "b1870ad7-4685-42a3-576a-08ddb01159d5",
"chunkId": "749aba1e-0db5-4033-cfa6-08ddb0115da3",
"fileName": "Mars.pdf",
"quote": "surface of Mars is orange-red",
"pageNumber": 1,
"indexOnPage": 0
},
{
"documentId": "b1870ad7-4685-42a3-576a-08ddb01159d5",
"chunkId": "215e7197-513f-4fbe-cfa8-08ddb0115da3",
"fileName": "Mars.pdf",
"quote": "red-orange appearance of the Martian surface is caused by ferric oxide, or rust",
"pageNumber": 3,
"indexOnPage": 0
}
]
}
]
```
- The first piece of the response has the following characteristics:
- The *streamState* property is set to `Start`.
- It contains the question and its reformulation (if not requested, *reformulatedQuestion* will be equal to *originalQuestion*).
- The *tokenUsage* section holds information about tokens used for reformulation (if done) and for the embedding of the question.
- Then, there are as many elements for the actual answer as necessary:
- Each one contains a token.
- The *streamState* property is set to `Append`.
- *originalQuestion*, *reformulatedQuestion*, *tokenUsage* and *citations* are always `null`.
- The stream ends when an element with *streamState* equals `End` is received. This element contains token usage information for the question and the whole answer, and the list of citations.
## Limitations & FAQ
- **VECTOR column size**: Maximum allowed is 1998. For `text-embedding-3-large`, set `Dimensions` <= 1998.
- **Supported file types**: PDF, DOCX, TXT, MD.
- **Known Issues**: See [Issues](https://github.com/marcominerva/SqlDatabaseVectorSearch/issues)
## Contributing
Contributions are welcome! Please open issues or pull requests. For major changes, discuss them first via an issue.
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
---
> [!NOTE] > [!NOTE]
> If you prefer to use Entity Framework Core, check out the [master branch](https://github.com/marcominerva/SqlDatabaseVectorSearch/tree/master). > If you prefer to use straight SQL, check out the [sql branch](https://github.com/marcominerva/SqlDatabaseVectorSearch/tree/sql).
![SQL Database Vector Search](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/sql/SqlDatabaseVectorSearch.png)
### Setup
- [Create an Azure SQL Database](https://learn.microsoft.com/en-us/azure/azure-sql/database/single-database-create-quickstart) on a server that has the Vector Support feature enabled
- Execute the [Scripts.sql](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/sql/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/sql/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/sql/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 [`Dimensions`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/sql/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/sql/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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

-32
View File
@@ -1,32 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqlDatabaseVectorSearch", "SqlDatabaseVectorSearch\SqlDatabaseVectorSearch.csproj", "{A30F41AA-3FC1-41BE-99B7-7637A6EADDDC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0D00EFA8-60BD-47AF-BE33-9D219B8AC7F6}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
Directory.Build.props = Directory.Build.props
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A30F41AA-3FC1-41BE-99B7-7637A6EADDDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A30F41AA-3FC1-41BE-99B7-7637A6EADDDC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A30F41AA-3FC1-41BE-99B7-7637A6EADDDC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A30F41AA-3FC1-41BE-99B7-7637A6EADDDC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F8D9A242-E395-4B2D-BF14-0C15B70E9D10}
EndGlobalSection
EndGlobal
+8
View File
@@ -0,0 +1,8 @@
<Solution>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
<File Path="Directory.Build.props" />
<File Path="README.md" />
</Folder>
<Project Path="SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj" />
</Solution>
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
<link href="_content/Blazor.Bootstrap/blazor.bootstrap.css" rel="stylesheet" />
<script src="https://kit.fontawesome.com/f7a7b34f96.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="@Assets["css/app.css"]" />
<link rel="stylesheet" href="@Assets["SqlDatabaseVectorSearch.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<ReconnectModal />
<script src="_framework/blazor.web.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<!-- Add chart.js reference if chart components are used in your application. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.0.1/chart.umd.js" integrity="sha512-gQhCDsnnnUfaRzD8k1L5llCCV6O9HN09zClIzzeJ8OJ9MpGmIlCxm+pdCkqTwqJ4JcjbojFr79rl2F1mzcoLMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Add chartjs-plugin-datalabels.min.js reference if chart components with data label feature is used in your application. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js" integrity="sha512-JPcRR8yFa8mmCsfrw4TNte1ZvF1e3+1SdGMslZvmrzDYxS69J7J49vkFL8u6u8PlPJK+H3voElBtUCzaXj+6ig==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Add sortable.js reference if SortableList component is used in your application. -->
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<script src="_content/Blazor.Bootstrap/blazor.bootstrap.js" asp-append-version="true"></script>
<script src="js/functions.js" asp-append-version="true"></script>
</body>
</html>
@@ -0,0 +1,59 @@
@inherits LayoutComponentBase
<Toasts class="p-3" AutoHide="true" Placement="ToastsPlacement.TopRight" />
<BlazorBootstrapLayout StickyHeader="true">
<HeaderSection>
<a href="/swagger" target="_blank" class="text-decoration-none" title="OpenAPI documentation">
<Icon Name="IconName.FileTypeJson" Class="ps-3 ps-lg-2" Size="IconSize.x2" Color="IconColor.Muted"></Icon>
</a>
<a href="https://github.com/marcominerva/SqlDatabaseVectorSearch" target="_blank" class="text-decoration-none" title="View on GitHub">
<Icon Name="IconName.Github" Class="ps-4 ps-lg-4" Size="IconSize.x2" Color="IconColor.Muted"></Icon>
</a>
</HeaderSection>
<SidebarSection>
<Sidebar2 Href="/"
IconName="IconName.Search"
Title="SQL Vector Search"
DataProvider="Sidebar2DataProvider"
WidthUnit="Unit.Px" />
</SidebarSection>
<ContentSection>
@Body
</ContentSection>
</BlazorBootstrapLayout>
@code {
private IEnumerable<NavItem> navItems = default!;
private Task<Sidebar2DataProviderResult> Sidebar2DataProvider(Sidebar2DataProviderRequest request)
{
if (navItems is null)
{
navItems = GetNavItems();
}
var result = request.ApplyTo(navItems);
return Task.FromResult(result);
}
private IEnumerable<NavItem> GetNavItems()
{
navItems = [
new() { Id = "1", Href = "/", IconName = IconName.HouseDoorFill, Text = "Home", Match = NavLinkMatch.All},
new() { Id = "2", Href= "/documents", IconName = IconName.FileText, Text = "Documents" },
new() { Id = "3", Href = "/ask", IconName = IconName.ChatDots, Text = "Ask"}
];
return navItems;
}
}
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@@ -0,0 +1,20 @@
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@@ -0,0 +1,31 @@
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
<dialog id="components-reconnect-modal" data-nosnippet>
<div class="components-reconnect-container">
<div class="components-rejoining-animation" aria-hidden="true">
<div></div>
<div></div>
</div>
<p class="components-reconnect-first-attempt-visible">
Rejoining the server...
</p>
<p class="components-reconnect-repeated-attempt-visible">
Rejoin failed... Trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
</p>
<p class="components-reconnect-failed-visible">
Failed to rejoin.<br />Please retry or reload the page.
</p>
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
Retry
</button>
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<button id="components-resume-button" class="components-pause-visible">
Resume
</button>
<p class="components-resume-failed-visible">
Failed to resume the session.<br />Please reload the page.
</p>
</div>
</dialog>
@@ -0,0 +1,157 @@
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
display: none;
}
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: block;
}
#components-reconnect-modal {
background-color: white;
width: 20rem;
margin: 20vh auto;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
&[open]
{
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
animation-fill-mode: both;
}
}
#components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1;
}
@keyframes components-reconnect-modal-slideUp {
0% {
transform: translateY(30px) scale(0.95);
}
100% {
transform: translateY(0);
}
}
@keyframes components-reconnect-modal-fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes components-reconnect-modal-fadeOutOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.components-reconnect-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#components-reconnect-modal p {
margin: 0;
text-align: center;
}
#components-reconnect-modal button {
border: 0;
background-color: #6b9ed2;
color: white;
padding: 4px 24px;
border-radius: 4px;
}
#components-reconnect-modal button:hover {
background-color: #3b6ea2;
}
#components-reconnect-modal button:active {
background-color: #6b9ed2;
}
.components-rejoining-animation {
position: relative;
width: 80px;
height: 80px;
}
.components-rejoining-animation div {
position: absolute;
border: 3px solid #0087ff;
opacity: 1;
border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes components-rejoining-animation {
0% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 80px;
height: 80px;
opacity: 0;
}
}
@@ -0,0 +1,63 @@
// Set up event handlers
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);
const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resume);
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
// Reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await Blazor.reconnect();
if (!successful) {
// We have been able to reach the server, but the circuit is no longer available.
// We'll reload the page so the user can continue using the app as quickly as possible.
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
} catch (err) {
// We got an exception, server is currently unavailable
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
} catch {
location.reload();
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}
@@ -0,0 +1,348 @@
@page "/ask"
@using System.Text.RegularExpressions
@inject IServiceProvider ServiceProvider
@inject IJSRuntime JSRuntime
<PageTitle>Chat with your data</PageTitle>
<div class="card mx-auto mt-2">
<div class="card-body">
@foreach (var message in messages)
{
if (message.Role == "user")
{
<div class="d-flex align-items-baseline text-end justify-content-end">
<div class="pe-2">
<div>
<div class="card card-text d-inline-block p-2 px-3 m-1">
<Markdown style="overflow-y:auto;">@message.Text</Markdown>
</div>
</div>
</div>
<div class="position-relative avatar">
<Image src="/images/user.png" class="img-fluid rounded-circle" alt="" />
</div>
</div>
}
else if (message.Role == "assistant")
{
<div class="d-flex align-items-baseline">
<div class="position-relative avatar">
<Image src="/images/assistant.png" class="img-fluid rounded-circle" alt="" />
</div>
<div class="pe-2">
<div>
@if (message.Text is null)
{
<div class="card card-text d-inline-block p-3 px-3 m-1">
<div class="progress-chat" role="progressbar" aria-label="I'm thinking" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar-chat">
<div class="progress-bar-indeterminate"></div>
</div>
</div>
</div>
}
else
{
<div class="card card-text d-inline-block p-2 px-3 m-1">
<div class="message-content">
<div class="streaming-content">
<div class="streaming-text @(message.Status == MessageStatus.Streaming ? "streaming-text-with-spinner" : "")">
<Markdown style="overflow-y:auto;">@message.Text</Markdown>
</div>
@if (message.Status == MessageStatus.Streaming)
{
<div class="streaming-spinner-bottom-left">
<Spinner Size="SpinnerSize.Small" Color="SpinnerColor.Primary" />
</div>
}
</div>
</div>
@if (message.Status == MessageStatus.Completed)
{
<div class="d-flex justify-content-between">
<div class="text-start bg-transparent mt-3">
<Tooltip Title="@message.TokenUsage" IsHtml="true" Color="TooltipColor.Primary" Placement="TooltipPlacement.Bottom">
<Icon Class="d-flex text-body-secondary" Name="IconName.InfoCircle"></Icon>
</Tooltip>
</div>
<div class="text-end bg-transparent">
<Tooltip Title="@toolTipText" Color="TooltipColor.Dark" Placement="TooltipPlacement.Bottom">
<Button Type="ButtonType.Button" Outline="false" @onclick="@(async () => await CopyToClipboardAsync(message.Text))">
@if (showCopyConfirmation)
{
<Icon Name="IconName.Check" Class="text-success" />
}
else
{
<Icon Name="IconName.Clipboard" />
}
</Button>
</Tooltip>
</div>
</div>
@if (message.Citations is not null && message.Citations.Count() > 0)
{
<div class="mt-3 d-flex flex-wrap">
@foreach (var citation in message.Citations)
{
<div class="border rounded p-2 me-2 mb-2 citation-box small">
<div>
<strong>@citation.FileName</strong> @if (citation.PageNumber.GetValueOrDefault() > 0)
{
<span class="ms-2">pag. @citation.PageNumber</span>
}
</div>
<div class="text-secondary small mt-1">@citation.Quote</div>
</div>
}
</div>
}
}
</div>
}
</div>
</div>
</div>
}
}
<div @ref="chat"></div>
</div>
<div class="card-footer bg-white w-100 bottom-0 m-0 p-1">
<div class="input-group">
<span class="input-group-text bg-transparent border-0">
<Tooltip Title="Messages aren't stored in any way on either the client or the server." Color="TooltipColor.Primary" Placement="TooltipPlacement.Bottom">
<Icon Class="d-flex text-body-secondary" Name="IconName.InfoCircle"></Icon>
</Tooltip>
</span>
<input @ref="askInput" type="text" @bind="@question" @bind:event="oninput" placeholder="Ask me anything..." class="form-control border-0" maxlength="2000" @onkeydown="HandleKeyDown" />
<div class="input-group-text bg-transparent border-0">
<Button Type="ButtonType.Submit" @ref="askButton" Color="ButtonColor.Primary" Disabled="@(isAsking || string.IsNullOrWhiteSpace(question))" @onclick="AskQuestion">
<Icon Name="IconName.Send" />
</Button>
<Button Type="ButtonType.Reset" @ref="resetButton" Class="ms-2" Color="ButtonColor.Secondary" Disabled="@isAsking" @onclick="Reset">
<Icon CustomIconName="bi bi-x-lg" />
</Button>
</div>
</div>
</div>
</div>
@code
{
private Button askButton = default!;
private Button resetButton = default!;
private ElementReference askInput = default!;
private ElementReference chat = default!;
private IList<Message> messages = [];
private string? question;
private Guid conversationId = Guid.NewGuid();
private bool isAsking = false;
private bool showCopyConfirmation = false;
private string toolTipText = "Copy to Clipboard";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
{
return;
}
await JSRuntime.InvokeVoidAsync("setFocus", askInput);
}
private async Task HandleKeyDown(KeyboardEventArgs e)
{
if (isAsking)
{
return;
}
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(question))
{
await AskQuestion();
}
else if (e.Key == "ArrowUp" && messages.Count >= 2)
{
question = messages[^2].Text;
}
}
private async Task AskQuestion()
{
isAsking = true;
var userQuestion = new Question(conversationId, question!);
var userMessage = new Message { Text = userQuestion.Text, Role = "user", Status = MessageStatus.Completed };
messages.Add(userMessage);
var assistantMessage = new Message { Role = "assistant", Status = MessageStatus.New };
messages.Add(assistantMessage);
question = null;
await Task.Yield();
await EnsureMessageIsVisibleAsync();
try
{
await using var scope = ServiceProvider.CreateAsyncScope();
var vectorSearchService = scope.ServiceProvider.GetRequiredService<VectorSearchService>();
var response = vectorSearchService.AskStreamingAsync(userQuestion);
await foreach (var delta in response)
{
if (delta.StreamState == StreamState.Start)
{
userMessage.Text = delta.ReformulatedQuestion;
assistantMessage.TokenUsage = FormatTokenUsage(delta.TokenUsage);
assistantMessage.Status = MessageStatus.Streaming;
}
else if (delta.StreamState == StreamState.Append)
{
// Adds tokens to the assistant message as they are received.
assistantMessage.Text += delta.Answer;
}
else if (delta.StreamState == StreamState.End)
{
// Get citations from the response.
assistantMessage.Citations = delta.Citations?.Select(c => new Citation
{
DocumentId = c.DocumentId,
ChunkId = c.ChunkId,
FileName = c.FileName,
Quote = c.Quote,
PageNumber = c.PageNumber,
IndexOnPage = c.IndexOnPage
});
assistantMessage.Status = MessageStatus.Completed;
assistantMessage.TokenUsage += FormatTokenUsage(delta.TokenUsage);
}
await Task.Yield();
StateHasChanged();
await EnsureMessageIsVisibleAsync();
}
}
catch (Exception ex)
{
assistantMessage.Text = $"There was an error while processing the question: {ex.Message}";
assistantMessage.Status = MessageStatus.Completed;
}
finally
{
await EnsureMessageIsVisibleAsync();
isAsking = false;
}
}
private void Reset()
{
question = null;
conversationId = Guid.NewGuid();
messages.Clear();
}
private async Task CopyToClipboardAsync(string text)
{
if (text is null)
return;
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
showCopyConfirmation = true;
toolTipText = "Copied!";
StateHasChanged();
await Task.Delay(3000); // Shows the checkmark for 3 seconds
toolTipText = "Copy to Clipboard";
showCopyConfirmation = false;
StateHasChanged();
}
private static string FormatTokenUsage(TokenUsageResponse? tokenUsageResponse)
{
if (tokenUsageResponse is null)
{
return string.Empty;
}
var reformulation = tokenUsageResponse.Reformulation is not null
? $"<p><strong>Reformulation:</strong><br />{FormatTokenUsageDetails(tokenUsageResponse.Reformulation)}</p>"
: string.Empty;
var embeddingTokenCount = tokenUsageResponse.EmbeddingTokenCount.HasValue
? $"<p><strong>Embedding Token Count:</strong> {tokenUsageResponse.EmbeddingTokenCount}</p>"
: string.Empty;
var question = tokenUsageResponse.Question is not null
? $"<p><strong>Question:</strong><br />{FormatTokenUsageDetails(tokenUsageResponse.Question)}</p>"
: string.Empty;
return $"{reformulation}{embeddingTokenCount}{question}";
static string FormatTokenUsageDetails(TokenUsage? tokenUsage)
{
if (tokenUsage is null)
{
return string.Empty;
}
return $"Prompt tokens: {tokenUsage.PromptTokens}<br />" +
$"Completion tokens: {tokenUsage.CompletionTokens}<br />" +
$"Total tokens: {tokenUsage.TotalTokens}";
}
}
private async Task EnsureMessageIsVisibleAsync()
{
await JSRuntime.InvokeVoidAsync("scrollTo", chat);
}
public enum MessageStatus
{
New,
Streaming,
Completed
}
public class Message
{
public string? Text { get; set; }
public required string Role { get; set; }
public MessageStatus Status { get; set; } = MessageStatus.New;
public string? TokenUsage { get; set; }
// List of citations extracted from the answer.
public IEnumerable<Citation>? Citations { get; set; }
}
public class Citation
{
public Guid DocumentId { get; set; }
public Guid ChunkId { get; set; }
public string FileName { get; set; } = null!;
public string Quote { get; set; } = null!;
public int? PageNumber { get; set; }
public int IndexOnPage { get; set; }
}
}
@@ -0,0 +1,124 @@
.tooltip-inner {
text-align: left;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
border: 2px solid #ddd;
padding: 2px;
flex: none;
}
input:focus {
outline: 0px !important;
box-shadow: none !important;
}
input[type="checkbox"],
input[type="checkbox"] + label {
cursor: pointer;
}
.card-body {
overflow: auto;
height: 560px;
}
@media (min-width: 768px) {
.card-body {
height: 650px;
}
}
@media (min-width: 2560px) {
.card-body {
height: 1020px;
}
}
.card-text {
border: 2px solid #ddd;
border-radius: 8px;
}
.progress-chat {
width: 200px;
height: 4px;
}
.progress-bar-chat {
height: 4px;
background-color: rgba(5, 114, 206, 0.2);
width: 100%;
overflow: hidden;
}
.progress-bar-indeterminate {
width: 100%;
height: 100%;
background-color: rgb(5, 114, 206);
animation: indeterminate-animation 1s infinite linear;
transform-origin: 0% 50%;
}
@keyframes indeterminate-animation {
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
}
.message-content {
position: relative;
}
.streaming-content {
position: relative;
min-height: 1.5em;
}
.streaming-text {
/* Add padding to make space for the spinner when streaming */
}
.streaming-text-with-spinner {
padding-bottom: 28px; /* Space for spinner (16px height + 8px margin + 4px extra) */
}
.streaming-spinner-bottom-left {
position: absolute;
bottom: 2px;
left: 0px;
z-index: 10;
}
.btn-clipboard {
line-height: 1;
color: var(--bs-body-color);
background-color: var(--bd-pre-bg);
border: 0;
border-radius: .25rem;
margin-right: -.4em
}
.btn-clipboard:hover {
color: var(--bs-link-hover-color)
}
.btn-clipboard:focus {
z-index: 3
}
.btn-clipboard {
position: relative;
z-index: 2;
}
@@ -0,0 +1,278 @@
@page "/documents"
@using MimeMapping
@inject IServiceProvider ServiceProvider
@inject IJSRuntime JSRuntime
<ConfirmDialog @ref="dialog" />
<PageTitle>Documents</PageTitle>
<h4 class="mb-4">
<Icon Name="IconName.Upload" class="me-2" />
Upload new document
</h4>
<EditForm Model="Model" Enhance OnValidSubmit="UploadFile">
<DataAnnotationsValidator />
<div class="row">
<div class="col-md-5 col-sm-4 col-5">
<div class="input-group">
<span class="input-group-text">
<Tooltip Title="PDF, DOCX, TXT and MD files are supported" Color="TooltipColor.Primary" Placement="TooltipPlacement.Bottom">
<Icon Class="d-flex text-body-secondary" Name="IconName.InfoCircle"></Icon>
</Tooltip>
</span>
<InputFile class="form-control" OnChange="@((e) => Model.File = e.File)" accept=".pdf,.docx,.txt,.md" id="fileInput" />
</div>
</div>
<div class="col-md-5 col-sm-5 col-5">
<div class="input-group">
<span class="input-group-text">
<Tooltip Title="The unique identifier (GUID) of the document. If not provided, a new one will be generated. If you specify an existing Document ID, the corresponding document will be overwritten." Color="TooltipColor.Primary" Placement="TooltipPlacement.Bottom">
<Icon Class="d-flex text-body-secondary me-2" Name="IconName.InfoCircle"></Icon>
</Tooltip>
Document ID
</span>
<TextInput Placeholder="Enter a valid GUID or leave empty for auto-generation" @bind-Value="@Model.DocumentId" />
</div>
<ValidationMessage For="@(() => Model.DocumentId)" />
</div>
<div class="col-md-2 col-sm-3 col-2">
<div class="d-grid gap-2">
<Button @ref="uploadButton" Type="ButtonType.Submit" Color="ButtonColor.Primary" To="#" Disabled="@(Model.File is null)" Class="w-100 py-2 fw-semibold shadow-sm"><Icon Name="IconName.Upload" /><span class="d-none d-lg-inline ps-3">Upload</span></Button>
</div>
</div>
</div>
</EditForm>
@if (isLoading && documents.Count == 0)
{
<div class="text-center">
<Spinner Type="SpinnerType.Dots" Class="me-3 mt-4" Color="SpinnerColor.Primary" />
</div>
}
else
{
<h4 class="mt-4 mb-4">
<Icon Name="IconName.Files" class="me-2" />
Available documents
</h4>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 border rounded overflow-hidden">
<thead class="table-light sticky-top">
<tr>
<th style="width:40px;"></th>
<th class="text-secondary">ID</th>
<th class="text-secondary">Name</th>
<th class="text-secondary">Content type</th>
<th class="text-secondary text-center">Chunks</th>
<th class="text-secondary">Created</th>
</tr>
</thead>
<tbody>
@foreach (var document in documents)
{
<tr class="@((document.IsSelected ? "table-primary" : null))">
<td>
<div class="d-flex justify-content-center align-items-center">
<CheckboxInput @bind-Value="document.IsSelected" />
</div>
</td>
<td class="text-break small">@document.Id</td>
<td class="fw-medium">@document.Name</td>
<td>
<span class="badge content-type-badge px-2 py-1 rounded-pill small">
@document.ContentType
</span>
</td>
<td class="text-center">@document.ChunkCount</td>
<td class="small text-secondary">@document.LocalCreationDateString</td>
</tr>
}
</tbody>
</table>
</div>
<div class="my-4"></div>
<div class="row">
<div class="col-md-2 col-sm-3 col-2">
<div class="d-grid gap-2">
<Button @ref="deleteButton" Color="ButtonColor.Danger" Disabled="@(!documents.Any(d => d.IsSelected))" @onclick="DeleteSelectedDocuments" Class="w-100 py-2 fw-semibold shadow-sm">
<Icon Name="IconName.Trash" /><span class="d-none d-lg-inline ps-3">Delete</span>
</Button>
</div>
</div>
</div>
}
@code {
private ConfirmDialog dialog = default!;
private Button uploadButton = default!;
private Button deleteButton = default!;
private bool isLoading = true;
private IList<SelectableDocument> documents = [];
private UploadDocument Model { get; set; } = new();
[Inject]
protected ToastService ToastService { get; set; } = default!;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
{
return;
}
await using var scope = ServiceProvider.CreateAsyncScope();
await LoadDocumentsAsync(scope.ServiceProvider);
StateHasChanged();
}
private async Task LoadDocumentsAsync(IServiceProvider services)
{
isLoading = true;
try
{
var documentService = services.GetRequiredService<DocumentService>();
var dbDocuments = await documentService.GetAsync();
documents.Clear();
foreach (var dbDocument in dbDocuments)
{
documents.Add(new SelectableDocument(dbDocument.Id, dbDocument.Name, dbDocument.CreationDate, dbDocument.ChunkCount)
{
LocalCreationDateString = await GetLocalDateTimeStringAsync(dbDocument.CreationDate)
});
}
}
finally
{
isLoading = false;
}
}
private async Task UploadFile()
{
if (Model.File is null)
{
return;
}
uploadButton.ShowLoading();
var fileName = Model.File.Name;
try
{
await using var inputStream = Model.File.OpenReadStream(20 * 1024 * 1024); // 20 MB
await using var stream = await inputStream.GetMemoryStreamAsync();
await using var scope = ServiceProvider.CreateAsyncScope();
var vectorSearchService = scope.ServiceProvider.GetRequiredService<VectorSearchService>();
var documentId = string.IsNullOrWhiteSpace(Model.DocumentId) ? null : (Guid?)Guid.Parse(Model.DocumentId);
await vectorSearchService.ImportAsync(stream, fileName, MimeUtility.GetMimeMapping(fileName), documentId);
ToastService.Notify(await CreateToastMessageAsync(ToastType.Success, "Upload document", $"The document {fileName} has been successfully uploaded and indexed."));
Model = new UploadDocument();
await JSRuntime.InvokeVoidAsync("resetFileInput", "fileInput");
await LoadDocumentsAsync(scope.ServiceProvider);
}
catch (Exception ex)
{
ToastService.Notify(await CreateToastMessageAsync(ToastType.Danger, "Upload error", $"There was an error while uploading the document {fileName}: {ex.Message}"));
}
finally
{
uploadButton.HideLoading();
}
}
private async Task DeleteSelectedDocuments()
{
var selectedDocumentIds = documents?.Where(d => d.IsSelected).Select(d => d.Id) ?? [];
var options = new ConfirmDialogOptions
{
YesButtonText = "Yes",
YesButtonColor = ButtonColor.Danger,
NoButtonText = "No",
NoButtonColor = ButtonColor.Secondary
};
var confirmation = await dialog.ShowAsync(
title: "Delete the selected documents?",
message1: "This will delete the documents and all the corresponding embeddings. The operation cannot be undone.",
message2: "Do you want to proceed?",
confirmDialogOptions: options);
if (!confirmation)
{
return;
}
try
{
deleteButton.ShowLoading();
await using var scope = ServiceProvider.CreateAsyncScope();
var documentService = scope.ServiceProvider.GetRequiredService<DocumentService>();
await documentService.DeleteAsync(selectedDocumentIds);
await LoadDocumentsAsync(scope.ServiceProvider);
ToastService.Notify(await CreateToastMessageAsync(ToastType.Info, "Delete documents", "The selected documents have been successfully deleted."));
}
catch (Exception ex)
{
ToastService.Notify(await CreateToastMessageAsync(ToastType.Danger, "Delete error", $"There was an error while deleting the documents: {ex.Message}"));
}
finally
{
deleteButton.HideLoading();
}
}
private async Task<ToastMessage> CreateToastMessageAsync(ToastType toastType, string title, string message)
{
var toastMessage = new ToastMessage
{
Type = toastType,
Title = title,
HelpText = await GetLocalDateTimeStringAsync(DateTimeOffset.UtcNow),
Message = message
};
return toastMessage;
}
private async Task<string> GetLocalDateTimeStringAsync(DateTimeOffset dateTime)
{
return await JSRuntime.InvokeAsync<string>("getLocalTime", dateTime);
}
private record class SelectableDocument(Guid Id, string Name, DateTimeOffset CreationDate, int ChunkCount) : Document(Id, Name, CreationDate, ChunkCount)
{
public bool IsSelected { get; set; }
public string ContentType => MimeUtility.GetMimeMapping(Name);
public string LocalCreationDateString { get; set; } = string.Empty;
}
public class UploadDocument
{
public IBrowserFile? File { get; set; }
[RegularExpression(@"^(\{|\()?[0-9a-fA-F]{8}(-?)[0-9a-fA-F]{4}(-?)[0-9a-fA-F]{4}(-?)[0-9a-fA-F]{4}(-?)[0-9a-fA-F]{12}(\}|\))?$", ErrorMessage = "Invalid GUID format.")]
public string? DocumentId { get; set; }
}
}
@@ -0,0 +1,39 @@
@page "/Error"
@using System.Diagnostics
@rendermode @(new InteractiveServerRenderMode(prerender: false))
<div class="d-flex align-items-center justify-content-center">
<div class="text-center">
@if (Code == 404)
{
<PageTitle>Page Not Found</PageTitle>
<h1 class="display-1 fw-bold">404</h1>
<p class="fs-3"><span class="text-danger">Ops!</span> Page Not Found.</p>
<p class="lead">
The page you're looking for does not exists.
</p>
}
else if (Code > 0)
{
<PageTitle>Unexpected Error</PageTitle>
<h1 class="display-1 fw-bold">500</h1>
<p class="fs-3"><span class="text-danger">Ops!</span> Unexpected error.</p>
<p class="lead">
An unexpected error occurred while loading the page. Please, wait a minute and try again.
</p>
}
<a title="Back to Home" href="/" class="btn btn-primary"><i class="bi bi-house-door-fill"></i> Back to Home</a>
</div>
</div>
@code {
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
[Parameter]
[SupplyParameterFromQuery(Name = "code")]
public int Code { get; set; }
}
@@ -0,0 +1,37 @@
@page "/"
@rendermode @(new InteractiveServerRenderMode(prerender: false))
<PageTitle>SQL Database Vector Search</PageTitle>
<h1>SQL Database Vector Search</h1>
<p class="mt-3 p-3 rounded bg-light text-dark shadow-sm">
A Blazor Web App and Minimal API for Retrieval Augmented Generation (RAG) and vector search using the native VECTOR type in <img src="/images/sqldatabase.svg" style="height:1.5em;vertical-align:middle;" /> Azure SQL Database with <img src="/images/openai.svg" style="height:1.5em;vertical-align:middle;" /> Azure OpenAI.
</p>
<p>
This application allows you to:
<ul>
<li>Load documents (PDF, DOCX, TXT, MD)</li>
<li>Generate embeddings and save them as vectors in Azure SQL Database</li>
<li>Perform semantic search and RAG using Azure OpenAI</li>
<li>Interact via a Blazor Web App or programmatically via Minimal API</li>
</ul>
Embeddings and chat completion are powered by <a href="https://github.com/microsoft/semantic-kernel" target="_blank">Semantic Kernel</a>. Vectors are managed with <a href="https://github.com/efcore/EfCore.SqlServer.VectorSearch" target="_blank">EFCore.SqlServer.VectorSearch</a>.
</p>
<h3>Supported Features</h3>
<ul>
<li><strong>Conversation History with Question Reformulation</strong>: View and reformulate your conversation history for better clarity and understanding.</li>
<li><strong>Information about Token Usage</strong>: Access detailed information about token usage for transparency and management.</li>
<li><strong>Response Streaming</strong>: Receive real-time streaming of responses for a seamless and efficient user experience.</li>
<li><strong>Citations</strong>: Get citations for the sources used to justify each answer, allowing you to verify and understand the origin of the content.</li>
</ul>
<p class="mt-3 p-3 rounded bg-light text-dark shadow-sm">
Try <a href="/documents">uploading a document</a> or <a href="/ask">ask a question</a> to get started!
</p>
<p class="mt-4">
<em>For API usage and more details, see the <a href="https://github.com/marcominerva/SqlDatabaseVectorSearch#how-to-use" target="_blank">README</a>.</em>
</p>
@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
@@ -0,0 +1,16 @@
@using System.ComponentModel.DataAnnotations
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using SqlDatabaseVectorSearch
@using SqlDatabaseVectorSearch.Components
@using SqlDatabaseVectorSearch.Components.Layout
@using SqlDatabaseVectorSearch.Extensions
@using SqlDatabaseVectorSearch.Models
@using SqlDatabaseVectorSearch.Services
@using BlazorBootstrap
@@ -0,0 +1,32 @@
using System.Text;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using SqlDatabaseVectorSearch.TextChunkers;
namespace SqlDatabaseVectorSearch.ContentDecoders;
public class DocxContentDecoder(IServiceProvider serviceProvider) : IContentDecoder
{
public Task<IEnumerable<Chunk>> DecodeAsync(Stream stream, string contentType, CancellationToken cancellationToken = default)
{
var textChunker = serviceProvider.GetRequiredKeyedService<ITextChunker>(contentType);
// Open a Word document for read-only access.
using var document = WordprocessingDocument.Open(stream, false);
var body = document.MainDocumentPart?.Document?.Body;
var content = new StringBuilder();
foreach (var p in body?.Descendants<Paragraph>() ?? [])
{
content.AppendLine(p.InnerText);
}
var paragraphs = textChunker.Split(content.ToString().Trim());
// Pages do not exist in the OpenXML format until they are rendered by a word processor.
// See https://stackoverflow.com/questions/43700252/how-to-get-page-numbers-based-on-openxmlelement for more details.
// Therefore, we will not assign a page number.
return Task.FromResult(paragraphs.Select((text, index) => new Chunk(null, index, text)).ToList().AsEnumerable());
}
}
@@ -0,0 +1,8 @@
namespace SqlDatabaseVectorSearch.ContentDecoders;
public interface IContentDecoder
{
Task<IEnumerable<Chunk>> DecodeAsync(Stream stream, string contentType, CancellationToken cancellationToken = default);
}
public record class Chunk(int? PageNumber, int IndexOnPage, string Content);
@@ -0,0 +1,33 @@
using SqlDatabaseVectorSearch.TextChunkers;
using UglyToad.PdfPig;
using UglyToad.PdfPig.Content;
using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter;
using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor;
namespace SqlDatabaseVectorSearch.ContentDecoders;
public class PdfContentDecoder(IServiceProvider serviceProvider) : IContentDecoder
{
public Task<IEnumerable<Chunk>> DecodeAsync(Stream stream, string contentType, CancellationToken cancellationToken = default)
{
var textChunker = serviceProvider.GetRequiredKeyedService<ITextChunker>(contentType);
// Read the content of the PDF document.
using var pdfDocument = PdfDocument.Open(stream);
var paragraphs = pdfDocument.GetPages().SelectMany(page => GetPageParagraphs(page, textChunker)).ToList();
return Task.FromResult(paragraphs.AsEnumerable());
}
private static IEnumerable<Chunk> GetPageParagraphs(Page pdfPage, ITextChunker textChunker)
{
var letters = pdfPage.Letters;
var words = NearestNeighbourWordExtractor.Instance.GetWords(letters);
var textBlocks = DocstrumBoundingBoxes.Instance.GetBlocks(words);
var pageText = string.Join($"{Environment.NewLine}{Environment.NewLine}", textBlocks.Select(t => t.Text.ReplaceLineEndings(" ")));
var paragraphs = textChunker.Split(pageText.Trim());
return paragraphs.Where(p => !string.IsNullOrWhiteSpace(p)).Select((text, index) => new Chunk(pdfPage.Number, index, text));
}
}
@@ -0,0 +1,17 @@
using SqlDatabaseVectorSearch.TextChunkers;
namespace SqlDatabaseVectorSearch.ContentDecoders;
public class TextContentDecoder(IServiceProvider serviceProvider) : IContentDecoder
{
public async Task<IEnumerable<Chunk>> DecodeAsync(Stream stream, string contentType, CancellationToken cancellationToken = default)
{
var textChunker = serviceProvider.GetRequiredKeyedService<ITextChunker>(contentType);
using var readStream = new StreamReader(stream);
var content = await readStream.ReadToEndAsync(cancellationToken);
var paragraphs = textChunker.Split(content.Trim());
return paragraphs.Select((text, index) => new Chunk(null, index, text)).ToList();
}
}
@@ -0,0 +1,51 @@
using EntityFramework.Exceptions.SqlServer;
using Microsoft.EntityFrameworkCore;
using SqlDatabaseVectorSearch.Data.Entities;
namespace SqlDatabaseVectorSearch.Data;
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options)
{
public virtual DbSet<Document> Documents { get; set; }
public virtual DbSet<DocumentChunk> DocumentChunks { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseExceptionProcessor();
//optionsBuilder.EnableSensitiveDataLogging();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Document>(entity =>
{
entity.ToTable("Documents");
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).ValueGeneratedOnAdd();
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(255);
});
modelBuilder.Entity<DocumentChunk>(entity =>
{
entity.ToTable("DocumentChunks");
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).ValueGeneratedOnAdd();
entity.Property(e => e.Content).IsRequired();
entity.Property(e => e.Embedding)
.HasColumnType("vector(1536)")
.IsRequired();
entity.HasOne(d => d.Document).WithMany(p => p.Chunks)
.HasForeignKey(d => d.DocumentId)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("FK_DocumentChunks_Documents");
});
}
}
@@ -0,0 +1,12 @@
namespace SqlDatabaseVectorSearch.Data.Entities;
public class Document
{
public Guid Id { get; set; }
public required string Name { get; set; }
public DateTimeOffset CreationDate { get; set; }
public virtual ICollection<DocumentChunk> Chunks { get; set; } = [];
}
@@ -0,0 +1,22 @@
using Microsoft.Data.SqlTypes;
namespace SqlDatabaseVectorSearch.Data.Entities;
public class DocumentChunk
{
public Guid Id { get; set; }
public Guid DocumentId { get; set; }
public int Index { get; set; }
public int? PageNumber { get; set; }
public int IndexOnPage { get; set; }
public required string Content { get; set; }
public required SqlVector<float> Embedding { get; set; }
public virtual Document Document { get; set; } = null!;
}
@@ -0,0 +1,99 @@
// <auto-generated />
using System;
using Microsoft.Data.SqlTypes;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SqlDatabaseVectorSearch.Data;
#nullable disable
namespace SqlDatabaseVectorSearch.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("00000000000000_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0-rc.1.25451.107")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("SqlDatabaseVectorSearch.Data.Entities.Document", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("CreationDate")
.HasColumnType("datetimeoffset");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.HasKey("Id");
b.ToTable("Documents", (string)null);
});
modelBuilder.Entity("SqlDatabaseVectorSearch.Data.Entities.DocumentChunk", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<Guid>("DocumentId")
.HasColumnType("uniqueidentifier");
b.Property<SqlVector<float>>("Embedding")
.HasColumnType("vector(1536)");
b.Property<int>("Index")
.HasColumnType("int");
b.Property<int>("IndexOnPage")
.HasColumnType("int");
b.Property<int?>("PageNumber")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("DocumentId");
b.ToTable("DocumentChunks", (string)null);
});
modelBuilder.Entity("SqlDatabaseVectorSearch.Data.Entities.DocumentChunk", b =>
{
b.HasOne("SqlDatabaseVectorSearch.Data.Entities.Document", "Document")
.WithMany("Chunks")
.HasForeignKey("DocumentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_DocumentChunks_Documents");
b.Navigation("Document");
});
modelBuilder.Entity("SqlDatabaseVectorSearch.Data.Entities.Document", b =>
{
b.Navigation("Chunks");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,67 @@
using System;
using Microsoft.Data.SqlTypes;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SqlDatabaseVectorSearch.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Documents",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
CreationDate = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Documents", x => x.Id);
});
migrationBuilder.CreateTable(
name: "DocumentChunks",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
DocumentId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Index = table.Column<int>(type: "int", nullable: false),
PageNumber = table.Column<int>(type: "int", nullable: true),
IndexOnPage = table.Column<int>(type: "int", nullable: false),
Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
Embedding = table.Column<SqlVector<float>>(type: "vector(1536)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DocumentChunks", x => x.Id);
table.ForeignKey(
name: "FK_DocumentChunks_Documents",
column: x => x.DocumentId,
principalTable: "Documents",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DocumentChunks_DocumentId",
table: "DocumentChunks",
column: "DocumentId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DocumentChunks");
migrationBuilder.DropTable(
name: "Documents");
}
}
}
@@ -0,0 +1,96 @@
// <auto-generated />
using System;
using Microsoft.Data.SqlTypes;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SqlDatabaseVectorSearch.Data;
#nullable disable
namespace SqlDatabaseVectorSearch.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("SqlDatabaseVectorSearch.Data.Entities.Document", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("CreationDate")
.HasColumnType("datetimeoffset");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.HasKey("Id");
b.ToTable("Documents", (string)null);
});
modelBuilder.Entity("SqlDatabaseVectorSearch.Data.Entities.DocumentChunk", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<Guid>("DocumentId")
.HasColumnType("uniqueidentifier");
b.Property<SqlVector<float>>("Embedding")
.HasColumnType("vector(1536)");
b.Property<int>("Index")
.HasColumnType("int");
b.Property<int>("IndexOnPage")
.HasColumnType("int");
b.Property<int?>("PageNumber")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("DocumentId");
b.ToTable("DocumentChunks", (string)null);
});
modelBuilder.Entity("SqlDatabaseVectorSearch.Data.Entities.DocumentChunk", b =>
{
b.HasOne("SqlDatabaseVectorSearch.Data.Entities.Document", "Document")
.WithMany("Chunks")
.HasForeignKey("DocumentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_DocumentChunks_Documents");
b.Navigation("Document");
});
modelBuilder.Entity("SqlDatabaseVectorSearch.Data.Entities.Document", b =>
{
b.Navigation("Chunks");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,44 @@
using System.ComponentModel;
using MinimalHelpers.FluentValidation;
using SqlDatabaseVectorSearch.Models;
using SqlDatabaseVectorSearch.Services;
namespace SqlDatabaseVectorSearch.Endpoints;
public class AskEndpoints : IEndpointRouteHandlerBuilder
{
public static void MapEndpoints(IEndpointRouteBuilder endpoints)
{
endpoints.MapPost("/api/ask", async (Question question, VectorSearchService vectorSearchService, CancellationToken cancellationToken,
[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, cancellationToken);
return TypedResults.Ok(response);
})
.WithValidation<Question>()
.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");
endpoints.MapPost("/api/ask-streaming", (Question question, VectorSearchService vectorSearchService, CancellationToken cancellationToken,
[Description("If true, the question will be reformulated taking into account the context of the chat identified by the given ConversationId.")] bool reformulate = true) =>
{
async IAsyncEnumerable<Response> Stream()
{
// Requests a streaming response.
var responseStream = vectorSearchService.AskStreamingAsync(question, reformulate, cancellationToken);
await foreach (var delta in responseStream)
{
yield return delta;
}
}
return Stream();
})
.WithValidation<Question>()
.WithSummary("Asks a question and gets the response as streaming")
.WithDescription("The question will be reformulated taking into account the context of the chat identified by the given ConversationId.")
.WithTags("Ask");
}
}
@@ -0,0 +1,67 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Http.HttpResults;
using MimeMapping;
using SqlDatabaseVectorSearch.Models;
using SqlDatabaseVectorSearch.Services;
namespace SqlDatabaseVectorSearch.Endpoints;
public class DocumentEndpoints : IEndpointRouteHandlerBuilder
{
public static void MapEndpoints(IEndpointRouteBuilder endpoints)
{
var documentsApiGroup = endpoints.MapGroup("/api/documents").WithTags("Documents");
documentsApiGroup.MapGet(string.Empty, async (DocumentService documentService, CancellationToken cancellationToken) =>
{
var documents = await documentService.GetAsync(cancellationToken);
return TypedResults.Ok(documents);
})
.WithSummary("Gets the list of documents");
documentsApiGroup.MapPost(string.Empty, async (IFormFile file, VectorSearchService vectorSearchService, CancellationToken cancellationToken,
[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();
// Note: file.ContentType is not 100% reliable (for example, for markdown file).
var response = await vectorSearchService.ImportAsync(stream, file.FileName, MimeUtility.GetMimeMapping(file.FileName), documentId, cancellationToken);
return TypedResults.Ok(response);
})
.DisableAntiforgery()
.ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("Uploads a document")
.WithDescription("Uploads a document to SQL Database and saves its embedding using the native VECTOR type. The document will be indexed and used to answer questions. Currently, PDF, DOCX, TXT and MD files are supported.");
documentsApiGroup.MapGet("{documentId:guid}/chunks", async (Guid documentId, DocumentService documentService, CancellationToken cancellationToken) =>
{
var documents = await documentService.GetChunksAsync(documentId, cancellationToken);
return TypedResults.Ok(documents);
})
.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<Results<Ok<DocumentChunk>, NotFound>> (Guid documentId, Guid documentChunkId, DocumentService documentService, CancellationToken cancellationToken) =>
{
var chunk = await documentService.GetChunkEmbeddingAsync(documentId, documentChunkId, cancellationToken);
if (chunk is null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(chunk);
})
.ProducesProblem(StatusCodes.Status404NotFound)
.WithSummary("Gets the details of a given chunk, includings its embedding");
documentsApiGroup.MapDelete("{documentId:guid}", async (Guid documentId, DocumentService documentService, CancellationToken cancellationToken) =>
{
await documentService.DeleteAsync(documentId, cancellationToken);
return TypedResults.NoContent();
})
.WithSummary("Deletes a document")
.WithDescription("This endpoint deletes the document and all its chunks.");
}
}
@@ -0,0 +1,40 @@
using System.Text.RegularExpressions;
using Microsoft.Net.Http.Headers;
namespace SqlDatabaseVectorSearch.Extensions;
public static partial class RequestExtensions
{
[GeneratedRegex("(android|bb\\d+|meego).+mobile|avantgo|bada\\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\\.(browser|link)|vodafone|wap|windows ce|xda|xiino", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled)]
private static partial Regex MobileBrowserRegex { get; }
[GeneratedRegex("1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\\-(n|u)|c55\\/|capi|ccwa|cdm\\-|cell|chtm|cldc|cmd\\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\\-s|devi|dica|dmob|do(c|p)o|ds(12|\\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\\-|_)|g1 u|g560|gene|gf\\-5|g\\-mo|go(\\.w|od)|gr(ad|un)|haie|hcit|hd\\-(m|p|t)|hei\\-|hi(pt|ta)|hp( i|ip)|hs\\-c|ht(c(\\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\\-(20|go|ma)|i230|iac( |\\-|\\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\\/)|klon|kpt |kwc\\-|kyo(c|k)|le(no|xi)|lg( g|\\/(k|l|u)|50|54|\\-[a-w])|libw|lynx|m1\\-w|m3ga|m50\\/|ma(te|ui|xo)|mc(01|21|ca)|m\\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\\-2|po(ck|rt|se)|prox|psio|pt\\-g|qa\\-a|qc(07|12|21|32|60|\\-[2-7]|i\\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\\-|oo|p\\-)|sdk\\/|se(c(\\-|0|1)|47|mc|nd|ri)|sgh\\-|shar|sie(\\-|m)|sk\\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\\-|v\\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\\-|tdg\\-|tel(i|m)|tim\\-|t\\-mo|to(pl|sh)|ts(70|m\\-|m3|m5)|tx\\-9|up(\\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\\-|your|zeto|zte\\-", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled)]
private static partial Regex MobileBrowserVersionRegex { get; }
[GeneratedRegex(@"^/(?<culture>[a-z]{2})(/|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex RouteCultureRegex { get; }
public static bool IsMobileRequest(this HttpContext httpContext)
=> httpContext.Request.IsMobile();
public static bool IsMobile(this HttpRequest request)
{
var userAgent = request.Headers[HeaderNames.UserAgent].ToString();
var isMobileBrowser = false;
if (userAgent?.Length > 4 && (MobileBrowserRegex.IsMatch(userAgent) || MobileBrowserVersionRegex.IsMatch(userAgent.AsSpan(0, 4))))
{
isMobileBrowser = true;
}
return isMobileBrowser;
}
public static bool IsApiRequest(this HttpContext httpContext)
=> httpContext.Request.Path.StartsWithSegments("/api");
public static bool IsSwaggerRequest(this HttpContext httpContext)
=> httpContext.Request.Path.StartsWithSegments("/swagger");
public static bool IsWebRequest(this HttpContext httpContext)
=> !httpContext.IsApiRequest() && !httpContext.IsSwaggerRequest();
}
@@ -0,0 +1,16 @@
namespace SqlDatabaseVectorSearch.Extensions;
public static class StreamExtensions
{
public static async Task<MemoryStream> GetMemoryStreamAsync(this Stream stream)
{
// Use a BufferedStream to read the file in chunks
using var bufferedStream = new BufferedStream(stream);
var ms = new MemoryStream();
await bufferedStream.CopyToAsync(ms);
ms.Position = 0;
return ms;
}
}
@@ -0,0 +1,3 @@
namespace SqlDatabaseVectorSearch.Models;
public record class ChatResponse(string? Text, TokenUsage? TokenUsage = null);
@@ -0,0 +1,16 @@
namespace SqlDatabaseVectorSearch.Models;
public class Citation
{
public Guid DocumentId { get; set; }
public Guid ChunkId { get; set; }
public string FileName { get; set; } = null!;
public string Quote { get; set; } = null!;
public int? PageNumber { get; set; }
public int IndexOnPage { get; set; }
}
@@ -1,14 +1,3 @@
using System.Text.Json; namespace SqlDatabaseVectorSearch.Models;
namespace SqlDatabaseVectorSearch.Models; public record class DocumentChunk(Guid Id, int Index, string Content, int? PageNumber, int IndexOnPage, float[]? Embedding = null);
public record class DocumentChunk(Guid Id, int Index, string Content, float[]? Embedding)
{
public DocumentChunk(Guid Id, int Index, string Content) : this(Id, Index, Content, (float[]?)null)
{
}
public DocumentChunk(Guid Id, int Index, string Content, string Embedding) : this(Id, Index, Content, JsonSerializer.Deserialize<float[]?>(Embedding))
{
}
}
@@ -0,0 +1,3 @@
namespace SqlDatabaseVectorSearch.Models;
public record class ImportDocumentResponse(Guid DocumentId, int EmbeddingTokenCount);
+8 -1
View File
@@ -1,3 +1,10 @@
namespace SqlDatabaseVectorSearch.Models; namespace SqlDatabaseVectorSearch.Models;
public record class Response(string Question, string Answer); // Question and Answer can be null when using response streaming.
public record class Response(string? OriginalQuestion, string? ReformulatedQuestion, string? Answer, StreamState? StreamState = null, TokenUsageResponse? TokenUsage = null, IEnumerable<Citation>? Citations = null)
{
public Response(string? token, StreamState streamState, TokenUsageResponse? tokenUsageResponse = null, IEnumerable<Citation>? citations = null)
: this(null, null, token, streamState, tokenUsageResponse, citations)
{
}
}
@@ -0,0 +1,8 @@
namespace SqlDatabaseVectorSearch.Models;
public enum StreamState
{
Start,
Append,
End
}
@@ -0,0 +1,6 @@
namespace SqlDatabaseVectorSearch.Models;
public record class TokenUsage(int PromptTokens, int CompletionTokens)
{
public int TotalTokens => PromptTokens + CompletionTokens;
}
@@ -0,0 +1,9 @@
namespace SqlDatabaseVectorSearch.Models;
public record class TokenUsageResponse(TokenUsage? Reformulation, int? EmbeddingTokenCount, TokenUsage? Question)
{
public TokenUsageResponse(TokenUsage? question)
: this(null, null, question)
{
}
}
@@ -1,3 +0,0 @@
namespace SqlDatabaseVectorSearch.Models;
public record class UploadDocumentResponse(Guid DocumentId);
+91 -74
View File
@@ -1,10 +1,15 @@
using System.ComponentModel; using System.Net.Mime;
using Microsoft.AspNetCore.Http.HttpResults; using System.Text.Json.Serialization;
using Microsoft.Data.SqlClient; using FluentValidation;
using Microsoft.EntityFrameworkCore;
using Microsoft.SemanticKernel; using Microsoft.SemanticKernel;
using SqlDatabaseVectorSearch.Models; using SqlDatabaseVectorSearch.Components;
using SqlDatabaseVectorSearch.ContentDecoders;
using SqlDatabaseVectorSearch.Data;
using SqlDatabaseVectorSearch.Extensions;
using SqlDatabaseVectorSearch.Services; using SqlDatabaseVectorSearch.Services;
using SqlDatabaseVectorSearch.Settings; using SqlDatabaseVectorSearch.Settings;
using SqlDatabaseVectorSearch.TextChunkers;
using TinyHelpers.AspNetCore.Extensions; using TinyHelpers.AspNetCore.Extensions;
using TinyHelpers.AspNetCore.OpenApi; using TinyHelpers.AspNetCore.OpenApi;
@@ -12,15 +17,24 @@ var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true); builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true);
// Add services to the container. // Add services to the container.
var aiSettings = builder.Configuration.GetSection<AzureOpenAISettings>("AzureOpenAI")!; var aiSettings = builder.Services.ConfigureAndGet<AzureOpenAISettings>(builder.Configuration, "AzureOpenAI")!;
var appSettings = builder.Services.ConfigureAndGet<AppSettings>(builder.Configuration, nameof(AppSettings))!; var appSettings = builder.Services.ConfigureAndGet<AppSettings>(builder.Configuration, nameof(AppSettings))!;
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddBlazorBootstrap();
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddScoped(_ => builder.Services.AddSqlServer<ApplicationDbContext>(builder.Configuration.GetConnectionString("SqlConnection"), optionsAction: options =>
{ {
var sqlConnection = new SqlConnection(builder.Configuration.GetConnectionString("SqlConnection")); options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
return sqlConnection;
}); });
builder.Services.AddHybridCache(options => builder.Services.AddHybridCache(options =>
@@ -31,100 +45,103 @@ builder.Services.AddHybridCache(options =>
}; };
}); });
builder.Services.ConfigureHttpClientDefaults(configure =>
{
configure.AddStandardResilienceHandler(options =>
{
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(15);
options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(2);
});
});
// Semantic Kernel is used to generate embeddings and to reformulate questions taking into account all the previous interactions, // 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. // so that embeddings themselves can be generated more accurately.
builder.Services.AddKernel() builder.Services.AddKernel()
.AddAzureOpenAITextEmbeddingGeneration(aiSettings.Embedding.Deployment, aiSettings.Embedding.Endpoint, aiSettings.Embedding.ApiKey, dimensions: aiSettings.Embedding.Dimensions) .AddAzureOpenAIEmbeddingGenerator(aiSettings.Embedding.Deployment, aiSettings.Embedding.Endpoint, aiSettings.Embedding.ApiKey, modelId: aiSettings.Embedding.ModelId, dimensions: aiSettings.Embedding.Dimensions)
.AddAzureOpenAIChatCompletion(aiSettings.ChatCompletion.Deployment, aiSettings.ChatCompletion.Endpoint, aiSettings.ChatCompletion.ApiKey); .AddAzureOpenAIChatCompletion(aiSettings.ChatCompletion.Deployment, aiSettings.ChatCompletion.Endpoint, aiSettings.ChatCompletion.ApiKey, modelId: aiSettings.ChatCompletion.ModelId);
builder.Services.AddKeyedSingleton<IContentDecoder, PdfContentDecoder>(MediaTypeNames.Application.Pdf);
builder.Services.AddKeyedSingleton<IContentDecoder, DocxContentDecoder>("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
builder.Services.AddKeyedSingleton<IContentDecoder, TextContentDecoder>(MediaTypeNames.Text.Plain);
builder.Services.AddKeyedSingleton<IContentDecoder, TextContentDecoder>(MediaTypeNames.Text.Markdown);
builder.Services.AddKeyedSingleton<ITextChunker, DefaultTextChunker>(KeyedService.AnyKey);
builder.Services.AddKeyedSingleton<ITextChunker, MarkdownTextChunker>(MediaTypeNames.Text.Markdown);
builder.Services.AddSingleton<TokenizerService>();
builder.Services.AddSingleton<ChatService>(); builder.Services.AddSingleton<ChatService>();
builder.Services.AddScoped<DocumentService>();
builder.Services.AddScoped<VectorSearchService>(); builder.Services.AddScoped<VectorSearchService>();
builder.Services.AddOpenApi(options => builder.Services.AddOpenApi(options =>
{ {
options.AddDefaultResponse(); options.RemoveServerList();
options.AddDefaultProblemDetailsResponse();
}); });
ValidatorOptions.Global.LanguageManager.Enabled = false;
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
builder.Services.AddDefaultProblemDetails(); builder.Services.AddDefaultProblemDetails();
builder.Services.AddDefaultExceptionHandler(); builder.Services.AddDefaultExceptionHandler();
var app = builder.Build(); var app = builder.Build();
await ConfigureDatabaseAsync(app.Services);
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseExceptionHandler(); app.UseWhen(context => context.IsWebRequest(), builder =>
app.UseStatusCodePages();
if (app.Environment.IsDevelopment())
{ {
if (!app.Environment.IsDevelopment())
{
builder.UseExceptionHandler("/error", createScopeForErrors: true);
// The default HSTS value is 30 days.
builder.UseHsts();
}
builder.UseStatusCodePagesWithRedirects("/error?code={0}");
});
app.UseWhen(context => context.IsApiRequest(), builder =>
{
app.UseExceptionHandler(new ExceptionHandlerOptions
{
StatusCodeSelector = exception => exception switch
{
NotSupportedException => StatusCodes.Status501NotImplemented,
_ => StatusCodes.Status500InternalServerError
}
});
builder.UseStatusCodePages();
});
app.MapOpenApi(); app.MapOpenApi();
app.UseSwaggerUI(options => app.UseSwaggerUI(options =>
{ {
options.RoutePrefix = string.Empty;
options.SwaggerEndpoint("/openapi/v1.json", builder.Environment.ApplicationName); options.SwaggerEndpoint("/openapi/v1.json", builder.Environment.ApplicationName);
}); });
}
var documentsApiGroup = app.MapGroup("/api/documents").WithTags("Documents"); app.UseRouting();
app.UseRequestLocalization();
documentsApiGroup.MapGet(string.Empty, async (VectorSearchService vectorSearchService) => app.UseAntiforgery();
{
var documents = await vectorSearchService.GetDocumentsAsync();
return TypedResults.Ok(documents);
})
.WithSummary("Gets the list of documents");
documentsApiGroup.MapGet("{documentId:guid}/chunks", async (Guid documentId, VectorSearchService vectorSearchService) => app.MapStaticAssets();
{ app.MapRazorComponents<App>()
var documents = await vectorSearchService.GetDocumentChunksAsync(documentId); .AddInteractiveServerRenderMode();
return TypedResults.Ok(documents);
})
.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<Results<Ok<DocumentChunk>, NotFound>> (Guid documentId, Guid documentChunkId, VectorSearchService vectorSearchService) => app.MapEndpoints();
{
var chunk = await vectorSearchService.GetDocumentChunkEmbeddingAsync(documentId, documentChunkId);
if (chunk is null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(chunk);
})
.ProducesProblem(StatusCodes.Status404NotFound)
.WithSummary("Gets the details of a given chunk, includings its embedding");
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);
return TypedResults.Ok(new UploadDocumentResponse(documentId.Value));
})
.DisableAntiforgery()
.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();
})
.WithSummary("Deletes a document")
.WithDescription("This endpoint deletes the document and all its chunks.");
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);
})
.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(); app.Run();
static async Task ConfigureDatabaseAsync(IServiceProvider serviceProvider)
{
await using var scope = serviceProvider.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await dbContext.Database.MigrateAsync();
}
@@ -5,8 +5,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "", "applicationUrl": "https://localhost:7025;http://localhost:5178",
"applicationUrl": "https://localhost:7024;http://localhost:5178",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
+200 -48
View File
@@ -1,95 +1,247 @@
using System.Text; using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
using OpenAI.Chat;
using SqlDatabaseVectorSearch.Models;
using SqlDatabaseVectorSearch.Settings;
using Entities = SqlDatabaseVectorSearch.Data.Entities;
namespace SqlDatabaseVectorSearch.Services; namespace SqlDatabaseVectorSearch.Services;
public class ChatService(IChatCompletionService chatCompletionService, HybridCache cache) public class ChatService(IChatCompletionService chatCompletionService, TokenizerService tokenizerService, HybridCache cache, IOptions<AppSettings> appSettingsOptions, ILogger<ChatService> logger)
{ {
public async Task<string> CreateQuestionAsync(Guid conversationId, string question) private readonly AppSettings appSettings = appSettingsOptions.Value;
private static readonly string systemPromptForReformulation = """
You are a helpful assistant that reformulates questions to perform embeddings search.
Your task is to reformulate the question taking into account the context of the chat.
The reformulated question must always explicitly contain the subject of the question.
You MUST reformulate the question in the SAME language as the user's question. For example, if the user asks a question in English, the reformulated question MUST be in English. If the user asks in Italian, the reformulated question MUST be in Italian.
If asking a clarifying question to the user would help, ask the question.
Never add "in this chat", "in the context of this chat", "in the context of our conversation", "search for" or something like that in your answer.
""";
private static readonly string systemPromptForAnswering = """
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 Italy?" and in this chat there isn't information about Italy, you should reply something like:
- This information isn't available in the given context.
- I'm sorry, I don't know the answer to that question.
- I don't have that information.
- I don't know.
- Given the context, I can't answer that question.
- I'm sorry, I don't have enough information to answer that question.
Never answer questions that are not related to this chat.
LANGUAGE RULE: You MUST ALWAYS answer in the SAME language as the user's question. For example, if the user asks a question in English, the answer MUST be in English. If the user asks in Italian, the answer MUST be in Italian. This rule applies NO MATTER what language the documents are written in. The language of your response must match the language of the question, NOT the language of the documents.
FORMATTING REQUIREMENT: Your answer MUST ALWAYS end with a period followed by a space before the citations block.
If your answer doesn't naturally end with a period, you MUST add one followed by a space.
After the answer, you need to include citations following the XML format below ONLY IF you know the answer and are providing information from the context. If you do NOT know the answer, DO NOT include the citations section at all.
<citation document-id="document_id" chunk-id="chunk_id" filename="string" page-number="page_number" index-on-page="index_on_page">exact quote here</citation>
<citation document-id="document_id" chunk-id="chunk_id" filename="string" page-number="page_number" index-on-page="index_on_page">exact quote here</citation>
The entire list of XML citations MUST be enclosed between and (U+3010 and U+3011) and must exactly match the above format.
The quote in each <citation> MUST be MAXIMUM 5 words, taken word-for-word from the search result.
IMPORTANT CITATION RULES:
1. NEVER put citations inside your answer text.
2. ALWAYS provide your complete answer FIRST.
3. ONLY AFTER completing your answer, add ALL citations in a block at the very end.
4. The citations block MUST be the last thing in your response, with absolutely nothing (no text, no spaces, no newlines, no punctuation, no comments) after it.
5. NEVER reference citations by number or mention them in your answer text.
6. The citations MUST ALWAYS follow the XML format exactly as shown below. Any other format is NOT ACCEPTED.
7. If you add anything after the citations block, your answer will be considered invalid.
8. If you do NOT know the answer, DO NOT include the citations block at all.
9. ALWAYS check that your answer ends with a period followed by a space before adding citations.
---
Example of a correct answer:
The capital of Italy is Rome.
<citation document-id="123" chunk-id="456" filename="italy.pdf" page-number="1" index-on-page="1">capital of Italy is Rome</citation>
Example of a correct answer when you do NOT know the answer:
I'm sorry, I don't know the answer to that question.
Example of an incorrect answer (NOT ACCEPTED):
The capital of Italy is Rome
<citation document-id="123" chunk-id="456" filename="italy.pdf" page-number="1" index-on-page="1">capital of Italy is Rome</citation>
Thank you for your question.
Another incorrect example (NOT ACCEPTED):
The capital of Italy is Rome.
<citation document-id="123" chunk-id="456" filename="italy.pdf" page-number="1" index-on-page="1">capital of Italy is Rome</citation>
[1] italy.pdf, page 1
---
Only the correct format is accepted. If you do not follow the XML format exactly, or if you add anything after the citations block, your answer will be considered invalid.
If you do NOT know the answer, DO NOT include the citations block at all.
Remember to ALWAYS end your answer with a period followed by a space before adding citations.
""";
public async Task<ChatResponse> CreateReformulateQuestionAsync(Guid conversationId, string question, CancellationToken cancellationToken = default)
{ {
var chat = await GetChatHistoryAsync(conversationId); var chat = await GetChatHistoryAsync(conversationId, cancellationToken);
var settings = new AzureOpenAIPromptExecutionSettings
{
ChatSystemPrompt = systemPromptForReformulation
};
var embeddingQuestion = $""" var embeddingQuestion = $"""
Reformulate the following question taking into account the context of the chat to perform embeddings search: Reformulate the following question:
--- ---
{question} {question}
---
You must reformulate the question in the same language of the user's question.
Never add "in this chat", "in the context of this chat", "in the context of our conversation", "search for" or something like that in your answer.
"""; """;
chat.AddUserMessage(embeddingQuestion); chat.AddUserMessage(embeddingQuestion);
var reformulatedQuestion = await chatCompletionService.GetChatMessageContentAsync(chat)!; var reformulatedQuestion = await chatCompletionService.GetChatMessageContentAsync(chat, settings, cancellationToken: cancellationToken);
chat.AddAssistantMessage(reformulatedQuestion.Content!); chat.AddAssistantMessage(reformulatedQuestion.Content!);
await UpdateCacheAsync(conversationId, chat); await UpdateCacheAsync(conversationId, chat, cancellationToken);
return reformulatedQuestion.Content!; var tokenUsage = GetTokenUsage(reformulatedQuestion);
logger.LogDebug("Reformulation: {TokenUsage}", tokenUsage);
return new(reformulatedQuestion.Content!, tokenUsage);
} }
public async Task<string> AskQuestionAsync(Guid conversationId, IEnumerable<string> chunks, string question) public async Task<ChatResponse> AskQuestionAsync(Guid conversationId, IEnumerable<Entities.DocumentChunk> chunks, string question, CancellationToken cancellationToken = default)
{ {
var chat = new ChatHistory(""" var (chat, settings) = CreateChatAsync(chunks, question);
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 answer = await chatCompletionService.GetChatMessageContentAsync(chat, settings, cancellationToken: cancellationToken);
// Add question and answer to the chat history.
await SetChatHistoryAsync(conversationId, question, answer.Content!, cancellationToken);
var tokenUsage = GetTokenUsage(answer);
logger.LogDebug("Ask question: {TokenUsage}", tokenUsage);
return new(answer.Content!, tokenUsage);
}
public async IAsyncEnumerable<ChatResponse> AskStreamingAsync(Guid conversationId, IEnumerable<Entities.DocumentChunk> chunks, string question, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var (chat, settings) = CreateChatAsync(chunks, question);
var answer = new StringBuilder();
await foreach (var token in chatCompletionService.GetStreamingChatMessageContentsAsync(chat, settings, cancellationToken: cancellationToken))
{
if (!string.IsNullOrEmpty(token.Content))
{
yield return new(token.Content);
answer.Append(token.Content);
}
else if (token.Content is null)
{
// Token usage is returned in the last message, when the Content is null.
var tokenUsage = GetTokenUsage(token);
if (tokenUsage is not null)
{
logger.LogDebug("Ask streaming: {TokenUsage}", tokenUsage);
yield return new(null, tokenUsage);
}
}
}
// Add question and answer to the chat history.
await SetChatHistoryAsync(conversationId, question, answer.ToString(), cancellationToken).ConfigureAwait(false);
}
private static TokenUsage? GetTokenUsage(Microsoft.SemanticKernel.ChatMessageContent message) =>
message.InnerContent is ChatCompletion content && content.Usage is not null
? new(content.Usage.InputTokenCount, content.Usage.OutputTokenCount) : null;
private static TokenUsage? GetTokenUsage(Microsoft.SemanticKernel.StreamingChatMessageContent message) =>
message.InnerContent is StreamingChatCompletionUpdate content && content.Usage is not null
? new(content.Usage.InputTokenCount, content.Usage.OutputTokenCount) : null;
private (ChatHistory Chat, AzureOpenAIPromptExecutionSettings Settings) CreateChatAsync(IEnumerable<Entities.DocumentChunk> chunks, string question)
{
var settings = new AzureOpenAIPromptExecutionSettings
{
MaxTokens = appSettings.MaxOutputTokens,
ChatSystemPrompt = systemPromptForAnswering
};
var prompt = new StringBuilder($"""
Answer the following question:
---
{question}
=====
Using the following information: Using the following information:
"""); """);
// TODO: Ensure that chunks are not too long, according to the model max token. var availableTokens = appSettings.MaxInputTokens
foreach (var text in chunks) - tokenizerService.CountChatCompletionTokens(systemPromptForAnswering) // System prompt.
- tokenizerService.CountChatCompletionTokens(prompt.ToString()) // Initial user prompt.
- appSettings.MaxOutputTokens; // To ensure there is enough space for the answer.
foreach (var chunk in chunks)
{ {
prompt.AppendLine("---"); var text = $"--- {chunk.Document.Name} (Document ID: {chunk.Document.Id} | Chunk ID: {chunk.Id} | Page Number: {chunk.PageNumber} | Index on Page: {chunk.IndexOnPage}) {Environment.NewLine}{chunk.Content}{Environment.NewLine}";
prompt.Append(text);
var tokenCount = tokenizerService.CountChatCompletionTokens(text);
if (tokenCount > availableTokens)
{
// There isn't enough space to add the current chunk.
break;
} }
prompt.AppendLine($""" prompt.Append(text);
===== availableTokens -= tokenCount;
Answer the following question: if (availableTokens <= 0)
--- {
{question} // There isn't enough space to add more chunks.
"""); break;
}
}
var chat = new ChatHistory();
chat.AddUserMessage(prompt.ToString()); chat.AddUserMessage(prompt.ToString());
var answer = await chatCompletionService.GetChatMessageContentAsync(chat)!; return (chat, settings);
// Add question and answer to the chat history.
await SetChatHistoryAsync(conversationId, question, answer.Content!);
return answer.Content!;
} }
private async Task UpdateCacheAsync(Guid conversationId, ChatHistory chat) private async Task UpdateCacheAsync(Guid conversationId, ChatHistory chat, CancellationToken cancellationToken)
=> await cache.SetAsync(conversationId.ToString(), chat);
private async Task<ChatHistory> GetChatHistoryAsync(Guid conversationId)
{ {
var historyCache = await cache.GetOrCreateAsync(conversationId.ToString(), if (chat.Count > appSettings.MessageLimit)
(cancellationToken) => {
chat.RemoveRange(0, chat.Count - appSettings.MessageLimit);
}
await cache.SetAsync(conversationId.ToString(), chat, cancellationToken: cancellationToken);
}
private async Task<ChatHistory> GetChatHistoryAsync(Guid conversationId, CancellationToken cancellationToken)
{
var chat = await cache.GetOrCreateAsync(conversationId.ToString(), (cancellationToken) =>
{ {
return ValueTask.FromResult<ChatHistory>([]); return ValueTask.FromResult<ChatHistory>([]);
}); }, cancellationToken: cancellationToken);
var chat = new ChatHistory(historyCache);
return chat; return chat;
} }
private async Task SetChatHistoryAsync(Guid conversationId, string question, string answer) private async Task SetChatHistoryAsync(Guid conversationId, string question, string answer, CancellationToken cancellationToken)
{ {
var history = await GetChatHistoryAsync(conversationId); var chat = await GetChatHistoryAsync(conversationId, cancellationToken);
history.AddUserMessage(question); chat.AddUserMessage(question);
history.AddAssistantMessage(answer); chat.AddAssistantMessage(answer);
await UpdateCacheAsync(conversationId, history); await UpdateCacheAsync(conversationId, chat, cancellationToken);
} }
} }
@@ -0,0 +1,42 @@
using System.Data;
using Microsoft.EntityFrameworkCore;
using SqlDatabaseVectorSearch.Data;
using SqlDatabaseVectorSearch.Models;
namespace SqlDatabaseVectorSearch.Services;
public class DocumentService(ApplicationDbContext dbContext)
{
public async Task<IEnumerable<Document>> GetAsync(CancellationToken cancellationToken = default)
{
var documents = await dbContext.Documents.OrderBy(d => d.Name)
.Select(d => new Document(d.Id, d.Name, d.CreationDate, d.Chunks.Count))
.ToListAsync(cancellationToken);
return documents;
}
public async Task<IEnumerable<DocumentChunk>> GetChunksAsync(Guid documentId, CancellationToken cancellationToken = default)
{
var documentChunks = await dbContext.DocumentChunks.Where(c => c.DocumentId == documentId).OrderBy(c => c.Index)
.Select(c => new DocumentChunk(c.Id, c.Index, c.Content, c.PageNumber, c.IndexOnPage, null))
.ToListAsync(cancellationToken);
return documentChunks;
}
public async Task<DocumentChunk?> GetChunkEmbeddingAsync(Guid documentId, Guid documentChunkId, CancellationToken cancellationToken = default)
{
var documentChunk = await dbContext.DocumentChunks.Where(c => c.Id == documentChunkId && c.DocumentId == documentId)
.Select(c => new DocumentChunk(c.Id, c.Index, c.Content, c.PageNumber, c.IndexOnPage, c.Embedding.Memory.ToArray()))
.FirstOrDefaultAsync(cancellationToken);
return documentChunk;
}
public Task DeleteAsync(Guid documentId, CancellationToken cancellationToken = default)
=> dbContext.Documents.Where(d => d.Id == documentId).ExecuteDeleteAsync(cancellationToken);
public Task DeleteAsync(IEnumerable<Guid> documentIds, CancellationToken cancellationToken = default)
=> dbContext.Documents.Where(d => documentIds.Contains(d.Id)).ExecuteDeleteAsync(cancellationToken);
}
@@ -0,0 +1,18 @@
using Microsoft.Extensions.Options;
using Microsoft.ML.Tokenizers;
using SqlDatabaseVectorSearch.Settings;
namespace SqlDatabaseVectorSearch.Services;
public class TokenizerService(IOptions<AzureOpenAISettings> settingsOptions)
{
private readonly TiktokenTokenizer chatCompletiontokenizer = TiktokenTokenizer.CreateForModel(settingsOptions.Value.ChatCompletion.ModelId);
private readonly TiktokenTokenizer embeddingTokenizer = TiktokenTokenizer.CreateForModel(settingsOptions.Value.Embedding.ModelId);
public int CountChatCompletionTokens(string input)
=> chatCompletiontokenizer.CountTokens(input);
public int CountEmbeddingTokens(string input)
=> embeddingTokenizer.CountTokens(input);
}
@@ -1,131 +1,200 @@
using System.Data; using System.Data;
using System.Data.Common; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.RegularExpressions;
using Dapper; using Microsoft.Data.SqlTypes;
using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel.Embeddings; using SqlDatabaseVectorSearch.ContentDecoders;
using Microsoft.SemanticKernel.Text; using SqlDatabaseVectorSearch.Data;
using SqlDatabaseVectorSearch.Models; using SqlDatabaseVectorSearch.Models;
using SqlDatabaseVectorSearch.Settings; using SqlDatabaseVectorSearch.Settings;
using UglyToad.PdfPig; using ChatResponse = SqlDatabaseVectorSearch.Models.ChatResponse;
using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor; using Entities = SqlDatabaseVectorSearch.Data.Entities;
namespace SqlDatabaseVectorSearch.Services; namespace SqlDatabaseVectorSearch.Services;
public class VectorSearchService(SqlConnection sqlConnection, ITextEmbeddingGenerationService textEmbeddingGenerationService, ChatService chatService, TimeProvider timeProvider, IOptions<AppSettings> appSettingsOptions) public partial class VectorSearchService(IServiceProvider serviceProvider, ApplicationDbContext dbContext, DocumentService documentService, IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator, TokenizerService tokenizerService, ChatService chatService, TimeProvider timeProvider, IOptions<AppSettings> appSettingsOptions, ILogger<VectorSearchService> logger)
{ {
private readonly AppSettings appSettings = appSettingsOptions.Value; private readonly AppSettings appSettings = appSettingsOptions.Value;
public async Task<Guid> ImportAsync(Stream stream, string name, Guid? documentId) public async Task<ImportDocumentResponse> ImportAsync(Stream stream, string name, string contentType, Guid? documentId, CancellationToken cancellationToken = default)
{ {
// Extract the contents of the file (currently, only PDF files are supported). // Extract the contents of the file.
var content = await GetContentAsync(stream); var decoder = serviceProvider.GetKeyedService<IContentDecoder>(contentType) ?? throw new NotSupportedException($"Content type '{contentType}' is not supported.");
var chunks = await decoder.DecodeAsync(stream, contentType, cancellationToken);
var chunkContents = chunks.Select(p => p.Content).ToList();
await sqlConnection.OpenAsync(); // We get the token count of the whole document because it is the total number of token used by embedding (it may be necessary, for example, for cost analysis).
await using var transaction = await sqlConnection.BeginTransactionAsync(); var tokenCount = tokenizerService.CountEmbeddingTokens(string.Join(" ", chunkContents));
var strategy = dbContext.Database.CreateExecutionStrategy();
var document = await strategy.ExecuteAsync(async (cancellationToken) =>
{
await dbContext.Database.BeginTransactionAsync(cancellationToken);
if (documentId.HasValue) if (documentId.HasValue)
{ {
// If the user is importing a document that already exists, delete the previous one. // If the user is importing a document that already exists, delete the previous one.
await DeleteDocumentAsync(documentId.Value, transaction); await documentService.DeleteAsync(documentId.Value, cancellationToken);
} }
documentId = await sqlConnection.ExecuteScalarAsync<Guid>($""" var document = new Entities.Document { Id = documentId.GetValueOrDefault(), Name = name, CreationDate = timeProvider.GetUtcNow() };
INSERT INTO Documents (Id, [Name], CreationDate) dbContext.Documents.Add(document);
OUTPUT INSERTED.Id
VALUES (@Id, @Name, @CreationDate);
""", new { Id = documentId.GetValueOrDefault(Guid.NewGuid()), Name = name, CreationDate = timeProvider.GetUtcNow() },
transaction);
// Split the content into chunks and generate the embeddings for each one. // Process paragraphs in batches.
var paragraphs = TextChunker.SplitPlainTextParagraphs(TextChunker.SplitPlainTextLines(content, appSettings.MaxTokensPerLine), appSettings.MaxTokensPerParagraph, appSettings.OverlapTokens); var embeddings = new List<Embedding<float>>();
var embeddings = await textEmbeddingGenerationService.GenerateEmbeddingsAsync(paragraphs); foreach (var batch in chunkContents.Chunk(appSettings.EmbeddingBatchSize))
{
logger.LogDebug("Processing batch of {Count} chunks for embedding generation...", batch.Length);
// Generate embeddings for this batch.
var batchEmbeddings = await embeddingGenerator.GenerateAsync(batch, cancellationToken: cancellationToken);
embeddings.AddRange(batchEmbeddings);
}
// Save the document chunks and the corresponding embedding in the database. // Save the document chunks and the corresponding embedding in the database.
foreach (var (index, paragraph) in paragraphs.Index()) foreach (var (index, embedding) in embeddings.Index())
{ {
await sqlConnection.ExecuteAsync($""" var chunk = chunks.ElementAt(index);
INSERT INTO DocumentChunks (DocumentId, [Index], Content, Embedding) logger.LogDebug("Storing a chunk of {TokenCount} tokens.", tokenizerService.CountEmbeddingTokens(chunk.Content));
VALUES (@DocumentId, @Index, @Content, CAST(@Embedding AS VECTOR({embeddings[index].Length})));
""", new { DocumentId = documentId, Index = index, Content = paragraph, Embedding = JsonSerializer.Serialize(embeddings[index]) }, var documentChunk = new Entities.DocumentChunk
transaction); {
Document = document,
Index = index,
PageNumber = chunk.PageNumber,
IndexOnPage = chunk.IndexOnPage,
Content = chunk.Content,
Embedding = new SqlVector<float>(embedding.Vector)
};
dbContext.DocumentChunks.Add(documentChunk);
} }
await transaction.CommitAsync(); await dbContext.SaveChangesAsync(cancellationToken);
await dbContext.Database.CommitTransactionAsync(cancellationToken);
return documentId.Value; return document;
}, cancellationToken);
return new(document.Id, tokenCount);
} }
public async Task<IEnumerable<Document>> GetDocumentsAsync() public async Task<Response> AskQuestionAsync(Question question, bool reformulate = true, CancellationToken cancellationToken = default)
{ {
var documents = await sqlConnection.QueryAsync<Document>(""" // It the user doesn't want to reforulate the question, CreateContextAsync returns the original one.
SELECT Id, [Name], CreationDate, ChunkCount = (SELECT COUNT(*) FROM DocumentChunks WHERE DocumentId = Documents.Id) var (reformulatedQuestion, embeddingTokenCount, chunks) = await CreateContextAsync(question, reformulate, cancellationToken);
FROM Documents
ORDER BY [Name];
""");
return documents; var (fullAnswer, tokenUsage) = await chatService.AskQuestionAsync(question.ConversationId, chunks, reformulatedQuestion.Text!, cancellationToken);
// Extract citations from the answer.
var (answer, citations) = ExtractCitations(fullAnswer);
return new(question.Text, reformulatedQuestion.Text!, answer, StreamState.End, new(reformulatedQuestion.TokenUsage, embeddingTokenCount, tokenUsage), citations);
} }
public async Task<IEnumerable<DocumentChunk>> GetDocumentChunksAsync(Guid documentId) public async IAsyncEnumerable<Response> AskStreamingAsync(Question question, bool reformulate = true, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{ {
var documentChunks = await sqlConnection.QueryAsync<DocumentChunk>(""" // It the user doesn't want to reforulate the question, CreateContextAsync returns the original one.
SELECT Id, [Index], Content var (reformulatedQuestion, embeddingTokenCount, chunks) = await CreateContextAsync(question, reformulate, cancellationToken);
FROM DocumentChunks
WHERE DocumentId = @DocumentId
ORDER BY [Index];
""", new { documentId });
return documentChunks; var answerStream = chatService.AskStreamingAsync(question.ConversationId, chunks, reformulatedQuestion.Text!, cancellationToken: cancellationToken);
// The first message contains the question and the corresponding token usage (if reformulated).
yield return new(question.Text, reformulatedQuestion.Text!, null, StreamState.Start, new(reformulatedQuestion.TokenUsage, embeddingTokenCount, null));
TokenUsageResponse? tokenUsageResponse = null;
var fullAnswer = new StringBuilder();
var citationsStarted = false;
// Returns each token as a partial response.
await foreach (var (token, tokenUsage) in answerStream)
{
if (token is not null) // token can be null when the stream ends.
{
fullAnswer.Append(token);
if (token.Contains('【'))
{
// Citations start when we encounter a token containing a 【 character.
// We need to track it because we don't want to return the citations in the actual response.
citationsStarted = true;
} }
public async Task<DocumentChunk?> GetDocumentChunkEmbeddingAsync(Guid documentId, Guid documentChunkId) if (!citationsStarted)
{ {
var documentChunk = await sqlConnection.QueryFirstOrDefaultAsync<DocumentChunk>(""" yield return new(token, StreamState.Append);
SELECT Id, [Index], Content, CAST(Embedding AS NVARCHAR(MAX)) AS Embedding }
FROM DocumentChunks }
WHERE Id = @DocumentChunkId AND DocumentId = @DocumentId; else
""", new { documentId, documentChunkId }); {
// Token usage is expected in the last message, when token is null.
return documentChunk; tokenUsageResponse ??= tokenUsage is not null ? new(tokenUsage) : null;
}
} }
public Task DeleteDocumentAsync(Guid documentId, DbTransaction? transaction = null) // Extract citations at the end of streaming.
=> sqlConnection.ExecuteAsync("DELETE FROM Documents WHERE Id = @DocumentId", new { DocumentId = documentId }, transaction); var (_, citations) = ExtractCitations(fullAnswer.ToString());
yield return new(null, StreamState.End, tokenUsageResponse, citations);
}
public async Task<Response> AskQuestionAsync(Question question, bool reformulate = true) private async Task<(ChatResponse ReformulatedQuestion, int EmbeddingTokenCount, IEnumerable<Entities.DocumentChunk> Chunks)> CreateContextAsync(Question question, bool reformulate, CancellationToken cancellationToken)
{ {
// Reformulate the following question taking into account the context of the chat to perform keyword search and embeddings: // Reformulate the question taking into account the context of the chat to perform keyword search and embeddings.
var reformulatedQuestion = reformulate ? await chatService.CreateQuestionAsync(question.ConversationId, question.Text) : question.Text; var reformulatedQuestion = reformulate ? await chatService.CreateReformulateQuestionAsync(question.ConversationId, question.Text, cancellationToken) : new(question.Text);
var embeddingTokenCount = tokenizerService.CountEmbeddingTokens(reformulatedQuestion.Text!);
logger.LogDebug("Embedding Token Count: {EmbeddingTokenCount}", embeddingTokenCount);
// Perform Vector Search on SQL Database. // Perform Vector Search on SQL Database.
var questionEmbedding = await textEmbeddingGenerationService.GenerateEmbeddingAsync(reformulatedQuestion); var questionEmbedding = await embeddingGenerator.GenerateVectorAsync(reformulatedQuestion.Text!, cancellationToken: cancellationToken);
var embeddingVector = new SqlVector<float>(questionEmbedding);
var chunks = await sqlConnection.QueryAsync<string>($""" var chunks = await dbContext.DocumentChunks.Include(c => c.Document)
SELECT TOP (@MaxRelevantChunks) Content .OrderBy(c => EF.Functions.VectorDistance("cosine", c.Embedding, embeddingVector))
FROM DocumentChunks .Take(appSettings.MaxRelevantChunks)
ORDER BY VECTOR_DISTANCE('cosine', Embedding, CAST(@QuestionEmbedding AS VECTOR({questionEmbedding.Length}))); .ToListAsync(cancellationToken);
""", new { appSettings.MaxRelevantChunks, QuestionEmbedding = JsonSerializer.Serialize(questionEmbedding) });
var answer = await chatService.AskQuestionAsync(question.ConversationId, chunks, reformulatedQuestion); return (reformulatedQuestion, embeddingTokenCount, chunks);
return new Response(reformulatedQuestion, answer);
} }
private static Task<string> GetContentAsync(Stream stream) private static (string, IEnumerable<Citation>) ExtractCitations(string? text)
{ {
var content = new StringBuilder(); var citations = new List<Citation>();
// Read the content of the PDF document. if (string.IsNullOrEmpty(text))
using var pdfDocument = PdfDocument.Open(stream);
foreach (var page in pdfDocument.GetPages().Where(x => x is not null))
{ {
var pageContent = ContentOrderTextExtractor.GetText(page) ?? string.Empty; return (text ?? string.Empty, citations);
content.AppendLine(pageContent);
} }
return Task.FromResult(content.ToString()); var matches = CitationRegEx.Matches(text);
foreach (Match match in matches)
{
if (match.Success)
{
citations.Add(new Citation
{
DocumentId = Guid.Parse(match.Groups["documentId"].Value),
ChunkId = Guid.Parse(match.Groups["chunkId"].Value),
FileName = match.Groups["filename"].Value,
PageNumber = int.TryParse(match.Groups["pageNumber"].Value, out var pageNumber) && pageNumber > 0 ? pageNumber : null,
IndexOnPage = int.TryParse(match.Groups["indexOnPage"].Value, out var indexOnPage) ? indexOnPage : 0,
Quote = match.Groups["quote"].Value
});
} }
} }
// Remove all content between 【 and 】.
var cleanText = RemoveCitationsRegEx.Replace(text, string.Empty).TrimEnd();
return (cleanText, citations.OrderBy(c => c.FileName).ThenBy(c => c.PageNumber));
}
[GeneratedRegex(@"<citation\s+document-id=(?:""|'|)(?<documentId>[^""']*)(?:""|'|)\s+chunk-id=(?:""|'|)(?<chunkId>[^""']*)(?:""|'|)\s+filename=(?:""|'|)(?<filename>[^""']*)(?:""|'|)\s+page-number=(?:""|'|)(?<pageNumber>[^""']*)(?:""|'|)\s+index-on-page=(?:""|'|)(?<indexOnPage>[^""']*)(?:""|'|)>\s*(?<quote>.*?)\s*</citation>", RegexOptions.Singleline)]
private static partial Regex CitationRegEx { get; }
[GeneratedRegex(@"【.*?】", RegexOptions.Singleline)]
private static partial Regex RemoveCitationsRegEx { get; }
}
@@ -2,13 +2,21 @@
public class AppSettings public class AppSettings
{ {
public int EmbeddingBatchSize { get; init; } = 32;
public int MaxTokensPerLine { get; init; } = 300; public int MaxTokensPerLine { get; init; } = 300;
public int MaxTokensPerParagraph { get; init; } = 1024; public int MaxTokensPerParagraph { get; init; } = 1000;
public int OverlapTokens { get; init; } = 100; public int OverlapTokens { get; init; } = 100;
public int MaxRelevantChunks { get; init; } = 5; public int MaxRelevantChunks { get; init; } = 5;
public int MaxInputTokens { get; init; } = 16385;
public int MaxOutputTokens { get; init; } = 800;
public TimeSpan MessageExpiration { get; init; } public TimeSpan MessageExpiration { get; init; }
public int MessageLimit { get; set; } = 20;
} }
@@ -4,7 +4,7 @@ public class AzureOpenAISettings
{ {
public required ServiceSettings ChatCompletion { get; init; } public required ServiceSettings ChatCompletion { get; init; }
public required EmbeddingServiceSettings Embedding { get; init; } public required EmbeddingSettings Embedding { get; init; }
} }
public class ServiceSettings public class ServiceSettings
@@ -13,10 +13,12 @@ public class ServiceSettings
public required string Deployment { get; init; } public required string Deployment { get; init; }
public required string ModelId { get; init; }
public required string ApiKey { get; init; } public required string ApiKey { get; init; }
} }
public class EmbeddingServiceSettings : ServiceSettings public class EmbeddingSettings : ServiceSettings
{ {
public int? Dimensions { get; set; } public int? Dimensions { get; set; }
} }
@@ -1,22 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<NoWarn>$(NoWarn);SKEXP0001;SKEXP0010;SKEXP0050;EXTEXP0018</NoWarn> <NoWarn>$(NoWarn);SKEXP0010;SKEXP0050</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" /> <PackageReference Include="Blazor.Bootstrap" Version="3.5.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" /> <PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" /> <PackageReference Include="EntityFrameworkCore.Exceptions.SqlServer" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.9.24556.5" /> <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.32.0" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
<PackageReference Include="MinimalHelpers.OpenApi" Version="2.1.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.9" />
<PackageReference Include="PdfPig" Version="0.1.9" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.2.0" /> <PrivateAssets>all</PrivateAssets>
<PackageReference Include="TinyHelpers.AspNetCore" Version="4.0.6" /> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.7.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
<PackageReference Include="Microsoft.ML.Tokenizers" Version="2.0.0" />
<PackageReference Include="Microsoft.ML.Tokenizers.Data.Cl100kBase" Version="2.0.0" />
<PackageReference Include="Microsoft.ML.Tokenizers.Data.O200kBase" Version="2.0.0" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.77.0" />
<PackageReference Include="MimeMapping" Version="4.0.0" />
<PackageReference Include="MinimalHelpers.FluentValidation" Version="1.1.8" />
<PackageReference Include="MinimalHelpers.Routing.Analyzers" Version="1.2.2" />
<PackageReference Include="PdfPig" Version="0.1.14" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.1" />
<PackageReference Include="TinyHelpers.AspNetCore" Version="4.2.12" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -0,0 +1,19 @@
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel.Text;
using SqlDatabaseVectorSearch.Services;
using SqlDatabaseVectorSearch.Settings;
namespace SqlDatabaseVectorSearch.TextChunkers;
public class DefaultTextChunker(TokenizerService tokenizerService, IOptions<AppSettings> appSettingsOptions) : ITextChunker
{
private readonly AppSettings appSettings = appSettingsOptions.Value;
public IList<string> Split(string text)
{
var lines = TextChunker.SplitPlainTextLines(text, appSettings.MaxTokensPerLine, tokenizerService.CountEmbeddingTokens);
var paragraphs = TextChunker.SplitPlainTextParagraphs(lines, appSettings.MaxTokensPerParagraph, appSettings.OverlapTokens, tokenCounter: tokenizerService.CountEmbeddingTokens);
return paragraphs;
}
}
@@ -0,0 +1,6 @@
namespace SqlDatabaseVectorSearch.TextChunkers;
public interface ITextChunker
{
IList<string> Split(string text);
}
@@ -0,0 +1,19 @@
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel.Text;
using SqlDatabaseVectorSearch.Services;
using SqlDatabaseVectorSearch.Settings;
namespace SqlDatabaseVectorSearch.TextChunkers;
public class MarkdownTextChunker(TokenizerService tokenizerService, IOptions<AppSettings> appSettingsOptions) : ITextChunker
{
private readonly AppSettings appSettings = appSettingsOptions.Value;
public IList<string> Split(string text)
{
var lines = TextChunker.SplitMarkDownLines(text, appSettings.MaxTokensPerLine, tokenizerService.CountEmbeddingTokens);
var paragraphs = TextChunker.SplitMarkdownParagraphs(lines, appSettings.MaxTokensPerParagraph, appSettings.OverlapTokens, tokenCounter: tokenizerService.CountEmbeddingTokens);
return paragraphs;
}
}
@@ -0,0 +1,12 @@
using FluentValidation;
using SqlDatabaseVectorSearch.Models;
namespace SqlDatabaseVectorSearch.Validations;
public class QuestionValidator : AbstractValidator<Question>
{
public QuestionValidator()
{
RuleFor(x => x.Text).NotEmpty().MaximumLength(4096).WithName("Question Text");
}
}
@@ -3,7 +3,8 @@
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning", "Microsoft.AspNetCore": "Warning",
"Microsoft.KernelMemory": "Debug" "Microsoft.AspNetCore.Watch.BrowserRefresh": "Warning",
"SqlDatabaseVectorSearch": "Debug"
} }
} }
} }
+10 -4
View File
@@ -6,11 +6,13 @@
"ChatCompletion": { "ChatCompletion": {
"Endpoint": "", "Endpoint": "",
"Deployment": "", "Deployment": "",
"ModelId": "", // gpt-4o, gpt-4, gpt-3.5, etc. Note that for gpt-4.1 and gpt-5 models, the ModelId must be set to gpt-4o.
"ApiKey": "" "ApiKey": ""
}, },
"Embedding": { "Embedding": {
"Endpoint": "", "Endpoint": "",
"Deployment": "", "Deployment": "",
"ModelId": "", // text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002
"ApiKey": "", "ApiKey": "",
// Set this value only if you're using a model that allows to specify the dimensions of the embeddings // 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. // (e.g. text-embedding-3-small or text-embedding-3-large). Currently, a maximum value of 1998 is supported.
@@ -18,11 +20,15 @@
} }
}, },
"AppSettings": { "AppSettings": {
"MaxTokenPerLine": 300, "EmbeddingBatchSize": 32,
"MaxTokensPerParagraph": 1024, "MaxTokensPerLine": 300,
"MaxTokensPerParagraph": 1000,
"OverlapTokens": 100, "OverlapTokens": 100,
"MaxRelevantChunks": 10, "MaxRelevantChunks": 50,
"MessageExpiration": "00:05:00" "MaxInputTokens": 32768,
"MaxOutputTokens": 800,
"MessageExpiration": "00:05:00",
"MessageLimit": 20
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
@@ -0,0 +1,76 @@
body, html {
margin: 0;
padding: 0;
height: 100%;
}
:root {
--bb-sidebar2-width: 270px;
--bb-sidebar2-collapsed-width: 50px;
--bb-sidebar2-background-color: rgba(234, 234, 234, 1);
--bb-sidebar2-top-row-background-color: rgba(0,0,0,0.08);
--bb-sidebar2-top-row-border-color: rgb(194,192,192);
--bb-sidebar2-title-text-color: rgb(0,0,0);
--bb-sidebar2-brand-icon-color: rgb(0,0,0);
--bb-sidebar2-brand-image-width: 24px;
--bb-sidebar2-brand-image-height: 24px;
--bb-sidebar2-title-badge-text-color: rgb(255,255,255);
--bb-sidebar2-title-badge-background-color: rgba(25,135,84,var(--bs-bg-opacity,1));
--bb-sidebar2-navbar-toggler-icon-color: rgb(0,0,0);
--bb-sidebar2-navbar-toggler-background-color: rgba(0,0,0,0.08);
--bb-sidebar2-content-border-color: rgb(194,192,192);
--bb-sidebar2-nav-item-text-color: rgba(0,0,0,0.9);
--bb-sidebar2-nav-item-text-active-color-rgb: 0,0,0;
--bb-sidebar2-nav-item-text-hover-color: rgba(var(--bb-sidebar-nav-item-text-active-color-rgb),0.9);
--bb-sidebar2-nav-item-text-active-color: rgba(var(--bb-sidebar-nav-item-text-active-color-rgb),0.9);
--bb-sidebar2-nav-item-background-hover-color: rgba(var(--bb-sidebar-nav-item-text-active-color-rgb),0.08);
--bb-sidebar2-nav-item-group-background-color: rgba(var(--bb-sidebar-nav-item-text-active-color-rgb),0.08);
}
.bb-sidebar2 nav .nav-item a:hover {
background-color: rgba(0,0,0,0.08) !important;
color: rgba(0,0,0,0.9) !important;
}
.bb-sidebar2 nav .nav-item a.active {
background-color: rgb(194,192,192) !important;
color: rgba(0,0,0,0.9) !important;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.content-type-badge {
background-color: #e5e7eb !important;
color: #495057 !important;
border: 1px solid #d1d5db !important;
}
.citation-box {
width: fit-content;
max-width: 100%;
background-color: #f8f9fa;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1 @@
<svg id="uuid-adbdae8e-5a41-46d1-8c18-aa73cdbfee32" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><defs><radialGradient id="uuid-2a7407aa-b787-48dd-a96a-0d81ab6e93bb" cx="-67.981" cy="793.199" r=".45" gradientTransform="translate(-17939.03 20368.029) rotate(45) scale(25.091 -34.149)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#83b9f9" /><stop offset="1" stop-color="#0078d4" /></radialGradient></defs><path d="m0,2.7v12.6c0,1.491,1.209,2.7,2.7,2.7h12.6c1.491,0,2.7-1.209,2.7-2.7V2.7c0-1.491-1.209-2.7-2.7-2.7H2.7C1.209,0,0,1.209,0,2.7ZM10.8,0v3.6c0,3.976,3.224,7.2,7.2,7.2h-3.6c-3.976,0-7.199,3.222-7.2,7.198v-3.598c0-3.976-3.224-7.2-7.2-7.2h3.6c3.976,0,7.2-3.224,7.2-7.2Z" fill="url(#uuid-2a7407aa-b787-48dd-a96a-0d81ab6e93bb)" stroke-width="0" /></svg>

After

Width:  |  Height:  |  Size: 805 B

@@ -0,0 +1 @@
<svg id="a96792b7-ce28-4ca3-9767-4e065ef4820f" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><defs><linearGradient id="ef16bf9d-a8b6-4181-b6cd-66fc5203f956" x1="2.59" y1="10.16" x2="15.41" y2="10.16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#005ba1" /><stop offset="0.07" stop-color="#0060a9" /><stop offset="0.36" stop-color="#0071c8" /><stop offset="0.52" stop-color="#0078d4" /><stop offset="0.64" stop-color="#0074cd" /><stop offset="0.82" stop-color="#006abb" /><stop offset="1" stop-color="#005ba1" /></linearGradient><radialGradient id="bf3846c3-4d74-4743-ab9a-f334c248bd92" cx="9.36" cy="10.57" r="7.07" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2f2f2" /><stop offset="0.58" stop-color="#eee" /><stop offset="1" stop-color="#e6e6e6" /></radialGradient></defs><title>Icon-databases-130</title><path d="M9,5.14c-3.54,0-6.41-1-6.41-2.32V15.18c0,1.27,2.82,2.3,6.32,2.32H9c3.54,0,6.41-1,6.41-2.32V2.82C15.41,4.11,12.54,5.14,9,5.14Z" fill="url(#ef16bf9d-a8b6-4181-b6cd-66fc5203f956)" /><path d="M15.41,2.82c0,1.29-2.87,2.32-6.41,2.32s-6.41-1-6.41-2.32S5.46.5,9,.5s6.41,1,6.41,2.32" fill="#e8e8e8" /><path d="M13.92,2.63c0,.82-2.21,1.48-4.92,1.48S4.08,3.45,4.08,2.63,6.29,1.16,9,1.16s4.92.66,4.92,1.47" fill="#50e6ff" /><path d="M9,3a11.55,11.55,0,0,0-3.89.57A11.42,11.42,0,0,0,9,4.11a11.15,11.15,0,0,0,3.89-.58A11.84,11.84,0,0,0,9,3Z" fill="#198ab3" /><path d="M12.9,11.4V8H12v4.13h2.46V11.4ZM5.76,9.73a1.83,1.83,0,0,1-.51-.31.44.44,0,0,1-.12-.32.34.34,0,0,1,.15-.3.68.68,0,0,1,.42-.12,1.62,1.62,0,0,1,1,.29V8.11a2.58,2.58,0,0,0-1-.16,1.64,1.64,0,0,0-1.09.34,1.08,1.08,0,0,0-.42.89c0,.51.32.91,1,1.21a2.88,2.88,0,0,1,.62.36.42.42,0,0,1,.15.32.38.38,0,0,1-.16.31.81.81,0,0,1-.45.11,1.66,1.66,0,0,1-1.09-.42V12a2.17,2.17,0,0,0,1.07.24,1.88,1.88,0,0,0,1.18-.33A1.08,1.08,0,0,0,6.84,11a1.05,1.05,0,0,0-.25-.7A2.42,2.42,0,0,0,5.76,9.73ZM11,11.32a2.34,2.34,0,0,0,.33-1.26A2.32,2.32,0,0,0,11,9a1.81,1.81,0,0,0-.7-.75,2,2,0,0,0-1-.26,2.11,2.11,0,0,0-1.08.27A1.86,1.86,0,0,0,7.49,9a2.46,2.46,0,0,0-.26,1.14,2.26,2.26,0,0,0,.24,1,1.76,1.76,0,0,0,.69.74,2.06,2.06,0,0,0,1,.3l.86,1h1.21L10,12.08A1.79,1.79,0,0,0,11,11.32ZM10,11.07a.94.94,0,0,1-.76.35.92.92,0,0,1-.76-.36,1.52,1.52,0,0,1-.29-1,1.53,1.53,0,0,1,.29-1,1,1,0,0,1,.78-.37.87.87,0,0,1,.75.37,1.62,1.62,0,0,1,.27,1A1.46,1.46,0,0,1,10,11.07Z" fill="url(#bf3846c3-4d74-4743-ab9a-f334c248bd92)" /></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

@@ -0,0 +1,19 @@
window.setFocus = (element) => {
if (element) {
element.focus();
}
};
window.scrollTo = (element) => {
if (element) {
element.scrollIntoView();
}
}
window.resetFileInput = (elementId) => {
document.getElementById(elementId).value = '';
};
function getLocalTime(utcDateTime) {
return new Date(utcDateTime).toLocaleString();
}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB