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.
This commit is contained in:
Christian Tzolov
2024-05-02 12:28:37 +03:00
committed by Mark Pollack
parent 1c93ae50a8
commit a49a2d213f
39 changed files with 1013 additions and 455 deletions

View File

@@ -110,7 +110,7 @@ For a hands-on guide to PromptTemplate, see the [PromptTemplate API guide](https
**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.
Get insights on Output Parsers in our [concept guide](https://docs.spring.io/spring-ai/reference/concepts.html#_output_parsing)..
For implementation details, visit the [OutputParser API guide](https://docs.spring.io/spring-ai/reference/api/output-parser.html).
For implementation details, visit the [StructuredOutputConverter API guide](https://docs.spring.io/spring-ai/reference/api/output-parser.html).
### Incorporating your data

View File

@@ -40,10 +40,10 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
@@ -90,11 +90,11 @@ class AnthropicChatClientIT {
}
@Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter listOutputConverter = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String format = listOutputConverter.getFormat();
String template = """
List five {subject}
{format}
@@ -104,15 +104,15 @@ class AnthropicChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.chatClient.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = listOutputConverter.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConverter() {
MapOutputConverter mapOutputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = mapOutputConverter.getFormat();
String template = """
Provide me a List of {subject}
{format}
@@ -122,7 +122,7 @@ class AnthropicChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = mapOutputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@@ -131,11 +131,11 @@ class AnthropicChatClientIT {
}
@Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> beanOutputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = beanOutputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -144,18 +144,18 @@ class AnthropicChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = beanOutputConverter.convert(generation.getOutput().getContent());
logger.info("" + actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}
@Test
void beanStreamOutputParserRecords() {
void beanStreamOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> beanOutputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = beanOutputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -173,7 +173,7 @@ class AnthropicChatClientIT {
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
ActorsFilmsRecord actorsFilms = beanOutputConverter.convert(generationTextFromStream);
logger.info("" + actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);

View File

@@ -30,14 +30,14 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.Generation;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
@@ -74,11 +74,11 @@ class AzureOpenAiChatClientIT {
}
@Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter outputConverter = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
List five {subject}
{format}
@@ -88,16 +88,16 @@ class AzureOpenAiChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = outputConverter.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConverter() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Provide me a List of {subject}
{format}
@@ -107,17 +107,17 @@ class AzureOpenAiChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@Test
void beanOutputParser() {
void beanOutputConverter() {
BeanOutputParser<ActorsFilms> outputParser = new BeanOutputParser<>(ActorsFilms.class);
BeanOutputConverter<ActorsFilms> outputConverter = new BeanOutputConverter<>(ActorsFilms.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography for a random actor.
{format}
@@ -126,7 +126,7 @@ class AzureOpenAiChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
ActorsFilms actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilms actorsFilms = outputConverter.convert(generation.getOutput().getContent());
assertThat(actorsFilms.actor()).isNotNull();
}
@@ -134,11 +134,11 @@ class AzureOpenAiChatClientIT {
}
@Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -147,16 +147,16 @@ class AzureOpenAiChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent());
System.out.println(actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}
@Test
void beanStreamOutputParserRecords() {
void beanStreamOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputParser = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String template = """
@@ -177,7 +177,7 @@ class AzureOpenAiChatClientIT {
.filter(Objects::nonNull)
.collect(Collectors.joining());
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
ActorsFilmsRecord actorsFilms = outputParser.convert(generationTextFromStream);
System.out.println(actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);

View File

@@ -27,22 +27,21 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.AssistantMessage;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.Generation;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
@@ -108,9 +107,9 @@ class BedrockAnthropicChatClientIT {
}
@Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter outputParser = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String template = """
@@ -122,15 +121,15 @@ class BedrockAnthropicChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.client.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = outputParser.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConvert() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Provide me a List of {subject}
{format}
@@ -140,7 +139,7 @@ class BedrockAnthropicChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@@ -149,11 +148,11 @@ class BedrockAnthropicChatClientIT {
}
@Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConvert = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConvert.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
Remove non JSON tex blocks from the output.
@@ -164,17 +163,17 @@ class BedrockAnthropicChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = outputConvert.convert(generation.getOutput().getContent());
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}
@Test
void beanStreamOutputParserRecords() {
void beanStreamOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -193,7 +192,7 @@ class BedrockAnthropicChatClientIT {
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
ActorsFilmsRecord actorsFilms = outputConverter.convert(generationTextFromStream);
logger.info("" + actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);

View File

@@ -15,12 +15,21 @@
*/
package org.springframework.ai.bedrock.anthropic3;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi;
import org.springframework.ai.chat.ChatResponse;
@@ -32,9 +41,9 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
@@ -45,16 +54,6 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.MimeTypeUtils;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@@ -112,11 +111,11 @@ class BedrockAnthropic3ChatClientIT {
}
@Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter outputConverter = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
List five {subject}
{format}
@@ -126,15 +125,15 @@ class BedrockAnthropic3ChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.client.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = outputConverter.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConverter() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Provide me a List of {subject}
{format}
@@ -145,7 +144,7 @@ class BedrockAnthropic3ChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@@ -154,11 +153,11 @@ class BedrockAnthropic3ChatClientIT {
}
@Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -169,17 +168,17 @@ class BedrockAnthropic3ChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent());
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}
@Test
void beanStreamOutputParserRecords() {
void beanStreamOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -198,7 +197,7 @@ class BedrockAnthropic3ChatClientIT {
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
ActorsFilmsRecord actorsFilms = outputConverter.convert(generationTextFromStream);
logger.info("" + actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);

View File

@@ -25,8 +25,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.messages.AssistantMessage;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.regions.Region;
@@ -34,14 +32,15 @@ import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi;
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatModel;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.Generation;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
@@ -104,11 +103,11 @@ class BedrockCohereChatClientIT {
}
@Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter outputConverter = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
List five {subject}
{format}
@@ -118,15 +117,15 @@ class BedrockCohereChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.client.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = outputConverter.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConverter() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Remove Markdown code blocks from the output.
Provide me a List of {subject}
@@ -137,7 +136,7 @@ class BedrockCohereChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@@ -146,11 +145,11 @@ class BedrockCohereChatClientIT {
}
@Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -160,17 +159,17 @@ class BedrockCohereChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent());
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}
@Test
void beanStreamOutputParserRecords() {
void beanStreamOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -189,7 +188,7 @@ class BedrockCohereChatClientIT {
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
ActorsFilmsRecord actorsFilms = outputConverter.convert(generationTextFromStream);
System.out.println(actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);

View File

@@ -14,14 +14,21 @@
* limitations under the License.
*/
package org.springframework.ai.bedrock.jurassic2.api;
package org.springframework.ai.bedrock.jurassic2;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.bedrock.jurassic2.BedrockAi21Jurassic2ChatClient;
import org.springframework.ai.bedrock.jurassic2.BedrockAi21Jurassic2ChatOptions;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import org.springframework.ai.bedrock.jurassic2.api.Ai21Jurassic2ChatBedrockApi;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.Generation;
import org.springframework.ai.chat.messages.Message;
@@ -29,20 +36,13 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -109,10 +109,10 @@ class BedrockAi21Jurassic2ChatClientIT {
}
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConverter() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Provide me a List of {subject}
{format}
@@ -122,7 +122,7 @@ class BedrockAi21Jurassic2ChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}

View File

@@ -38,9 +38,9 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
@@ -104,11 +104,11 @@ class BedrockLlamaChatClientIT {
}
@Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter outputConverter = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
List five {subject}
{format}
@@ -118,15 +118,15 @@ class BedrockLlamaChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.client.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = outputConverter.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConverter() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Provide me a List of {subject}
{format}
@@ -136,7 +136,7 @@ class BedrockLlamaChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@@ -145,11 +145,11 @@ class BedrockLlamaChatClientIT {
}
@Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -160,17 +160,17 @@ class BedrockLlamaChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent());
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}
@Test
void beanStreamOutputParserRecords() {
void beanStreamOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -189,7 +189,7 @@ class BedrockLlamaChatClientIT {
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
ActorsFilmsRecord actorsFilms = outputConverter.convert(generationTextFromStream);
System.out.println(actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);

View File

@@ -26,23 +26,22 @@ import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.AssistantMessage;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi;
import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatModel;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.Generation;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
@@ -106,11 +105,11 @@ class BedrockTitanChatClientIT {
@Disabled("TODO: Fix the parser instructions to return the correct format")
@Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter outputConverter = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
List five {subject}
{format}
@@ -120,16 +119,16 @@ class BedrockTitanChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.client.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = outputConverter.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
@Disabled("TODO: Fix the parser instructions to return the correct format")
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConverter() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Remove Markdown code blocks from the output.
Provide me a List of {subject}
@@ -141,7 +140,7 @@ class BedrockTitanChatClientIT {
Generation generation = client.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@@ -151,11 +150,11 @@ class BedrockTitanChatClientIT {
@Disabled("TODO: Fix the parser instructions to return the correct format")
@Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -165,18 +164,18 @@ class BedrockTitanChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent());
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}
@Disabled("TODO: Fix the parser instructions to return the correct format")
@Test
void beanStreamOutputParserRecords() {
void beanStreamOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -195,7 +194,7 @@ class BedrockTitanChatClientIT {
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
ActorsFilmsRecord actorsFilms = outputConverter.convert(generationTextFromStream);
System.out.println(actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);

View File

@@ -37,11 +37,11 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.mistralai.api.MistralAiApi;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
@@ -96,11 +96,11 @@ class MistralAiChatClientIT {
}
@Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter outputConverter = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
List five {subject}
{format}
@@ -110,15 +110,15 @@ class MistralAiChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.chatClient.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = outputConverter.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConverter() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Provide me a List of {subject}
{format}
@@ -128,7 +128,7 @@ class MistralAiChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@@ -137,11 +137,11 @@ class MistralAiChatClientIT {
}
@Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -150,18 +150,18 @@ class MistralAiChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent());
logger.info("" + actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}
@Test
void beanStreamOutputParserRecords() {
void beanStreamOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -179,7 +179,7 @@ class MistralAiChatClientIT {
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
ActorsFilmsRecord actorsFilms = outputConverter.convert(generationTextFromStream);
logger.info("" + actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);

View File

@@ -26,30 +26,30 @@ import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.ollama.OllamaContainer;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.Generation;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.support.DefaultConversionService;
import org.testcontainers.ollama.OllamaContainer;
import static org.assertj.core.api.Assertions.assertThat;
@@ -119,11 +119,11 @@ class OllamaChatClientIT {
}
@Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter outputConverter = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
List five {subject}
{format}
@@ -133,15 +133,15 @@ class OllamaChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.client.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = outputConverter.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConvert() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Remove Markdown code blocks from the output.
Provide me a List of {subject}
@@ -153,7 +153,7 @@ class OllamaChatClientIT {
Generation generation = client.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@@ -162,11 +162,11 @@ class OllamaChatClientIT {
}
@Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -175,17 +175,17 @@ class OllamaChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent());
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}
@Test
void beanStreamOutputParserRecords() {
void beanStreamOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -204,7 +204,7 @@ class OllamaChatClientIT {
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
ActorsFilmsRecord actorsFilms = outputConverter.convert(generationTextFromStream);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
@@ -220,8 +220,7 @@ class OllamaChatClientIT {
@Bean
public OllamaChatClient ollamaChat(OllamaApi ollamaApi) {
return new OllamaChatClient(ollamaApi).withModel(MODEL)
.withDefaultOptions(OllamaOptions.create().withModel(MODEL).withTemperature(0.9f));
return new OllamaChatClient(ollamaApi, OllamaOptions.create().withModel(MODEL).withTemperature(0.9f));
}
}

View File

@@ -37,15 +37,15 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.OpenAiTestConfiguration;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.tool.MockWeatherService;
import org.springframework.ai.openai.testutils.AbstractIT;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.convert.support.DefaultConversionService;
@@ -67,7 +67,7 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void roleTest() {
UserMessage userMessage = new UserMessage(
"Tell me about 3 famous pirates from the Golden Age of Piracy and why they did.");
"Tell me about 3 famous pirates from the Golden Age of Piracy and what they did.");
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", "Bob", "voice", "pirate"));
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
@@ -78,11 +78,11 @@ class OpenAiChatClientIT extends AbstractIT {
}
@Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter outputConverter = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
List five {subject}
{format}
@@ -92,16 +92,16 @@ class OpenAiChatClientIT extends AbstractIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.chatClient.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = outputConverter.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConverter() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Provide me a List of {subject}
{format}
@@ -111,17 +111,17 @@ class OpenAiChatClientIT extends AbstractIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@Test
void beanOutputParser() {
void beanOutputConverter() {
BeanOutputParser<ActorsFilms> outputParser = new BeanOutputParser<>(ActorsFilms.class);
BeanOutputConverter<ActorsFilms> outputConverter = new BeanOutputConverter<>(ActorsFilms.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography for a random actor.
{format}
@@ -130,18 +130,18 @@ class OpenAiChatClientIT extends AbstractIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
ActorsFilms actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilms actorsFilms = outputConverter.convert(generation.getOutput().getContent());
}
record ActorsFilmsRecord(String actor, List<String> movies) {
}
@Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -150,18 +150,18 @@ class OpenAiChatClientIT extends AbstractIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent());
logger.info("" + actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}
@Test
void beanStreamOutputParserRecords() {
void beanStreamOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -179,7 +179,7 @@ class OpenAiChatClientIT extends AbstractIT {
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
ActorsFilmsRecord actorsFilms = outputConverter.convert(generationTextFromStream);
logger.info("" + actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);

View File

@@ -35,9 +35,9 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
@@ -75,9 +75,9 @@ class VertexAiGeminiChatClientIT {
}
@Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter outputParser = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String template = """
@@ -89,15 +89,15 @@ class VertexAiGeminiChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.client.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = outputParser.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
@Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConverter() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Provide me a List of {subject}
Remove the ```json outer brackets.
@@ -108,7 +108,7 @@ class VertexAiGeminiChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@@ -117,11 +117,11 @@ class VertexAiGeminiChatClientIT {
}
@Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConvert = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConvert.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
Remove the ```json outer brackets.
@@ -131,7 +131,7 @@ class VertexAiGeminiChatClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = outputConvert.convert(generation.getOutput().getContent());
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}
@@ -154,11 +154,11 @@ class VertexAiGeminiChatClientIT {
}
@Test
void beanStreamOutputParserRecords() {
void beanStreamOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
Remove the ```json outer brackets.
@@ -177,7 +177,7 @@ class VertexAiGeminiChatClientIT {
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
ActorsFilmsRecord actorsFilms = outputConverter.convert(generationTextFromStream);
// logger.info("" + actorsFilms);
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);

View File

@@ -29,9 +29,9 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.parser.BeanOutputParser;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.ai.parser.MapOutputParser;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.vertexai.palm2.api.VertexAiPaLm2Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -67,11 +67,11 @@ class VertexAiPaLm2ChatGenerationClientIT {
}
// @Test
void outputParser() {
void listOutputConverter() {
DefaultConversionService conversionService = new DefaultConversionService();
ListOutputParser outputParser = new ListOutputParser(conversionService);
ListOutputConverter outputConverter = new ListOutputConverter(conversionService);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
List five {subject}
{format}
@@ -81,16 +81,16 @@ class VertexAiPaLm2ChatGenerationClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.client.call(prompt).getResult();
List<String> list = outputParser.parse(generation.getOutput().getContent());
List<String> list = outputConverter.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
}
// @Test
void mapOutputParser() {
MapOutputParser outputParser = new MapOutputParser();
void mapOutputConverter() {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Provide me a List of {subject}
{format}
@@ -100,7 +100,7 @@ class VertexAiPaLm2ChatGenerationClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@@ -109,11 +109,11 @@ class VertexAiPaLm2ChatGenerationClientIT {
}
// @Test
void beanOutputParserRecords() {
void beanOutputConverterRecords() {
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
String format = outputParser.getFormat();
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks.
{format}
@@ -122,7 +122,7 @@ class VertexAiPaLm2ChatGenerationClientIT {
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = client.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent());
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
assertThat(actorsFilms.movies()).hasSize(5);
}

View File

@@ -15,17 +15,6 @@
*/
package org.springframework.ai.chat.prompt;
import org.antlr.runtime.Token;
import org.antlr.runtime.TokenStream;
import org.springframework.ai.chat.messages.Media;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.parser.OutputParser;
import org.springframework.core.io.Resource;
import org.springframework.util.StreamUtils;
import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.compiler.STLexer;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
@@ -37,6 +26,19 @@ import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import org.antlr.runtime.Token;
import org.antlr.runtime.TokenStream;
import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.compiler.STLexer;
import org.springframework.ai.chat.messages.Media;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.converter.StructuredOutputConverter;
import org.springframework.ai.parser.OutputParser;
import org.springframework.core.io.Resource;
import org.springframework.util.StreamUtils;
public class PromptTemplate implements PromptTemplateActions, PromptTemplateMessageActions {
private ST st;
@@ -49,6 +51,8 @@ public class PromptTemplate implements PromptTemplateActions, PromptTemplateMess
private OutputParser outputParser;
private StructuredOutputConverter structuredOutputConverter;
public PromptTemplate(Resource resource) {
try (InputStream inputStream = resource.getInputStream()) {
this.template = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
@@ -110,15 +114,31 @@ public class PromptTemplate implements PromptTemplateActions, PromptTemplateMess
}
}
/**
* @deprecated Use {@link #getOutputConverter()} instead.
*/
public OutputParser getOutputParser() {
return outputParser;
return this.outputParser;
}
/**
* @deprecated Use {@link #setOutputConverter(StructuredOutputConverter)}
* instead.
*/
public void setOutputParser(OutputParser outputParser) {
Objects.requireNonNull(outputParser, "Output Parser can not be null");
this.outputParser = outputParser;
}
public StructuredOutputConverter getOutputConverter() {
return this.structuredOutputConverter;
}
public void setOutputConverter(StructuredOutputConverter structuredOutputConverter) {
Objects.requireNonNull(structuredOutputConverter, "Structured Output Converter can not be null");
this.structuredOutputConverter = structuredOutputConverter;
}
public void add(String name, Object value) {
this.st.add(name, value);
this.dynamicModel.put(name, value);

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2023 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.converter;
import org.springframework.core.convert.support.DefaultConversionService;
/**
* Abstract {@link StructuredOutputConverter} implementation that uses a pre-configured
* {@link DefaultConversionService} to convert the LLM output into the desired type
* format.
*
* @param <T> Specifies the desired response type.
* @author Mark Pollack
* @author Christian Tzolov
*/
public abstract class AbstractConversionServiceOutputConverter<T> implements StructuredOutputConverter<T> {
private final DefaultConversionService conversionService;
public AbstractConversionServiceOutputConverter(DefaultConversionService conversionService) {
this.conversionService = conversionService;
}
public DefaultConversionService getConversionService() {
return this.conversionService;
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2023 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.converter;
import org.springframework.messaging.converter.MessageConverter;
/**
* Abstract {@link StructuredOutputConverter} implementation that uses a pre-configured
* {@link MessageConverter} to convert the LLM output into the desired type format.
*
* @param <T> Specifies the desired response type.
* @author Mark Pollack
* @author Christian Tzolov
*/
public abstract class AbstractMessageOutputConverter<T> implements StructuredOutputConverter<T> {
private MessageConverter messageConverter;
public AbstractMessageOutputConverter(MessageConverter messageConverter) {
this.messageConverter = messageConverter;
}
public MessageConverter getMessageConverter() {
return this.messageConverter;
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2023 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.converter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.module.jackson.JacksonModule;
import org.springframework.lang.NonNull;
import java.util.Map;
import java.util.Objects;
import static com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON;
import static com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12;
/**
* An implementation of {@link StructuredOutputConverter} that transforms the LLM output
* to a specific object type using JSON schema. This parser works by generating a JSON
* schema based on a given Java class, which is then used to validate and transform the
* LLM output into the desired type.
*
* @param <T> The target type to which the output will be converted.
* @author Mark Pollack
* @author Christian Tzolov
* @author Sebastian Ullrich
* @author Kirk Lund
*/
public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
/** Holds the generated JSON schema for the target type. */
private String jsonSchema;
/** The Java class representing the target type. */
@SuppressWarnings({ "FieldMayBeFinal", "rawtypes" })
private Class<T> clazz;
/** The object mapper used for deserialization and other JSON operations. */
@SuppressWarnings("FieldMayBeFinal")
private ObjectMapper objectMapper;
/**
* Constructor to initialize with the target type's class.
* @param clazz The target type's class.
*/
public BeanOutputConverter(Class<T> clazz) {
this(clazz, null);
}
/**
* Constructor to initialize with the target type's class, a custom object mapper, and
* a line endings normalizer to ensure consistent line endings on any platform.
* @param clazz The target type's class.
* @param objectMapper Custom object mapper for JSON operations. endings.
*/
public BeanOutputConverter(Class<T> clazz, ObjectMapper objectMapper) {
Objects.requireNonNull(clazz, "Java Class cannot be null;");
this.clazz = clazz;
this.objectMapper = objectMapper != null ? objectMapper : getObjectMapper();
generateSchema();
}
/**
* Generates the JSON schema for the target type.
*/
private void generateSchema() {
JacksonModule jacksonModule = new JacksonModule();
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(DRAFT_2020_12, PLAIN_JSON)
.with(jacksonModule);
SchemaGeneratorConfig config = configBuilder.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonNode = generator.generateSchema(this.clazz);
ObjectWriter objectWriter = new ObjectMapper().writer(new DefaultPrettyPrinter()
.withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator())));
try {
this.jsonSchema = objectWriter.writeValueAsString(jsonNode);
}
catch (JsonProcessingException e) {
throw new RuntimeException("Could not pretty print json schema for " + this.clazz, e);
}
}
@Override
/**
* Parses the given text to transform it to the desired target type.
* @param text The LLM output in string format.
* @return The parsed output in the desired target type.
*/
public T convert(@NonNull String text) {
try {
// If the response is a JSON Schema, extract the properties and use them as
// the response.
text = this.jsonSchemaToInstance(text);
return (T) this.objectMapper.readValue(text, this.clazz);
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* Converts a JSON Schema to an instance based on a given text.
* @param text The JSON Schema in string format.
* @return The JSON instance generated from the JSON Schema, or the original text if
* the input is not a JSON Schema.
*/
private String jsonSchemaToInstance(String text) {
try {
Map<String, Object> map = this.objectMapper.readValue(text, Map.class);
if (map.containsKey("$schema")) {
return this.objectMapper.writeValueAsString(map.get("properties"));
}
}
catch (Exception e) {
}
return text;
}
/**
* Configures and returns an object mapper for JSON operations.
* @return Configured object mapper.
*/
protected ObjectMapper getObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
/**
* Provides the expected format of the response, instructing that it should adhere to
* the generated JSON schema.
* @return The instruction format string.
*/
@Override
public String getFormat() {
String template = """
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```%s```
""";
return String.format(template, this.jsonSchema);
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2023 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.converter;
/**
* Implementations of this interface provides instructions for how the output of a
* language generative should be formatted.
*
* @author Mark Pollack
*/
public interface FormatProvider {
/**
* @return Returns a string containing instructions for how the output of a language
* generative should be formatted.
*/
String getFormat();
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2023 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.converter;
import java.util.List;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.lang.NonNull;
/**
* {@link StructuredOutputConverter} implementation that uses a
* {@link DefaultConversionService} to convert the LLM output into a
* {@link java.util.List} instance.
*
* @author Mark Pollack
* @author Christian Tzolov
*/
public class ListOutputConverter extends AbstractConversionServiceOutputConverter<List<String>> {
public ListOutputConverter(DefaultConversionService defaultConversionService) {
super(defaultConversionService);
}
@Override
public String getFormat() {
return """
Your response should be a list of comma separated values
eg: `foo, bar, baz`
""";
}
@Override
public List<String> convert(@NonNull String text) {
return this.getConversionService().convert(text, List.class);
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2023 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.converter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import org.springframework.lang.NonNull;
import org.springframework.messaging.Message;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.support.MessageBuilder;
/**
* {@link StructuredOutputConverter} implementation that uses a pre-configured
* {@link MappingJackson2MessageConverter} to convert the LLM output into a
* java.util.Map&lt;String, Object&gt; instance.
*
* @author Mark Pollack
* @author Christian Tzolov
*/
public class MapOutputConverter extends AbstractMessageOutputConverter<Map<String, Object>> {
public MapOutputConverter() {
super(new MappingJackson2MessageConverter());
}
@Override
public Map<String, Object> convert(@NonNull String text) {
Message<?> message = MessageBuilder.withPayload(text.getBytes(StandardCharsets.UTF_8)).build();
return (Map) this.getMessageConverter().fromMessage(message, HashMap.class);
}
@Override
public String getFormat() {
String raw = """
Your response should be in JSON format.
The data structure for the JSON should match this Java class: %s
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
""";
return String.format(raw, HashMap.class.getName());
}
}

View File

@@ -0,0 +1,13 @@
# Structured Output
* [Documentation](https://docs.spring.io/spring-ai/reference/concepts.html#_output_parsing)
* [Usage examples](https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java)
The output of AI models traditionally arrives as a text, even if you ask for the reply to be in JSON.
It may be the correct JSON, but it isnt a JSON data structure.
It is just a string.
Also, asking "for JSON" as part of the prompt isnt 100% accurate.
This intricacy has led to the emergence of a specialized field involving the creation of prompts to yield the intended output, followed by converting the resulting simple string into a usable data structure for application integration.
Structure output conversion employs meticulously crafted prompts, often necessitating multiple interactions with the model to achieve the desired formatting.

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2023 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
/**
* Converts the (raw) LLM output into a structured responses of type. The
* {@link FormatProvider#getFormat()} method should provide the LLM prompt description of
* the desired format.
*
* @param <T> Specifies the desired response type.
* @author Mark Pollack
* @author Christian Tzolov
*/
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
/**
* @deprecated Use the {@link #convert(Object)} instead.
*/
default T parse(@NonNull String source) {
return this.convert(source);
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,15 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.parser;
import org.springframework.core.convert.support.DefaultConversionService;
/**
* @deprecated Use the
* {@link org.springframework.ai.converter.AbstractConversionServiceOutputConverter}
* instead.
*
* Abstract {@link OutputParser} implementation that uses a pre-configured
* {@link DefaultConversionService} to convert the LLM output into the desired type
* format.
*
* @param <T> Specifies the desired response type.
* @author Mark Pollack
* @author Christian Tzolov
@@ -38,4 +42,4 @@ public abstract class AbstractConversionServiceOutputParser<T> implements Output
return conversionService;
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,14 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.parser;
import org.springframework.messaging.converter.MessageConverter;
/**
* @deprecated Use the
* {@link org.springframework.ai.converter.AbstractMessageOutputConverter} instead.
*
* Abstract {@link OutputParser} implementation that uses a pre-configured
* {@link MessageConverter} to convert the LLM output into the desired type format.
*
* @param <T> Specifies the desired response type.
* @author Mark Pollack
* @author Christian Tzolov
@@ -37,4 +40,4 @@ public abstract class AbstractMessageConverterOutputParser<T> implements OutputP
return this.messageConverter;
}
}
}

View File

@@ -34,11 +34,13 @@ import static com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON;
import static com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12;
/**
* @deprecated Use the {@link org.springframework.ai.converter.BeanOutputConverter}
* instead.
*
* An implementation of {@link OutputParser} that transforms the LLM output to a specific
* object type using JSON schema. This parser works by generating a JSON schema based on a
* given Java class, which is then used to validate and transform the LLM output into the
* desired type.
*
* @param <T> The target type to which the output will be converted.
* @author Mark Pollack
* @author Christian Tzolov

View File

@@ -20,6 +20,7 @@ package org.springframework.ai.parser;
* language generative should be formatted.
*
* @author Mark Pollack
* @deprecated Use the {@link org.springframework.ai.converter.FormatProvider} instead.
*/
public interface FormatProvider {

View File

@@ -20,9 +20,11 @@ import java.util.List;
import org.springframework.core.convert.support.DefaultConversionService;
/**
* @deprecated Use the {@link org.springframework.ai.converter.ListOutputConverter}
* instead.
*
* {@link OutputParser} implementation that uses a {@link DefaultConversionService} to
* convert the LLM output into a {@link java.util.List} instance.
*
* @author Mark Pollack
* @author Christian Tzolov
*/

View File

@@ -24,10 +24,12 @@ import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.support.MessageBuilder;
/**
* @deprecated Use the {@link org.springframework.ai.converter.MapOutputConverter}
* instead.
*
* {@link OutputParser} implementation that uses a pre-configured
* {@link MappingJackson2MessageConverter} to convert the LLM output into a
* java.util.Map&lt;String, Object&gt; instance.
*
* @author Mark Pollack
* @author Christian Tzolov
*/

View File

@@ -23,6 +23,8 @@ package org.springframework.ai.parser;
* @param <T> Specifies the desired response type.
* @author Mark Pollack
* @author Christian Tzolov
* @deprecated Use the {@link org.springframework.ai.converter.StructuredOutputConverter}
* instead.
*/
public interface OutputParser<T> extends Parser<T>, FormatProvider {

View File

@@ -1,7 +1,8 @@
Deprecated! uset the Structured output instead.
# Output Parsing
* [Documentation](https://docs.spring.io/spring-ai/reference/concepts.html#_output_parsing)
* [Usage examples](https://github.com/spring-projects/spring-ai/blob/main/spring-ai-openai/src/test/java/org/springframework/ai/openai/client/ClientIT.java)
The output of AI models traditionally arrives as a java.util.String, even if you ask for the reply to be in JSON. It may be the correct JSON, but it isnt a JSON data structure. It is just a string. Also, asking "for JSON" as part of the prompt isnt 100% accurate.

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.parser;
package org.springframework.ai.converter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
@@ -34,17 +34,18 @@ import static org.mockito.Mockito.when;
/**
* @author Sebastian Ullrich
* @author Kirk Lund
* @author Christian Tzolov
*/
@ExtendWith(MockitoExtension.class)
class BeanOutputParserTest {
class BeanOutputConverterTest {
@Mock
private ObjectMapper objectMapperMock;
@Test
public void shouldHavePreconfiguredDefaultObjectMapper() {
var parser = new BeanOutputParser<>(TestClass.class);
var objectMapper = parser.getObjectMapper();
public void shouldHavePreConfiguredDefaultObjectMapper() {
var converter = new BeanOutputConverter<>(TestClass.class);
var objectMapper = converter.getObjectMapper();
assertThat(objectMapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse();
}
@@ -52,8 +53,8 @@ class BeanOutputParserTest {
public void shouldUseProvidedObjectMapperForParsing() throws JsonProcessingException {
var testClass = new TestClass("some string");
when(objectMapperMock.readValue(anyString(), eq(TestClass.class))).thenReturn(testClass);
var parser = new BeanOutputParser<>(TestClass.class, objectMapperMock);
assertThat(parser.parse("{}")).isEqualTo(testClass);
var converter = new BeanOutputConverter<>(TestClass.class, objectMapperMock);
assertThat(converter.convert("{}")).isEqualTo(testClass);
}
@Nested
@@ -61,15 +62,15 @@ class BeanOutputParserTest {
@Test
public void shouldParseFieldNamesFromString() {
var parser = new BeanOutputParser<>(TestClass.class);
var testClass = parser.parse("{ \"someString\": \"some value\" }");
var converter = new BeanOutputConverter<>(TestClass.class);
var testClass = converter.convert("{ \"someString\": \"some value\" }");
assertThat(testClass.getSomeString()).isEqualTo("some value");
}
@Test
public void shouldParseJsonPropertiesFromString() {
var parser = new BeanOutputParser<>(TestClassWithJsonAnnotations.class);
var testClass = parser.parse("{ \"string_property\": \"some value\" }");
var converter = new BeanOutputConverter<>(TestClassWithJsonAnnotations.class);
var testClass = converter.convert("{ \"string_property\": \"some value\" }");
assertThat(testClass.getSomeString()).isEqualTo("some value");
}
@@ -80,12 +81,13 @@ class BeanOutputParserTest {
@Test
public void shouldReturnFormatContainingResponseInstructionsAndJsonSchema() {
var parser = new BeanOutputParser<>(TestClass.class);
assertThat(parser.getFormat()).isEqualTo(
var converter = new BeanOutputConverter<>(TestClass.class);
assertThat(converter.getFormat()).isEqualTo(
"""
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
@@ -101,8 +103,8 @@ class BeanOutputParserTest {
@Test
public void shouldReturnFormatContainingJsonSchemaIncludingPropertyAndPropertyDescription() {
var parser = new BeanOutputParser<>(TestClassWithJsonAnnotations.class);
assertThat(parser.getFormat()).contains("""
var converter = new BeanOutputConverter<>(TestClassWithJsonAnnotations.class);
assertThat(converter.getFormat()).contains("""
```{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
@@ -118,9 +120,9 @@ class BeanOutputParserTest {
@Test
void normalizesLineEndings() {
BeanOutputParser<TestClass> parser = new BeanOutputParser<>(TestClass.class);
var converter = new BeanOutputConverter<>(TestClass.class);
String formatOutput = parser.getFormat();
String formatOutput = converter.getFormat();
// validate that output contains \n line endings
assertThat(formatOutput).contains(System.lineSeparator()).doesNotContain("\r\n").doesNotContain("\r");

View File

@@ -13,23 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.prompt.parsers;
import org.junit.jupiter.api.Test;
import org.springframework.ai.parser.ListOutputParser;
import org.springframework.core.convert.support.DefaultConversionService;
package org.springframework.ai.converter;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.support.DefaultConversionService;
import static org.assertj.core.api.Assertions.assertThat;
class ListOutputParserTest {
class ListOutputConverterTest {
@Test
void csv() {
String csvAsString = "foo, bar, baz";
ListOutputParser listOutputParser = new ListOutputParser(new DefaultConversionService());
List<String> list = listOutputParser.parse(csvAsString);
ListOutputConverter listOutputConverter = new ListOutputConverter(new DefaultConversionService());
List<String> list = listOutputConverter.convert(csvAsString);
assertThat(list).containsExactlyElementsOf(List.of("foo", "bar", "baz"));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

View File

@@ -64,7 +64,7 @@
** xref:api/functions.adoc[Function Calling]
** xref:api/multimodality.adoc[Multimodality]
** xref:api/prompt.adoc[]
** xref:api/output-parser.adoc[]
** xref:api/structured-output-converter.adoc[Structured Output]
** xref:api/etl-pipeline.adoc[]
** xref:api/testing.adoc[]
** xref:api/generic-model.adoc[]

View File

@@ -1,123 +0,0 @@
[[OutputParser]]
= Output Parsers
The `OutputParser` interface allows you to obtain structured output, for example mapping the output to a Java class or an array of values from the String based output of AI Models.
You can think of it in terms similar to Spring JDBC's concept of a `RowMapper` or `ResultSetExtractor`.
Developers want to quickly turn results from an AI model into data types that can be passed to other functions and methods in their application.
The `OutputParser` helps achieve that goal.
== API Overview
This section provides a guide to the `OutputParser` interface.
=== OutputParser
Here is the `OutputParser` interface definition
```java
public interface OutputParser<T> extends Parser<T>, FormatProvider {
}
```
It combines the `Parser<T>` interface
```java
@FunctionalInterface
public interface Parser<T> {
T parse(String text);
}
```
and the `FormatProvider` interface
```java
public interface FormatProvider {
String getFormat();
}
```
The `Parser` interface parses text strings to produce instances of the type T.
The `FormatProvider` provides text instructions for the AI Model to format the output so that it can be parsed into the type T by the `Parser`.
These text instructions are most often appended to the end of the user input to the AI Model.
== Available Implementations
The `OutputParser` interface has the following available implementations.
* `BeanOutputParser`: Specifies the JSON schema for Java class and uses `DRAFT_2020_12` of the JSON schema specification as OpenAI has indicated this would give the best results.
The JSON output of the AI Model is then deserialized to a Java object, aka `JavaBean`.
* `MapOutputParser`: Similar to `BeanOutputParser` but the JSON payload is deserialized into a `java.util.Map<String, Object>` instance.
* `ListOutputParser`: Specifies the output to be a comma delimited list.
There has been considerable effort in recent OpenAI models to improve the model's ability to return JSON by simply specifying 'return in JSON', but not all models support such direct support for returning structured data.
== Example Usage
You can run a fully working example that demonstrates the use of `BeanOutputParser` as part of the https://github.com/Azure-Samples/spring-ai-azure-workshop[Spring AI Azure Workshop].
Part of this workshop code is reproduced below.
The use case for the example is to use the AI Model to generate the filmography for an actor.
The User prompt used is
```
String userMessage = """
Generate the filmography for the actor {actor}.
{format}
""";
```
The class `ActorsFilms` shown below
```java
public class ActorsFilms {
private String actor;
private List<String> movies;
// getters and toString omitted
}
```
Here is a controller class that shows these classes in use
```java
@GetMapping("/ai/output")
public ActorsFilms generate(@RequestParam(value = "actor", defaultValue = "Jeff Bridges") String actor) {
var outputParser = new BeanOutputParser<>(ActorsFilms.class);
String userMessage =
"""
Generate the filmography for the actor {actor}.
{format}
""";
PromptTemplate promptTemplate = new PromptTemplate(userMessage, Map.of("actor", actor, "format", outputParser.getFormat() ));
Prompt prompt = promptTemplate.create();
Generation generation = chatClient.call(prompt).getResult();
ActorsFilms actorsFilms = outputParser.parse(generation.getOutput().getContent());
return actorsFilms;
}
```

View File

@@ -0,0 +1,207 @@
[[StructuredOutputConverter]]
= Structured Output Converter
NOTE: As of 02.05.2024 the old `OutputParser`, `BeanOutputParser`, `ListOutputParser` and `MapOutputParser` classes are deprecated in favor of the new `StructuredOutputConverter`, `BeanOutputConverter`, `ListOutputConverter` and `MapOutputConverter` implementations.
Later are drop-in replacements for the former ones and provide the same functionality. The reason for the change was primarily naming, as there isn't any parsing being done, but also have aligned with the Spring `org.springframework.core.convert.converter` package brining in some improved functionality.
The ability of LLMs to produce structured outputs is important for downstream applications that rely on reliably parsing output values.
Developers want to quickly turn results from an AI model into data types, such as JSON, XML or Java Classes, that can be passed to other application functions and methods.
The Spring AI `Structured Output Converters` help to convert the LLM output into a structured format.
As shown in the following diagram, this approach operates around the LLM text completion endpoint:
image::structured-output-architecture.jpg[Structured Output Converter Architecture, width=900, align="center"]
Generating structured outputs from Large Language Models (LLMs) using generic completion APIs requires careful handling of inputs and outputs. The structured output converter plays a crucial role before and after the LLM call, ensuring the desired output structure is achieved.
Before the LLM call, the converter appends format instructions to the prompt, providing explicit guidance to the models on generating the desired output structure. These instructions act as a blueprint, shaping the model's response to conform to the specified format.
After the LLM call, the converter takes the model's output text and transforms it into instances of the structured type. This conversion process involves parsing the raw text output and mapping it to the corresponding structured data representation, such as JSON, XML, or domain-specific data structures.
TIP: The `StructuredOutputConverter` is a best effort to convert the model output into a structured output.
The AI Model is not guaranteed to return the structured output as requested.
The model may not understand the prompt or be unable to generate the structured output as requested.
Consider implementing a validation mechanism to ensure the model output is as expected.
TIP: The `StructuredOutputConverter` is not used for LLM xref:api/functions.adoc[Function Calling], as this feature inherently provides structured outputs by default.
== Structured Output API
The `StructuredOutputConverter` interface allows you to obtain structured output, such as mapping the output to a Java class or an array of values from the text-based AI Model output.
The interface definition is:
[source,java]
----
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
}
----
It combines the Spring https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/converter/Converter.html[Converter<String, T>] interface and the `FormatProvider` interface
[source,java]
----
public interface FormatProvider {
String getFormat();
}
----
The following diagram shows the data flow when using the structured output API.
image::structured-output-api.jpg[Structured Output API, width=900, align="center"]
The `FormatProvider` supplies specific formatting guidelines to the AI Model, enabling it to produce text outputs that can be converted into the designated target type `T` using the `Converter`. Here is an example of such formatting instructions:
----
Your response should be in JSON format.
The data structure for the JSON should match this Java class: java.util.HashMap
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
----
The format instructions are most often appended to the end of the user input using the xref:api/prompt.adoc#_prompttemplate[PromptTemplate] like this:
[source,java]
----
StructuredOutputConverter outputConverter = ...
String userInputTemplate = """
... user text input ....
{format}
"""; // user input with a "format" placeholder.
Prompt prompt = new Prompt(
new PromptTemplate(
userInputTemplate,
Map.of(..., "format", outputConverter.getFormat()) // replace the "format" placeholder with the converter's format.
).createMessage());
----
The Converter<String, T> is responsible to transform output text from the model into instances of the specified type `T`.
=== Available Converters
Currently, Spring AI provides `AbstractConversionServiceOutputConverter`, `AbstractMessageOutputConverter`, `BeanOutputConverter`, `MapOutputConverter` and `ListOutputConverter` implementations:
image::structured-output-hierarchy4.jpg[Structured Output Class Hierarchy, width=900, align="center"]
* `AbstractConversionServiceOutputConverter<T>` - Offers a pre-configured link:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/support/GenericConversionService.html[GenericConversionService] for transforming LLM output into the desired format. No default `FormatProvider` implementation is provided.
* `AbstractMessageOutputConverter` - Supplies a pre-configured https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jms/support/converter/MessageConverter.html[MessageConverter] for converting LLM output into the desired format. No default `FormatProvider` implementation is provided.
* `BeanOutputConverter` - Configured with a designated Java class (e.g., Bean), this converter employs a `FormatProvider` implementation that directs the AI Model to produce a JSON response compliant with a `DRAFT_2020_12`, `JSON Schema` derived from the specified Java class. Subsequently, it utilizes an `ObjectMapper` to deserialize the JSON output into a Java object instance of the target class.
* `MapOutputConverter` - Extends the functionality of `AbstractMessageOutputConverter` with a `FormatProvider` implementation that guides the AI Model to generate an RFC8259 compliant JSON response. Additionally, it incorporates a converter implementation that utilizes the provided `MessageConverter` to translate the JSON payload into a `java.util.Map<String, Object>` instance.
* `ListOutputConverter` - Extends the `AbstractConversionServiceOutputConverter` and includes a `FormatProvider` implementation tailored for comma-delimited list output. The converter implementation employs the provided `ConversionService` to transform the model text output into a `java.util.List`.
== Using Converters
The following sections provide guides how to use the available converters to generate structured outputs.
=== Bean Output Converter
The following example shows how to use `BeanOutputConverter` to generate the filmography for an actor.
The target record representing actor's filmography:
[source,java]
----
record ActorsFilms(String actor, List<String> movies) {
}
----
Here is how to apply the BeanOutputConverter:
[source,java]
----
BeanConverter<ActorsFilms> beanOutputConverter =
new BeanOutputConverter<>(ActorsFilms.class);
String format = beanOutputConverter.getFormat();
String actor = "Tom Hanks";
String template = """
Generate the filmography of 5 movies for {actor}.
{format}
""";
Generation generation = chatClient.call(
new Prompt(new PromptTemplate(template, Map.of("actor", actor, "format", format)).createMessage())).getResult();
ActorsFilms actorsFilms = beanOutputConverter.convert(generation.getOutput().getContent());
----
=== Map Output Converter
Following sniped shows how to use `MapOutputConverter` to generate a list of numbers.
[source,java]
----
MapOutputConverter mapOutputConverter = new MapOutputConverter();
String format = mapOutputConverter.getFormat();
String template = """
Provide me a List of {subject}
{format}
""";
PromptTemplate promptTemplate = new PromptTemplate(template,
Map.of("subject", "an array of numbers from 1 to 9 under they key name 'numbers'", "format", format));
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
Map<String, Object> result = mapOutputConverter.convert(generation.getOutput().getContent());
----
=== List Output Converter
Following snippet shows how to use `ListOutputConverter` to generate a list of ice cream flavors.
[source,java]
----
ListOutputConverter listOutputConverter = new ListOutputConverter(new DefaultConversionService());
String format = listOutputConverter.getFormat();
String template = """
List five {subject}
{format}
""";
PromptTemplate promptTemplate = new PromptTemplate(template,
Map.of("subject", "ice cream flavors", "format", format));
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.chatClient.call(prompt).getResult();
List<String> list = listOutputConverter.convert(generation.getOutput().getContent());
----
== Supported AI Models
The following AI Models have been tested to support List, Map and Bean structured outputs.
[cols="2,5"]
|====
| Model | Integration Tests / Samples
| xref:api/chat/openai-chat.adoc[OpenAI] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java[OpenAiChatClientIT]
| xref:api/chat/anthropic-chat.adoc[Anthropic Claude 3] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatClientIT.java[AnthropicChatClientIT.java]
| xref:api/chat/azure-openai-chat.adoc[Azure OpenAI] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatClientIT.java[AzureOpenAiChatClientIT.java]
| xref:api/chat/mistralai-chat.adoc[Mistral AI] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java[MistralAiChatClientIT.java]
| xref:api/chat/ollama-chat.adoc[Ollama] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatClientIT.java[OllamaChatClientIT.java]
| xref:api/chat/vertexai-gemini-chat.adoc[Vertex AI Gemini] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatClientIT.java[VertexAiGeminiChatClientIT.java]
| xref:api/chat/bedrock/bedrock-anthropic.adoc[Bedrock Anthropic 2] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic/BedrockAnthropicChatClientIT.java[BedrockAnthropicChatClientIT.java]
| xref:api/chat/bedrock/bedrock-anthropic3.adoc[Bedrock Anthropic 3] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic3/BedrockAnthropic3ChatClientIT.java[BedrockAnthropic3ChatClientIT.java]
| xref:api/chat/bedrock/bedrock-cohere.adoc[Bedrock Cohere] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/BedrockCohereChatClientIT.java[BedrockCohereChatClientIT.java]
| xref:api/chat/bedrock/bedrock-llama.adoc[Bedrock Llama] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/llama/BedrockLlamaChatClientIT.java[BedrockLlamaChatClientIT.java.java]
|====
== Build-in JSON mode
Some AI Models provide dedicated configuration options to generate structured (usually JSON) output.
* xref:api/chat/openai-chat.adoc[OpenAI] - provides a `spring.ai.openai.chat.options.responseFormat` options specifying the format that the model must output. Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON.
* xref:api/chat/ollama-chat.adoc[Ollama] - provides a `spring.ai.ollama.chat.options.format` option to specify the format to return a response in. Currently the only accepted value is `json`.
* xref:api/chat/mistralai-chat.adoc[Mistral AI] - provides a `spring.ai.mistralai.chat.options.responseFormat` option to specify the format to return a response in. Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON.