Skip to content

Commit

Permalink
Merge pull request #887 from quarkiverse/#881
Browse files Browse the repository at this point in the history
Make chat memory available to the system message template
  • Loading branch information
geoand authored Sep 18, 2024
2 parents 4271b7c + 03406c1 commit e136212
Show file tree
Hide file tree
Showing 8 changed files with 535 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,15 @@ public Object implement(Input input) {

private static Object doImplement(AiServiceMethodCreateInfo methodCreateInfo, Object[] methodArgs,
QuarkusAiServiceContext context, Audit audit) {
Optional<SystemMessage> systemMessage = prepareSystemMessage(methodCreateInfo, methodArgs);
Object memoryId = memoryId(methodCreateInfo, methodArgs, context.chatMemoryProvider != null);
Optional<SystemMessage> systemMessage = prepareSystemMessage(methodCreateInfo, methodArgs,
context.hasChatMemory() ? context.chatMemory(memoryId).messages() : Collections.emptyList());
UserMessage userMessage = prepareUserMessage(context, methodCreateInfo, methodArgs);

if (audit != null) {
audit.initialMessages(systemMessage, userMessage);
}

Object memoryId = memoryId(methodCreateInfo, methodArgs, context.chatMemoryProvider != null);
boolean needsMemorySeed = needsMemorySeed(context, memoryId); // we need to know figure this out before we add the system and user message

boolean hasMethodSpecificTools = methodCreateInfo.getToolClassNames() != null
Expand Down Expand Up @@ -416,7 +417,8 @@ public Moderation call() {
return moderationFuture;
}

private static Optional<SystemMessage> prepareSystemMessage(AiServiceMethodCreateInfo createInfo, Object[] methodArgs) {
private static Optional<SystemMessage> prepareSystemMessage(AiServiceMethodCreateInfo createInfo, Object[] methodArgs,
List<ChatMessage> previousChatMessages) {
if (createInfo.getSystemMessageInfo().isEmpty()) {
return Optional.empty();
}
Expand All @@ -428,6 +430,7 @@ private static Optional<SystemMessage> prepareSystemMessage(AiServiceMethodCreat
}

templateParams.put(ResponseSchemaUtil.templateParam(), createInfo.getResponseSchemaInfo().outputFormatInstructions());
templateParams.put("chat_memory", previousChatMessages);
Prompt prompt = PromptTemplate.from(systemMessageInfo.text().get()).apply(templateParams);
return Optional.of(prompt.toSystemMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ public void add(ChatMessage message) {
newMessages.remove(systemMessage.get()); // need to replace existing system message
}
}
newMessages.add(0, message); // the system message must be in the first position
} else {
newMessages.add(message);
}
newMessages.add(message);
}

private static Optional<SystemMessage> findSystemMessage(List<ChatMessage> messages) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.quarkiverse.langchain4j.runtime.template;

import java.util.List;
import java.util.StringJoiner;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import io.quarkus.qute.TemplateExtension;

@TemplateExtension
public class ChatMessageTemplateExtension {

/**
* Extracts and formats a dialogue between the user and the assistant from a list of chat messages. The user and assistant
* messages are prefixed with the provided {@code userPrefix} and {@code assistantPrefix}, separated by the specified
* {@code delimiter}.
*
* @param chatMessages the list of chat messages to process.
* @param userPrefix the prefix for user messages.
* @param assistantPrefix the prefix for assistant messages.
* @param delimiter the delimiter between each message.
* @return A formatted string representing the conversation between the user and the assistant.
*/
static String extractDialogue(List<ChatMessage> chatMessages, String userPrefix, String assistantPrefix, String delimiter) {

if (chatMessages == null || chatMessages.isEmpty())
return "";

StringJoiner joiner = new StringJoiner(delimiter == null ? "\n" : delimiter);
userPrefix = (userPrefix == null) ? "User: " : userPrefix;
assistantPrefix = (assistantPrefix == null) ? "Assistant: " : assistantPrefix;

for (ChatMessage chatMessage : chatMessages) {
switch (chatMessage.type()) {
case AI -> {
AiMessage aiMessage = (AiMessage) chatMessage;
if (!aiMessage.hasToolExecutionRequests())
joiner.add("%s%s".formatted(assistantPrefix, aiMessage.text()));
}
case USER -> joiner.add("%s%s".formatted(userPrefix, chatMessage.text()));
case SYSTEM, TOOL_EXECUTION_RESULT -> {
continue;
}
}
}

return joiner.toString();
}

/**
* Extracts and formats a dialogue between the user and the assistant from a list of chat messages.
*
* @param chatMessages the list of chat messages to process.
* @param delimiter the delimiter between each message.
* @return A formatted string representing the conversation between the user and the assistant.
*
*/
static String extractDialogue(List<ChatMessage> chatMessages, String delimiter) {
return extractDialogue(chatMessages, null, null, delimiter);
}

/**
* Extracts and formats a dialogue between the user and the assistant from a list of chat messages.
*
* @param chatMessages the list of chat messages to process.
* @return A formatted string representing the conversation between the user and the assistant.
*
*/
static String extractDialogue(List<ChatMessage> chatMessages) {
return extractDialogue(chatMessages, null, null, null);
}
}
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
** xref:agent-and-tools.adoc[Agent and Tools]
** xref:retrievers.adoc[Embeddings and Document Retrievers]
** xref:prompt-engineering.adoc[Prompt Engineering 101]
** xref:prompt-generation.adoc[Prompt Generation]
** xref:guardrails.adoc[Guardrails]
* LLMs
Expand Down
6 changes: 6 additions & 0 deletions docs/modules/ROOT/pages/ai-services.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ AI methods can take parameters referenced in system and user messages using the
String writeAPoem(String topic, int lines);
----

[NOTE]
====
The value of `@SystemMessage` is also a template, which in addition to be able to reference the various parameters of the method,
also has access to the `chat_history` parameter which can be used to iterate over the chat history.
====

[#_ai_method_return_type]
=== AI Method Return Type

Expand Down
114 changes: 114 additions & 0 deletions docs/modules/ROOT/pages/prompt-generation.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
== Prompt Generation

When writing a prompt, it may be useful to access or modify some of the variables passed as input to the `AiService`.
https://quarkus.io/guides/qute[Qute] can be used to automatically handle these variables within the prompt.

For example, suppose you want to create a prompt that, given a conversation and a follow-up question, rephrases the follow-up question as a standalone question. https://quarkus.io/guides/qute[Qute] simplifies this by allowing you to define the prompt in the following format:

[source,java]
----
@SystemMessage("""
Given the following conversation and a follow-up question,
rephrase the follow-up question to be a standalone question.
Context:
{#for m in chatMessages}
{#if m.type.name() == "USER"}
User: {m.text()}
{/if}
{#if m.type.name() == "AI"}
Assistant: {m.text()}
{/if}
{/for}""")
public String rephrase(List<ChatMessage> chatMessages, @UserMessage String question);
----

In this example, the `chatMessages` list is automatically processed by https://quarkus.io/guides/qute[Qute] and transformed into the following format:

[source]
----
User: <text>
Assistant: <text>
...
----

This allows for the dynamic construction of prompts based on the provided input. For more information on how to use https://quarkus.io/guides/qute[Qute], see the official documentation.

== ChatMessage Formatting with TemplateExtensions

In the previous section we described how to use https://quarkus.io/guides/qute[Qute] to dynamically manage variables passed to an `AiService`. To simplify the prompt structure, a https://quarkus.io/guides/qute-reference#template_extension_methods[TemplateExtension] is provided for `List<ChatMessage>` objects that provides methods to automatically format the contents of the list. This means that whenever a `List<ChatMessage>` is passed as a parameter to an `AiService`, the extension methods can be used to format the list without having to manually write loops or conditionals.

The list of extension methods are:

- `extractDialogue(userPrefix, assistantPrefix, delimiter)`: +
Formats the conversation by applying custom prefixes for user and assistant messages, and custom delimiter to separate them. This method is the most flexible and allows full customisation of the output format.

- `extractDialogue(delimiter)`: +
Formats the conversation using the default prefixes (`User:` and `Assistant:`) but allows for the specification of a custom delimiter between messages.

- `extractDialogue()`: +
Provides the simplest formatting, using the default prefixes (`User:` and `Assistant:`) and separating messages with a newline. This is useful for basic formatting without the need for additional customization.

*Example 1: Using custom prefixes and delimiter*:

[source,java]
----
@SystemMessage("""
Given the following conversation and a follow-up question,
rephrase the follow-up question to be a standalone question.
Context:
{chatMessages.extractDialogue("U:", "A:", "|")}""")
public String rephrase(List<ChatMessage> chatMessages, @UserMessage String question);
----
This would format the conversation using `U:` and `A:` as prefixes, and `|` as the delimiter between messages.

*Example 2: Using a custom delimiter*:

[source,java]
----
@SystemMessage("""
Given the following conversation and a follow-up question,
rephrase the follow-up question to be a standalone question.
Context:
{chatMessages.extractDialogue("-")}""")
public String rephrase(List<ChatMessage> chatMessages, @UserMessage String question);
----
In this case, the conversation will be formatted with the default `User:` and `Assistant:` prefixes, but messages will be separated by `-`.

*Example 3: Using the default formatting*:

[source,java]
----
@SystemMessage("""
Given the following conversation and a follow-up question,
rephrase the follow-up question to be a standalone question.
Context:
{chatMessages.extractDialogue}""")
public String rephrase(List<ChatMessage> chatMessages, @UserMessage String question);
----
This will format the conversation using the default prefixes (`User:` and `Assistant:`) and a newline between each message, resulting in a simple structured output.

== Using the `chat_memory` placeholder

When working with `AiService` instances that have memory enabled, you have access to a special placeholder called `chat_memory`. This placeholder allows you to refer directly to the list of `ChatMessage` objects stored in the memory of the `AiService`, simplifying your prompt construction.

Instead of passing the `List<ChatMessage>` as a parameter, you can use the `chat_memory` placeholder in your `@SystemMessage` to automatically include the conversation history. +

Since `chat_memory` refers to a `List<ChatMessage>`, you can use the https://quarkus.io/guides/qute-reference#template_extension_methods[TemplateExtension] methods available for `List<ChatMessage>` to format the list directly in the prompt.

*Example*:

[source,java]
----
@SystemMessage("""
Given the following conversation and a follow-up question,
rephrase the follow-up question to be a standalone question.
Context:
{chat_memory.extractDialogue}""")
public String rephrase(@UserMessage String question);
----

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import static dev.langchain4j.data.message.ChatMessageType.AI;
import static dev.langchain4j.data.message.ChatMessageType.SYSTEM;
import static dev.langchain4j.data.message.ChatMessageType.USER;
import static dev.langchain4j.data.message.UserMessage.userMessage;
import static java.time.Month.JULY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
Expand Down Expand Up @@ -768,16 +767,16 @@ void should_keep_chat_memory_and_add_new_system_message() throws IOException {
// assert request
assertMultipleRequestMessage(getRequestAsMap(),
List.of(
new MessageContent("system", secondSystemMessage),
new MessageContent("user", firstUserMessage),
new MessageContent("assistant", firstAiMessage),
new MessageContent("system", secondSystemMessage),
new MessageContent("user", secondUserMessage)));

// assert chat memory
assertThat(chatMemory.messages()).hasSize(5)
.extracting(ChatMessage::type, ChatMessage::text)
.containsExactly(tuple(USER, firstUserMessage), tuple(AI, firstAiMessage),
tuple(SYSTEM, secondSystemMessage), tuple(USER, secondUserMessage), tuple(AI, secondAiMessage));
.containsExactly(tuple(SYSTEM, secondSystemMessage), tuple(USER, firstUserMessage), tuple(AI, firstAiMessage),
tuple(USER, secondUserMessage), tuple(AI, secondAiMessage));
}

interface ChatWithSeparateMemoryForEachUser {
Expand Down
Loading

0 comments on commit e136212

Please sign in to comment.