1
1
/*
2
- * Copyright 2023-2024 the original author or authors.
2
+ * Copyright 2023-2025 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
20
20
import java .io .InputStream ;
21
21
import java .nio .charset .Charset ;
22
22
import java .util .HashMap ;
23
- import java .util .HashSet ;
24
23
import java .util .List ;
25
24
import java .util .Map ;
26
25
import java .util .Map .Entry ;
27
26
import java .util .Set ;
28
27
29
- import org .antlr .runtime .Token ;
30
- import org .antlr .runtime .TokenStream ;
31
- import org .stringtemplate .v4 .ST ;
32
- import org .stringtemplate .v4 .compiler .STLexer ;
28
+ import org .springframework .ai .template .TemplateRenderer ;
29
+ import org .springframework .ai .template .st .StTemplateRenderer ;
30
+ import org .springframework .util .Assert ;
33
31
34
32
import org .springframework .ai .chat .messages .Message ;
35
33
import org .springframework .ai .chat .messages .UserMessage ;
36
34
import org .springframework .ai .content .Media ;
37
35
import org .springframework .core .io .Resource ;
38
36
import org .springframework .util .StreamUtils ;
39
37
38
+ /**
39
+ * A template for creating prompts. It allows you to define a template string with
40
+ * placeholders for variables, and then render the template with specific values for those
41
+ * variables.
42
+ * <p>
43
+ * NOTE: This class will be marked as final in the next release. If you subclass this
44
+ * class, you should consider using the built-in implementation together with the new
45
+ * PromptTemplateRenderer interface, which is designed to give you more flexibility and
46
+ * control over the rendering process.
47
+ */
40
48
public class PromptTemplate implements PromptTemplateActions , PromptTemplateMessageActions {
41
49
50
+ private static final TemplateRenderer DEFAULT_TEMPLATE_RENDERER = StTemplateRenderer .builder ().build ();
51
+
52
+ /**
53
+ * @deprecated will become private in the next release. If you're subclassing this
54
+ * class, re-consider using the built-in implementation together with the new
55
+ * PromptTemplateRenderer interface, designed to give you more flexibility and control
56
+ * over the rendering process.
57
+ */
58
+ @ Deprecated
42
59
protected String template ;
43
60
61
+ /**
62
+ * @deprecated in favor of {@link TemplateRenderer}
63
+ */
64
+ @ Deprecated
44
65
protected TemplateFormat templateFormat = TemplateFormat .ST ;
45
66
46
- private ST st ;
67
+ private final Map < String , Object > variables = new HashMap <>() ;
47
68
48
- private Map < String , Object > dynamicModel = new HashMap <>() ;
69
+ private final TemplateRenderer renderer ;
49
70
50
71
public PromptTemplate (Resource resource ) {
51
- try (InputStream inputStream = resource .getInputStream ()) {
52
- this .template = StreamUtils .copyToString (inputStream , Charset .defaultCharset ());
53
- }
54
- catch (IOException ex ) {
55
- throw new RuntimeException ("Failed to read resource" , ex );
56
- }
57
- try {
58
- this .st = new ST (this .template , '{' , '}' );
59
- }
60
- catch (Exception ex ) {
61
- throw new IllegalArgumentException ("The template string is not valid." , ex );
62
- }
72
+ this (resource , new HashMap <>(), DEFAULT_TEMPLATE_RENDERER );
63
73
}
64
74
65
75
public PromptTemplate (String template ) {
66
- this .template = template ;
67
- // If the template string is not valid, an exception will be thrown
68
- try {
69
- this .st = new ST (this .template , '{' , '}' );
70
- }
71
- catch (Exception ex ) {
72
- throw new IllegalArgumentException ("The template string is not valid." , ex );
73
- }
76
+ this (template , new HashMap <>(), DEFAULT_TEMPLATE_RENDERER );
77
+ }
78
+
79
+ public PromptTemplate (String template , Map <String , Object > variables ) {
80
+ this (template , variables , DEFAULT_TEMPLATE_RENDERER );
74
81
}
75
82
76
- public PromptTemplate (String template , Map <String , Object > model ) {
83
+ public PromptTemplate (Resource resource , Map <String , Object > variables ) {
84
+ this (resource , variables , DEFAULT_TEMPLATE_RENDERER );
85
+ }
86
+
87
+ PromptTemplate (String template , Map <String , Object > variables , TemplateRenderer renderer ) {
88
+ Assert .hasText (template , "template cannot be null or empty" );
89
+ Assert .notNull (variables , "variables cannot be null" );
90
+ Assert .noNullElements (variables .keySet (), "variables keys cannot be null" );
91
+ Assert .notNull (renderer , "renderer cannot be null" );
92
+
77
93
this .template = template ;
78
- // If the template string is not valid, an exception will be thrown
79
- try {
80
- this .st = new ST (this .template , '{' , '}' );
81
- for (Entry <String , Object > entry : model .entrySet ()) {
82
- add (entry .getKey (), entry .getValue ());
83
- }
84
- }
85
- catch (Exception ex ) {
86
- throw new IllegalArgumentException ("The template string is not valid." , ex );
87
- }
94
+ this .variables .putAll (variables );
95
+ this .renderer = renderer ;
88
96
}
89
97
90
- public PromptTemplate (Resource resource , Map <String , Object > model ) {
98
+ PromptTemplate (Resource resource , Map <String , Object > variables , TemplateRenderer renderer ) {
99
+ Assert .notNull (resource , "resource cannot be null" );
100
+ Assert .notNull (variables , "variables cannot be null" );
101
+ Assert .noNullElements (variables .keySet (), "variables keys cannot be null" );
102
+ Assert .notNull (renderer , "renderer cannot be null" );
103
+
91
104
try (InputStream inputStream = resource .getInputStream ()) {
92
105
this .template = StreamUtils .copyToString (inputStream , Charset .defaultCharset ());
106
+ Assert .hasText (template , "template cannot be null or empty" );
93
107
}
94
108
catch (IOException ex ) {
95
109
throw new RuntimeException ("Failed to read resource" , ex );
96
110
}
97
- // If the template string is not valid, an exception will be thrown
98
- try {
99
- this .st = new ST (this .template , '{' , '}' );
100
- for (Entry <String , Object > entry : model .entrySet ()) {
101
- this .add (entry .getKey (), entry .getValue ());
102
- }
103
- }
104
- catch (Exception ex ) {
105
- throw new IllegalArgumentException ("The template string is not valid." , ex );
106
- }
111
+ this .variables .putAll (variables );
112
+ this .renderer = renderer ;
107
113
}
108
114
109
115
public void add (String name , Object value ) {
110
- this .st .add (name , value );
111
- this .dynamicModel .put (name , value );
116
+ this .variables .put (name , value );
112
117
}
113
118
114
119
public String getTemplate () {
115
120
return this .template ;
116
121
}
117
122
123
+ /**
124
+ * @deprecated in favor of {@link TemplateRenderer}
125
+ */
126
+ @ Deprecated
118
127
public TemplateFormat getTemplateFormat () {
119
128
return this .templateFormat ;
120
129
}
121
130
122
- // Render Methods
131
+ // From PromptTemplateStringActions.
132
+
123
133
@ Override
124
134
public String render () {
125
- validate (this .dynamicModel );
126
- return this .st .render ();
135
+ return this .renderer .apply (template , this .variables );
127
136
}
128
137
129
138
@ Override
130
- public String render (Map <String , Object > model ) {
131
- validate (model );
132
- for (Entry <String , Object > entry : model .entrySet ()) {
133
- if (this .st .getAttribute (entry .getKey ()) != null ) {
134
- this .st .remove (entry .getKey ());
135
- }
139
+ public String render (Map <String , Object > additionalVariables ) {
140
+ Map <String , Object > combinedVariables = new HashMap <>(this .variables );
141
+
142
+ for (Entry <String , Object > entry : additionalVariables .entrySet ()) {
136
143
if (entry .getValue () instanceof Resource ) {
137
- this . st . add (entry .getKey (), renderResource ((Resource ) entry .getValue ()));
144
+ combinedVariables . put (entry .getKey (), renderResource ((Resource ) entry .getValue ()));
138
145
}
139
146
else {
140
- this . st . add (entry .getKey (), entry .getValue ());
147
+ combinedVariables . put (entry .getKey (), entry .getValue ());
141
148
}
142
-
143
149
}
144
- return this .st .render ();
150
+
151
+ return this .renderer .apply (template , combinedVariables );
145
152
}
146
153
147
154
private String renderResource (Resource resource ) {
@@ -153,6 +160,8 @@ private String renderResource(Resource resource) {
153
160
}
154
161
}
155
162
163
+ // From PromptTemplateMessageActions.
164
+
156
165
@ Override
157
166
public Message createMessage () {
158
167
return new UserMessage (render ());
@@ -164,10 +173,12 @@ public Message createMessage(List<Media> mediaList) {
164
173
}
165
174
166
175
@ Override
167
- public Message createMessage (Map <String , Object > model ) {
168
- return new UserMessage (render (model ));
176
+ public Message createMessage (Map <String , Object > additionalVariables ) {
177
+ return new UserMessage (render (additionalVariables ));
169
178
}
170
179
180
+ // From PromptTemplateActions.
181
+
171
182
@ Override
172
183
public Prompt create () {
173
184
return new Prompt (render (new HashMap <>()));
@@ -179,59 +190,89 @@ public Prompt create(ChatOptions modelOptions) {
179
190
}
180
191
181
192
@ Override
182
- public Prompt create (Map <String , Object > model ) {
183
- return new Prompt (render (model ));
193
+ public Prompt create (Map <String , Object > additionalVariables ) {
194
+ return new Prompt (render (additionalVariables ));
184
195
}
185
196
186
197
@ Override
187
- public Prompt create (Map <String , Object > model , ChatOptions modelOptions ) {
188
- return new Prompt (render (model ), modelOptions );
198
+ public Prompt create (Map <String , Object > additionalVariables , ChatOptions modelOptions ) {
199
+ return new Prompt (render (additionalVariables ), modelOptions );
189
200
}
190
201
202
+ // Compatibility
203
+
204
+ /**
205
+ * @deprecated in favor of {@link TemplateRenderer}.
206
+ */
207
+ @ Deprecated
191
208
public Set <String > getInputVariables () {
192
- TokenStream tokens = this .st .impl .tokens ;
193
- Set <String > inputVariables = new HashSet <>();
194
- boolean isInsideList = false ;
195
-
196
- for (int i = 0 ; i < tokens .size (); i ++) {
197
- Token token = tokens .get (i );
198
-
199
- if (token .getType () == STLexer .LDELIM && i + 1 < tokens .size ()
200
- && tokens .get (i + 1 ).getType () == STLexer .ID ) {
201
- if (i + 2 < tokens .size () && tokens .get (i + 2 ).getType () == STLexer .COLON ) {
202
- inputVariables .add (tokens .get (i + 1 ).getText ());
203
- isInsideList = true ;
204
- }
205
- }
206
- else if (token .getType () == STLexer .RDELIM ) {
207
- isInsideList = false ;
208
- }
209
- else if (!isInsideList && token .getType () == STLexer .ID ) {
210
- inputVariables .add (token .getText ());
211
- }
212
- }
209
+ throw new UnsupportedOperationException (
210
+ "The template rendering logic is now provided by PromptTemplateRenderer" );
211
+ }
213
212
214
- return inputVariables ;
213
+ /**
214
+ * @deprecated in favor of {@link TemplateRenderer}.
215
+ */
216
+ @ Deprecated
217
+ protected void validate (Map <String , Object > model ) {
218
+ throw new UnsupportedOperationException ("Validation is now provided by the PromptTemplateRenderer" );
215
219
}
216
220
217
- private Set <String > getModelKeys (Map <String , Object > model ) {
218
- Set <String > dynamicVariableNames = new HashSet <>(this .dynamicModel .keySet ());
219
- Set <String > modelVariables = new HashSet <>(model .keySet ());
220
- modelVariables .addAll (dynamicVariableNames );
221
- return modelVariables ;
221
+ public Builder mutate () {
222
+ return new Builder ().template (this .template ).variables (this .variables ).renderer (this .renderer );
222
223
}
223
224
224
- protected void validate (Map <String , Object > model ) {
225
+ // Builder
226
+
227
+ public static Builder builder () {
228
+ return new Builder ();
229
+ }
230
+
231
+ public static class Builder {
232
+
233
+ private String template ;
234
+
235
+ private Resource resource ;
225
236
226
- Set <String > templateTokens = getInputVariables ();
227
- Set <String > modelKeys = getModelKeys (model );
237
+ private Map <String , Object > variables = new HashMap <>();
228
238
229
- // Check if model provides all keys required by the template
230
- if (!modelKeys .containsAll (templateTokens )) {
231
- templateTokens .removeAll (modelKeys );
232
- throw new IllegalStateException (
233
- "Not all template variables were replaced. Missing variable names are " + templateTokens );
239
+ private TemplateRenderer renderer = DEFAULT_TEMPLATE_RENDERER ;
240
+
241
+ private Builder () {
242
+ }
243
+
244
+ public Builder template (String template ) {
245
+ this .template = template ;
246
+ return this ;
247
+ }
248
+
249
+ public Builder resource (Resource resource ) {
250
+ this .resource = resource ;
251
+ return this ;
252
+ }
253
+
254
+ public Builder variables (Map <String , Object > variables ) {
255
+ this .variables = variables ;
256
+ return this ;
257
+ }
258
+
259
+ public Builder renderer (TemplateRenderer renderer ) {
260
+ this .renderer = renderer ;
261
+ return this ;
262
+ }
263
+
264
+ public PromptTemplate build () {
265
+ if (this .template != null && this .resource != null ) {
266
+ throw new IllegalArgumentException ("Only one of template or resource can be set" );
267
+ }
268
+ else if (this .resource != null ) {
269
+ return new PromptTemplate (this .resource , this .variables , this .renderer );
270
+ }
271
+ else {
272
+ return new PromptTemplate (this .template , this .variables , this .renderer );
273
+ }
234
274
}
275
+
235
276
}
236
277
237
278
}
0 commit comments