mirror of
https://github.com/marcominerva/SqlDatabaseVectorSearch.git
synced 2026-06-20 12:23:10 +00:00
Enhance ChatService with new prompts and simplifications
Updated ChatService to include static readonly fields for reformulation and answering prompts. Replaced the existing ChatSystemPrompt in CreateQuestionAsync. Simplified GetTokenUsage using expression-bodied members. Modified SetChatHistoryAsync to ensure correct chat history updates in the cache.
This commit is contained in:
@@ -15,111 +15,15 @@ public class ChatService(IChatCompletionService chatCompletionService, Tokenizer
|
||||
{
|
||||
private readonly AppSettings appSettings = appSettingsOptions.Value;
|
||||
|
||||
public async Task<ChatResponse> CreateQuestionAsync(Guid conversationId, string question, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var chat = await GetChatHistoryAsync(conversationId, cancellationToken);
|
||||
|
||||
var settings = new AzureOpenAIPromptExecutionSettings
|
||||
{
|
||||
ChatSystemPrompt = """
|
||||
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 of the user's question. For example, if the user asks a question in English, the answer must be in English.
|
||||
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.
|
||||
"""
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static TokenUsage? GetTokenUsage(Microsoft.SemanticKernel.ChatMessageContent message)
|
||||
{
|
||||
if (message.InnerContent is ChatCompletion content && content.Usage is not null)
|
||||
{
|
||||
return new(content.Usage.InputTokenCount, content.Usage.OutputTokenCount);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static TokenUsage? GetTokenUsage(Microsoft.SemanticKernel.StreamingChatMessageContent message)
|
||||
{
|
||||
if (message.InnerContent is StreamingChatCompletionUpdate content && content.Usage is not null)
|
||||
{
|
||||
return new(content.Usage.InputTokenCount, content.Usage.OutputTokenCount);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private (ChatHistory Chat, AzureOpenAIPromptExecutionSettings Settings) CreateChatAsync(IEnumerable<Entities.DocumentChunk> chunks, string question)
|
||||
{
|
||||
var settings = new AzureOpenAIPromptExecutionSettings
|
||||
{
|
||||
MaxTokens = appSettings.MaxOutputTokens
|
||||
};
|
||||
|
||||
var chat = new ChatHistory("""
|
||||
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:
|
||||
@@ -177,7 +81,96 @@ public class ChatService(IChatCompletionService chatCompletionService, Tokenizer
|
||||
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> CreateQuestionAsync(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
|
||||
};
|
||||
|
||||
var chat = new ChatHistory(systemPromptForAnswering);
|
||||
|
||||
var prompt = new StringBuilder($"""
|
||||
Answer the following question:
|
||||
@@ -189,7 +182,7 @@ public class ChatService(IChatCompletionService chatCompletionService, Tokenizer
|
||||
""");
|
||||
|
||||
var availableTokens = appSettings.MaxInputTokens
|
||||
- tokenizerService.CountChatCompletionTokens(chat[0].ToString()) // System prompt.
|
||||
- tokenizerService.CountChatCompletionTokens(systemPromptForAnswering) // System prompt.
|
||||
- tokenizerService.CountChatCompletionTokens(prompt.ToString()) // Initial user prompt.
|
||||
- appSettings.MaxOutputTokens; // To ensure there is enough space for the answer.
|
||||
|
||||
@@ -215,6 +208,7 @@ public class ChatService(IChatCompletionService chatCompletionService, Tokenizer
|
||||
}
|
||||
|
||||
chat.AddUserMessage(prompt.ToString());
|
||||
|
||||
return (chat, settings);
|
||||
}
|
||||
|
||||
@@ -230,22 +224,21 @@ public class ChatService(IChatCompletionService chatCompletionService, Tokenizer
|
||||
|
||||
private async Task<ChatHistory> GetChatHistoryAsync(Guid conversationId, CancellationToken cancellationToken)
|
||||
{
|
||||
var historyCache = await cache.GetOrCreateAsync(conversationId.ToString(), (cancellationToken) =>
|
||||
var chat = await cache.GetOrCreateAsync(conversationId.ToString(), (cancellationToken) =>
|
||||
{
|
||||
return ValueTask.FromResult<ChatHistory>([]);
|
||||
}, cancellationToken: cancellationToken);
|
||||
|
||||
var chat = new ChatHistory(historyCache);
|
||||
return chat;
|
||||
}
|
||||
|
||||
private async Task SetChatHistoryAsync(Guid conversationId, string question, string answer, CancellationToken cancellationToken)
|
||||
{
|
||||
var history = await GetChatHistoryAsync(conversationId, cancellationToken);
|
||||
var chat = await GetChatHistoryAsync(conversationId, cancellationToken);
|
||||
|
||||
history.AddUserMessage(question);
|
||||
history.AddAssistantMessage(answer);
|
||||
chat.AddUserMessage(question);
|
||||
chat.AddAssistantMessage(answer);
|
||||
|
||||
await UpdateCacheAsync(conversationId, history, cancellationToken);
|
||||
await UpdateCacheAsync(conversationId, chat, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user