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); } }