From 481fc8efe8e7574b28a1eca8b2f27a4e93ecc96c Mon Sep 17 00:00:00 2001 From: Thomas Vitale Date: Thu, 8 May 2025 00:38:32 +0100 Subject: [PATCH] Self-contained prompt templates in advisors The built-in advisors that perform prompt augmentation have been updated to use self-contained templates. The goal is for each advisor to be able to perform templating operations without affecting nor being affected by templating and prompt decisions in other advisors. * QuestionAnswerAdvisor * PromptChatMemoryAdvisor * VectorStoreChatMemoryAdvisor Documentation and upgrade notes have been updated accordingly Signed-off-by: Thomas Vitale --- .../vectorstore/QuestionAnswerAdvisor.java | 11 +++-- .../VectorStoreChatMemoryAdvisor.java | 43 ++++++++---------- .../advisor/PromptChatMemoryAdvisor.java | 44 +++++++++++-------- .../modules/ROOT/pages/api/chat-memory.adoc | 26 +++++++++++ .../api/retrieval-augmented-generation.adoc | 7 ++- .../modules/ROOT/pages/upgrade-notes.adoc | 15 +++++++ .../advisor/QuestionAnswerAdvisorIT.java | 1 + 7 files changed, 97 insertions(+), 50 deletions(-) diff --git a/advisors/spring-ai-advisors-vector-store/src/main/java/org/springframework/ai/chat/client/advisor/vectorstore/QuestionAnswerAdvisor.java b/advisors/spring-ai-advisors-vector-store/src/main/java/org/springframework/ai/chat/client/advisor/vectorstore/QuestionAnswerAdvisor.java index 3d651fe3271..defb0574f24 100644 --- a/advisors/spring-ai-advisors-vector-store/src/main/java/org/springframework/ai/chat/client/advisor/vectorstore/QuestionAnswerAdvisor.java +++ b/advisors/spring-ai-advisors-vector-store/src/main/java/org/springframework/ai/chat/client/advisor/vectorstore/QuestionAnswerAdvisor.java @@ -25,6 +25,7 @@ import org.springframework.ai.chat.client.ChatClientResponse; import org.springframework.ai.chat.client.advisor.api.AdvisorChain; import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; +import org.springframework.ai.chat.messages.UserMessage; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; @@ -56,6 +57,7 @@ public class QuestionAnswerAdvisor implements BaseAdvisor { public static final String FILTER_EXPRESSION = "qa_filter_expression"; private static final PromptTemplate DEFAULT_PROMPT_TEMPLATE = new PromptTemplate(""" + {query} Context information is below, surrounded by --------------------- @@ -124,12 +126,9 @@ public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChai : documents.stream().map(Document::getText).collect(Collectors.joining(System.lineSeparator())); // 3. Augment the user prompt with the document context. - String augmentedUserText = this.promptTemplate.mutate() - .template(chatClientRequest.prompt().getUserMessage().getText() + System.lineSeparator() - + this.promptTemplate.getTemplate()) - .variables(Map.of("question_answer_context", documentContext)) - .build() - .render(); + UserMessage userMessage = chatClientRequest.prompt().getUserMessage(); + String augmentedUserText = this.promptTemplate + .render(Map.of("query", userMessage.getText(), "question_answer_context", documentContext)); // 4. Update ChatClientRequest with augmented prompt. return chatClientRequest.mutate() diff --git a/advisors/spring-ai-advisors-vector-store/src/main/java/org/springframework/ai/chat/client/advisor/vectorstore/VectorStoreChatMemoryAdvisor.java b/advisors/spring-ai-advisors-vector-store/src/main/java/org/springframework/ai/chat/client/advisor/vectorstore/VectorStoreChatMemoryAdvisor.java index 0866c5bd7bd..6c2f8a733b1 100644 --- a/advisors/spring-ai-advisors-vector-store/src/main/java/org/springframework/ai/chat/client/advisor/vectorstore/VectorStoreChatMemoryAdvisor.java +++ b/advisors/spring-ai-advisors-vector-store/src/main/java/org/springframework/ai/chat/client/advisor/vectorstore/VectorStoreChatMemoryAdvisor.java @@ -56,7 +56,8 @@ public class VectorStoreChatMemoryAdvisor extends AbstractChatMemoryAdvisor { - private String systemTextAdvise = DEFAULT_SYSTEM_TEXT_ADVISE; + private PromptTemplate systemPromptTemplate = DEFAULT_SYSTEM_PROMPT_TEMPLATE; protected Builder(VectorStore chatMemory) { super(chatMemory); } public Builder systemTextAdvise(String systemTextAdvise) { - this.systemTextAdvise = systemTextAdvise; + this.systemPromptTemplate = new PromptTemplate(systemTextAdvise); + return this; + } + + public Builder systemPromptTemplate(PromptTemplate systemPromptTemplate) { + this.systemPromptTemplate = systemPromptTemplate; return this; } @Override public VectorStoreChatMemoryAdvisor build() { return new VectorStoreChatMemoryAdvisor(this.chatMemory, this.conversationId, this.chatMemoryRetrieveSize, - this.systemTextAdvise, this.order); + this.systemPromptTemplate, this.order); } } diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/PromptChatMemoryAdvisor.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/PromptChatMemoryAdvisor.java index aa2927523c3..21fec14dd4c 100644 --- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/PromptChatMemoryAdvisor.java +++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/PromptChatMemoryAdvisor.java @@ -46,7 +46,8 @@ */ public class PromptChatMemoryAdvisor extends AbstractChatMemoryAdvisor { - private static final String DEFAULT_SYSTEM_TEXT_ADVISE = """ + private static final PromptTemplate DEFAULT_SYSTEM_PROMPT_TEMPLATE = new PromptTemplate(""" + {instructions} Use the conversation memory from the MEMORY section to provide accurate answers. @@ -55,29 +56,34 @@ public class PromptChatMemoryAdvisor extends AbstractChatMemoryAdvisor { - private String systemTextAdvise = DEFAULT_SYSTEM_TEXT_ADVISE; + private PromptTemplate systemPromptTemplate = DEFAULT_SYSTEM_PROMPT_TEMPLATE; protected Builder(ChatMemory chatMemory) { super(chatMemory); } public Builder systemTextAdvise(String systemTextAdvise) { - this.systemTextAdvise = systemTextAdvise; + this.systemPromptTemplate = new PromptTemplate(systemTextAdvise); + return this; + } + + public Builder systemPromptTemplate(PromptTemplate systemPromptTemplate) { + this.systemPromptTemplate = systemPromptTemplate; return this; } public PromptChatMemoryAdvisor build() { return new PromptChatMemoryAdvisor(this.chatMemory, this.conversationId, this.chatMemoryRetrieveSize, - this.systemTextAdvise, this.order); + this.systemPromptTemplate, this.order); } } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc index 8b1427f3f97..3b8253c9f3c 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc @@ -244,6 +244,32 @@ chatClient.prompt() .content(); ---- +=== PromptChatMemoryAdvisor + +==== Custom Template + +The `PromptChatMemoryAdvisor` uses a default template to augment the system message with the retrieved conversation memory. You can customize this behavior by providing your own `PromptTemplate` object via the `.promptTemplate()` builder method. + +NOTE: The `PromptTemplate` provided here customizes how the advisor merges retrieved memory with the system message. This is distinct from configuring a `TemplateRenderer` on the `ChatClient` itself (using `.templateRenderer()`), which affects the rendering of the initial user/system prompt content *before* the advisor runs. See xref:api/chatclient.adoc#_prompt_templates[ChatClient Prompt Templates] for more details on client-level template rendering. + +The custom `PromptTemplate` can use any `TemplateRenderer` implementation (by default, it uses `StPromptTemplate` based on the https://www.stringtemplate.org/[StringTemplate] engine). The important requirement is that the template must contain the following two placeholders: + +* an `instructions` placeholder to receive the original system message. +* a `memory` placeholder to receive the retrieved conversation memory. + +=== VectorStoreChatMemoryAdvisor + +==== Custom Template + +The `VectorStoreChatMemoryAdvisor` uses a default template to augment the system message with the retrieved conversation memory. You can customize this behavior by providing your own `PromptTemplate` object via the `.promptTemplate()` builder method. + +NOTE: The `PromptTemplate` provided here customizes how the advisor merges retrieved memory with the system message. This is distinct from configuring a `TemplateRenderer` on the `ChatClient` itself (using `.templateRenderer()`), which affects the rendering of the initial user/system prompt content *before* the advisor runs. See xref:api/chatclient.adoc#_prompt_templates[ChatClient Prompt Templates] for more details on client-level template rendering. + +The custom `PromptTemplate` can use any `TemplateRenderer` implementation (by default, it uses `StPromptTemplate` based on the https://www.stringtemplate.org/[StringTemplate] engine). The important requirement is that the template must contain the following two placeholders: + +* an `instructions` placeholder to receive the original system message. +* a `long_term_memory` placeholder to receive the retrieved conversation memory. + == Memory in Chat Model If you're working directly with a `ChatModel` instead of a `ChatClient`, you can manage the memory explicitly: diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/retrieval-augmented-generation.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/retrieval-augmented-generation.adoc index fdafefc3de1..d9bea8b0462 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/retrieval-augmented-generation.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/retrieval-augmented-generation.adoc @@ -82,13 +82,18 @@ The `QuestionAnswerAdvisor` uses a default template to augment the user question NOTE: The `PromptTemplate` provided here customizes how the advisor merges retrieved context with the user query. This is distinct from configuring a `TemplateRenderer` on the `ChatClient` itself (using `.templateRenderer()`), which affects the rendering of the initial user/system prompt content *before* the advisor runs. See xref:api/chatclient.adoc#_prompt_templates[ChatClient Prompt Templates] for more details on client-level template rendering. -The custom `PromptTemplate` can use any `TemplateRenderer` implementation (by default, it uses `StPromptTemplate` based on the https://www.stringtemplate.org/[StringTemplate] engine). The important requirement is that the template must contain a placeholder to receive the retrieved context, which the advisor provides under the key `question_answer_context`. +The custom `PromptTemplate` can use any `TemplateRenderer` implementation (by default, it uses `StPromptTemplate` based on the https://www.stringtemplate.org/[StringTemplate] engine). The important requirement is that the template must contain the following two placeholders: + +* a `query` placeholder to receive the user question. +* a `question_answer_context` placeholder to receive the retrieved context. [source,java] ---- PromptTemplate customPromptTemplate = PromptTemplate.builder() .renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build()) .template(""" + + Context information is below. --------------------- diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/upgrade-notes.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/upgrade-notes.adoc index 6db86f730ac..ac5d469ec1a 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/upgrade-notes.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/upgrade-notes.adoc @@ -36,6 +36,21 @@ For details, refer to: * `MessageAggregator` has a new method to aggregate messages from `ChatClientRequest`. The previous method aggregating messages from the old `AdvisedRequest` has been removed, since it was already marked as deprecated in M8. * In `SimpleLoggerAdvisor`, the `requestToString` input argument needs to be updated to use `ChatClientRequest`. It’s a breaking change since the alternative was not part of M8 yet. Same thing about the constructor. +==== Self-contained Templates in Advisors + +The built-in advisors that perform prompt augmentation have been updated to use self-contained templates. The goal is for each advisor to be able to perform templating operations without affecting nor being affected by templating and prompt decisions in other advisors. +If you were providing custom templates for the following advisors, you'll need to update them to ensure all expected placeholders are included. + +* The `QuestionAnswerAdvisor` expects a template with the following placeholders (see xref:api/retrieval-augmented-generation.adoc#_questionansweradvisor[more details]): +** a `query` placeholder to receive the user question. +** a `question_answer_context` placeholder to receive the retrieved context. +* The `PromptChatMemoryAdvisor` expects a template with the following placeholders (see xref:api/chat-memory.adoc#_promptchatmemoryadvisor[more details]): +** an `instructions` placeholder to receive the original system message. +** a `memory` placeholder to receive the retrieved conversation memory. +* The `VectorStoreChatMemoryAdvisor` expects a template with the following placeholders (see xref:api/chat-memory.adoc#_vectorstorechatmemoryadvisor[more details]): +** an `instructions` placeholder to receive the original system message. +** a `long_term_memory` placeholder to receive the retrieved conversation memory. + === Breaking Changes The Watson AI model was removed as it was based on the older text generation that is considered outdated as there is a new chat generation model available. Hopefully Watson will reappear in a future version of Spring AI diff --git a/spring-ai-integration-tests/src/test/java/org/springframework/ai/integration/tests/client/advisor/QuestionAnswerAdvisorIT.java b/spring-ai-integration-tests/src/test/java/org/springframework/ai/integration/tests/client/advisor/QuestionAnswerAdvisorIT.java index cee8e158cd2..bfe9a1496bd 100644 --- a/spring-ai-integration-tests/src/test/java/org/springframework/ai/integration/tests/client/advisor/QuestionAnswerAdvisorIT.java +++ b/spring-ai-integration-tests/src/test/java/org/springframework/ai/integration/tests/client/advisor/QuestionAnswerAdvisorIT.java @@ -128,6 +128,7 @@ void qaCustomPromptTemplate() { PromptTemplate customPromptTemplate = PromptTemplate.builder() .renderer(StTemplateRenderer.builder().startDelimiterToken('$').endDelimiterToken('$').build()) .template(""" + $query$ Context information is below, surrounded by ---------------------