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:
Marco Minerva
2025-07-16 16:33:52 +02:00
parent 595d6f974f
commit 505ccff8b7
+99 -106
View File
@@ -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);
} }
} }