mirror of
https://github.com/marcominerva/SqlDatabaseVectorSearch.git
synced 2026-08-04 09:48:57 +00:00
Refactor models, update usage tracking, and clean citations
- Enabled additional C# code style suggestions in .editorconfig. - Removed citation rendering and extraction from Ask.razor. - Deleted Citation, ChatResponse, and TokenUsage models and references. - Updated TokenUsageResponse to use UsageDetails from Microsoft.Extensions.AI. - Refactored Response model: removed citations, added ConversationId, and adopted new token usage structure. - Updated HybridCacheSessionStoreService with new cache key format and DeleteSessionAsync method. - Refactored VectorSearchService to remove AppSettings dependency, update method signatures, and use UsageDetails for token tracking. - Updated NuGet package references in SqlDatabaseVectorSearch.csproj.
This commit is contained in:
@@ -124,6 +124,7 @@ csharp_style_prefer_null_check_over_type_check = true:suggestion
|
||||
|
||||
# Modifier preferences
|
||||
csharp_prefer_static_local_function = true:suggestion
|
||||
csharp_prefer_static_anonymous_function = true:suggestion
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
|
||||
|
||||
# Code-block preferences
|
||||
@@ -139,9 +140,11 @@ csharp_prefer_system_threading_lock = true:suggestion
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_deconstructed_variable_declaration = false:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion
|
||||
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_prefer_index_operator = true:suggestion
|
||||
csharp_style_prefer_range_operator = true:suggestion
|
||||
csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:none
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:none
|
||||
|
||||
@@ -83,23 +83,6 @@
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@if (message.Citations is not null && message.Citations.Count() > 0)
|
||||
{
|
||||
<div class="mt-3 d-flex flex-wrap">
|
||||
@foreach (var citation in message.Citations)
|
||||
{
|
||||
<div class="border rounded p-2 me-2 mb-2 citation-box small">
|
||||
<div>
|
||||
<strong>@citation.FileName</strong> @if (citation.PageNumber.GetValueOrDefault() > 0)
|
||||
{
|
||||
<span class="ms-2">pag. @citation.PageNumber</span>
|
||||
}
|
||||
</div>
|
||||
<div class="text-secondary small mt-1">@citation.Quote</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -212,17 +195,6 @@
|
||||
}
|
||||
else if (delta.StreamState == StreamState.End)
|
||||
{
|
||||
// Get citations from the response.
|
||||
assistantMessage.Citations = delta.Citations?.Select(c => new Citation
|
||||
{
|
||||
DocumentId = c.DocumentId,
|
||||
ChunkId = c.ChunkId,
|
||||
FileName = c.FileName,
|
||||
Quote = c.Quote,
|
||||
PageNumber = c.PageNumber,
|
||||
IndexOnPage = c.IndexOnPage
|
||||
});
|
||||
|
||||
assistantMessage.Status = MessageStatus.Completed;
|
||||
assistantMessage.TokenUsage += FormatTokenUsage(delta.TokenUsage);
|
||||
}
|
||||
@@ -282,26 +254,22 @@
|
||||
? $"<p><strong>Reformulation:</strong><br />{FormatTokenUsageDetails(tokenUsageResponse.Reformulation)}</p>"
|
||||
: string.Empty;
|
||||
|
||||
var embeddingTokenCount = tokenUsageResponse.EmbeddingTokenCount.HasValue
|
||||
? $"<p><strong>Embedding Token Count:</strong> {tokenUsageResponse.EmbeddingTokenCount}</p>"
|
||||
: string.Empty;
|
||||
|
||||
var question = tokenUsageResponse.Question is not null
|
||||
? $"<p><strong>Question:</strong><br />{FormatTokenUsageDetails(tokenUsageResponse.Question)}</p>"
|
||||
: string.Empty;
|
||||
|
||||
return $"{reformulation}{embeddingTokenCount}{question}";
|
||||
return $"{reformulation}{question}";
|
||||
|
||||
static string FormatTokenUsageDetails(TokenUsage? tokenUsage)
|
||||
static string FormatTokenUsageDetails(Microsoft.Extensions.AI.UsageDetails? tokenUsage)
|
||||
{
|
||||
if (tokenUsage is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return $"Prompt tokens: {tokenUsage.PromptTokens}<br />" +
|
||||
$"Completion tokens: {tokenUsage.CompletionTokens}<br />" +
|
||||
$"Total tokens: {tokenUsage.TotalTokens}";
|
||||
return $"Input tokens: {tokenUsage.InputTokenCount}<br />" +
|
||||
$"Output tokens: {tokenUsage.OutputTokenCount}<br />" +
|
||||
$"Total tokens: {tokenUsage.TotalTokenCount}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,23 +294,5 @@
|
||||
public MessageStatus Status { get; set; } = MessageStatus.New;
|
||||
|
||||
public string? TokenUsage { get; set; }
|
||||
|
||||
// List of citations extracted from the answer.
|
||||
public IEnumerable<Citation>? Citations { get; set; }
|
||||
}
|
||||
|
||||
public class Citation
|
||||
{
|
||||
public Guid DocumentId { get; set; }
|
||||
|
||||
public Guid ChunkId { get; set; }
|
||||
|
||||
public string FileName { get; set; } = null!;
|
||||
|
||||
public string Quote { get; set; } = null!;
|
||||
|
||||
public int? PageNumber { get; set; }
|
||||
|
||||
public int IndexOnPage { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace SqlDatabaseVectorSearch.Models;
|
||||
|
||||
public record class ChatResponse(string? Text, TokenUsage? TokenUsage = null);
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace SqlDatabaseVectorSearch.Models;
|
||||
|
||||
public class Citation
|
||||
{
|
||||
public Guid DocumentId { get; set; }
|
||||
|
||||
public Guid ChunkId { get; set; }
|
||||
|
||||
public string FileName { get; set; } = null!;
|
||||
|
||||
public string Quote { get; set; } = null!;
|
||||
|
||||
public int? PageNumber { get; set; }
|
||||
|
||||
public int IndexOnPage { get; set; }
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
namespace SqlDatabaseVectorSearch.Models;
|
||||
|
||||
// Question and Answer can be null when using response streaming.
|
||||
public record class Response(string? OriginalQuestion, string? ReformulatedQuestion, string? Answer, StreamState? StreamState = null, TokenUsageResponse? TokenUsage = null, IEnumerable<Citation>? Citations = null)
|
||||
public record class Response(Guid ConversationId, string? OriginalQuestion, string? ReformulatedQuestion, string? Answer, StreamState? StreamState = null, TokenUsageResponse? TokenUsage = null)
|
||||
{
|
||||
public Response(string? token, StreamState streamState, TokenUsageResponse? tokenUsageResponse = null, IEnumerable<Citation>? citations = null)
|
||||
: this(null, null, token, streamState, tokenUsageResponse, citations)
|
||||
public Response(Guid conversationId, string? token, StreamState streamState, TokenUsageResponse? tokenUsageResponse = null)
|
||||
: this(conversationId, null, null, token, streamState, tokenUsageResponse)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public record class RagResponse(Guid ConversationId, string OriginalQuestion, string ReformulatedQuestion, string Answer);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace SqlDatabaseVectorSearch.Models;
|
||||
|
||||
public record class TokenUsage(int PromptTokens, int CompletionTokens)
|
||||
{
|
||||
public int TotalTokens => PromptTokens + CompletionTokens;
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
namespace SqlDatabaseVectorSearch.Models;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
public record class TokenUsageResponse(TokenUsage? Reformulation, int? EmbeddingTokenCount, TokenUsage? Question)
|
||||
{
|
||||
public TokenUsageResponse(TokenUsage? question)
|
||||
: this(null, null, question)
|
||||
{
|
||||
}
|
||||
}
|
||||
namespace SqlDatabaseVectorSearch.Models;
|
||||
|
||||
public record class TokenUsageResponse(UsageDetails? Reformulation, UsageDetails? Question);
|
||||
|
||||
@@ -8,23 +8,30 @@ public class HybridCacheSessionStoreService(HybridCache cache) : AgentSessionSto
|
||||
{
|
||||
public override async ValueTask<AgentSession> GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sessionContent = await cache.GetOrCreateAsync(
|
||||
GetCacheKey(conversationId),
|
||||
async ct =>
|
||||
{
|
||||
var session = await agent.CreateSessionAsync(ct);
|
||||
return await agent.SerializeSessionAsync(session, cancellationToken: ct);
|
||||
},
|
||||
cancellationToken: cancellationToken);
|
||||
var key = GetKey(agent, conversationId);
|
||||
var sessionContent = await cache.GetOrCreateAsync(key, async ct =>
|
||||
{
|
||||
var session = await agent.CreateSessionAsync(ct);
|
||||
return await agent.SerializeSessionAsync(session, cancellationToken: ct);
|
||||
}, cancellationToken: cancellationToken);
|
||||
|
||||
return await agent.DeserializeSessionAsync(sessionContent, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public override async ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = GetKey(agent, conversationId);
|
||||
var sessionContent = await agent.SerializeSessionAsync(session, cancellationToken: cancellationToken);
|
||||
await cache.SetAsync(GetCacheKey(conversationId), sessionContent, cancellationToken: cancellationToken);
|
||||
|
||||
await cache.SetAsync(key, sessionContent, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private static string GetCacheKey(string conversationId) => $"agent-session:{conversationId}";
|
||||
}
|
||||
public override async ValueTask DeleteSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = GetKey(agent, conversationId);
|
||||
await cache.RemoveAsync(key, cancellationToken);
|
||||
}
|
||||
|
||||
private static string GetKey(AIAgent agent, string conversationId)
|
||||
=> $"{agent.Id}:{conversationId}";
|
||||
}
|
||||
@@ -15,10 +15,8 @@ using SqlDatabaseVectorSearch.Workflows;
|
||||
namespace SqlDatabaseVectorSearch.Services;
|
||||
|
||||
public partial class VectorSearchService([FromKeyedServices("EmbeddingWorkflow")] Workflow workflow, [FromKeyedServices("ReformulationAgent")] AIAgent reformulationAgent, [FromKeyedServices("RagAgent")] AIAgent ragAgent,
|
||||
[FromKeyedServices("RagAgent")] AgentSessionStore sessionStore, IOptions<AppSettings> appSettingsOptions)
|
||||
[FromKeyedServices("RagAgent")] AgentSessionStore sessionStore)
|
||||
{
|
||||
private readonly AppSettings appSettings = appSettingsOptions.Value;
|
||||
|
||||
public async Task<StoreEmbeddingResponse> ImportAsync(FormFileEmbeddingRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var run = await InProcessExecution.RunAsync(workflow, request, cancellationToken: cancellationToken);
|
||||
@@ -34,8 +32,9 @@ public partial class VectorSearchService([FromKeyedServices("EmbeddingWorkflow")
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<RagResponse> AskQuestionAsync(Question question, bool reformulate = true, CancellationToken cancellationToken = default)
|
||||
public async Task<Response> AskQuestionAsync(Question question, bool reformulate = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
UsageDetails? reformulationUsage = null;
|
||||
var reformulatedQuestion = question.Text;
|
||||
var session = await sessionStore.GetSessionAsync(ragAgent, question.ConversationId.ToString(), cancellationToken);
|
||||
|
||||
@@ -44,62 +43,48 @@ public partial class VectorSearchService([FromKeyedServices("EmbeddingWorkflow")
|
||||
// Reformulates the question taking into account the context of the chat to perform keyword search and embeddings.
|
||||
var reformulationResponse = await reformulationAgent.RunAsync(question.Text, session, cancellationToken: cancellationToken);
|
||||
reformulatedQuestion = reformulationResponse.Text;
|
||||
reformulationUsage = reformulationResponse.Usage;
|
||||
}
|
||||
|
||||
var response = await ragAgent.RunAsync(reformulatedQuestion, session, cancellationToken: cancellationToken);
|
||||
|
||||
await sessionStore.SaveSessionAsync(ragAgent, question.ConversationId.ToString(), session, cancellationToken);
|
||||
|
||||
session.TryGetInMemoryChatHistory(out var chatHistory);
|
||||
|
||||
return new(question.ConversationId, question.Text, reformulatedQuestion, response.Text);
|
||||
return new(question.ConversationId, question.Text, reformulatedQuestion, response.Text, null, new TokenUsageResponse(reformulationUsage, response.Usage));
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<Response> AskStreamingAsync(Question question, bool reformulate = true, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
yield return null!;
|
||||
UsageDetails? reformulationUsage = null;
|
||||
var reformulatedQuestion = question.Text;
|
||||
var session = await sessionStore.GetSessionAsync(ragAgent, question.ConversationId.ToString(), cancellationToken);
|
||||
|
||||
//// It the user doesn't want to reforulate the question, CreateContextAsync returns the original one.
|
||||
//var (reformulatedQuestion, embeddingTokenCount, chunks) = await CreateContextAsync(question, reformulate, cancellationToken);
|
||||
if (reformulate)
|
||||
{
|
||||
// Reformulates the question taking into account the context of the chat to perform keyword search and embeddings.
|
||||
var reformulationResponse = await reformulationAgent.RunAsync(question.Text, session, cancellationToken: cancellationToken);
|
||||
reformulatedQuestion = reformulationResponse.Text;
|
||||
reformulationUsage = reformulationResponse.Usage;
|
||||
}
|
||||
|
||||
//var answerStream = chatService.AskStreamingAsync(question.ConversationId, chunks, reformulatedQuestion.Text!, cancellationToken: cancellationToken);
|
||||
// The first message contains the question and the corresponding token usage (if reformulated).
|
||||
yield return new(question.ConversationId, question.Text, reformulatedQuestion, null, StreamState.Start, new(reformulationUsage, null));
|
||||
|
||||
//// The first message contains the question and the corresponding token usage (if reformulated).
|
||||
//yield return new(question.Text, reformulatedQuestion.Text!, null, StreamState.Start, new(reformulatedQuestion.TokenUsage, embeddingTokenCount, null));
|
||||
var updates = new List<AgentResponseUpdate>();
|
||||
|
||||
//TokenUsageResponse? tokenUsageResponse = null;
|
||||
//var fullAnswer = new StringBuilder();
|
||||
//var citationsStarted = false;
|
||||
await foreach (var update in ragAgent.RunStreamingAsync(reformulatedQuestion, session, cancellationToken: cancellationToken))
|
||||
{
|
||||
updates.Add(update);
|
||||
if (!string.IsNullOrEmpty(update.Text))
|
||||
{
|
||||
yield return new(question.ConversationId, update.Text, StreamState.Append);
|
||||
}
|
||||
}
|
||||
|
||||
//// Returns each token as a partial response.
|
||||
//await foreach (var (token, tokenUsage) in answerStream)
|
||||
//{
|
||||
// if (token is not null) // token can be null when the stream ends.
|
||||
// {
|
||||
// fullAnswer.Append(token);
|
||||
await sessionStore.SaveSessionAsync(ragAgent, question.ConversationId.ToString(), session, cancellationToken);
|
||||
var response = updates.ToAgentResponse();
|
||||
|
||||
// if (token.Contains('【'))
|
||||
// {
|
||||
// // Citations start when we encounter a token containing a 【 character.
|
||||
// // We need to track it because we don't want to return the citations in the actual response.
|
||||
// citationsStarted = true;
|
||||
// }
|
||||
|
||||
// if (!citationsStarted)
|
||||
// {
|
||||
// yield return new(token, StreamState.Append);
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // Token usage is expected in the last message, when token is null.
|
||||
// tokenUsageResponse ??= tokenUsage is not null ? new(tokenUsage) : null;
|
||||
// }
|
||||
//}
|
||||
|
||||
//// Extract citations at the end of streaming.
|
||||
//var (_, citations) = ExtractCitations(fullAnswer.ToString());
|
||||
//yield return new(null, StreamState.End, tokenUsageResponse, citations);
|
||||
yield return new(question.ConversationId, null, null, response.Text, StreamState.End, new TokenUsageResponse(null, response.Usage));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,34 +12,34 @@
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
|
||||
<PackageReference Include="EntityFrameworkCore.Exceptions.SqlServer" Version="10.0.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting" Version="1.10.0-preview.260610.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.10.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.10.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows.Generators" Version="1.10.0">
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting" Version="1.15.0-preview.260722.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.15.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.15.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows.Generators" Version="1.15.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.7.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.8.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.8.0" />
|
||||
<PackageReference Include="Microsoft.ML.Tokenizers" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.ML.Tokenizers.Data.Cl100kBase" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.ML.Tokenizers.Data.O200kBase" Version="2.0.0" />
|
||||
<PackageReference Include="MimeMapping" Version="4.0.0" />
|
||||
<PackageReference Include="MinimalHelpers.FluentValidation" Version="1.1.8" />
|
||||
<PackageReference Include="MinimalHelpers.Routing.Analyzers" Version="1.2.2" />
|
||||
<PackageReference Include="PdfPig" Version="0.1.14" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.1" />
|
||||
<PackageReference Include="TinyHelpers.AspNetCore" Version="4.2.12" />
|
||||
<PackageReference Include="PdfPig" Version="0.1.15" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
|
||||
<PackageReference Include="TinyHelpers.AspNetCore" Version="4.2.17" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user