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;
|
private readonly AppSettings appSettings = appSettingsOptions.Value;
|
||||||
|
|
||||||
public async Task<ChatResponse> CreateQuestionAsync(Guid conversationId, string question, CancellationToken cancellationToken = default)
|
private static readonly string systemPromptForReformulation = """
|
||||||
{
|
|
||||||
var chat = await GetChatHistoryAsync(conversationId, cancellationToken);
|
|
||||||
|
|
||||||
var settings = new AzureOpenAIPromptExecutionSettings
|
|
||||||
{
|
|
||||||
ChatSystemPrompt = """
|
|
||||||
You are a helpful assistant that reformulates questions to perform embeddings search.
|
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.
|
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.
|
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.
|
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.
|
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);
|
private static readonly string systemPromptForAnswering = """
|
||||||
|
|
||||||
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("""
|
|
||||||
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.
|
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:
|
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.
|
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.
|
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.
|
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($"""
|
var prompt = new StringBuilder($"""
|
||||||
Answer the following question:
|
Answer the following question:
|
||||||
@@ -189,7 +182,7 @@ public class ChatService(IChatCompletionService chatCompletionService, Tokenizer
|
|||||||
""");
|
""");
|
||||||
|
|
||||||
var availableTokens = appSettings.MaxInputTokens
|
var availableTokens = appSettings.MaxInputTokens
|
||||||
- tokenizerService.CountChatCompletionTokens(chat[0].ToString()) // System prompt.
|
- tokenizerService.CountChatCompletionTokens(systemPromptForAnswering) // System prompt.
|
||||||
- tokenizerService.CountChatCompletionTokens(prompt.ToString()) // Initial user prompt.
|
- tokenizerService.CountChatCompletionTokens(prompt.ToString()) // Initial user prompt.
|
||||||
- appSettings.MaxOutputTokens; // To ensure there is enough space for the answer.
|
- 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());
|
chat.AddUserMessage(prompt.ToString());
|
||||||
|
|
||||||
return (chat, settings);
|
return (chat, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,22 +224,21 @@ public class ChatService(IChatCompletionService chatCompletionService, Tokenizer
|
|||||||
|
|
||||||
private async Task<ChatHistory> GetChatHistoryAsync(Guid conversationId, CancellationToken cancellationToken)
|
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>([]);
|
return ValueTask.FromResult<ChatHistory>([]);
|
||||||
}, cancellationToken: cancellationToken);
|
}, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
var chat = new ChatHistory(historyCache);
|
|
||||||
return chat;
|
return chat;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SetChatHistoryAsync(Guid conversationId, string question, string answer, CancellationToken cancellationToken)
|
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);
|
chat.AddUserMessage(question);
|
||||||
history.AddAssistantMessage(answer);
|
chat.AddAssistantMessage(answer);
|
||||||
|
|
||||||
await UpdateCacheAsync(conversationId, history, cancellationToken);
|
await UpdateCacheAsync(conversationId, chat, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user