From dacead2e6487ca2d50e45e352a23a6850f71caa8 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 11 Sep 2024 14:15:32 +0300 Subject: [PATCH 1/3] Make chat memory available to the system message template Closes: #881 --- .../aiservice/AiServiceMethodImplementationSupport.java | 9 ++++++--- docs/modules/ROOT/pages/ai-services.adoc | 6 ++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/AiServiceMethodImplementationSupport.java b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/AiServiceMethodImplementationSupport.java index d6e427d11..5a1c3a816 100644 --- a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/AiServiceMethodImplementationSupport.java +++ b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/AiServiceMethodImplementationSupport.java @@ -124,14 +124,15 @@ public Object implement(Input input) { private static Object doImplement(AiServiceMethodCreateInfo methodCreateInfo, Object[] methodArgs, QuarkusAiServiceContext context, Audit audit) { - Optional systemMessage = prepareSystemMessage(methodCreateInfo, methodArgs); + Object memoryId = memoryId(methodCreateInfo, methodArgs, context.chatMemoryProvider != null); + Optional 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 @@ -416,7 +417,8 @@ public Moderation call() { return moderationFuture; } - private static Optional prepareSystemMessage(AiServiceMethodCreateInfo createInfo, Object[] methodArgs) { + private static Optional prepareSystemMessage(AiServiceMethodCreateInfo createInfo, Object[] methodArgs, + List previousChatMessages) { if (createInfo.getSystemMessageInfo().isEmpty()) { return Optional.empty(); } @@ -428,6 +430,7 @@ private static Optional 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()); } diff --git a/docs/modules/ROOT/pages/ai-services.adoc b/docs/modules/ROOT/pages/ai-services.adoc index 3999b09df..82d178747 100644 --- a/docs/modules/ROOT/pages/ai-services.adoc +++ b/docs/modules/ROOT/pages/ai-services.adoc @@ -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 From 428759d6c3d133c3260d50b6e3d0cd2afe28e35b Mon Sep 17 00:00:00 2001 From: Andrea Di Maio Date: Mon, 16 Sep 2024 10:16:17 +0200 Subject: [PATCH 2/3] Add TemplateExtension for the chat_memory placeholder --- .../DefaultCommittableChatMemory.java | 4 +- .../runtime/template/ChatMessageTemplate.java | 72 ++++ .../examples/aiservices/AiServicesTest.java | 7 +- .../deployment/ChatMemoryPlaceholderTest.java | 330 ++++++++++++++++++ 4 files changed, 408 insertions(+), 5 deletions(-) create mode 100644 core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/template/ChatMessageTemplate.java create mode 100644 model-providers/watsonx/deployment/src/test/java/com/ibm/langchain4j/watsonx/deployment/ChatMemoryPlaceholderTest.java diff --git a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/DefaultCommittableChatMemory.java b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/DefaultCommittableChatMemory.java index 7cc7d0057..24fef89d4 100644 --- a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/DefaultCommittableChatMemory.java +++ b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/DefaultCommittableChatMemory.java @@ -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 findSystemMessage(List messages) { diff --git a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/template/ChatMessageTemplate.java b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/template/ChatMessageTemplate.java new file mode 100644 index 000000000..a75fb3d3a --- /dev/null +++ b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/template/ChatMessageTemplate.java @@ -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 ChatMessageTemplate { + + /** + * 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 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 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 chatMessages) { + return extractDialogue(chatMessages, null, null, null); + } +} diff --git a/model-providers/openai/openai-vanilla/deployment/src/test/java/org/acme/examples/aiservices/AiServicesTest.java b/model-providers/openai/openai-vanilla/deployment/src/test/java/org/acme/examples/aiservices/AiServicesTest.java index 97b286c8c..6a4d0001e 100644 --- a/model-providers/openai/openai-vanilla/deployment/src/test/java/org/acme/examples/aiservices/AiServicesTest.java +++ b/model-providers/openai/openai-vanilla/deployment/src/test/java/org/acme/examples/aiservices/AiServicesTest.java @@ -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; @@ -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 { diff --git a/model-providers/watsonx/deployment/src/test/java/com/ibm/langchain4j/watsonx/deployment/ChatMemoryPlaceholderTest.java b/model-providers/watsonx/deployment/src/test/java/com/ibm/langchain4j/watsonx/deployment/ChatMemoryPlaceholderTest.java new file mode 100644 index 000000000..539e03349 --- /dev/null +++ b/model-providers/watsonx/deployment/src/test/java/com/ibm/langchain4j/watsonx/deployment/ChatMemoryPlaceholderTest.java @@ -0,0 +1,330 @@ +package com.ibm.langchain4j.watsonx.deployment; + +import java.util.Date; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.service.MemoryId; +import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.service.UserMessage; +import dev.langchain4j.store.memory.chat.ChatMemoryStore; +import io.quarkiverse.langchain4j.RegisterAiService; +import io.quarkiverse.langchain4j.watsonx.bean.Parameters; +import io.quarkiverse.langchain4j.watsonx.bean.TextGenerationRequest; +import io.quarkiverse.langchain4j.watsonx.runtime.config.ChatModelConfig; +import io.quarkiverse.langchain4j.watsonx.runtime.config.LangChain4jWatsonxConfig; +import io.quarkus.test.QuarkusUnitTest; + +public class ChatMemoryPlaceholderTest extends WireMockAbstract { + + @RegisterExtension + static QuarkusUnitTest unitTest = new QuarkusUnitTest() + .overrideRuntimeConfigKey("quarkus.langchain4j.watsonx.base-url", WireMockUtil.URL_WATSONX_SERVER) + .overrideRuntimeConfigKey("quarkus.langchain4j.watsonx.iam.base-url", WireMockUtil.URL_IAM_SERVER) + .overrideRuntimeConfigKey("quarkus.langchain4j.watsonx.api-key", WireMockUtil.API_KEY) + .overrideRuntimeConfigKey("quarkus.langchain4j.watsonx.project-id", WireMockUtil.PROJECT_ID) + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClass(WireMockUtil.class)); + + @Override + void handlerBeforeEach() { + mockServers.mockIAMBuilder(200) + .response("my_super_token", new Date()) + .build(); + } + + @ApplicationScoped + @RegisterAiService + public interface AiService { + @SystemMessage(""" + You are a helpful assistant + Context: + {chat_memory.extractDialogue}""") + public String answer(@MemoryId String id, @UserMessage String question); + } + + @ApplicationScoped + @RegisterAiService + public interface AiService2 { + @SystemMessage(""" + You are a helpful assistant + Context: + {chat_memory.extractDialogue(", ")}""") + public String answer(@MemoryId String id, @UserMessage String question); + } + + @ApplicationScoped + @RegisterAiService + public interface AiService3 { + @SystemMessage(""" + You are a helpful assistant + Context: + {chat_memory.extractDialogue("U: ", "A: ", ",")}""") + public String answer(@MemoryId String id, @UserMessage String question); + } + + @ApplicationScoped + @RegisterAiService(chatMemoryProviderSupplier = RegisterAiService.NoChatMemoryProviderSupplier.class) + public interface NoMemoryAiService { + @SystemMessage(""" + Context: + {chatMemory.extractDialogue}""") + public String rephrase(List chatMemory, @UserMessage String question); + } + + @Inject + AiService aiService; + + @Inject + AiService2 aiService2; + + @Inject + AiService3 aiService3; + + @Inject + NoMemoryAiService noMemoryAiService; + + @Inject + ChatMemoryStore chatMemoryStore; + + @Test + void extract_dialogue_test() throws Exception { + + LangChain4jWatsonxConfig.WatsonConfig watsonConfig = langchain4jWatsonConfig.defaultConfig(); + ChatModelConfig chatModelConfig = watsonConfig.chatModel(); + String modelId = langchain4jWatsonFixedRuntimeConfig.defaultConfig().chatModel().modelId(); + String chatMemoryId = "userId"; + String projectId = watsonConfig.projectId(); + Parameters parameters = Parameters.builder() + .decodingMethod(chatModelConfig.decodingMethod()) + .temperature(chatModelConfig.temperature()) + .minNewTokens(chatModelConfig.minNewTokens()) + .maxNewTokens(chatModelConfig.maxNewTokens()) + .build(); + + var input = """ + You are a helpful assistant + Context: + + Hello"""; + var body = new TextGenerationRequest(modelId, projectId, input, parameters); + mockServers.mockWatsonxBuilder(WireMockUtil.URL_WATSONX_CHAT_API, 200) + .body(mapper.writeValueAsString(body)) + .response(""" + { + "results": [ + { + "generated_text": "Hi!", + "generated_token_count": 5, + "input_token_count": 50, + "stop_reason": "eos_token" + } + ] + }""") + .build(); + + aiService.answer(chatMemoryId, "Hello"); + + input = """ + You are a helpful assistant + Context: + User: Hello + Assistant: Hi! + Hello + Hi! + What is your name?"""; + body = new TextGenerationRequest(modelId, projectId, input, parameters); + mockServers.mockWatsonxBuilder(WireMockUtil.URL_WATSONX_CHAT_API, 200) + .body(mapper.writeValueAsString(body)) + .response(""" + { + "results": [ + { + "generated_text": "My name is AiBot", + "generated_token_count": 5, + "input_token_count": 50, + "stop_reason": "eos_token" + } + ] + }""") + .build(); + + aiService.answer(chatMemoryId, "What is your name?"); + } + + @Test + void extract_dialogue_with_delimiter_test() throws Exception { + + LangChain4jWatsonxConfig.WatsonConfig watsonConfig = langchain4jWatsonConfig.defaultConfig(); + ChatModelConfig chatModelConfig = watsonConfig.chatModel(); + String modelId = langchain4jWatsonFixedRuntimeConfig.defaultConfig().chatModel().modelId(); + String chatMemoryId = "userId_with_delimiter"; + String projectId = watsonConfig.projectId(); + Parameters parameters = Parameters.builder() + .decodingMethod(chatModelConfig.decodingMethod()) + .temperature(chatModelConfig.temperature()) + .minNewTokens(chatModelConfig.minNewTokens()) + .maxNewTokens(chatModelConfig.maxNewTokens()) + .build(); + + var input = """ + You are a helpful assistant + Context: + + Hello"""; + var body = new TextGenerationRequest(modelId, projectId, input, parameters); + mockServers.mockWatsonxBuilder(WireMockUtil.URL_WATSONX_CHAT_API, 200) + .body(mapper.writeValueAsString(body)) + .response(""" + { + "results": [ + { + "generated_text": "Hi!", + "generated_token_count": 5, + "input_token_count": 50, + "stop_reason": "eos_token" + } + ] + }""") + .build(); + + aiService2.answer(chatMemoryId, "Hello"); + + input = """ + You are a helpful assistant + Context: + User: Hello, Assistant: Hi! + Hello + Hi! + What is your name?"""; + body = new TextGenerationRequest(modelId, projectId, input, parameters); + mockServers.mockWatsonxBuilder(WireMockUtil.URL_WATSONX_CHAT_API, 200) + .body(mapper.writeValueAsString(body)) + .response(""" + { + "results": [ + { + "generated_text": "My name is AiBot", + "generated_token_count": 5, + "input_token_count": 50, + "stop_reason": "eos_token" + } + ] + }""") + .build(); + + aiService2.answer(chatMemoryId, "What is your name?"); + } + + @Test + void extract_dialogue_with_all_params_test() throws Exception { + + LangChain4jWatsonxConfig.WatsonConfig watsonConfig = langchain4jWatsonConfig.defaultConfig(); + ChatModelConfig chatModelConfig = watsonConfig.chatModel(); + String modelId = langchain4jWatsonFixedRuntimeConfig.defaultConfig().chatModel().modelId(); + String chatMemoryId = "userId_with_all_params"; + String projectId = watsonConfig.projectId(); + Parameters parameters = Parameters.builder() + .decodingMethod(chatModelConfig.decodingMethod()) + .temperature(chatModelConfig.temperature()) + .minNewTokens(chatModelConfig.minNewTokens()) + .maxNewTokens(chatModelConfig.maxNewTokens()) + .build(); + + var input = """ + You are a helpful assistant + Context: + + Hello"""; + var body = new TextGenerationRequest(modelId, projectId, input, parameters); + mockServers.mockWatsonxBuilder(WireMockUtil.URL_WATSONX_CHAT_API, 200) + .body(mapper.writeValueAsString(body)) + .response(""" + { + "results": [ + { + "generated_text": "Hi!", + "generated_token_count": 5, + "input_token_count": 50, + "stop_reason": "eos_token" + } + ] + }""") + .build(); + + aiService3.answer(chatMemoryId, "Hello"); + + input = """ + You are a helpful assistant + Context: + U: Hello,A: Hi! + Hello + Hi! + What is your name?"""; + body = new TextGenerationRequest(modelId, projectId, input, parameters); + mockServers.mockWatsonxBuilder(WireMockUtil.URL_WATSONX_CHAT_API, 200) + .body(mapper.writeValueAsString(body)) + .response(""" + { + "results": [ + { + "generated_text": "My name is AiBot", + "generated_token_count": 5, + "input_token_count": 50, + "stop_reason": "eos_token" + } + ] + }""") + .build(); + + aiService3.answer(chatMemoryId, "What is your name?"); + } + + @Test + void extract_dialogue_no_memory_test() throws Exception { + + LangChain4jWatsonxConfig.WatsonConfig watsonConfig = langchain4jWatsonConfig.defaultConfig(); + ChatModelConfig chatModelConfig = watsonConfig.chatModel(); + String modelId = langchain4jWatsonFixedRuntimeConfig.defaultConfig().chatModel().modelId(); + String chatMemoryId = "userId_with_all_params"; + String projectId = watsonConfig.projectId(); + Parameters parameters = Parameters.builder() + .decodingMethod(chatModelConfig.decodingMethod()) + .temperature(chatModelConfig.temperature()) + .minNewTokens(chatModelConfig.minNewTokens()) + .maxNewTokens(chatModelConfig.maxNewTokens()) + .build(); + + var input = """ + Context: + User: Hello + Assistant: Hi! + User: What is your name? + Assistant: My name is AiBot + Hello"""; + var body = new TextGenerationRequest(modelId, projectId, input, parameters); + mockServers.mockWatsonxBuilder(WireMockUtil.URL_WATSONX_CHAT_API, 200) + .body(mapper.writeValueAsString(body)) + .response(""" + { + "results": [ + { + "generated_text": "Done!", + "generated_token_count": 5, + "input_token_count": 50, + "stop_reason": "eos_token" + } + ] + }""") + .build(); + + noMemoryAiService.rephrase(chatMemoryStore.getMessages(chatMemoryId), "Hello"); + } +} From 03406c13cd6b63d8ccdf308a2787162ca468f57e Mon Sep 17 00:00:00 2001 From: Andrea Di Maio Date: Tue, 17 Sep 2024 21:50:48 +0200 Subject: [PATCH 3/3] Add Templating Engine documentation --- ...java => ChatMessageTemplateExtension.java} | 2 +- docs/modules/ROOT/nav.adoc | 1 + .../modules/ROOT/pages/prompt-generation.adoc | 114 ++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) rename core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/template/{ChatMessageTemplate.java => ChatMessageTemplateExtension.java} (98%) create mode 100644 docs/modules/ROOT/pages/prompt-generation.adoc diff --git a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/template/ChatMessageTemplate.java b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/template/ChatMessageTemplateExtension.java similarity index 98% rename from core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/template/ChatMessageTemplate.java rename to core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/template/ChatMessageTemplateExtension.java index a75fb3d3a..44928331c 100644 --- a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/template/ChatMessageTemplate.java +++ b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/template/ChatMessageTemplateExtension.java @@ -8,7 +8,7 @@ import io.quarkus.qute.TemplateExtension; @TemplateExtension -public class ChatMessageTemplate { +public class ChatMessageTemplateExtension { /** * Extracts and formats a dialogue between the user and the assistant from a list of chat messages. The user and assistant diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index c81874033..1dbf3f2c5 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -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 diff --git a/docs/modules/ROOT/pages/prompt-generation.adoc b/docs/modules/ROOT/pages/prompt-generation.adoc new file mode 100644 index 000000000..18b81ab55 --- /dev/null +++ b/docs/modules/ROOT/pages/prompt-generation.adoc @@ -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 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: +Assistant: +... +---- + +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` objects that provides methods to automatically format the contents of the list. This means that whenever a `List` 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 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 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 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` 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`, you can use the https://quarkus.io/guides/qute-reference#template_extension_methods[TemplateExtension] methods available for `List` 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); +---- +