diff --git a/.editorconfig b/.editorconfig
index 8f8512a..430f71b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -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
diff --git a/SqlDatabaseVectorSearch/Components/Pages/Ask.razor b/SqlDatabaseVectorSearch/Components/Pages/Ask.razor
index b18fa18..9cf8cb6 100644
--- a/SqlDatabaseVectorSearch/Components/Pages/Ask.razor
+++ b/SqlDatabaseVectorSearch/Components/Pages/Ask.razor
@@ -83,23 +83,6 @@
- @if (message.Citations is not null && message.Citations.Count() > 0)
- {
-
- @foreach (var citation in message.Citations)
- {
-
-
- @citation.FileName @if (citation.PageNumber.GetValueOrDefault() > 0)
- {
- pag. @citation.PageNumber
- }
-
-
@citation.Quote
-
- }
-
- }
}
}
@@ -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 @@
? $"Reformulation:
{FormatTokenUsageDetails(tokenUsageResponse.Reformulation)}
"
: string.Empty;
- var embeddingTokenCount = tokenUsageResponse.EmbeddingTokenCount.HasValue
- ? $"Embedding Token Count: {tokenUsageResponse.EmbeddingTokenCount}
"
- : string.Empty;
-
var question = tokenUsageResponse.Question is not null
? $"Question:
{FormatTokenUsageDetails(tokenUsageResponse.Question)}
"
: 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}
" +
- $"Completion tokens: {tokenUsage.CompletionTokens}
" +
- $"Total tokens: {tokenUsage.TotalTokens}";
+ return $"Input tokens: {tokenUsage.InputTokenCount}
" +
+ $"Output tokens: {tokenUsage.OutputTokenCount}
" +
+ $"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? 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; }
}
}
\ No newline at end of file
diff --git a/SqlDatabaseVectorSearch/Models/ChatResponse.cs b/SqlDatabaseVectorSearch/Models/ChatResponse.cs
deleted file mode 100644
index 1f67ca9..0000000
--- a/SqlDatabaseVectorSearch/Models/ChatResponse.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace SqlDatabaseVectorSearch.Models;
-
-public record class ChatResponse(string? Text, TokenUsage? TokenUsage = null);
\ No newline at end of file
diff --git a/SqlDatabaseVectorSearch/Models/Citation.cs b/SqlDatabaseVectorSearch/Models/Citation.cs
deleted file mode 100644
index 04fb64b..0000000
--- a/SqlDatabaseVectorSearch/Models/Citation.cs
+++ /dev/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; }
-}
\ No newline at end of file
diff --git a/SqlDatabaseVectorSearch/Models/Response.cs b/SqlDatabaseVectorSearch/Models/Response.cs
index 20e6e55..3aed02f 100644
--- a/SqlDatabaseVectorSearch/Models/Response.cs
+++ b/SqlDatabaseVectorSearch/Models/Response.cs
@@ -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? 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? 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);
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/SqlDatabaseVectorSearch/Models/TokenUsage.cs b/SqlDatabaseVectorSearch/Models/TokenUsage.cs
deleted file mode 100644
index 9a39649..0000000
--- a/SqlDatabaseVectorSearch/Models/TokenUsage.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace SqlDatabaseVectorSearch.Models;
-
-public record class TokenUsage(int PromptTokens, int CompletionTokens)
-{
- public int TotalTokens => PromptTokens + CompletionTokens;
-}
diff --git a/SqlDatabaseVectorSearch/Models/TokenUsageResponse.cs b/SqlDatabaseVectorSearch/Models/TokenUsageResponse.cs
index 500a72e..1dae2e0 100644
--- a/SqlDatabaseVectorSearch/Models/TokenUsageResponse.cs
+++ b/SqlDatabaseVectorSearch/Models/TokenUsageResponse.cs
@@ -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);
diff --git a/SqlDatabaseVectorSearch/Services/HybridCacheSessionStoreService.cs b/SqlDatabaseVectorSearch/Services/HybridCacheSessionStoreService.cs
index 2ae9b7b..e66a48f 100644
--- a/SqlDatabaseVectorSearch/Services/HybridCacheSessionStoreService.cs
+++ b/SqlDatabaseVectorSearch/Services/HybridCacheSessionStoreService.cs
@@ -8,23 +8,30 @@ public class HybridCacheSessionStoreService(HybridCache cache) : AgentSessionSto
{
public override async ValueTask 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}";
+}
\ No newline at end of file
diff --git a/SqlDatabaseVectorSearch/Services/VectorSearchService.cs b/SqlDatabaseVectorSearch/Services/VectorSearchService.cs
index d3b2c92..1668fe7 100644
--- a/SqlDatabaseVectorSearch/Services/VectorSearchService.cs
+++ b/SqlDatabaseVectorSearch/Services/VectorSearchService.cs
@@ -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 appSettingsOptions)
+ [FromKeyedServices("RagAgent")] AgentSessionStore sessionStore)
{
- private readonly AppSettings appSettings = appSettingsOptions.Value;
-
public async Task 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 AskQuestionAsync(Question question, bool reformulate = true, CancellationToken cancellationToken = default)
+ public async Task 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 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();
- //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));
}
}
diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj
index b0062e0..fd24438 100644
--- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj
+++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj
@@ -12,34 +12,34 @@
-
-
-
-
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
-
-
-
+
+
+