Skip to content

Commit a49a2d2

Browse files
tzolovmarkpollack
authored andcommitted
Replace the OutputParser by a StructuredOutputConverter API
- Old OutputParser, BeanOutputParser, ListOutputParser and MapOutputParser classes are depredated in favour of the new StructuredOutputConverter, BeanOutputConverter, ListOutputConverter and MapOutputConverter implementations. Later are drop-in replacements for the former ones, and provide the same functionality. - Keep the existing parser package and classes for backward compatibility. - Adjust the PromptTemplate for backward compatibility - Update all existing tests to use the new Structured Output API. - Improve the documentation for structured outputs.
1 parent 1c93ae5 commit a49a2d2

File tree

39 files changed

+1012
-454
lines changed

39 files changed

+1012
-454
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ For a hands-on guide to PromptTemplate, see the [PromptTemplate API guide](https
110110
**Output Parsers:** AI model outputs often come as raw `java.lang.String` values. Output Parsers restructure these raw strings into more programmer-friendly formats, such as CSV or JSON.
111111

112112
Get insights on Output Parsers in our [concept guide](https://docs.spring.io/spring-ai/reference/concepts.html#_output_parsing)..
113-
For implementation details, visit the [OutputParser API guide](https://docs.spring.io/spring-ai/reference/api/output-parser.html).
113+
For implementation details, visit the [StructuredOutputConverter API guide](https://docs.spring.io/spring-ai/reference/api/output-parser.html).
114114

115115
### Incorporating your data
116116

models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatClientIT.java

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@
4040
import org.springframework.ai.chat.prompt.Prompt;
4141
import org.springframework.ai.chat.prompt.PromptTemplate;
4242
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
43+
import org.springframework.ai.converter.BeanOutputConverter;
44+
import org.springframework.ai.converter.ListOutputConverter;
45+
import org.springframework.ai.converter.MapOutputConverter;
4346
import org.springframework.ai.model.function.FunctionCallbackWrapper;
44-
import org.springframework.ai.parser.BeanOutputParser;
45-
import org.springframework.ai.parser.ListOutputParser;
46-
import org.springframework.ai.parser.MapOutputParser;
4747
import org.springframework.beans.factory.annotation.Autowired;
4848
import org.springframework.beans.factory.annotation.Value;
4949
import org.springframework.boot.test.context.SpringBootTest;
@@ -90,11 +90,11 @@ void roleTest() {
9090
}
9191

9292
@Test
93-
void outputParser() {
93+
void listOutputConverter() {
9494
DefaultConversionService conversionService = new DefaultConversionService();
95-
ListOutputParser outputParser = new ListOutputParser(conversionService);
95+
ListOutputConverter listOutputConverter = new ListOutputConverter(conversionService);
9696

97-
String format = outputParser.getFormat();
97+
String format = listOutputConverter.getFormat();
9898
String template = """
9999
List five {subject}
100100
{format}
@@ -104,15 +104,15 @@ void outputParser() {
104104
Prompt prompt = new Prompt(promptTemplate.createMessage());
105105
Generation generation = this.chatClient.call(prompt).getResult();
106106

107-
List<String> list = outputParser.parse(generation.getOutput().getContent());
107+
List<String> list = listOutputConverter.convert(generation.getOutput().getContent());
108108
assertThat(list).hasSize(5);
109109
}
110110

111111
@Test
112-
void mapOutputParser() {
113-
MapOutputParser outputParser = new MapOutputParser();
112+
void mapOutputConverter() {
113+
MapOutputConverter mapOutputConverter = new MapOutputConverter();
114114

115-
String format = outputParser.getFormat();
115+
String format = mapOutputConverter.getFormat();
116116
String template = """
117117
Provide me a List of {subject}
118118
{format}
@@ -122,7 +122,7 @@ void mapOutputParser() {
122122
Prompt prompt = new Prompt(promptTemplate.createMessage());
123123
Generation generation = chatClient.call(prompt).getResult();
124124

125-
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
125+
Map<String, Object> result = mapOutputConverter.convert(generation.getOutput().getContent());
126126
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
127127

128128
}
@@ -131,11 +131,11 @@ record ActorsFilmsRecord(String actor, List<String> movies) {
131131
}
132132

133133
@Test
134-
void beanOutputParserRecords() {
134+
void beanOutputConverterRecords() {
135135

136-
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
136+
BeanOutputConverter<ActorsFilmsRecord> beanOutputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
137137

138-
String format = outputParser.getFormat();
138+
String format = beanOutputConverter.getFormat();
139139
String template = """
140140
Generate the filmography of 5 movies for Tom Hanks.
141141
{format}
@@ -144,18 +144,18 @@ void beanOutputParserRecords() {
144144
Prompt prompt = new Prompt(promptTemplate.createMessage());
145145
Generation generation = chatClient.call(prompt).getResult();
146146

147-
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
147+
ActorsFilmsRecord actorsFilms = beanOutputConverter.convert(generation.getOutput().getContent());
148148
logger.info("" + actorsFilms);
149149
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
150150
assertThat(actorsFilms.movies()).hasSize(5);
151151
}
152152

153153
@Test
154-
void beanStreamOutputParserRecords() {
154+
void beanStreamOutputConverterRecords() {
155155

156-
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
156+
BeanOutputConverter<ActorsFilmsRecord> beanOutputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
157157

158-
String format = outputParser.getFormat();
158+
String format = beanOutputConverter.getFormat();
159159
String template = """
160160
Generate the filmography of 5 movies for Tom Hanks.
161161
{format}
@@ -173,7 +173,7 @@ void beanStreamOutputParserRecords() {
173173
.map(AssistantMessage::getContent)
174174
.collect(Collectors.joining());
175175

176-
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
176+
ActorsFilmsRecord actorsFilms = beanOutputConverter.convert(generationTextFromStream);
177177
logger.info("" + actorsFilms);
178178
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
179179
assertThat(actorsFilms.movies()).hasSize(5);

models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatClientIT.java

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@
3030
import org.springframework.ai.chat.ChatResponse;
3131
import org.springframework.ai.chat.Generation;
3232
import org.springframework.ai.chat.messages.AssistantMessage;
33-
import org.springframework.ai.parser.BeanOutputParser;
34-
import org.springframework.ai.parser.ListOutputParser;
35-
import org.springframework.ai.parser.MapOutputParser;
33+
import org.springframework.ai.chat.messages.Message;
34+
import org.springframework.ai.chat.messages.UserMessage;
3635
import org.springframework.ai.chat.prompt.Prompt;
3736
import org.springframework.ai.chat.prompt.PromptTemplate;
3837
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
39-
import org.springframework.ai.chat.messages.Message;
40-
import org.springframework.ai.chat.messages.UserMessage;
38+
import org.springframework.ai.converter.BeanOutputConverter;
39+
import org.springframework.ai.converter.ListOutputConverter;
40+
import org.springframework.ai.converter.MapOutputConverter;
4141
import org.springframework.beans.factory.annotation.Autowired;
4242
import org.springframework.boot.SpringBootConfiguration;
4343
import org.springframework.boot.test.context.SpringBootTest;
@@ -74,11 +74,11 @@ void roleTest() {
7474
}
7575

7676
@Test
77-
void outputParser() {
77+
void listOutputConverter() {
7878
DefaultConversionService conversionService = new DefaultConversionService();
79-
ListOutputParser outputParser = new ListOutputParser(conversionService);
79+
ListOutputConverter outputConverter = new ListOutputConverter(conversionService);
8080

81-
String format = outputParser.getFormat();
81+
String format = outputConverter.getFormat();
8282
String template = """
8383
List five {subject}
8484
{format}
@@ -88,16 +88,16 @@ void outputParser() {
8888
Prompt prompt = new Prompt(promptTemplate.createMessage());
8989
Generation generation = chatClient.call(prompt).getResult();
9090

91-
List<String> list = outputParser.parse(generation.getOutput().getContent());
91+
List<String> list = outputConverter.convert(generation.getOutput().getContent());
9292
assertThat(list).hasSize(5);
9393

9494
}
9595

9696
@Test
97-
void mapOutputParser() {
98-
MapOutputParser outputParser = new MapOutputParser();
97+
void mapOutputConverter() {
98+
MapOutputConverter outputConverter = new MapOutputConverter();
9999

100-
String format = outputParser.getFormat();
100+
String format = outputConverter.getFormat();
101101
String template = """
102102
Provide me a List of {subject}
103103
{format}
@@ -107,17 +107,17 @@ void mapOutputParser() {
107107
Prompt prompt = new Prompt(promptTemplate.createMessage());
108108
Generation generation = chatClient.call(prompt).getResult();
109109

110-
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
110+
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
111111
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
112112

113113
}
114114

115115
@Test
116-
void beanOutputParser() {
116+
void beanOutputConverter() {
117117

118-
BeanOutputParser<ActorsFilms> outputParser = new BeanOutputParser<>(ActorsFilms.class);
118+
BeanOutputConverter<ActorsFilms> outputConverter = new BeanOutputConverter<>(ActorsFilms.class);
119119

120-
String format = outputParser.getFormat();
120+
String format = outputConverter.getFormat();
121121
String template = """
122122
Generate the filmography for a random actor.
123123
{format}
@@ -126,19 +126,19 @@ void beanOutputParser() {
126126
Prompt prompt = new Prompt(promptTemplate.createMessage());
127127
Generation generation = chatClient.call(prompt).getResult();
128128

129-
ActorsFilms actorsFilms = outputParser.parse(generation.getOutput().getContent());
129+
ActorsFilms actorsFilms = outputConverter.convert(generation.getOutput().getContent());
130130
assertThat(actorsFilms.actor()).isNotNull();
131131
}
132132

133133
record ActorsFilmsRecord(String actor, List<String> movies) {
134134
}
135135

136136
@Test
137-
void beanOutputParserRecords() {
137+
void beanOutputConverterRecords() {
138138

139-
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
139+
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
140140

141-
String format = outputParser.getFormat();
141+
String format = outputConverter.getFormat();
142142
String template = """
143143
Generate the filmography of 5 movies for Tom Hanks.
144144
{format}
@@ -147,16 +147,16 @@ void beanOutputParserRecords() {
147147
Prompt prompt = new Prompt(promptTemplate.createMessage());
148148
Generation generation = chatClient.call(prompt).getResult();
149149

150-
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
150+
ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent());
151151
System.out.println(actorsFilms);
152152
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
153153
assertThat(actorsFilms.movies()).hasSize(5);
154154
}
155155

156156
@Test
157-
void beanStreamOutputParserRecords() {
157+
void beanStreamOutputConverterRecords() {
158158

159-
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
159+
BeanOutputConverter<ActorsFilmsRecord> outputParser = new BeanOutputConverter<>(ActorsFilmsRecord.class);
160160

161161
String format = outputParser.getFormat();
162162
String template = """
@@ -177,7 +177,7 @@ void beanStreamOutputParserRecords() {
177177
.filter(Objects::nonNull)
178178
.collect(Collectors.joining());
179179

180-
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
180+
ActorsFilmsRecord actorsFilms = outputParser.convert(generationTextFromStream);
181181
System.out.println(actorsFilms);
182182
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
183183
assertThat(actorsFilms.movies()).hasSize(5);

models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic/BedrockAnthropicChatClientIT.java

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,21 @@
2727
import org.slf4j.Logger;
2828
import org.slf4j.LoggerFactory;
2929
import reactor.core.publisher.Flux;
30-
31-
import org.springframework.ai.chat.ChatResponse;
32-
import org.springframework.ai.chat.messages.AssistantMessage;
3330
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
3431
import software.amazon.awssdk.regions.Region;
3532

3633
import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi;
34+
import org.springframework.ai.chat.ChatResponse;
3735
import org.springframework.ai.chat.Generation;
38-
import org.springframework.ai.parser.BeanOutputParser;
39-
import org.springframework.ai.parser.ListOutputParser;
40-
import org.springframework.ai.parser.MapOutputParser;
36+
import org.springframework.ai.chat.messages.AssistantMessage;
37+
import org.springframework.ai.chat.messages.Message;
38+
import org.springframework.ai.chat.messages.UserMessage;
4139
import org.springframework.ai.chat.prompt.Prompt;
4240
import org.springframework.ai.chat.prompt.PromptTemplate;
4341
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
44-
import org.springframework.ai.chat.messages.Message;
45-
import org.springframework.ai.chat.messages.UserMessage;
42+
import org.springframework.ai.converter.BeanOutputConverter;
43+
import org.springframework.ai.converter.ListOutputConverter;
44+
import org.springframework.ai.converter.MapOutputConverter;
4645
import org.springframework.beans.factory.annotation.Autowired;
4746
import org.springframework.beans.factory.annotation.Value;
4847
import org.springframework.boot.SpringBootConfiguration;
@@ -108,9 +107,9 @@ void roleTest() {
108107
}
109108

110109
@Test
111-
void outputParser() {
110+
void listOutputConverter() {
112111
DefaultConversionService conversionService = new DefaultConversionService();
113-
ListOutputParser outputParser = new ListOutputParser(conversionService);
112+
ListOutputConverter outputParser = new ListOutputConverter(conversionService);
114113

115114
String format = outputParser.getFormat();
116115
String template = """
@@ -122,15 +121,15 @@ void outputParser() {
122121
Prompt prompt = new Prompt(promptTemplate.createMessage());
123122
Generation generation = this.client.call(prompt).getResult();
124123

125-
List<String> list = outputParser.parse(generation.getOutput().getContent());
124+
List<String> list = outputParser.convert(generation.getOutput().getContent());
126125
assertThat(list).hasSize(5);
127126
}
128127

129128
@Test
130-
void mapOutputParser() {
131-
MapOutputParser outputParser = new MapOutputParser();
129+
void mapOutputConvert() {
130+
MapOutputConverter outputConverter = new MapOutputConverter();
132131

133-
String format = outputParser.getFormat();
132+
String format = outputConverter.getFormat();
134133
String template = """
135134
Provide me a List of {subject}
136135
{format}
@@ -140,7 +139,7 @@ void mapOutputParser() {
140139
Prompt prompt = new Prompt(promptTemplate.createMessage());
141140
Generation generation = client.call(prompt).getResult();
142141

143-
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
142+
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
144143
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
145144

146145
}
@@ -149,11 +148,11 @@ record ActorsFilmsRecord(String actor, List<String> movies) {
149148
}
150149

151150
@Test
152-
void beanOutputParserRecords() {
151+
void beanOutputConverterRecords() {
153152

154-
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
153+
BeanOutputConverter<ActorsFilmsRecord> outputConvert = new BeanOutputConverter<>(ActorsFilmsRecord.class);
155154

156-
String format = outputParser.getFormat();
155+
String format = outputConvert.getFormat();
157156
String template = """
158157
Generate the filmography of 5 movies for Tom Hanks.
159158
Remove non JSON tex blocks from the output.
@@ -164,17 +163,17 @@ void beanOutputParserRecords() {
164163
Prompt prompt = new Prompt(promptTemplate.createMessage());
165164
Generation generation = client.call(prompt).getResult();
166165

167-
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
166+
ActorsFilmsRecord actorsFilms = outputConvert.convert(generation.getOutput().getContent());
168167
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
169168
assertThat(actorsFilms.movies()).hasSize(5);
170169
}
171170

172171
@Test
173-
void beanStreamOutputParserRecords() {
172+
void beanStreamOutputConverterRecords() {
174173

175-
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
174+
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
176175

177-
String format = outputParser.getFormat();
176+
String format = outputConverter.getFormat();
178177
String template = """
179178
Generate the filmography of 5 movies for Tom Hanks.
180179
{format}
@@ -193,7 +192,7 @@ void beanStreamOutputParserRecords() {
193192
.map(AssistantMessage::getContent)
194193
.collect(Collectors.joining());
195194

196-
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
195+
ActorsFilmsRecord actorsFilms = outputConverter.convert(generationTextFromStream);
197196
logger.info("" + actorsFilms);
198197
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
199198
assertThat(actorsFilms.movies()).hasSize(5);

0 commit comments

Comments
 (0)