From 3a765fdccfaaca303d5cae4e4579c03c3a3445c5 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Fri, 31 Jul 2026 16:37:02 +0200 Subject: [PATCH] Refactor embedding workflow: add ExtractChunksExecutor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored the embedding workflow to introduce ExtractChunksExecutor, responsible for content decoding and chunk extraction before embedding generation. Registered ExtractChunksExecutor and GenerateEmbeddingExecutor as singletons, and StoreEmbeddingExecutor as scoped in DI. Updated workflow to sequence ExtractChunksExecutor → GenerateEmbeddingExecutor → StoreEmbeddingExecutor. Modified GenerateEmbeddingExecutor to accept ExtractChunksResponse. Updated ChatClientAgentOptions usage to use object initializers for clarity. Added ExtractChunksExecutor.cs. --- .../Components/Pages/Ask.razor | 1 + SqlDatabaseVectorSearch/Program.cs | 9 +++++--- .../Workflows/ExtractChunksExecutor.cs | 21 +++++++++++++++++++ .../Workflows/GenerateEmbeddingExecutor.cs | 11 ++++------ 4 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 SqlDatabaseVectorSearch/Workflows/ExtractChunksExecutor.cs diff --git a/SqlDatabaseVectorSearch/Components/Pages/Ask.razor b/SqlDatabaseVectorSearch/Components/Pages/Ask.razor index b0bfa6b..f9b13d1 100644 --- a/SqlDatabaseVectorSearch/Components/Pages/Ask.razor +++ b/SqlDatabaseVectorSearch/Components/Pages/Ask.razor @@ -186,6 +186,7 @@ question = null; await InvokeAsync(StateHasChanged); await Task.Yield(); + try { await using var scope = ServiceScopeFactory.CreateAsyncScope(); diff --git a/SqlDatabaseVectorSearch/Program.cs b/SqlDatabaseVectorSearch/Program.cs index 3175991..76177f0 100644 --- a/SqlDatabaseVectorSearch/Program.cs +++ b/SqlDatabaseVectorSearch/Program.cs @@ -102,15 +102,18 @@ 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 extractChunksExecutor = services.GetRequiredService(); var generateEmbeddingExecutor = services.GetRequiredService(); var storeEmbeddingExecutor = services.GetRequiredService(); - var workflow = new WorkflowBuilder(generateEmbeddingExecutor).WithName(key) + var workflow = new WorkflowBuilder(extractChunksExecutor).WithName(key) + .AddEdge(extractChunksExecutor, generateEmbeddingExecutor) .AddEdge(generateEmbeddingExecutor, storeEmbeddingExecutor) .WithOutputFrom(storeEmbeddingExecutor) .Build(validateOrphans: true); @@ -122,7 +125,7 @@ builder.Services.AddAIAgent("ReformulationAgent", (services, key) => { var chatClient = services.GetRequiredService(); - return chatClient.AsAIAgent(new ChatClientAgentOptions() + return chatClient.AsAIAgent(new() { Id = key.ToLowerInvariant(), Name = key, @@ -210,7 +213,7 @@ builder.Services.AddAIAgent("RagAgent", (services, key) => { var chatClient = services.GetRequiredService(); - return chatClient.AsAIAgent(new ChatClientAgentOptions + return chatClient.AsAIAgent(new() { Id = key.ToLowerInvariant(), Name = key, diff --git a/SqlDatabaseVectorSearch/Workflows/ExtractChunksExecutor.cs b/SqlDatabaseVectorSearch/Workflows/ExtractChunksExecutor.cs new file mode 100644 index 0000000..a43ea19 --- /dev/null +++ b/SqlDatabaseVectorSearch/Workflows/ExtractChunksExecutor.cs @@ -0,0 +1,21 @@ +using Microsoft.Agents.AI.Workflows; +using SqlDatabaseVectorSearch.ContentDecoders; + +namespace SqlDatabaseVectorSearch.Workflows; + +public partial class ExtractChunksExecutor(IServiceProvider serviceProvider, ILogger logger) : Executor(nameof(ExtractChunksExecutor)) +{ + [MessageHandler] + private async ValueTask HandleAsync(EmbeddingRequest request, IWorkflowContext context, CancellationToken cancellationToken) + { + // Extract the contents of the file. + var decoder = serviceProvider.GetKeyedService(request.ContentType) ?? throw new NotSupportedException($"Content type '{request.ContentType}' is not supported."); + var chunks = await decoder.DecodeAsync(request.Content, request.ContentType, cancellationToken); + + logger.LogDebug("Extracted {Count} chunks from '{FileName}'.", chunks.Count(), request.FileName); + + return new(request, chunks); + } +} + +public record class ExtractChunksResponse(EmbeddingRequest Request, IEnumerable Chunks); diff --git a/SqlDatabaseVectorSearch/Workflows/GenerateEmbeddingExecutor.cs b/SqlDatabaseVectorSearch/Workflows/GenerateEmbeddingExecutor.cs index d3c5144..8907c67 100644 --- a/SqlDatabaseVectorSearch/Workflows/GenerateEmbeddingExecutor.cs +++ b/SqlDatabaseVectorSearch/Workflows/GenerateEmbeddingExecutor.cs @@ -7,17 +7,14 @@ using SqlDatabaseVectorSearch.Settings; namespace SqlDatabaseVectorSearch.Workflows; -public partial class GenerateEmbeddingExecutor(IServiceProvider serviceProvider, IEmbeddingGenerator> embeddingGenerator, TokenizerService tokenizerService, IOptions appSettingsOptions, ILogger logger) : Executor(nameof(GenerateEmbeddingExecutor)) +public partial class GenerateEmbeddingExecutor(IEmbeddingGenerator> embeddingGenerator, TokenizerService tokenizerService, IOptions appSettingsOptions, ILogger logger) : Executor(nameof(GenerateEmbeddingExecutor)) { private readonly AppSettings appSettings = appSettingsOptions.Value; [MessageHandler] - private async ValueTask HandleAsync(EmbeddingRequest request, IWorkflowContext context, CancellationToken cancellationToken) + private async ValueTask HandleAsync(ExtractChunksResponse chunks, IWorkflowContext context, CancellationToken cancellationToken) { - // Extract the contents of the file. - var decoder = serviceProvider.GetKeyedService(request.ContentType) ?? throw new NotSupportedException($"Content type '{request.ContentType}' is not supported."); - var chunks = await decoder.DecodeAsync(request.Content, request.ContentType, cancellationToken); - var chunkContents = chunks.Select(p => p.Content).ToList(); + var chunkContents = chunks.Chunks.Select(p => p.Content).ToList(); // We get the token count of the whole document because it is the total number of tokens used by the embedding (it may be necessary, for example, for cost analysis). var tokenCount = tokenizerService.CountEmbeddingTokens(string.Join(" ", chunkContents)); @@ -33,7 +30,7 @@ public partial class GenerateEmbeddingExecutor(IServiceProvider serviceProvider, embeddings.AddRange(batchEmbeddings); } - return new EmbeddingResponse(request, chunks, embeddings, tokenCount); + return new EmbeddingResponse(chunks.Request, chunks.Chunks, embeddings, tokenCount); } }