Skip to content

Commit 38ca31d

Browse files
authored
Add builtin customized RenderTemplate implementation. (#3935)
1 parent 5e20734 commit 38ca31d

File tree

8 files changed

+1346
-2
lines changed

8 files changed

+1346
-2
lines changed

examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/MultiAgentExample.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import org.springframework.ai.chat.messages.AssistantMessage;
3333
import org.springframework.ai.chat.messages.Message;
3434
import org.springframework.ai.chat.model.ChatModel;
35+
import org.springframework.ai.template.StringTemplateRenderer;
36+
import org.springframework.ai.template.TemplateRenderer;
3537

3638
import java.util.List;
3739
import java.util.Map;
@@ -393,6 +395,112 @@ public void example5_llmRoutingAgent() throws Exception {
393395
System.out.println("LLM路由示例执行完成");
394396
}
395397

398+
/**
399+
* 示例5.5:使用自定义 TemplateRenderer 与多智能体协作
400+
*
401+
* 展示如何在多智能体场景中使用 StringTemplateRenderer.builder() 来定制占位符分隔符。
402+
* 使用 [[variable]] 替代默认的 {variable} 作为占位符格式。
403+
*/
404+
public void example5_5_customTemplateRenderer() throws Exception {
405+
// 使用 StringTemplateRenderer.builder() 创建自定义分隔符的 TemplateRenderer
406+
// 使用 [[ 和 ]] 作为占位符分隔符
407+
TemplateRenderer customRenderer = StringTemplateRenderer.builder()
408+
.startDelimiterToken("[[")
409+
.endDelimiterToken("]]")
410+
.build();
411+
412+
// 创建专业化的子Agent - 注意 instruction 中使用 [[variable]] 格式
413+
ReactAgent writerAgent = ReactAgent.builder()
414+
.name("writer_agent")
415+
.model(chatModel)
416+
.description("擅长创作各类文章,包括散文、诗歌等文学作品")
417+
.instruction("""
418+
你是一个知名的作家,擅长写作和创作。
419+
当前主题:[[topic]]
420+
文体要求:[[style]]
421+
字数要求:[[word_count]]
422+
请根据用户的提问进行回答。
423+
""")
424+
.templateRenderer(customRenderer)
425+
.outputKey("writer_output")
426+
.build();
427+
428+
ReactAgent reviewerAgent = ReactAgent.builder()
429+
.name("reviewer_agent")
430+
.model(chatModel)
431+
.description("擅长对文章进行评论、修改和润色")
432+
.instruction("""
433+
你是一个知名的评论家,擅长对文章进行评论和修改。
434+
评审标准:[[review_criteria]]
435+
关注要点:[[focus_points]]
436+
对于散文类文章,请确保文章中必须包含对于风景的描述。
437+
""")
438+
.templateRenderer(customRenderer)
439+
.outputKey("reviewer_output")
440+
.build();
441+
442+
ReactAgent translatorAgent = ReactAgent.builder()
443+
.name("translator_agent")
444+
.model(chatModel)
445+
.description("擅长将文章翻译成各种语言")
446+
.instruction("""
447+
你是一个专业的翻译家,能够准确地将文章翻译成目标语言。
448+
目标语言:[[target_language]]
449+
翻译风格:[[translation_style]]
450+
""")
451+
.templateRenderer(customRenderer)
452+
.outputKey("translator_output")
453+
.build();
454+
455+
// 创建路由Agent
456+
LlmRoutingAgent routingAgent = LlmRoutingAgent.builder()
457+
.name("content_routing_agent")
458+
.description("根据用户需求智能路由到合适的专家Agent")
459+
.model(chatModel)
460+
.subAgents(List.of(writerAgent, reviewerAgent, translatorAgent))
461+
.build();
462+
463+
// 使用 - 传入带有自定义占位符变量的输入
464+
System.out.println("自定义模板路由测试1: 写作请求");
465+
Map<String, Object> writerInput = Map.of(
466+
"input", "帮我写一篇关于春天的散文",
467+
"topic", "春天",
468+
"style", "散文",
469+
"word_count", "200字左右"
470+
);
471+
Optional<OverAllState> result1 = routingAgent.invoke(writerInput);
472+
if (result1.isPresent()) {
473+
result1.get().value("writer_output").ifPresent(output ->
474+
System.out.println("写作输出: " + output));
475+
}
476+
477+
System.out.println("\n自定义模板路由测试2: 评审请求");
478+
Map<String, Object> reviewerInput = Map.of(
479+
"input", "请帮我修改这篇文章:春天来了,花开了。",
480+
"review_criteria", "语言流畅、描述生动",
481+
"focus_points", "修辞手法、意境营造"
482+
);
483+
Optional<OverAllState> result2 = routingAgent.invoke(reviewerInput);
484+
if (result2.isPresent()) {
485+
result2.get().value("reviewer_output").ifPresent(output ->
486+
System.out.println("评审输出: " + output));
487+
}
488+
489+
System.out.println("\n自定义模板路由测试3: 翻译请求");
490+
Map<String, Object> translatorInput = Map.of(
491+
"input", "请将以下内容翻译成英文:春暖花开",
492+
"target_language", "英文",
493+
"translation_style", "文学性翻译"
494+
);
495+
Optional<OverAllState> result3 = routingAgent.invoke(translatorInput);
496+
if (result3.isPresent()) {
497+
result3.get().value("translator_output").ifPresent(output ->
498+
System.out.println("翻译输出: " + output));
499+
}
500+
501+
System.out.println("\n自定义TemplateRenderer多智能体示例执行完成");
502+
}
503+
396504
/**
397505
* 示例6:优化路由准确性
398506
*

examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/AgentsExample.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
import org.springframework.ai.chat.model.ChatModel;
4747
import org.springframework.ai.chat.model.ToolContext;
4848
import org.springframework.ai.converter.BeanOutputConverter;
49+
import org.springframework.ai.template.StringTemplateRenderer;
50+
import org.springframework.ai.template.TemplateRenderer;
51+
import org.springframework.ai.template.st.StTemplateRenderer;
4952
import org.springframework.ai.tool.ToolCallback;
5053
import org.springframework.ai.tool.annotation.ToolParam;
5154
import org.springframework.ai.tool.function.FunctionToolCallback;
@@ -183,6 +186,76 @@ public static void instructionUsage() {
183186

184187
// ==================== System Prompt ====================
185188

189+
/**
190+
* 示例6.5:使用自定义 TemplateRenderer 定制占位符分隔符
191+
*
192+
* 展示如何使用 StringTemplateRenderer.builder() 来定制占位符的起始和结束分隔符。
193+
* 默认使用 {variable},这里演示使用 {{variable}} 作为占位符。
194+
*/
195+
public static void customTemplateRendererExample() throws GraphRunnerException {
196+
DashScopeApi dashScopeApi = DashScopeApi.builder()
197+
.apiKey(System.getenv("AI_DASHSCOPE_API_KEY"))
198+
.build();
199+
200+
ChatModel chatModel = DashScopeChatModel.builder()
201+
.dashScopeApi(dashScopeApi)
202+
.build();
203+
204+
// 使用 StringTemplateRenderer.builder() 创建自定义分隔符的 TemplateRenderer
205+
// 使用 {{ 和 }} 作为占位符分隔符
206+
TemplateRenderer customRenderer = StringTemplateRenderer.builder()
207+
.startDelimiterToken("{{")
208+
.endDelimiterToken("}}")
209+
.build();
210+
211+
// 使用自定义分隔符的 systemPrompt
212+
String systemPrompt = """
213+
你是一个专业的{{role}}助手。
214+
你的专业领域是{{domain}}。
215+
请用{{language}}语言回答用户的问题。
216+
""";
217+
218+
// 使用自定义分隔符的 instruction
219+
String instruction = """
220+
用户询问的主题是:{{topic}}
221+
请根据以下要求回答:
222+
1. 保持专业性
223+
2. 提供具体示例
224+
3. 语言要{{style}}
225+
""";
226+
227+
ReactAgent agent = ReactAgent.builder()
228+
.name("custom_template_agent")
229+
.model(chatModel)
230+
.systemPrompt(systemPrompt)
231+
.instruction(instruction)
232+
.templateRenderer(customRenderer)
233+
.build();
234+
235+
// 使用时,状态中的变量会自动替换 {{ }} 包裹的占位符
236+
Map<String, Object> inputs = Map.of(
237+
"input", "请介绍一下Spring框架的核心特性",
238+
"role", "技术专家",
239+
"domain", "Java企业级开发",
240+
"language", "中文",
241+
"topic", "Spring框架",
242+
"style", "简洁易懂"
243+
);
244+
245+
Optional<OverAllState> result = agent.invoke(inputs);
246+
if (result.isPresent()) {
247+
List<Message> messages = (List<Message>) result.get().value("messages").orElse(List.of());
248+
for (Message message : messages) {
249+
if (message instanceof AssistantMessage) {
250+
System.out.println("Agent回复: " + ((AssistantMessage) message).getText());
251+
}
252+
}
253+
}
254+
}
255+
256+
/**
257+
* 示例7:动态 System Prompt
258+
*/
186259
public static void dynamicSystemPrompt() {
187260
DashScopeApi dashScopeApi = DashScopeApi.builder()
188261
.apiKey(System.getenv("AI_DASHSCOPE_API_KEY"))

spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.springframework.ai.chat.model.ChatModel;
4343
import org.springframework.ai.chat.prompt.ChatOptions;
4444
import org.springframework.ai.support.ToolCallbacks;
45+
import org.springframework.ai.template.TemplateRenderer;
4546
import org.springframework.ai.tool.ToolCallback;
4647
import org.springframework.ai.tool.ToolCallbackProvider;
4748
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
@@ -59,6 +60,10 @@ public abstract class Builder {
5960

6061
protected String systemPrompt;
6162

63+
// Provide customized template renderer instance, for example
64+
// SaaStTemplateRenderer.builder().startDelimiterToken("{{").endDelimiterToken("}}").build()
65+
protected TemplateRenderer templateRenderer;
66+
6267
protected ChatModel model;
6368

6469
protected ChatOptions chatOptions;
@@ -219,6 +224,11 @@ public Builder systemPrompt(String systemPrompt) {
219224
return this;
220225
}
221226

227+
public Builder templateRenderer(TemplateRenderer templateRenderer) {
228+
this.templateRenderer = templateRenderer;
229+
return this;
230+
}
231+
222232
public Builder outputKey(String outputKey) {
223233
this.outputKey = outputKey;
224234
return this;

spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ public ReactAgent build() {
8484
llmNodeBuilder.systemPrompt(systemPrompt);
8585
}
8686

87+
if (templateRenderer != null) {
88+
llmNodeBuilder.templateRenderer(templateRenderer);
89+
}
90+
8791
if (instruction != null) {
8892
llmNodeBuilder.instruction(instruction);
8993
}

spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/node/AgentLlmNode.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.ai.chat.prompt.ChatOptions;
4040
import org.springframework.ai.chat.prompt.PromptTemplate;
4141
import org.springframework.ai.model.tool.ToolCallingChatOptions;
42+
import org.springframework.ai.template.TemplateRenderer;
4243
import org.springframework.ai.tool.ToolCallback;
4344

4445
import org.springframework.lang.Nullable;
@@ -79,6 +80,8 @@ public class AgentLlmNode implements NodeActionWithConfig {
7980

8081
private String systemPrompt;
8182

83+
private TemplateRenderer templateRenderer;
84+
8285
private String instruction;
8386

8487
private ToolCallingChatOptions chatOptions;
@@ -91,6 +94,7 @@ public AgentLlmNode(Builder builder) {
9194
this.outputSchema = builder.outputSchema;
9295
this.systemPrompt = builder.systemPrompt;
9396
this.instruction = builder.instruction;
97+
this.templateRenderer = builder.templateRenderer;
9498
if (builder.advisors != null) {
9599
this.advisors = builder.advisors;
96100
}
@@ -353,8 +357,11 @@ private ToolCallingChatOptions buildChatOptions(ChatOptions chatOptions, List<To
353357
}
354358

355359
private String renderPromptTemplate(String prompt, Map<String, Object> params) {
356-
PromptTemplate promptTemplate = new PromptTemplate(prompt);
357-
return promptTemplate.render(params);
360+
PromptTemplate.Builder builder = PromptTemplate.builder().template(prompt);
361+
if (templateRenderer != null) {
362+
builder.renderer(templateRenderer);
363+
}
364+
return builder.build().render(params);
358365
}
359366

360367
public void augmentUserMessage(List<Message> messages, String outputSchema) {
@@ -506,6 +513,8 @@ public static class Builder {
506513

507514
private String systemPrompt;
508515

516+
private TemplateRenderer templateRenderer;
517+
509518
private ChatClient chatClient;
510519

511520
private List<Advisor> advisors;
@@ -540,6 +549,11 @@ public Builder systemPrompt(String systemPrompt) {
540549
return this;
541550
}
542551

552+
public Builder templateRenderer(TemplateRenderer templateRenderer) {
553+
this.templateRenderer = templateRenderer;
554+
return this;
555+
}
556+
543557
public Builder advisors(List<Advisor> advisors) {
544558
this.advisors = advisors;
545559
return this;

0 commit comments

Comments
 (0)