Refactor embedding request creation and processing

Consolidate embedding request logic into a new EmbeddingRequest record with static factory methods for IFormFile and stream input. Remove FormFileToEmbeddingRequestExecutor and FormFileEmbeddingRequest, updating all usages and dependency injection accordingly. Clean up related code in Program.cs, VectorSearchService, and _Imports.razor. Enhance XML documentation for EmbeddingRequest to clarify intent and usage.
This commit is contained in:
Marco Minerva
2026-07-27 17:57:30 +02:00
parent c7138d571c
commit 96fd8adf0f
7 changed files with 31 additions and 25 deletions
@@ -177,7 +177,7 @@ else
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);
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."));
@@ -13,4 +13,5 @@
@using SqlDatabaseVectorSearch.Extensions
@using SqlDatabaseVectorSearch.Models
@using SqlDatabaseVectorSearch.Services
@using SqlDatabaseVectorSearch.Workflows
@using BlazorBootstrap
@@ -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()
+1 -4
View File
@@ -92,18 +92,15 @@ builder.Services.AddScoped<DocumentService>();
builder.Services.AddScoped<VectorSearchService>();
builder.Services.AddScoped<ContextProvider>();
builder.Services.AddSingleton<FormFileToEmbeddingRequestExecutor>();
builder.Services.AddSingleton<GenerateEmbeddingExecutor>();
builder.Services.AddScoped<StoreEmbeddingExecutor>(); // This executor is registered as scoped because it uses the DbContext, which is also scoped.
builder.AddWorkflow("EmbeddingWorkflow", (services, key) =>
{
var formfileToConversionRequestExecutor = services.GetRequiredService<FormFileToEmbeddingRequestExecutor>();
var generateEmbeddingExecutor = services.GetRequiredService<GenerateEmbeddingExecutor>();
var storeEmbeddingExecutor = services.GetRequiredService<StoreEmbeddingExecutor>();
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);
@@ -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<StoreEmbeddingResponse> ImportAsync(FormFileEmbeddingRequest request, CancellationToken cancellationToken = default)
public async Task<StoreEmbeddingResponse> ImportAsync(EmbeddingRequest request, CancellationToken cancellationToken = default)
{
await using var run = await InProcessExecution.RunAsync(workflow, request, cancellationToken: cancellationToken);
var events = run.NewEvents.ToList();
@@ -0,0 +1,26 @@
namespace SqlDatabaseVectorSearch.Workflows;
public record class EmbeddingRequest(Stream Content, string FileName, string ContentType, Guid? DocumentId)
{
/// <summary>
/// Creates an <see cref="EmbeddingRequest"/> from an uploaded <see cref="IFormFile"/>.
/// </summary>
/// <param name="file">The uploaded file.</param>
/// <param name="documentId">The optional identifier of the document to overwrite.</param>
public static EmbeddingRequest FromFormFile(IFormFile file, Guid? documentId = null) => Create(file.OpenReadStream(), Path.GetFileName(file.FileName), documentId);
/// <summary>
/// Creates an <see cref="EmbeddingRequest"/> from a content stream, inferring the content type from the file name.
/// </summary>
/// <param name="content">The stream that contains the document content.</param>
/// <param name="fileName">The name of the document.</param>
/// <param name="documentId">The optional identifier of the document to overwrite.</param>
/// <remarks>
/// 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).
/// </remarks>
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);
}
}
@@ -1,18 +0,0 @@
using Microsoft.Agents.AI.Workflows;
namespace SqlDatabaseVectorSearch.Workflows;
public partial class FormFileToEmbeddingRequestExecutor() : Executor(nameof(FormFileToEmbeddingRequestExecutor))
{
[MessageHandler]
private ValueTask<EmbeddingRequest> 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);