Add streaming support and improve JSON serialization

- Updated `Response` record class to allow nullable `Question` and `Answer` properties; moved `StreamState` enum to a new file.
- Added `JsonStringEnumConverter` in `Program.cs` for better enum serialization.
- Corrected terminology in document upload endpoint description.
- Introduced `/api/ask-streaming` endpoint for streaming question responses.
- Added `AskStreamingAsync` method in `VectorSearchService` for handling streaming logic.
- Created `StreamState.cs` to define `StreamState` enum with `Start`, `Append`, and `End` values.
This commit is contained in:
Marco Minerva
2025-01-28 11:00:45 +01:00
parent 44c6193674
commit 1ef2d384ec
4 changed files with 57 additions and 9 deletions
+2 -8
View File
@@ -1,10 +1,4 @@
namespace SqlDatabaseVectorSearch.Models; namespace SqlDatabaseVectorSearch.Models;
public record class Response(string Question, string Answer, StreamState? StreamState = null); // Question and Asnwer can be null when using response streaming.
public record class Response(string? Question, string? Answer, StreamState? StreamState = null);
public enum StreamState
{
Start,
Append,
End
}
@@ -0,0 +1,8 @@
namespace SqlDatabaseVectorSearch.Models;
public enum StreamState
{
Start,
Append,
End
}
+28 -1
View File
@@ -1,4 +1,5 @@
using System.ComponentModel; using System.ComponentModel;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.SemanticKernel; using Microsoft.SemanticKernel;
@@ -49,6 +50,11 @@ builder.Services.AddSingleton<TokenizerService>();
builder.Services.AddSingleton<ChatService>(); builder.Services.AddSingleton<ChatService>();
builder.Services.AddScoped<VectorSearchService>(); builder.Services.AddScoped<VectorSearchService>();
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
builder.Services.AddOpenApi(options => builder.Services.AddOpenApi(options =>
{ {
options.RemoveServerList(); options.RemoveServerList();
@@ -114,7 +120,7 @@ documentsApiGroup.MapPost(string.Empty, async (IFormFile file, VectorSearchServi
.DisableAntiforgery() .DisableAntiforgery()
.ProducesProblem(StatusCodes.Status400BadRequest) .ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("Uploads a document") .WithSummary("Uploads a document")
.WithDescription("Uploads a document to SQL Database and saves its embedding using the new native Vector type. The document will be indexed and used to answer questions. Currently, only PDF files are supported."); .WithDescription("Uploads a document to SQL Database and saves its embedding using the native VECTOR type. The document will be indexed and used to answer questions. Currently, only PDF files are supported.");
documentsApiGroup.MapDelete("{documentId:guid}", async (Guid documentId, VectorSearchService vectorSearchService) => documentsApiGroup.MapDelete("{documentId:guid}", async (Guid documentId, VectorSearchService vectorSearchService) =>
{ {
@@ -134,4 +140,25 @@ app.MapPost("/api/ask", async (Question question, VectorSearchService vectorSear
.WithDescription("The question will be reformulated taking into account the context of the chat identified by the given ConversationId.") .WithDescription("The question will be reformulated taking into account the context of the chat identified by the given ConversationId.")
.WithTags("Ask"); .WithTags("Ask");
app.MapPost("/api/ask-streaming", (Question question, VectorSearchService vectorSearchService,
[Description("If true, the question will be reformulated taking into account the context of the chat identified by the given ConversationId.")] bool reformulate = true) =>
{
async IAsyncEnumerable<Response> Stream()
{
// Requests a streaming response.
var responseStream = vectorSearchService.AskStreamingAsync(question, reformulate);
await foreach (var delta in responseStream)
{
yield return delta;
await Task.Delay(50);
}
}
return Stream();
})
.WithSummary("Asks a question and gets the response as streaming")
.WithDescription("The question will be reformulated taking into account the context of the chat identified by the given ConversationId.")
.WithTags("Ask");
app.Run(); app.Run();
@@ -88,6 +88,25 @@ public class VectorSearchService(ApplicationDbContext dbContext, ITextEmbeddingG
return new Response(reformulatedQuestion, answer); return new Response(reformulatedQuestion, answer);
} }
public async IAsyncEnumerable<Response> AskStreamingAsync(Question question, bool reformulate = true)
{
var (reformulatedQuestion, chunks) = await CreateContextAsync(question, reformulate);
var answerStream = chatService.AskStreamingAsync(question.ConversationId, chunks, reformulatedQuestion);
// The first message contains the original question.
yield return new Response(reformulatedQuestion, null, StreamState.Start);
// Return each token as a partial response.
await foreach (var token in answerStream)
{
yield return new Response(null, token, StreamState.Append);
}
// The last message tells the client that the stream has ended.
yield return new Response(null, null, StreamState.End);
}
private async Task<(string Question, IEnumerable<string> Chunks)> CreateContextAsync(Question question, bool reformulate = true) private async Task<(string Question, IEnumerable<string> Chunks)> CreateContextAsync(Question question, bool reformulate = true)
{ {
// Reformulate the following question taking into account the context of the chat to perform keyword search and embeddings: // Reformulate the following question taking into account the context of the chat to perform keyword search and embeddings: