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 <ThomasVitale@users.noreply.github.com>
This commit is contained in:
Thomas Vitale
2025-05-08 00:38:32 +01:00
committed by Mark Pollack
parent 2ea518686f
commit d619e25cb5
7 changed files with 95 additions and 48 deletions

View File

@@ -25,6 +25,7 @@ import org.springframework.ai.chat.client.ChatClientRequest;
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 class QuestionAnswerAdvisor implements BaseAdvisor {
: 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()

View File

@@ -56,7 +56,8 @@ public class VectorStoreChatMemoryAdvisor extends AbstractChatMemoryAdvisor<Vect
private static final String DOCUMENT_METADATA_MESSAGE_TYPE = "messageType";
private static final String DEFAULT_SYSTEM_TEXT_ADVISE = """
private static final PromptTemplate DEFAULT_SYSTEM_PROMPT_TEMPLATE = new PromptTemplate("""
{instructions}
Use the long term conversation memory from the LONG_TERM_MEMORY section to provide accurate answers.
@@ -64,24 +65,14 @@ public class VectorStoreChatMemoryAdvisor extends AbstractChatMemoryAdvisor<Vect
LONG_TERM_MEMORY:
{long_term_memory}
---------------------
""";
""");
private final String systemTextAdvise;
private final PromptTemplate systemPromptTemplate;
/**
* Constructor for VectorStoreChatMemoryAdvisor.
* @param vectorStore the vector store instance used for managing and querying
* documents.
* @param defaultConversationId the default conversation ID used if none is provided
* in the context.
* @param chatHistoryWindowSize the window size for the chat history retrieval.
* @param systemTextAdvise the system text advice used for the chat advisor system.
* @param order the order of precedence for this advisor in the chain.
*/
private VectorStoreChatMemoryAdvisor(VectorStore vectorStore, String defaultConversationId,
int chatHistoryWindowSize, String systemTextAdvise, int order) {
int chatHistoryWindowSize, PromptTemplate systemPromptTemplate, int order) {
super(vectorStore, defaultConversationId, chatHistoryWindowSize, true, order);
this.systemTextAdvise = systemTextAdvise;
this.systemPromptTemplate = systemPromptTemplate;
}
public static Builder builder(VectorStore chatMemory) {
@@ -127,11 +118,8 @@ public class VectorStoreChatMemoryAdvisor extends AbstractChatMemoryAdvisor<Vect
// 2. Augment the system message.
SystemMessage systemMessage = chatClientRequest.prompt().getSystemMessage();
String augmentedSystemText = PromptTemplate.builder()
.template(systemMessage.getText() + System.lineSeparator() + this.systemTextAdvise)
.variables(Map.of("long_term_memory", longTermMemory))
.build()
.render();
String augmentedSystemText = this.systemPromptTemplate
.render(Map.of("instructions", systemMessage.getText(), "long_term_memory", longTermMemory));
// 3. Create a new request with the augmented system message.
ChatClientRequest processedChatClientRequest = chatClientRequest.mutate()
@@ -187,21 +175,26 @@ public class VectorStoreChatMemoryAdvisor extends AbstractChatMemoryAdvisor<Vect
public static class Builder extends AbstractChatMemoryAdvisor.AbstractBuilder<VectorStore> {
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);
}
}

View File

@@ -46,7 +46,8 @@ import org.springframework.ai.chat.model.MessageAggregator;
*/
public class PromptChatMemoryAdvisor extends AbstractChatMemoryAdvisor<ChatMemory> {
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<ChatMemor
{memory}
---------------------
""";
""");
private final String systemTextAdvise;
private final PromptTemplate systemPromptTemplate;
public PromptChatMemoryAdvisor(ChatMemory chatMemory) {
this(chatMemory, DEFAULT_SYSTEM_TEXT_ADVISE);
this(chatMemory, DEFAULT_SYSTEM_PROMPT_TEMPLATE.getTemplate());
}
public PromptChatMemoryAdvisor(ChatMemory chatMemory, String systemTextAdvise) {
public PromptChatMemoryAdvisor(ChatMemory chatMemory, String systemPromptTemplate) {
super(chatMemory);
this.systemTextAdvise = systemTextAdvise;
this.systemPromptTemplate = new PromptTemplate(systemPromptTemplate);
}
public PromptChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int chatHistoryWindowSize,
String systemTextAdvise) {
this(chatMemory, defaultConversationId, chatHistoryWindowSize, systemTextAdvise,
String systemPromptTemplate) {
this(chatMemory, defaultConversationId, chatHistoryWindowSize, new PromptTemplate(systemPromptTemplate),
Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER);
}
public PromptChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int chatHistoryWindowSize,
String systemTextAdvise, int order) {
String systemPromptTemplate, int order) {
this(chatMemory, defaultConversationId, chatHistoryWindowSize, new PromptTemplate(systemPromptTemplate), order);
}
private PromptChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int chatHistoryWindowSize,
PromptTemplate systemPromptTemplate, int order) {
super(chatMemory, defaultConversationId, chatHistoryWindowSize, true, order);
this.systemTextAdvise = systemTextAdvise;
this.systemPromptTemplate = systemPromptTemplate;
}
public static Builder builder(ChatMemory chatMemory) {
@@ -119,11 +125,8 @@ public class PromptChatMemoryAdvisor extends AbstractChatMemoryAdvisor<ChatMemor
// 2. Augment the system message.
SystemMessage systemMessage = chatClientRequest.prompt().getSystemMessage();
String augmentedSystemText = PromptTemplate.builder()
.template(systemMessage.getText() + System.lineSeparator() + this.systemTextAdvise)
.variables(Map.of("memory", memory))
.build()
.render();
String augmentedSystemText = this.systemPromptTemplate
.render(Map.of("instructions", systemMessage.getText(), "memory", memory));
// 3. Create a new request with the augmented system message.
ChatClientRequest processedChatClientRequest = chatClientRequest.mutate()
@@ -151,20 +154,25 @@ public class PromptChatMemoryAdvisor extends AbstractChatMemoryAdvisor<ChatMemor
public static class Builder extends AbstractChatMemoryAdvisor.AbstractBuilder<ChatMemory> {
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);
}
}

View File

@@ -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:

View File

@@ -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("""
<query>
Context information is below.
---------------------

View File

@@ -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`. Its 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

View File

@@ -128,6 +128,7 @@ public class QuestionAnswerAdvisorIT {
PromptTemplate customPromptTemplate = PromptTemplate.builder()
.renderer(StTemplateRenderer.builder().startDelimiterToken('$').endDelimiterToken('$').build())
.template("""
$query$
Context information is below, surrounded by ---------------------