Refactor embedding workflow: add ExtractChunksExecutor

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.
This commit is contained in:
Marco Minerva
2026-07-31 16:37:02 +02:00
parent 601e075b03
commit 3a765fdccf
4 changed files with 32 additions and 10 deletions
@@ -186,6 +186,7 @@
question = null; question = null;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await Task.Yield(); await Task.Yield();
try try
{ {
await using var scope = ServiceScopeFactory.CreateAsyncScope(); await using var scope = ServiceScopeFactory.CreateAsyncScope();
+6 -3
View File
@@ -102,15 +102,18 @@ builder.Services.AddScoped<DocumentService>();
builder.Services.AddScoped<VectorSearchService>(); builder.Services.AddScoped<VectorSearchService>();
builder.Services.AddScoped<ContextProvider>(); builder.Services.AddScoped<ContextProvider>();
builder.Services.AddSingleton<ExtractChunksExecutor>();
builder.Services.AddSingleton<GenerateEmbeddingExecutor>(); builder.Services.AddSingleton<GenerateEmbeddingExecutor>();
builder.Services.AddScoped<StoreEmbeddingExecutor>(); // This executor is registered as scoped because it uses the DbContext, which is also scoped. builder.Services.AddScoped<StoreEmbeddingExecutor>(); // This executor is registered as scoped because it uses the DbContext, which is also scoped.
builder.AddWorkflow("EmbeddingWorkflow", (services, key) => builder.AddWorkflow("EmbeddingWorkflow", (services, key) =>
{ {
var extractChunksExecutor = services.GetRequiredService<ExtractChunksExecutor>();
var generateEmbeddingExecutor = services.GetRequiredService<GenerateEmbeddingExecutor>(); var generateEmbeddingExecutor = services.GetRequiredService<GenerateEmbeddingExecutor>();
var storeEmbeddingExecutor = services.GetRequiredService<StoreEmbeddingExecutor>(); var storeEmbeddingExecutor = services.GetRequiredService<StoreEmbeddingExecutor>();
var workflow = new WorkflowBuilder(generateEmbeddingExecutor).WithName(key) var workflow = new WorkflowBuilder(extractChunksExecutor).WithName(key)
.AddEdge(extractChunksExecutor, generateEmbeddingExecutor)
.AddEdge(generateEmbeddingExecutor, storeEmbeddingExecutor) .AddEdge(generateEmbeddingExecutor, storeEmbeddingExecutor)
.WithOutputFrom(storeEmbeddingExecutor) .WithOutputFrom(storeEmbeddingExecutor)
.Build(validateOrphans: true); .Build(validateOrphans: true);
@@ -122,7 +125,7 @@ builder.Services.AddAIAgent("ReformulationAgent", (services, key) =>
{ {
var chatClient = services.GetRequiredService<IChatClient>(); var chatClient = services.GetRequiredService<IChatClient>();
return chatClient.AsAIAgent(new ChatClientAgentOptions() return chatClient.AsAIAgent(new()
{ {
Id = key.ToLowerInvariant(), Id = key.ToLowerInvariant(),
Name = key, Name = key,
@@ -210,7 +213,7 @@ builder.Services.AddAIAgent("RagAgent", (services, key) =>
{ {
var chatClient = services.GetRequiredService<IChatClient>(); var chatClient = services.GetRequiredService<IChatClient>();
return chatClient.AsAIAgent(new ChatClientAgentOptions return chatClient.AsAIAgent(new()
{ {
Id = key.ToLowerInvariant(), Id = key.ToLowerInvariant(),
Name = key, Name = key,
@@ -0,0 +1,21 @@
using Microsoft.Agents.AI.Workflows;
using SqlDatabaseVectorSearch.ContentDecoders;
namespace SqlDatabaseVectorSearch.Workflows;
public partial class ExtractChunksExecutor(IServiceProvider serviceProvider, ILogger<ExtractChunksExecutor> logger) : Executor(nameof(ExtractChunksExecutor))
{
[MessageHandler]
private async ValueTask<ExtractChunksResponse> HandleAsync(EmbeddingRequest request, IWorkflowContext context, CancellationToken cancellationToken)
{
// Extract the contents of the file.
var decoder = serviceProvider.GetKeyedService<IContentDecoder>(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<Chunk> Chunks);
@@ -7,17 +7,14 @@ using SqlDatabaseVectorSearch.Settings;
namespace SqlDatabaseVectorSearch.Workflows; namespace SqlDatabaseVectorSearch.Workflows;
public partial class GenerateEmbeddingExecutor(IServiceProvider serviceProvider, IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator, TokenizerService tokenizerService, IOptions<AppSettings> appSettingsOptions, ILogger<GenerateEmbeddingExecutor> logger) : Executor(nameof(GenerateEmbeddingExecutor)) public partial class GenerateEmbeddingExecutor(IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator, TokenizerService tokenizerService, IOptions<AppSettings> appSettingsOptions, ILogger<GenerateEmbeddingExecutor> logger) : Executor(nameof(GenerateEmbeddingExecutor))
{ {
private readonly AppSettings appSettings = appSettingsOptions.Value; private readonly AppSettings appSettings = appSettingsOptions.Value;
[MessageHandler] [MessageHandler]
private async ValueTask<EmbeddingResponse> HandleAsync(EmbeddingRequest request, IWorkflowContext context, CancellationToken cancellationToken) private async ValueTask<EmbeddingResponse> HandleAsync(ExtractChunksResponse chunks, IWorkflowContext context, CancellationToken cancellationToken)
{ {
// Extract the contents of the file. var chunkContents = chunks.Chunks.Select(p => p.Content).ToList();
var decoder = serviceProvider.GetKeyedService<IContentDecoder>(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();
// 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). // 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)); var tokenCount = tokenizerService.CountEmbeddingTokens(string.Join(" ", chunkContents));
@@ -33,7 +30,7 @@ public partial class GenerateEmbeddingExecutor(IServiceProvider serviceProvider,
embeddings.AddRange(batchEmbeddings); embeddings.AddRange(batchEmbeddings);
} }
return new EmbeddingResponse(request, chunks, embeddings, tokenCount); return new EmbeddingResponse(chunks.Request, chunks.Chunks, embeddings, tokenCount);
} }
} }