diff --git a/SqlDatabaseVectorSearch/Components/Pages/Documents.razor b/SqlDatabaseVectorSearch/Components/Pages/Documents.razor index 486826c..165064a 100644 --- a/SqlDatabaseVectorSearch/Components/Pages/Documents.razor +++ b/SqlDatabaseVectorSearch/Components/Pages/Documents.razor @@ -177,7 +177,7 @@ else var vectorSearchService = scope.ServiceProvider.GetRequiredService(); var documentId = string.IsNullOrWhiteSpace(Model.DocumentId) ? null : (Guid?)Guid.Parse(Model.DocumentId); - //await vectorSearchService.ImportAsync(stream, fileName, MimeUtility.GetMimeMapping(fileName), documentId); + await vectorSearchService.ImportAsync(EmbeddingRequest.Create(stream, fileName, documentId)); ToastService.Notify(await CreateToastMessageAsync(ToastType.Success, "Upload document", $"The document {fileName} has been successfully uploaded and indexed.")); diff --git a/SqlDatabaseVectorSearch/Components/_Imports.razor b/SqlDatabaseVectorSearch/Components/_Imports.razor index ef23f97..6ef6fce 100644 --- a/SqlDatabaseVectorSearch/Components/_Imports.razor +++ b/SqlDatabaseVectorSearch/Components/_Imports.razor @@ -13,4 +13,5 @@ @using SqlDatabaseVectorSearch.Extensions @using SqlDatabaseVectorSearch.Models @using SqlDatabaseVectorSearch.Services +@using SqlDatabaseVectorSearch.Workflows @using BlazorBootstrap \ No newline at end of file diff --git a/SqlDatabaseVectorSearch/Endpoints/DocumentEndpoints.cs b/SqlDatabaseVectorSearch/Endpoints/DocumentEndpoints.cs index f42caeb..a167f3d 100644 --- a/SqlDatabaseVectorSearch/Endpoints/DocumentEndpoints.cs +++ b/SqlDatabaseVectorSearch/Endpoints/DocumentEndpoints.cs @@ -23,7 +23,7 @@ public class DocumentEndpoints : IEndpointRouteHandlerBuilder 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) => { - var result = await vectorSearchService.ImportAsync(new FormFileEmbeddingRequest(file, documentId), cancellationToken); + var result = await vectorSearchService.ImportAsync(EmbeddingRequest.FromFormFile(file, documentId), cancellationToken); return TypedResults.Ok(result); }) .DisableAntiforgery() diff --git a/SqlDatabaseVectorSearch/Program.cs b/SqlDatabaseVectorSearch/Program.cs index 716dd7e..afdbb93 100644 --- a/SqlDatabaseVectorSearch/Program.cs +++ b/SqlDatabaseVectorSearch/Program.cs @@ -92,18 +92,15 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); // This executor is registered as scoped because it uses the DbContext, which is also scoped. builder.AddWorkflow("EmbeddingWorkflow", (services, key) => { - var formfileToConversionRequestExecutor = services.GetRequiredService(); var generateEmbeddingExecutor = services.GetRequiredService(); var storeEmbeddingExecutor = services.GetRequiredService(); - var workflow = new WorkflowBuilder(formfileToConversionRequestExecutor).WithName(key) - .AddEdge(formfileToConversionRequestExecutor, generateEmbeddingExecutor) + var workflow = new WorkflowBuilder(generateEmbeddingExecutor).WithName(key) .AddEdge(generateEmbeddingExecutor, storeEmbeddingExecutor) .WithOutputFrom(storeEmbeddingExecutor) .Build(validateOrphans: true); diff --git a/SqlDatabaseVectorSearch/Services/VectorSearchService.cs b/SqlDatabaseVectorSearch/Services/VectorSearchService.cs index 71f6045..e6edd4e 100644 --- a/SqlDatabaseVectorSearch/Services/VectorSearchService.cs +++ b/SqlDatabaseVectorSearch/Services/VectorSearchService.cs @@ -17,7 +17,7 @@ namespace SqlDatabaseVectorSearch.Services; public partial class VectorSearchService([FromKeyedServices("EmbeddingWorkflow")] Workflow workflow, [FromKeyedServices("ReformulationAgent")] AIAgent reformulationAgent, [FromKeyedServices("RagAgent")] AIAgent ragAgent, [FromKeyedServices("RagAgent")] AgentSessionStore sessionStore) { - public async Task ImportAsync(FormFileEmbeddingRequest request, CancellationToken cancellationToken = default) + public async Task ImportAsync(EmbeddingRequest request, CancellationToken cancellationToken = default) { await using var run = await InProcessExecution.RunAsync(workflow, request, cancellationToken: cancellationToken); var events = run.NewEvents.ToList(); diff --git a/SqlDatabaseVectorSearch/Workflows/EmbeddingRequest.cs b/SqlDatabaseVectorSearch/Workflows/EmbeddingRequest.cs new file mode 100644 index 0000000..9837162 --- /dev/null +++ b/SqlDatabaseVectorSearch/Workflows/EmbeddingRequest.cs @@ -0,0 +1,26 @@ +namespace SqlDatabaseVectorSearch.Workflows; + +public record class EmbeddingRequest(Stream Content, string FileName, string ContentType, Guid? DocumentId) +{ + /// + /// Creates an from an uploaded . + /// + /// The uploaded file. + /// The optional identifier of the document to overwrite. + public static EmbeddingRequest FromFormFile(IFormFile file, Guid? documentId = null) => Create(file.OpenReadStream(), Path.GetFileName(file.FileName), documentId); + + /// + /// Creates an from a content stream, inferring the content type from the file name. + /// + /// The stream that contains the document content. + /// The name of the document. + /// The optional identifier of the document to overwrite. + /// + /// The content type is inferred from the file name because the content type declared by the client is not always reliable (for example, for Markdown files). + /// + public static EmbeddingRequest Create(Stream content, string fileName, Guid? documentId = null) + { + var name = Path.GetFileName(fileName); + return new EmbeddingRequest(content, name, MimeMapping.MimeUtility.GetMimeMapping(name), documentId); + } +} \ No newline at end of file diff --git a/SqlDatabaseVectorSearch/Workflows/FormFileToEmbeddingRequestExecutor.cs b/SqlDatabaseVectorSearch/Workflows/FormFileToEmbeddingRequestExecutor.cs deleted file mode 100644 index 4e93aa2..0000000 --- a/SqlDatabaseVectorSearch/Workflows/FormFileToEmbeddingRequestExecutor.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.Agents.AI.Workflows; - -namespace SqlDatabaseVectorSearch.Workflows; - -public partial class FormFileToEmbeddingRequestExecutor() : Executor(nameof(FormFileToEmbeddingRequestExecutor)) -{ - [MessageHandler] - private ValueTask HandleAsync(FormFileEmbeddingRequest request, IWorkflowContext context, CancellationToken cancellationToken) - { - // Note: file.ContentType is not 100% reliable (for example, for markdown file). - var embeddingRequest = new EmbeddingRequest(request.File.OpenReadStream(), Path.GetFileName(request.File.FileName), MimeMapping.MimeUtility.GetMimeMapping(request.File.FileName), request.DocumentId); - return ValueTask.FromResult(embeddingRequest); - } -} - -public record class FormFileEmbeddingRequest(IFormFile File, Guid? DocumentId); - -public record class EmbeddingRequest(Stream Content, string FileName, string ContentType, Guid? DocumentId); \ No newline at end of file