diff --git a/README.md b/README.md index 1c53af2bd..6feaf43c5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatClientIT.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatClientIT.java index 74bf7a3d0..7bc7ef2af 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatClientIT.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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); diff --git a/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatClientIT.java b/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatClientIT.java index a2fa5e24d..253e13d57 100644 --- a/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatClientIT.java +++ b/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilms.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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); diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic/BedrockAnthropicChatClientIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic/BedrockAnthropicChatClientIT.java index 9ec99a9db..43eb7e776 100644 --- a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic/BedrockAnthropicChatClientIT.java +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic/BedrockAnthropicChatClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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); diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic3/BedrockAnthropic3ChatClientIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic3/BedrockAnthropic3ChatClientIT.java index 4fa594020..8a98b8821 100644 --- a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic3/BedrockAnthropic3ChatClientIT.java +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic3/BedrockAnthropic3ChatClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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); diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/BedrockCohereChatClientIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/BedrockCohereChatClientIT.java index 95c700685..567926810 100644 --- a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/BedrockCohereChatClientIT.java +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/BedrockCohereChatClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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); diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/jurassic2/api/BedrockAi21Jurassic2ChatClientIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/jurassic2/BedrockAi21Jurassic2ChatClientIT.java similarity index 92% rename from models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/jurassic2/api/BedrockAi21Jurassic2ChatClientIT.java rename to models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/jurassic2/BedrockAi21Jurassic2ChatClientIT.java index 0fca363d7..f6614b852 100644 --- a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/jurassic2/api/BedrockAi21Jurassic2ChatClientIT.java +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/jurassic2/BedrockAi21Jurassic2ChatClientIT.java @@ -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 result = outputParser.parse(generation.getOutput().getContent()); + Map result = outputConverter.convert(generation.getOutput().getContent()); assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)); } diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/llama/BedrockLlamaChatClientIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/llama/BedrockLlamaChatClientIT.java index 554afcf9f..3ecaf8650 100644 --- a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/llama/BedrockLlamaChatClientIT.java +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/llama/BedrockLlamaChatClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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); diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/BedrockTitanChatClientIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/BedrockTitanChatClientIT.java index f3a523be2..54af175be 100644 --- a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/BedrockTitanChatClientIT.java +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/BedrockTitanChatClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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); diff --git a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java index 00cbf6973..87e6acbdb 100644 --- a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java +++ b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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); diff --git a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatClientIT.java b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatClientIT.java index 298eb2fd1..6f4254fa4 100644 --- a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatClientIT.java +++ b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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)); } } diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java index 82f034ffa..e82395ff0 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilms.class); + BeanOutputConverter 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 movies) { } @Test - void beanOutputParserRecords() { + void beanOutputConverterRecords() { - BeanOutputParser outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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); diff --git a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatClientIT.java b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatClientIT.java index 3f5783c6e..b9cfa126c 100644 --- a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatClientIT.java +++ b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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); diff --git a/models/spring-ai-vertex-ai-palm2/src/test/java/org/springframework/ai/vertexai/palm2/VertexAiPaLm2ChatGenerationClientIT.java b/models/spring-ai-vertex-ai-palm2/src/test/java/org/springframework/ai/vertexai/palm2/VertexAiPaLm2ChatGenerationClientIT.java index b66133267..5fd43b0d5 100644 --- a/models/spring-ai-vertex-ai-palm2/src/test/java/org/springframework/ai/vertexai/palm2/VertexAiPaLm2ChatGenerationClientIT.java +++ b/models/spring-ai-vertex-ai-palm2/src/test/java/org/springframework/ai/vertexai/palm2/VertexAiPaLm2ChatGenerationClientIT.java @@ -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 list = outputParser.parse(generation.getOutput().getContent()); + List 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 result = outputParser.parse(generation.getOutput().getContent()); + Map 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 outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class); + BeanOutputConverter 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); } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/PromptTemplate.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/PromptTemplate.java index 8bf2166fd..b663e990d 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/PromptTemplate.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/PromptTemplate.java @@ -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); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/converter/AbstractConversionServiceOutputConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/converter/AbstractConversionServiceOutputConverter.java new file mode 100644 index 000000000..6f209fa86 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/converter/AbstractConversionServiceOutputConverter.java @@ -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 Specifies the desired response type. + * @author Mark Pollack + * @author Christian Tzolov + */ +public abstract class AbstractConversionServiceOutputConverter implements StructuredOutputConverter { + + private final DefaultConversionService conversionService; + + public AbstractConversionServiceOutputConverter(DefaultConversionService conversionService) { + this.conversionService = conversionService; + } + + public DefaultConversionService getConversionService() { + return this.conversionService; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/converter/AbstractMessageOutputConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/converter/AbstractMessageOutputConverter.java new file mode 100644 index 000000000..05077025c --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/converter/AbstractMessageOutputConverter.java @@ -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 Specifies the desired response type. + * @author Mark Pollack + * @author Christian Tzolov + */ +public abstract class AbstractMessageOutputConverter implements StructuredOutputConverter { + + private MessageConverter messageConverter; + + public AbstractMessageOutputConverter(MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + public MessageConverter getMessageConverter() { + return this.messageConverter; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java new file mode 100644 index 000000000..a26b5e97b --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java @@ -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 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 implements StructuredOutputConverter { + + /** Holds the generated JSON schema for the target type. */ + private String jsonSchema; + + /** The Java class representing the target type. */ + @SuppressWarnings({ "FieldMayBeFinal", "rawtypes" }) + private Class 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 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 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 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); + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/converter/FormatProvider.java b/spring-ai-core/src/main/java/org/springframework/ai/converter/FormatProvider.java new file mode 100644 index 000000000..9afbc14e3 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/converter/FormatProvider.java @@ -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(); + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/converter/ListOutputConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/converter/ListOutputConverter.java new file mode 100644 index 000000000..d8b4dea51 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/converter/ListOutputConverter.java @@ -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> { + + 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 convert(@NonNull String text) { + return this.getConversionService().convert(text, List.class); + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/converter/MapOutputConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/converter/MapOutputConverter.java new file mode 100644 index 000000000..57f1ea93f --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/converter/MapOutputConverter.java @@ -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<String, Object> instance. + * + * @author Mark Pollack + * @author Christian Tzolov + */ +public class MapOutputConverter extends AbstractMessageOutputConverter> { + + public MapOutputConverter() { + super(new MappingJackson2MessageConverter()); + } + + @Override + public Map 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()); + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/converter/README.md b/spring-ai-core/src/main/java/org/springframework/ai/converter/README.md new file mode 100644 index 000000000..f1a4f7004 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/converter/README.md @@ -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 isn’t a JSON data structure. +It is just a string. +Also, asking "for JSON" as part of the prompt isn’t 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. diff --git a/spring-ai-core/src/main/java/org/springframework/ai/converter/StructuredOutputConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/converter/StructuredOutputConverter.java new file mode 100644 index 000000000..4756468a8 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/converter/StructuredOutputConverter.java @@ -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 Specifies the desired response type. + * @author Mark Pollack + * @author Christian Tzolov + */ +public interface StructuredOutputConverter extends Converter, FormatProvider { + + /** + * @deprecated Use the {@link #convert(Object)} instead. + */ + default T parse(@NonNull String source) { + return this.convert(source); + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/parser/AbstractConversionServiceOutputParser.java b/spring-ai-core/src/main/java/org/springframework/ai/parser/AbstractConversionServiceOutputParser.java index 7f2f61ee0..4e5224863 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/parser/AbstractConversionServiceOutputParser.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/parser/AbstractConversionServiceOutputParser.java @@ -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 Specifies the desired response type. * @author Mark Pollack * @author Christian Tzolov @@ -38,4 +42,4 @@ public abstract class AbstractConversionServiceOutputParser implements Output return conversionService; } -} +} \ No newline at end of file diff --git a/spring-ai-core/src/main/java/org/springframework/ai/parser/AbstractMessageConverterOutputParser.java b/spring-ai-core/src/main/java/org/springframework/ai/parser/AbstractMessageConverterOutputParser.java index 146ce323b..cc1e71504 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/parser/AbstractMessageConverterOutputParser.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/parser/AbstractMessageConverterOutputParser.java @@ -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 Specifies the desired response type. * @author Mark Pollack * @author Christian Tzolov @@ -37,4 +40,4 @@ public abstract class AbstractMessageConverterOutputParser implements OutputP return this.messageConverter; } -} +} \ No newline at end of file diff --git a/spring-ai-core/src/main/java/org/springframework/ai/parser/BeanOutputParser.java b/spring-ai-core/src/main/java/org/springframework/ai/parser/BeanOutputParser.java index 7be67324d..edd472220 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/parser/BeanOutputParser.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/parser/BeanOutputParser.java @@ -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 The target type to which the output will be converted. * @author Mark Pollack * @author Christian Tzolov diff --git a/spring-ai-core/src/main/java/org/springframework/ai/parser/FormatProvider.java b/spring-ai-core/src/main/java/org/springframework/ai/parser/FormatProvider.java index e6692965f..087c15fe5 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/parser/FormatProvider.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/parser/FormatProvider.java @@ -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 { diff --git a/spring-ai-core/src/main/java/org/springframework/ai/parser/ListOutputParser.java b/spring-ai-core/src/main/java/org/springframework/ai/parser/ListOutputParser.java index 5e0b99c74..d4f243835 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/parser/ListOutputParser.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/parser/ListOutputParser.java @@ -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 */ diff --git a/spring-ai-core/src/main/java/org/springframework/ai/parser/MapOutputParser.java b/spring-ai-core/src/main/java/org/springframework/ai/parser/MapOutputParser.java index 20f2857c7..a222d7940 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/parser/MapOutputParser.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/parser/MapOutputParser.java @@ -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<String, Object> instance. - * * @author Mark Pollack * @author Christian Tzolov */ diff --git a/spring-ai-core/src/main/java/org/springframework/ai/parser/OutputParser.java b/spring-ai-core/src/main/java/org/springframework/ai/parser/OutputParser.java index d0fca1f4e..57ad9c274 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/parser/OutputParser.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/parser/OutputParser.java @@ -23,6 +23,8 @@ package org.springframework.ai.parser; * @param Specifies the desired response type. * @author Mark Pollack * @author Christian Tzolov + * @deprecated Use the {@link org.springframework.ai.converter.StructuredOutputConverter} + * instead. */ public interface OutputParser extends Parser, FormatProvider { diff --git a/spring-ai-core/src/main/java/org/springframework/ai/parser/README.md b/spring-ai-core/src/main/java/org/springframework/ai/parser/README.md index 740e330f2..debadf142 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/parser/README.md +++ b/spring-ai-core/src/main/java/org/springframework/ai/parser/README.md @@ -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 isn’t a JSON data structure. It is just a string. Also, asking "for JSON" as part of the prompt isn’t 100% accurate. diff --git a/spring-ai-core/src/test/java/org/springframework/ai/parser/BeanOutputParserTest.java b/spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java similarity index 78% rename from spring-ai-core/src/test/java/org/springframework/ai/parser/BeanOutputParserTest.java rename to spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java index 8df495e40..dacad64ff 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/parser/BeanOutputParserTest.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java @@ -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 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"); diff --git a/spring-ai-core/src/test/java/org/springframework/ai/prompt/parsers/ListOutputParserTest.java b/spring-ai-core/src/test/java/org/springframework/ai/converter/ListOutputConverterTest.java similarity index 77% rename from spring-ai-core/src/test/java/org/springframework/ai/prompt/parsers/ListOutputParserTest.java rename to spring-ai-core/src/test/java/org/springframework/ai/converter/ListOutputConverterTest.java index 43ccfb54f..4c0795a1c 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/prompt/parsers/ListOutputParserTest.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/converter/ListOutputConverterTest.java @@ -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 list = listOutputParser.parse(csvAsString); + ListOutputConverter listOutputConverter = new ListOutputConverter(new DefaultConversionService()); + List list = listOutputConverter.convert(csvAsString); assertThat(list).containsExactlyElementsOf(List.of("foo", "bar", "baz")); } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/structured-output-api.jpg b/spring-ai-docs/src/main/antora/modules/ROOT/images/structured-output-api.jpg new file mode 100644 index 000000000..7a4ae6d96 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/structured-output-api.jpg differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/structured-output-architecture.jpg b/spring-ai-docs/src/main/antora/modules/ROOT/images/structured-output-architecture.jpg new file mode 100644 index 000000000..a76aa25a5 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/structured-output-architecture.jpg differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/structured-output-hierarchy4.jpg b/spring-ai-docs/src/main/antora/modules/ROOT/images/structured-output-hierarchy4.jpg new file mode 100644 index 000000000..6aa6d138d Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/structured-output-hierarchy4.jpg differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc index 5c05e9d62..ce729e714 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc @@ -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[] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/output-parser.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/output-parser.adoc deleted file mode 100644 index d919c5e1f..000000000 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/output-parser.adoc +++ /dev/null @@ -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 extends Parser, FormatProvider { - -} -``` - -It combines the `Parser` interface - -```java -@FunctionalInterface -public interface Parser { - 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` 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 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; - } -``` - - - - - - - - - diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc new file mode 100644 index 000000000..aa15e12cd --- /dev/null +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc @@ -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 extends Converter, FormatProvider { + +} +---- + +It combines the Spring https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/converter/Converter.html[Converter] 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 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` - 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` 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 movies) { +} +---- + +Here is how to apply the BeanOutputConverter: + +[source,java] +---- +BeanConverter 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 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 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. + + + + + +