mirror of
https://github.com/marcominerva/SqlDatabaseVectorSearch.git
synced 2026-08-04 09:48:57 +00:00
Refactor: migrate to Microsoft.Agents.AI for RAG/chat
Replaced SemanticKernel with Microsoft.Agents.AI and agent-based abstractions for chat and RAG workflows. Removed ChatService in favor of AIAgent instances for question reformulation and answering. Added agent registration/configuration in Program.cs, new RagResponse record, and ContextProvider for context search. Introduced HybridCacheSessionStoreService for session persistence. Switched to direct OpenAIClient usage with request tracing. Reduced max relevant context chunks to 30. Removed SemanticKernel from project references and suppressed MEAI001 warning. Refactored code for improved style and DI alignment.
This commit is contained in:
@@ -8,3 +8,5 @@ public record class Response(string? OriginalQuestion, string? ReformulatedQuest
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record class RagResponse(Guid ConversationId, string OriginalQuestion, string ReformulatedQuestion, string Answer);
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
using System.ClientModel;
|
using System.ClientModel;
|
||||||
|
using System.ClientModel.Primitives;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
using Microsoft.Agents.AI;
|
||||||
using Microsoft.Agents.AI.Hosting;
|
using Microsoft.Agents.AI.Hosting;
|
||||||
using Microsoft.Agents.AI.Workflows;
|
using Microsoft.Agents.AI.Workflows;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using Microsoft.SemanticKernel;
|
|
||||||
using OpenAI;
|
using OpenAI;
|
||||||
using OpenAI.Responses;
|
using OpenAI.Responses;
|
||||||
using SqlDatabaseVectorSearch.Components;
|
using SqlDatabaseVectorSearch.Components;
|
||||||
@@ -44,14 +48,6 @@ builder.Services.AddSqlServer<ApplicationDbContext>(builder.Configuration.GetCon
|
|||||||
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
|
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddHybridCache(options =>
|
|
||||||
{
|
|
||||||
options.DefaultEntryOptions = new()
|
|
||||||
{
|
|
||||||
LocalCacheExpiration = appSettings.MessageExpiration
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.ConfigureHttpClientDefaults(configure =>
|
builder.Services.ConfigureHttpClientDefaults(configure =>
|
||||||
{
|
{
|
||||||
configure.AddStandardResilienceHandler(options =>
|
configure.AddStandardResilienceHandler(options =>
|
||||||
@@ -76,16 +72,12 @@ builder.Services.AddChatClient(_ =>
|
|||||||
var chatClient = new OpenAIClient(new ApiKeyCredential(aiSettings.ChatCompletion.ApiKey), new()
|
var chatClient = new OpenAIClient(new ApiKeyCredential(aiSettings.ChatCompletion.ApiKey), new()
|
||||||
{
|
{
|
||||||
Endpoint = new(aiSettings.ChatCompletion.Endpoint),
|
Endpoint = new(aiSettings.ChatCompletion.Endpoint),
|
||||||
|
Transport = new HttpClientPipelineTransport(new HttpClient(new TraceHttpClientHandler()))
|
||||||
}).GetResponsesClient().AsIChatClientWithStoredOutputDisabled(aiSettings.ChatCompletion.Deployment);
|
}).GetResponsesClient().AsIChatClientWithStoredOutputDisabled(aiSettings.ChatCompletion.Deployment);
|
||||||
|
|
||||||
return chatClient;
|
return chatClient;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Semantic Kernel is used to generate embeddings and to reformulate questions taking into account all the previous interactions,
|
|
||||||
// so that embeddings themselves can be generated more accurately.
|
|
||||||
builder.Services.AddKernel()
|
|
||||||
.AddAzureOpenAIChatCompletion(aiSettings.ChatCompletion.Deployment, aiSettings.ChatCompletion.Endpoint, aiSettings.ChatCompletion.ApiKey, modelId: aiSettings.ChatCompletion.ModelId);
|
|
||||||
|
|
||||||
builder.Services.AddKeyedSingleton<IContentDecoder, PdfContentDecoder>(MediaTypeNames.Application.Pdf);
|
builder.Services.AddKeyedSingleton<IContentDecoder, PdfContentDecoder>(MediaTypeNames.Application.Pdf);
|
||||||
builder.Services.AddKeyedSingleton<IContentDecoder, DocxContentDecoder>("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
builder.Services.AddKeyedSingleton<IContentDecoder, DocxContentDecoder>("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||||
builder.Services.AddKeyedSingleton<IContentDecoder, TextContentDecoder>(MediaTypeNames.Text.Plain);
|
builder.Services.AddKeyedSingleton<IContentDecoder, TextContentDecoder>(MediaTypeNames.Text.Plain);
|
||||||
@@ -95,10 +87,10 @@ builder.Services.AddKeyedSingleton<ITextChunker, DefaultTextChunker>(KeyedServic
|
|||||||
builder.Services.AddKeyedSingleton<ITextChunker, MarkdownTextChunker>(MediaTypeNames.Text.Markdown);
|
builder.Services.AddKeyedSingleton<ITextChunker, MarkdownTextChunker>(MediaTypeNames.Text.Markdown);
|
||||||
|
|
||||||
builder.Services.AddSingleton<TokenizerService>();
|
builder.Services.AddSingleton<TokenizerService>();
|
||||||
builder.Services.AddSingleton<ChatService>();
|
|
||||||
|
|
||||||
builder.Services.AddScoped<DocumentService>();
|
builder.Services.AddScoped<DocumentService>();
|
||||||
builder.Services.AddScoped<VectorSearchService>();
|
builder.Services.AddScoped<VectorSearchService>();
|
||||||
|
builder.Services.AddScoped<ContextProvider>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<FormFileToEmbeddingRequestExecutor>();
|
builder.Services.AddSingleton<FormFileToEmbeddingRequestExecutor>();
|
||||||
builder.Services.AddSingleton<GenerateEmbeddingExecutor>();
|
builder.Services.AddSingleton<GenerateEmbeddingExecutor>();
|
||||||
@@ -119,6 +111,148 @@ builder.AddWorkflow("EmbeddingWorkflow", (services, key) =>
|
|||||||
return workflow;
|
return workflow;
|
||||||
}, ServiceLifetime.Scoped);
|
}, ServiceLifetime.Scoped);
|
||||||
|
|
||||||
|
builder.Services.AddAIAgent("ReformulationAgent", (services, key) =>
|
||||||
|
{
|
||||||
|
var chatClient = services.GetRequiredService<IChatClient>();
|
||||||
|
|
||||||
|
return chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||||
|
{
|
||||||
|
Name = key,
|
||||||
|
ChatOptions = new()
|
||||||
|
{
|
||||||
|
Instructions = """
|
||||||
|
You are a helpful assistant that reformulates questions to perform embeddings search.
|
||||||
|
Your task is to reformulate the question taking into account the context of the chat.
|
||||||
|
The reformulated question must always explicitly contain the subject of the question.
|
||||||
|
|
||||||
|
You MUST reformulate the question in the SAME language as the user's question.
|
||||||
|
For example, if the user asks a question in English, the reformulated question MUST be in English. If the user asks in Italian, the reformulated question MUST be in Italian.
|
||||||
|
|
||||||
|
Never add "in this chat", "in the context of this chat", "in the context of our conversation", "search for" or something like that in your answer.
|
||||||
|
Your answer must contain only the reformulated question and nothing else.
|
||||||
|
Never add follow-up messages, clarifications, notes, disclaimers, or requests for more information such as "if you give me more information, I can be more precise".
|
||||||
|
""",
|
||||||
|
Reasoning = new()
|
||||||
|
{
|
||||||
|
Effort = ReasoningEffort.None,
|
||||||
|
Output = ReasoningOutput.None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ChatHistoryProvider = new InMemoryChatHistoryProvider(new()
|
||||||
|
{
|
||||||
|
StorageInputRequestMessageFilter = _ => [],
|
||||||
|
StorageInputResponseMessageFilter = _ => []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loggerFactory: services.GetRequiredService<ILoggerFactory>(),
|
||||||
|
services: services);
|
||||||
|
});
|
||||||
|
|
||||||
|
var textSearchOptions = new TextSearchProviderOptions()
|
||||||
|
{
|
||||||
|
ContextFormatter = results =>
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.AppendLine("## Additional Context");
|
||||||
|
sb.AppendLine("Use the excerpts below to answer the user.");
|
||||||
|
sb.AppendLine("Citation rules:");
|
||||||
|
sb.AppendLine("- Do NOT add inline citations.");
|
||||||
|
sb.AppendLine("- At the END of your answer, add a single line exactly like:");
|
||||||
|
sb.AppendLine(" Sources: [SourceName](SourceLink), [SourceName](SourceLink)");
|
||||||
|
sb.AppendLine("- Include ONLY sources you actually used. No duplicates.");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("### Sources (copy/paste-ready)");
|
||||||
|
foreach (var (i, r) in results.Index())
|
||||||
|
{
|
||||||
|
var name = string.IsNullOrWhiteSpace(r.SourceName) ? $"Source {i + 1}" : r.SourceName;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(r.SourceLink))
|
||||||
|
{
|
||||||
|
sb.AppendLine($"- [{name}]({r.SourceLink})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.AppendLine($"- {name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("### Excerpts");
|
||||||
|
foreach (var (i, r) in results.Index())
|
||||||
|
{
|
||||||
|
var name = string.IsNullOrWhiteSpace(r.SourceName) ? $"Source {i + 1}" : r.SourceName;
|
||||||
|
|
||||||
|
sb.AppendLine($"[{i + 1}] {name}");
|
||||||
|
sb.AppendLine(r.Text);
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
builder.Services.AddHybridCache(options =>
|
||||||
|
{
|
||||||
|
options.DefaultEntryOptions = new()
|
||||||
|
{
|
||||||
|
LocalCacheExpiration = appSettings.MessageExpiration
|
||||||
|
};
|
||||||
|
});
|
||||||
|
builder.Services.AddSingleton<HybridCacheSessionStoreService>();
|
||||||
|
|
||||||
|
builder.Services.AddAIAgent("RagAgent", (services, key) =>
|
||||||
|
{
|
||||||
|
var chatClient = services.GetRequiredService<IChatClient>();
|
||||||
|
|
||||||
|
return chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||||
|
{
|
||||||
|
Name = key,
|
||||||
|
ChatOptions = new()
|
||||||
|
{
|
||||||
|
Instructions = """
|
||||||
|
You are a helpful assistant. Answer questions using the provided context and cite the source document when available.
|
||||||
|
You can use only the information provided in this chat to answer questions. If you don't know the answer, reply suggesting to refine the question.
|
||||||
|
|
||||||
|
For example, if the user asks "What is the capital of Italy?" and in this chat there isn't information about Italy, you should reply something like:
|
||||||
|
- This information isn't available in the given context.
|
||||||
|
- I'm sorry, I don't know the answer to that question.
|
||||||
|
- I don't have that information.
|
||||||
|
- I don't know.
|
||||||
|
- Given the context, I can't answer that question.
|
||||||
|
- I'm sorry, I don't have enough information to answer that question.
|
||||||
|
|
||||||
|
Never answer questions that are not related to this chat.
|
||||||
|
""",
|
||||||
|
Reasoning = new()
|
||||||
|
{
|
||||||
|
Effort = ReasoningEffort.Low,
|
||||||
|
Output = ReasoningOutput.None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ChatHistoryProvider = new InMemoryChatHistoryProvider(new()
|
||||||
|
{
|
||||||
|
ChatReducer = new MessageCountingChatReducer(appSettings.MessageLimit),
|
||||||
|
ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded,
|
||||||
|
StorageInputRequestMessageFilter = messages =>
|
||||||
|
{
|
||||||
|
return messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory
|
||||||
|
&& m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.AIContextProvider);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
AIContextProviders = [new TextSearchProvider(services.GetRequiredService<ContextProvider>().SearchAsync, textSearchOptions)]
|
||||||
|
},
|
||||||
|
loggerFactory: services.GetRequiredService<ILoggerFactory>(),
|
||||||
|
services: services);
|
||||||
|
}, ServiceLifetime.Scoped)
|
||||||
|
.WithSessionStore((services, _) =>
|
||||||
|
{
|
||||||
|
var sessionStore = services.GetRequiredService<HybridCacheSessionStoreService>();
|
||||||
|
return sessionStore;
|
||||||
|
}, withIsolation: false);
|
||||||
|
|
||||||
builder.Services.AddOpenApi(options =>
|
builder.Services.AddOpenApi(options =>
|
||||||
{
|
{
|
||||||
options.RemoveServerList();
|
options.RemoveServerList();
|
||||||
@@ -154,6 +288,7 @@ app.UseWhen(context => context.IsApiRequest(), builder =>
|
|||||||
{
|
{
|
||||||
app.UseExceptionHandler(new ExceptionHandlerOptions
|
app.UseExceptionHandler(new ExceptionHandlerOptions
|
||||||
{
|
{
|
||||||
|
SuppressDiagnosticsCallback = _ => false,
|
||||||
StatusCodeSelector = exception => exception switch
|
StatusCodeSelector = exception => exception switch
|
||||||
{
|
{
|
||||||
NotSupportedException => StatusCodes.Status501NotImplemented,
|
NotSupportedException => StatusCodes.Status501NotImplemented,
|
||||||
@@ -190,3 +325,47 @@ static async Task ConfigureDatabaseAsync(IServiceProvider serviceProvider)
|
|||||||
|
|
||||||
await dbContext.Database.MigrateAsync();
|
await dbContext.Database.MigrateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class TraceHttpClientHandler : HttpClientHandler
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions jsonSerializerOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var requestString = request.Content is null ? "(no request body)" : await request.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
|
PrintText($"Raw Request ({request.RequestUri})", ConsoleColor.Green);
|
||||||
|
PrintText(FormatJson(requestString), ConsoleColor.DarkGray);
|
||||||
|
PrintSeparator();
|
||||||
|
|
||||||
|
var response = await base.SendAsync(request, cancellationToken);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
|
||||||
|
static void PrintText(string message, ConsoleColor color)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = color;
|
||||||
|
Console.WriteLine(message);
|
||||||
|
Console.ResetColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PrintSeparator() => Console.WriteLine(new string('-', 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatJson(string input)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonElement = JsonSerializer.Deserialize<JsonElement>(input);
|
||||||
|
return JsonSerializer.Serialize(jsonElement, jsonSerializerOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Text;
|
|
||||||
using Microsoft.Extensions.Caching.Hybrid;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Microsoft.SemanticKernel.ChatCompletion;
|
|
||||||
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
|
|
||||||
using OpenAI.Chat;
|
|
||||||
using SqlDatabaseVectorSearch.Models;
|
|
||||||
using SqlDatabaseVectorSearch.Settings;
|
|
||||||
using Entities = SqlDatabaseVectorSearch.Data.Entities;
|
|
||||||
|
|
||||||
namespace SqlDatabaseVectorSearch.Services;
|
|
||||||
|
|
||||||
public class ChatService(IChatCompletionService chatCompletionService, TokenizerService tokenizerService, HybridCache cache, IOptions<AppSettings> appSettingsOptions, ILogger<ChatService> logger)
|
|
||||||
{
|
|
||||||
private readonly AppSettings appSettings = appSettingsOptions.Value;
|
|
||||||
|
|
||||||
private static readonly string systemPromptForReformulation = """
|
|
||||||
You are a helpful assistant that reformulates questions to perform embeddings search.
|
|
||||||
Your task is to reformulate the question taking into account the context of the chat.
|
|
||||||
The reformulated question must always explicitly contain the subject of the question.
|
|
||||||
You MUST reformulate the question in the SAME language as the user's question. For example, if the user asks a question in English, the reformulated question MUST be in English. If the user asks in Italian, the reformulated question MUST be in Italian.
|
|
||||||
|
|
||||||
If asking a clarifying question to the user would help, ask the question.
|
|
||||||
Never add "in this chat", "in the context of this chat", "in the context of our conversation", "search for" or something like that in your answer.
|
|
||||||
""";
|
|
||||||
|
|
||||||
private static readonly string systemPromptForAnswering = """
|
|
||||||
You can use only the information provided in this chat to answer questions. If you don't know the answer, reply suggesting to refine the question.
|
|
||||||
|
|
||||||
For example, if the user asks "What is the capital of Italy?" and in this chat there isn't information about Italy, you should reply something like:
|
|
||||||
- This information isn't available in the given context.
|
|
||||||
- I'm sorry, I don't know the answer to that question.
|
|
||||||
- I don't have that information.
|
|
||||||
- I don't know.
|
|
||||||
- Given the context, I can't answer that question.
|
|
||||||
- I'm sorry, I don't have enough information to answer that question.
|
|
||||||
|
|
||||||
Never answer questions that are not related to this chat.
|
|
||||||
|
|
||||||
LANGUAGE RULE: You MUST ALWAYS answer in the SAME language as the user's question. For example, if the user asks a question in English, the answer MUST be in English. If the user asks in Italian, the answer MUST be in Italian. This rule applies NO MATTER what language the documents are written in. The language of your response must match the language of the question, NOT the language of the documents.
|
|
||||||
|
|
||||||
FORMATTING REQUIREMENT: Your answer MUST ALWAYS end with a period followed by a space before the citations block.
|
|
||||||
If your answer doesn't naturally end with a period, you MUST add one followed by a space.
|
|
||||||
|
|
||||||
After the answer, you need to include citations following the XML format below ONLY IF you know the answer and are providing information from the context. If you do NOT know the answer, DO NOT include the citations section at all.
|
|
||||||
|
|
||||||
【<citation document-id="document_id" chunk-id="chunk_id" filename="string" page-number="page_number" index-on-page="index_on_page">exact quote here</citation>
|
|
||||||
<citation document-id="document_id" chunk-id="chunk_id" filename="string" page-number="page_number" index-on-page="index_on_page">exact quote here</citation>】
|
|
||||||
|
|
||||||
The entire list of XML citations MUST be enclosed between 【 and 】 (U+3010 and U+3011) and must exactly match the above format.
|
|
||||||
The quote in each <citation> MUST be MAXIMUM 5 words, taken word-for-word from the search result.
|
|
||||||
|
|
||||||
IMPORTANT CITATION RULES:
|
|
||||||
1. NEVER put citations inside your answer text.
|
|
||||||
2. ALWAYS provide your complete answer FIRST.
|
|
||||||
3. ONLY AFTER completing your answer, add ALL citations in a block at the very end.
|
|
||||||
4. The citations block MUST be the last thing in your response, with absolutely nothing (no text, no spaces, no newlines, no punctuation, no comments) after it.
|
|
||||||
5. NEVER reference citations by number or mention them in your answer text.
|
|
||||||
6. The citations MUST ALWAYS follow the XML format exactly as shown below. Any other format is NOT ACCEPTED.
|
|
||||||
7. If you add anything after the citations block, your answer will be considered invalid.
|
|
||||||
8. If you do NOT know the answer, DO NOT include the citations block at all.
|
|
||||||
9. ALWAYS check that your answer ends with a period followed by a space before adding citations.
|
|
||||||
|
|
||||||
---
|
|
||||||
Example of a correct answer:
|
|
||||||
The capital of Italy is Rome.
|
|
||||||
【<citation document-id="123" chunk-id="456" filename="italy.pdf" page-number="1" index-on-page="1">capital of Italy is Rome</citation>】
|
|
||||||
|
|
||||||
Example of a correct answer when you do NOT know the answer:
|
|
||||||
I'm sorry, I don't know the answer to that question.
|
|
||||||
|
|
||||||
Example of an incorrect answer (NOT ACCEPTED):
|
|
||||||
The capital of Italy is Rome
|
|
||||||
【<citation document-id="123" chunk-id="456" filename="italy.pdf" page-number="1" index-on-page="1">capital of Italy is Rome</citation>】
|
|
||||||
Thank you for your question.
|
|
||||||
|
|
||||||
Another incorrect example (NOT ACCEPTED):
|
|
||||||
The capital of Italy is Rome.
|
|
||||||
【<citation document-id="123" chunk-id="456" filename="italy.pdf" page-number="1" index-on-page="1">capital of Italy is Rome</citation>】
|
|
||||||
[1] italy.pdf, page 1
|
|
||||||
---
|
|
||||||
|
|
||||||
Only the correct format is accepted. If you do not follow the XML format exactly, or if you add anything after the citations block, your answer will be considered invalid.
|
|
||||||
If you do NOT know the answer, DO NOT include the citations block at all.
|
|
||||||
Remember to ALWAYS end your answer with a period followed by a space before adding citations.
|
|
||||||
""";
|
|
||||||
|
|
||||||
public async Task<ChatResponse> CreateReformulateQuestionAsync(Guid conversationId, string question, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var chat = await GetChatHistoryAsync(conversationId, cancellationToken);
|
|
||||||
|
|
||||||
var settings = new AzureOpenAIPromptExecutionSettings
|
|
||||||
{
|
|
||||||
ChatSystemPrompt = systemPromptForReformulation
|
|
||||||
};
|
|
||||||
|
|
||||||
var embeddingQuestion = $"""
|
|
||||||
Reformulate the following question:
|
|
||||||
---
|
|
||||||
{question}
|
|
||||||
""";
|
|
||||||
|
|
||||||
chat.AddUserMessage(embeddingQuestion);
|
|
||||||
|
|
||||||
var reformulatedQuestion = await chatCompletionService.GetChatMessageContentAsync(chat, settings, cancellationToken: cancellationToken);
|
|
||||||
|
|
||||||
chat.AddAssistantMessage(reformulatedQuestion.Content!);
|
|
||||||
|
|
||||||
await UpdateCacheAsync(conversationId, chat, cancellationToken);
|
|
||||||
|
|
||||||
var tokenUsage = GetTokenUsage(reformulatedQuestion);
|
|
||||||
logger.LogDebug("Reformulation: {TokenUsage}", tokenUsage);
|
|
||||||
|
|
||||||
return new(reformulatedQuestion.Content!, tokenUsage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ChatResponse> AskQuestionAsync(Guid conversationId, IEnumerable<Entities.DocumentChunk> chunks, string question, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var (chat, settings) = CreateChatAsync(chunks, question);
|
|
||||||
|
|
||||||
var answer = await chatCompletionService.GetChatMessageContentAsync(chat, settings, cancellationToken: cancellationToken);
|
|
||||||
|
|
||||||
// Add question and answer to the chat history.
|
|
||||||
await SetChatHistoryAsync(conversationId, question, answer.Content!, cancellationToken);
|
|
||||||
|
|
||||||
var tokenUsage = GetTokenUsage(answer);
|
|
||||||
logger.LogDebug("Ask question: {TokenUsage}", tokenUsage);
|
|
||||||
|
|
||||||
return new(answer.Content!, tokenUsage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async IAsyncEnumerable<ChatResponse> AskStreamingAsync(Guid conversationId, IEnumerable<Entities.DocumentChunk> chunks, string question, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var (chat, settings) = CreateChatAsync(chunks, question);
|
|
||||||
|
|
||||||
var answer = new StringBuilder();
|
|
||||||
await foreach (var token in chatCompletionService.GetStreamingChatMessageContentsAsync(chat, settings, cancellationToken: cancellationToken))
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(token.Content))
|
|
||||||
{
|
|
||||||
yield return new(token.Content);
|
|
||||||
answer.Append(token.Content);
|
|
||||||
}
|
|
||||||
else if (token.Content is null)
|
|
||||||
{
|
|
||||||
// Token usage is returned in the last message, when the Content is null.
|
|
||||||
var tokenUsage = GetTokenUsage(token);
|
|
||||||
if (tokenUsage is not null)
|
|
||||||
{
|
|
||||||
logger.LogDebug("Ask streaming: {TokenUsage}", tokenUsage);
|
|
||||||
yield return new(null, tokenUsage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add question and answer to the chat history.
|
|
||||||
await SetChatHistoryAsync(conversationId, question, answer.ToString(), cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TokenUsage? GetTokenUsage(Microsoft.SemanticKernel.ChatMessageContent message) =>
|
|
||||||
message.InnerContent is ChatCompletion content && content.Usage is not null
|
|
||||||
? new(content.Usage.InputTokenCount, content.Usage.OutputTokenCount) : null;
|
|
||||||
|
|
||||||
private static TokenUsage? GetTokenUsage(Microsoft.SemanticKernel.StreamingChatMessageContent message) =>
|
|
||||||
message.InnerContent is StreamingChatCompletionUpdate content && content.Usage is not null
|
|
||||||
? new(content.Usage.InputTokenCount, content.Usage.OutputTokenCount) : null;
|
|
||||||
|
|
||||||
private (ChatHistory Chat, AzureOpenAIPromptExecutionSettings Settings) CreateChatAsync(IEnumerable<Entities.DocumentChunk> chunks, string question)
|
|
||||||
{
|
|
||||||
var settings = new AzureOpenAIPromptExecutionSettings
|
|
||||||
{
|
|
||||||
MaxTokens = appSettings.MaxOutputTokens,
|
|
||||||
ChatSystemPrompt = systemPromptForAnswering
|
|
||||||
};
|
|
||||||
|
|
||||||
var prompt = new StringBuilder($"""
|
|
||||||
Answer the following question:
|
|
||||||
---
|
|
||||||
{question}
|
|
||||||
=====
|
|
||||||
Using the following information:
|
|
||||||
|
|
||||||
""");
|
|
||||||
|
|
||||||
var availableTokens = appSettings.MaxInputTokens
|
|
||||||
- tokenizerService.CountChatCompletionTokens(systemPromptForAnswering) // System prompt.
|
|
||||||
- tokenizerService.CountChatCompletionTokens(prompt.ToString()) // Initial user prompt.
|
|
||||||
- appSettings.MaxOutputTokens; // To ensure there is enough space for the answer.
|
|
||||||
|
|
||||||
foreach (var chunk in chunks)
|
|
||||||
{
|
|
||||||
var text = $"--- {chunk.Document.Name} (Document ID: {chunk.Document.Id} | Chunk ID: {chunk.Id} | Page Number: {chunk.PageNumber} | Index on Page: {chunk.IndexOnPage}) {Environment.NewLine}{chunk.Content}{Environment.NewLine}";
|
|
||||||
|
|
||||||
var tokenCount = tokenizerService.CountChatCompletionTokens(text);
|
|
||||||
if (tokenCount > availableTokens)
|
|
||||||
{
|
|
||||||
// There isn't enough space to add the current chunk.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt.Append(text);
|
|
||||||
|
|
||||||
availableTokens -= tokenCount;
|
|
||||||
if (availableTokens <= 0)
|
|
||||||
{
|
|
||||||
// There isn't enough space to add more chunks.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var chat = new ChatHistory();
|
|
||||||
chat.AddUserMessage(prompt.ToString());
|
|
||||||
|
|
||||||
return (chat, settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateCacheAsync(Guid conversationId, ChatHistory chat, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (chat.Count > appSettings.MessageLimit)
|
|
||||||
{
|
|
||||||
chat.RemoveRange(0, chat.Count - appSettings.MessageLimit);
|
|
||||||
}
|
|
||||||
|
|
||||||
await cache.SetAsync(conversationId.ToString(), chat, cancellationToken: cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<ChatHistory> GetChatHistoryAsync(Guid conversationId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var chat = await cache.GetOrCreateAsync(conversationId.ToString(), (cancellationToken) =>
|
|
||||||
{
|
|
||||||
return ValueTask.FromResult<ChatHistory>([]);
|
|
||||||
}, cancellationToken: cancellationToken);
|
|
||||||
|
|
||||||
return chat;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SetChatHistoryAsync(Guid conversationId, string question, string answer, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var chat = await GetChatHistoryAsync(conversationId, cancellationToken);
|
|
||||||
|
|
||||||
chat.AddUserMessage(question);
|
|
||||||
chat.AddAssistantMessage(answer);
|
|
||||||
|
|
||||||
await UpdateCacheAsync(conversationId, chat, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.Agents.AI;
|
||||||
|
using Microsoft.Agents.AI.Hosting;
|
||||||
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
|
|
||||||
|
namespace SqlDatabaseVectorSearch.Services;
|
||||||
|
|
||||||
|
public class HybridCacheSessionStoreService(HybridCache cache) : AgentSessionStore
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
return await agent.DeserializeSessionAsync(sessionContent, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var sessionContent = await agent.SerializeSessionAsync(session, cancellationToken: cancellationToken);
|
||||||
|
await cache.SetAsync(GetCacheKey(conversationId), sessionContent, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCacheKey(string conversationId) => $"agent-session:{conversationId}";
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using Microsoft.Agents.AI;
|
||||||
using System.Text.RegularExpressions;
|
using Microsoft.Agents.AI.Hosting;
|
||||||
using Microsoft.Agents.AI.Workflows;
|
using Microsoft.Agents.AI.Workflows;
|
||||||
using Microsoft.Data.SqlTypes;
|
using Microsoft.Data.SqlTypes;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -11,12 +11,11 @@ using SqlDatabaseVectorSearch.Data;
|
|||||||
using SqlDatabaseVectorSearch.Models;
|
using SqlDatabaseVectorSearch.Models;
|
||||||
using SqlDatabaseVectorSearch.Settings;
|
using SqlDatabaseVectorSearch.Settings;
|
||||||
using SqlDatabaseVectorSearch.Workflows;
|
using SqlDatabaseVectorSearch.Workflows;
|
||||||
using ChatResponse = SqlDatabaseVectorSearch.Models.ChatResponse;
|
|
||||||
using Entities = SqlDatabaseVectorSearch.Data.Entities;
|
|
||||||
|
|
||||||
namespace SqlDatabaseVectorSearch.Services;
|
namespace SqlDatabaseVectorSearch.Services;
|
||||||
|
|
||||||
public partial class VectorSearchService([FromKeyedServices("EmbeddingWorkflow")] Workflow workflow, ApplicationDbContext dbContext, IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator, TokenizerService tokenizerService, ChatService chatService, TimeProvider timeProvider, IOptions<AppSettings> appSettingsOptions, ILogger<VectorSearchService> logger)
|
public partial class VectorSearchService([FromKeyedServices("EmbeddingWorkflow")] Workflow workflow, [FromKeyedServices("ReformulationAgent")] AIAgent reformulationAgent, [FromKeyedServices("RagAgent")] AIAgent ragAgent,
|
||||||
|
[FromKeyedServices("RagAgent")] AgentSessionStore sessionStore, IOptions<AppSettings> appSettingsOptions)
|
||||||
{
|
{
|
||||||
private readonly AppSettings appSettings = appSettingsOptions.Value;
|
private readonly AppSettings appSettings = appSettingsOptions.Value;
|
||||||
|
|
||||||
@@ -35,119 +34,95 @@ public partial class VectorSearchService([FromKeyedServices("EmbeddingWorkflow")
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Response> AskQuestionAsync(Question question, bool reformulate = true, CancellationToken cancellationToken = default)
|
public async Task<RagResponse> AskQuestionAsync(Question question, bool reformulate = true, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// It the user doesn't want to reforulate the question, CreateContextAsync returns the original one.
|
var reformulatedQuestion = question.Text;
|
||||||
var (reformulatedQuestion, embeddingTokenCount, chunks) = await CreateContextAsync(question, reformulate, cancellationToken);
|
var session = await sessionStore.GetSessionAsync(ragAgent, question.ConversationId.ToString(), cancellationToken);
|
||||||
|
|
||||||
var (fullAnswer, tokenUsage) = await chatService.AskQuestionAsync(question.ConversationId, chunks, reformulatedQuestion.Text!, 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;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract citations from the answer.
|
var response = await ragAgent.RunAsync(reformulatedQuestion, session, cancellationToken: cancellationToken);
|
||||||
var (answer, citations) = ExtractCitations(fullAnswer);
|
|
||||||
|
|
||||||
return new(question.Text, reformulatedQuestion.Text!, answer, StreamState.End, new(reformulatedQuestion.TokenUsage, embeddingTokenCount, tokenUsage), citations);
|
await sessionStore.SaveSessionAsync(ragAgent, question.ConversationId.ToString(), session, cancellationToken);
|
||||||
|
|
||||||
|
session.TryGetInMemoryChatHistory(out var chatHistory);
|
||||||
|
|
||||||
|
return new(question.ConversationId, question.Text, reformulatedQuestion, response.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<Response> AskStreamingAsync(Question question, bool reformulate = true, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
public async IAsyncEnumerable<Response> AskStreamingAsync(Question question, bool reformulate = true, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// It the user doesn't want to reforulate the question, CreateContextAsync returns the original one.
|
yield return null!;
|
||||||
var (reformulatedQuestion, embeddingTokenCount, chunks) = await CreateContextAsync(question, reformulate, cancellationToken);
|
|
||||||
|
|
||||||
var answerStream = chatService.AskStreamingAsync(question.ConversationId, chunks, reformulatedQuestion.Text!, cancellationToken: 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);
|
||||||
|
|
||||||
// The first message contains the question and the corresponding token usage (if reformulated).
|
//var answerStream = chatService.AskStreamingAsync(question.ConversationId, chunks, reformulatedQuestion.Text!, cancellationToken: cancellationToken);
|
||||||
yield return new(question.Text, reformulatedQuestion.Text!, null, StreamState.Start, new(reformulatedQuestion.TokenUsage, embeddingTokenCount, null));
|
|
||||||
|
|
||||||
TokenUsageResponse? tokenUsageResponse = null;
|
//// The first message contains the question and the corresponding token usage (if reformulated).
|
||||||
var fullAnswer = new StringBuilder();
|
//yield return new(question.Text, reformulatedQuestion.Text!, null, StreamState.Start, new(reformulatedQuestion.TokenUsage, embeddingTokenCount, null));
|
||||||
var citationsStarted = false;
|
|
||||||
|
|
||||||
// Returns each token as a partial response.
|
//TokenUsageResponse? tokenUsageResponse = null;
|
||||||
await foreach (var (token, tokenUsage) in answerStream)
|
//var fullAnswer = new StringBuilder();
|
||||||
{
|
//var citationsStarted = false;
|
||||||
if (token is not null) // token can be null when the stream ends.
|
|
||||||
{
|
|
||||||
fullAnswer.Append(token);
|
|
||||||
|
|
||||||
if (token.Contains('【'))
|
//// Returns each token as a partial response.
|
||||||
{
|
//await foreach (var (token, tokenUsage) in answerStream)
|
||||||
// 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.
|
// if (token is not null) // token can be null when the stream ends.
|
||||||
citationsStarted = true;
|
// {
|
||||||
}
|
// fullAnswer.Append(token);
|
||||||
|
|
||||||
if (!citationsStarted)
|
// if (token.Contains('【'))
|
||||||
{
|
// {
|
||||||
yield return new(token, StreamState.Append);
|
// // 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;
|
||||||
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.
|
// if (!citationsStarted)
|
||||||
var (_, citations) = ExtractCitations(fullAnswer.ToString());
|
// {
|
||||||
yield return new(null, StreamState.End, tokenUsageResponse, citations);
|
// 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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<(ChatResponse ReformulatedQuestion, int EmbeddingTokenCount, IEnumerable<Entities.DocumentChunk> Chunks)> CreateContextAsync(Question question, bool reformulate, CancellationToken cancellationToken)
|
public class ContextProvider(ApplicationDbContext dbContext, IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator, IOptions<AppSettings> appSettingsOptions)
|
||||||
|
{
|
||||||
|
private readonly AppSettings appSettings = appSettingsOptions.Value;
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchAsync(string query, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Reformulate the question taking into account the context of the chat to perform keyword search and embeddings.
|
|
||||||
var reformulatedQuestion = reformulate ? await chatService.CreateReformulateQuestionAsync(question.ConversationId, question.Text, cancellationToken) : new(question.Text);
|
|
||||||
|
|
||||||
var embeddingTokenCount = tokenizerService.CountEmbeddingTokens(reformulatedQuestion.Text!);
|
|
||||||
logger.LogDebug("Embedding Token Count: {EmbeddingTokenCount}", embeddingTokenCount);
|
|
||||||
|
|
||||||
// Perform Vector Search on SQL Database.
|
// Perform Vector Search on SQL Database.
|
||||||
var questionEmbedding = await embeddingGenerator.GenerateVectorAsync(reformulatedQuestion.Text!, cancellationToken: cancellationToken);
|
var questionEmbedding = await embeddingGenerator.GenerateVectorAsync(query, cancellationToken: cancellationToken);
|
||||||
var embeddingVector = new SqlVector<float>(questionEmbedding);
|
var embeddingVector = new SqlVector<float>(questionEmbedding);
|
||||||
|
|
||||||
var chunks = await dbContext.DocumentChunks.Include(c => c.Document)
|
var chunks = await dbContext.DocumentChunks.Include(c => c.Document)
|
||||||
.OrderBy(c => EF.Functions.VectorDistance("cosine", c.Embedding, embeddingVector))
|
.OrderBy(c => EF.Functions.VectorDistance("cosine", c.Embedding, embeddingVector))
|
||||||
.Take(appSettings.MaxRelevantChunks)
|
.Take(appSettings.MaxRelevantChunks).Select(c => new TextSearchProvider.TextSearchResult
|
||||||
|
{
|
||||||
|
SourceLink = c.Id.ToString().ToLowerInvariant(),
|
||||||
|
SourceName = c.Document.Name,
|
||||||
|
Text = c.Content,
|
||||||
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
return (reformulatedQuestion, embeddingTokenCount, chunks);
|
return chunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (string, IEnumerable<Citation>) ExtractCitations(string? text)
|
|
||||||
{
|
|
||||||
var citations = new List<Citation>();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(text))
|
|
||||||
{
|
|
||||||
return (text ?? string.Empty, citations);
|
|
||||||
}
|
|
||||||
|
|
||||||
var matches = CitationRegEx.Matches(text);
|
|
||||||
|
|
||||||
foreach (Match match in matches)
|
|
||||||
{
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
citations.Add(new Citation
|
|
||||||
{
|
|
||||||
DocumentId = Guid.Parse(match.Groups["documentId"].Value),
|
|
||||||
ChunkId = Guid.Parse(match.Groups["chunkId"].Value),
|
|
||||||
FileName = match.Groups["filename"].Value,
|
|
||||||
PageNumber = int.TryParse(match.Groups["pageNumber"].Value, out var pageNumber) && pageNumber > 0 ? pageNumber : null,
|
|
||||||
IndexOnPage = int.TryParse(match.Groups["indexOnPage"].Value, out var indexOnPage) ? indexOnPage : 0,
|
|
||||||
Quote = match.Groups["quote"].Value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all content between 【 and 】.
|
|
||||||
var cleanText = RemoveCitationsRegEx.Replace(text, string.Empty).TrimEnd();
|
|
||||||
return (cleanText, citations.OrderBy(c => c.FileName).ThenBy(c => c.PageNumber));
|
|
||||||
}
|
|
||||||
|
|
||||||
[GeneratedRegex(@"<citation\s+document-id=(?:""|'|)(?<documentId>[^""']*)(?:""|'|)\s+chunk-id=(?:""|'|)(?<chunkId>[^""']*)(?:""|'|)\s+filename=(?:""|'|)(?<filename>[^""']*)(?:""|'|)\s+page-number=(?:""|'|)(?<pageNumber>[^""']*)(?:""|'|)\s+index-on-page=(?:""|'|)(?<indexOnPage>[^""']*)(?:""|'|)>\s*(?<quote>.*?)\s*</citation>", RegexOptions.Singleline)]
|
|
||||||
private static partial Regex CitationRegEx { get; }
|
|
||||||
|
|
||||||
[GeneratedRegex(@"【.*?】", RegexOptions.Singleline)]
|
|
||||||
private static partial Regex RemoveCitationsRegEx { get; }
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<NoWarn>$(NoWarn);SKEXP0010;SKEXP0050;OPENAI001;MAAI001</NoWarn>
|
<NoWarn>$(NoWarn);SKEXP0010;SKEXP0050;OPENAI001;MAAI001;MEAI001</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -34,7 +34,6 @@
|
|||||||
<PackageReference Include="Microsoft.ML.Tokenizers" Version="2.0.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.Cl100kBase" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.ML.Tokenizers.Data.O200kBase" Version="2.0.0" />
|
<PackageReference Include="Microsoft.ML.Tokenizers.Data.O200kBase" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.SemanticKernel" Version="1.77.0" />
|
|
||||||
<PackageReference Include="MimeMapping" Version="4.0.0" />
|
<PackageReference Include="MimeMapping" Version="4.0.0" />
|
||||||
<PackageReference Include="MinimalHelpers.FluentValidation" Version="1.1.8" />
|
<PackageReference Include="MinimalHelpers.FluentValidation" Version="1.1.8" />
|
||||||
<PackageReference Include="MinimalHelpers.Routing.Analyzers" Version="1.2.2" />
|
<PackageReference Include="MinimalHelpers.Routing.Analyzers" Version="1.2.2" />
|
||||||
|
|||||||
@@ -2,13 +2,12 @@
|
|||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SqlDatabaseVectorSearch.ContentDecoders;
|
using SqlDatabaseVectorSearch.ContentDecoders;
|
||||||
using SqlDatabaseVectorSearch.Models;
|
|
||||||
using SqlDatabaseVectorSearch.Services;
|
using SqlDatabaseVectorSearch.Services;
|
||||||
using SqlDatabaseVectorSearch.Settings;
|
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<VectorSearchService> logger) : Executor(nameof(GenerateEmbeddingExecutor))
|
public partial class GenerateEmbeddingExecutor(IServiceProvider serviceProvider, 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;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using Entities = SqlDatabaseVectorSearch.Data.Entities;
|
|||||||
|
|
||||||
namespace SqlDatabaseVectorSearch.Workflows;
|
namespace SqlDatabaseVectorSearch.Workflows;
|
||||||
|
|
||||||
public partial class StoreEmbeddingExecutor(ApplicationDbContext dbContext, DocumentService documentService, TokenizerService tokenizerService, TimeProvider timeProvider, ILogger<VectorSearchService> logger) : Executor(nameof(StoreEmbeddingExecutor))
|
public partial class StoreEmbeddingExecutor(ApplicationDbContext dbContext, DocumentService documentService, TokenizerService tokenizerService, TimeProvider timeProvider, ILogger<StoreEmbeddingExecutor> logger) : Executor(nameof(StoreEmbeddingExecutor))
|
||||||
{
|
{
|
||||||
[MessageHandler]
|
[MessageHandler]
|
||||||
private async ValueTask<StoreEmbeddingResponse> HandleAsync(EmbeddingResponse embeddingData, IWorkflowContext context, CancellationToken cancellationToken)
|
private async ValueTask<StoreEmbeddingResponse> HandleAsync(EmbeddingResponse embeddingData, IWorkflowContext context, CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"MaxTokensPerLine": 300,
|
"MaxTokensPerLine": 300,
|
||||||
"MaxTokensPerParagraph": 1000,
|
"MaxTokensPerParagraph": 1000,
|
||||||
"OverlapTokens": 100,
|
"OverlapTokens": 100,
|
||||||
"MaxRelevantChunks": 50,
|
"MaxRelevantChunks": 30,
|
||||||
"MaxInputTokens": 32768,
|
"MaxInputTokens": 32768,
|
||||||
"MaxOutputTokens": 800,
|
"MaxOutputTokens": 800,
|
||||||
"MessageExpiration": "00:05:00",
|
"MessageExpiration": "00:05:00",
|
||||||
|
|||||||
Reference in New Issue
Block a user