Merge ParametrizedTypeReferenceBeanOutputConverter into BeanOutputConverter
Add ParametrizedTypeReference constructoers along the Clas<T> such. Convert the Class into ParametrizedTypeReference internally. Update tests and docs.
This commit is contained in:
@@ -298,7 +298,8 @@ class OpenAiChatClientIT extends AbstractIT {
|
||||
.map(AssistantMessage::getContent)
|
||||
.collect(Collectors.joining());
|
||||
logger.info("Response: {}", content);
|
||||
assertThat(content).contains("bananas", "apple", "bowl");
|
||||
assertThat(content).contains("bananas", "apple");
|
||||
assertThat(content).containsAnyOf("bowl", "basket");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||
import org.springframework.ai.converter.ParameterizedTypeReferenceOutputConverter;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.openai.OpenAiTestConfiguration;
|
||||
import org.springframework.ai.openai.testutils.AbstractIT;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
@@ -39,10 +39,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest(classes = OpenAiTestConfiguration.class)
|
||||
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
|
||||
class OpenAiChatClientParametrizedTypeReferencefOutputConverterIT extends AbstractIT {
|
||||
class OpenAiChatClientTypeReferenceBeanOutputConverterIT extends AbstractIT {
|
||||
|
||||
private static final Logger logger = LoggerFactory
|
||||
.getLogger(OpenAiChatClientParametrizedTypeReferencefOutputConverterIT.class);
|
||||
.getLogger(OpenAiChatClientTypeReferenceBeanOutputConverterIT.class);
|
||||
|
||||
record ActorsFilmsRecord(String actor, List<String> movies) {
|
||||
}
|
||||
@@ -50,7 +50,7 @@ class OpenAiChatClientParametrizedTypeReferencefOutputConverterIT extends Abstra
|
||||
@Test
|
||||
void typeRefOutputConverterRecords() {
|
||||
|
||||
ParameterizedTypeReferenceOutputConverter<List<ActorsFilmsRecord>> outputConverter = new ParameterizedTypeReferenceOutputConverter<>(
|
||||
BeanOutputConverter<List<ActorsFilmsRecord>> outputConverter = new BeanOutputConverter<>(
|
||||
new ParameterizedTypeReference<List<ActorsFilmsRecord>>() {
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ class OpenAiChatClientParametrizedTypeReferencefOutputConverterIT extends Abstra
|
||||
@Test
|
||||
void typeRefStreamOutputConverterRecords() {
|
||||
|
||||
ParameterizedTypeReferenceOutputConverter<List<ActorsFilmsRecord>> outputConverter = new ParameterizedTypeReferenceOutputConverter<>(
|
||||
BeanOutputConverter<List<ActorsFilmsRecord>> outputConverter = new BeanOutputConverter<>(
|
||||
new ParameterizedTypeReference<List<ActorsFilmsRecord>>() {
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ public class VertexAiGeminiChatClientFunctionCallingIT {
|
||||
}
|
||||
|
||||
@Test
|
||||
// @Disabled("Google Vertex AI degraded support for parallel function calls")
|
||||
@Disabled("Google Vertex AI degraded support for parallel function calls")
|
||||
public void functionCallExplicitOpenApiSchema() {
|
||||
|
||||
UserMessage userMessage = new UserMessage(
|
||||
@@ -98,8 +98,8 @@ public class VertexAiGeminiChatClientFunctionCallingIT {
|
||||
""";
|
||||
|
||||
var promptOptions = VertexAiGeminiChatOptions.builder()
|
||||
// .withModel(VertexAiGeminiChatClient.ChatModel.GEMINI_PRO)
|
||||
.withModel(VertexAiGeminiChatClient.ChatModel.GEMINI_PRO_1_5_PRO)
|
||||
.withModel(VertexAiGeminiChatClient.ChatModel.GEMINI_PRO)
|
||||
// .withModel(VertexAiGeminiChatClient.ChatModel.GEMINI_PRO_1_5_PRO)
|
||||
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
|
||||
.withName("get_current_weather")
|
||||
.withDescription("Get the current weather in a given location")
|
||||
@@ -126,8 +126,8 @@ public class VertexAiGeminiChatClientFunctionCallingIT {
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = VertexAiGeminiChatOptions.builder()
|
||||
.withModel(VertexAiGeminiChatClient.ChatModel.GEMINI_PRO_1_5_PRO)
|
||||
// .withModel(VertexAiGeminiChatClient.ChatModel.GEMINI_PRO.getValue())
|
||||
// .withModel(VertexAiGeminiChatClient.ChatModel.GEMINI_PRO_1_5_PRO)
|
||||
.withModel(VertexAiGeminiChatClient.ChatModel.GEMINI_PRO.getValue())
|
||||
.withFunctionCallbacks(List.of(
|
||||
FunctionCallbackWrapper.builder(new MockWeatherService())
|
||||
.withSchemaType(SchemaType.OPEN_API_SCHEMA)
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
*/
|
||||
package org.springframework.ai.converter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.core.util.DefaultIndenter;
|
||||
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
@@ -27,10 +30,9 @@ 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.core.ParameterizedTypeReference;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import static com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON;
|
||||
import static com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12;
|
||||
@@ -38,23 +40,26 @@ import static com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_
|
||||
/**
|
||||
* 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.
|
||||
* schema based on a given Java class or parameterized type reference, which is then used
|
||||
* to validate and transform the LLM output into the desired type.
|
||||
*
|
||||
* @param <T> The target type to which the output will be converted.
|
||||
* @author Mark Pollack
|
||||
* @author Christian Tzolov
|
||||
* @author Sebastian Ullrich
|
||||
* @author Kirk Lund
|
||||
* @author Josh Long
|
||||
*/
|
||||
public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
|
||||
|
||||
/** Holds the generated JSON schema for the target type. */
|
||||
private String jsonSchema;
|
||||
|
||||
/** The Java class representing the target type. */
|
||||
/**
|
||||
* The target class type reference to which the output will be converted.
|
||||
*/
|
||||
@SuppressWarnings({ "FieldMayBeFinal", "rawtypes" })
|
||||
private Class<T> clazz;
|
||||
private TypeReference<T> typeRef;
|
||||
|
||||
/** The object mapper used for deserialization and other JSON operations. */
|
||||
@SuppressWarnings("FieldMayBeFinal")
|
||||
@@ -64,8 +69,8 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
|
||||
* Constructor to initialize with the target type's class.
|
||||
* @param clazz The target type's class.
|
||||
*/
|
||||
public BeanOutputConverter(Class<T> clazz) {
|
||||
this(clazz, null);
|
||||
public BeanOutputConverter(Class<T> typeClass) {
|
||||
this(ParameterizedTypeReference.forType(typeClass));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,8 +80,53 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
|
||||
* @param objectMapper Custom object mapper for JSON operations. endings.
|
||||
*/
|
||||
public BeanOutputConverter(Class<T> clazz, ObjectMapper objectMapper) {
|
||||
Objects.requireNonNull(clazz, "Java Class cannot be null;");
|
||||
this.clazz = clazz;
|
||||
this(ParameterizedTypeReference.forType(clazz), objectMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to initialize with the target class type reference.
|
||||
* @param typeRef The target class type reference.
|
||||
*/
|
||||
public BeanOutputConverter(ParameterizedTypeReference<T> typeRef) {
|
||||
this(new CustomizedTypeReference<>(typeRef), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to initialize with the target class type reference, a custom object
|
||||
* mapper, and a line endings normalizer to ensure consistent line endings on any
|
||||
* platform.
|
||||
* @param typeRef The target class type reference.
|
||||
* @param objectMapper Custom object mapper for JSON operations. endings.
|
||||
*/
|
||||
public BeanOutputConverter(ParameterizedTypeReference<T> typeRef, ObjectMapper objectMapper) {
|
||||
this(new CustomizedTypeReference<>(typeRef), objectMapper);
|
||||
}
|
||||
|
||||
private static class CustomizedTypeReference<T> extends TypeReference<T> {
|
||||
|
||||
private final Type type;
|
||||
|
||||
CustomizedTypeReference(ParameterizedTypeReference<T> typeRef) {
|
||||
this.type = typeRef.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to initialize with the target class type reference, a custom object
|
||||
* mapper, and a line endings normalizer to ensure consistent line endings on any
|
||||
* platform.
|
||||
* @param typeRef The target class type reference.
|
||||
* @param objectMapper Custom object mapper for JSON operations. endings.
|
||||
*/
|
||||
private BeanOutputConverter(TypeReference<T> typeRef, ObjectMapper objectMapper) {
|
||||
Objects.requireNonNull(typeRef, "Type reference cannot be null;");
|
||||
this.typeRef = typeRef;
|
||||
this.objectMapper = objectMapper != null ? objectMapper : getObjectMapper();
|
||||
generateSchema();
|
||||
}
|
||||
@@ -90,14 +140,14 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
|
||||
.with(jacksonModule);
|
||||
SchemaGeneratorConfig config = configBuilder.build();
|
||||
SchemaGenerator generator = new SchemaGenerator(config);
|
||||
JsonNode jsonNode = generator.generateSchema(this.clazz);
|
||||
JsonNode jsonNode = generator.generateSchema(this.typeRef.getType());
|
||||
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);
|
||||
throw new RuntimeException("Could not pretty print json schema for " + this.typeRef, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,34 +159,13 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
|
||||
*/
|
||||
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);
|
||||
return (T) this.objectMapper.readValue(text, this.typeRef);
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a JSON Schema to an instance based on a given text.
|
||||
* @param text The JSON Schema in string format.
|
||||
* @return The JSON instance generated from the JSON Schema, or the original text if
|
||||
* the input is not a JSON Schema.
|
||||
*/
|
||||
private String jsonSchemaToInstance(String text) {
|
||||
try {
|
||||
Map<String, Object> map = this.objectMapper.readValue(text, Map.class);
|
||||
if (map.containsKey("$schema")) {
|
||||
return this.objectMapper.writeValueAsString(map.get("properties"));
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures and returns an object mapper for JSON operations.
|
||||
* @return Configured object mapper.
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
/*
|
||||
* 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.Objects;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
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.core.ParameterizedTypeReference;
|
||||
import org.springframework.lang.NonNull;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
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 type reference, which is then used to validate and
|
||||
* transform the LLM output into the desired type.
|
||||
*
|
||||
* @param <T> The target type to which the output will be converted.
|
||||
* @author Mark Pollack
|
||||
* @author Christian Tzolov
|
||||
* @author Sebastian Ullrich
|
||||
* @author Kirk Lund
|
||||
* @author Josh Long
|
||||
*/
|
||||
public class ParameterizedTypeReferenceOutputConverter<T> implements StructuredOutputConverter<T> {
|
||||
|
||||
/** Holds the generated JSON schema for the target type. */
|
||||
private String jsonSchema;
|
||||
|
||||
/**
|
||||
* The target class type reference to which the output will be converted.
|
||||
*/
|
||||
@SuppressWarnings({ "FieldMayBeFinal", "rawtypes" })
|
||||
private TypeReference<T> typeRef;
|
||||
|
||||
/** The object mapper used for deserialization and other JSON operations. */
|
||||
@SuppressWarnings("FieldMayBeFinal")
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* Constructor to initialize with the target class type reference.
|
||||
* @param typeRef The target type's class.
|
||||
*/
|
||||
public ParameterizedTypeReferenceOutputConverter(ParameterizedTypeReference<T> typeRef) {
|
||||
this(new CustomizedTypeReference<>(typeRef), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to initialize with the target class type reference, a custom object
|
||||
* mapper, and a line endings normalizer to ensure consistent line endings on any
|
||||
* platform.
|
||||
* @param typeRef The target class type reference.
|
||||
* @param objectMapper Custom object mapper for JSON operations. endings.
|
||||
*/
|
||||
public ParameterizedTypeReferenceOutputConverter(ParameterizedTypeReference<T> typeRef, ObjectMapper objectMapper) {
|
||||
this(new CustomizedTypeReference<>(typeRef), objectMapper);
|
||||
}
|
||||
|
||||
private static class CustomizedTypeReference<T> extends TypeReference<T> {
|
||||
|
||||
private final Type type;
|
||||
|
||||
CustomizedTypeReference(ParameterizedTypeReference<T> typeRef) {
|
||||
this.type = typeRef.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to initialize with the target class type reference, a custom object
|
||||
* mapper, and a line endings normalizer to ensure consistent line endings on any
|
||||
* platform.
|
||||
* @param typeRef The target class type reference.
|
||||
* @param objectMapper Custom object mapper for JSON operations. endings.
|
||||
*/
|
||||
private ParameterizedTypeReferenceOutputConverter(TypeReference<T> typeRef, ObjectMapper objectMapper) {
|
||||
Objects.requireNonNull(typeRef, "Type reference cannot be null;");
|
||||
this.typeRef = typeRef;
|
||||
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.typeRef.getType());
|
||||
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.typeRef, 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 {
|
||||
return (T) this.objectMapper.readValue(text, this.typeRef);
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,9 +15,10 @@
|
||||
*/
|
||||
package org.springframework.ai.converter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@@ -26,10 +27,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Sebastian Ullrich
|
||||
@@ -44,43 +44,72 @@ class BeanOutputConverterTest {
|
||||
|
||||
@Test
|
||||
public void shouldHavePreConfiguredDefaultObjectMapper() {
|
||||
var converter = new BeanOutputConverter<>(TestClass.class);
|
||||
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<TestClass>() {
|
||||
});
|
||||
var objectMapper = converter.getObjectMapper();
|
||||
assertThat(objectMapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldUseProvidedObjectMapperForParsing() throws JsonProcessingException {
|
||||
var testClass = new TestClass("some string");
|
||||
when(objectMapperMock.readValue(anyString(), eq(TestClass.class))).thenReturn(testClass);
|
||||
var converter = new BeanOutputConverter<>(TestClass.class, objectMapperMock);
|
||||
assertThat(converter.convert("{}")).isEqualTo(testClass);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ParserTest {
|
||||
class ConverterTest {
|
||||
|
||||
@Test
|
||||
public void shouldParseFieldNamesFromString() {
|
||||
public void convertClassType() {
|
||||
var converter = new BeanOutputConverter<>(TestClass.class);
|
||||
var testClass = converter.convert("{ \"someString\": \"some value\" }");
|
||||
assertThat(testClass.getSomeString()).isEqualTo("some value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldParseJsonPropertiesFromString() {
|
||||
public void convertTypeReference() {
|
||||
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<TestClass>() {
|
||||
});
|
||||
var testClass = converter.convert("{ \"someString\": \"some value\" }");
|
||||
assertThat(testClass.getSomeString()).isEqualTo("some value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertTypeReferenceArray() {
|
||||
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<List<TestClass>>() {
|
||||
});
|
||||
List<TestClass> testClass = converter.convert("[{ \"someString\": \"some value\" }]");
|
||||
assertThat(testClass).hasSize(1);
|
||||
assertThat(testClass.get(0).getSomeString()).isEqualTo("some value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertClassTypeWithJsonAnnotations() {
|
||||
var converter = new BeanOutputConverter<>(TestClassWithJsonAnnotations.class);
|
||||
var testClass = converter.convert("{ \"string_property\": \"some value\" }");
|
||||
assertThat(testClass.getSomeString()).isEqualTo("some value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertTypeReferenceWithJsonAnnotations() {
|
||||
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<TestClassWithJsonAnnotations>() {
|
||||
});
|
||||
var testClass = converter.convert("{ \"string_property\": \"some value\" }");
|
||||
assertThat(testClass.getSomeString()).isEqualTo("some value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertTypeReferenceArrayWithJsonAnnotations() {
|
||||
var converter = new BeanOutputConverter<>(
|
||||
new ParameterizedTypeReference<List<TestClassWithJsonAnnotations>>() {
|
||||
});
|
||||
List<TestClassWithJsonAnnotations> testClass = converter
|
||||
.convert("[{ \"string_property\": \"some value\" }]");
|
||||
assertThat(testClass).hasSize(1);
|
||||
assertThat(testClass.get(0).getSomeString()).isEqualTo("some value");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FormatTest {
|
||||
|
||||
@Test
|
||||
public void shouldReturnFormatContainingResponseInstructionsAndJsonSchema() {
|
||||
public void formatClassType() {
|
||||
var converter = new BeanOutputConverter<>(TestClass.class);
|
||||
assertThat(converter.getFormat()).isEqualTo(
|
||||
"""
|
||||
@@ -102,7 +131,56 @@ class BeanOutputConverterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnFormatContainingJsonSchemaIncludingPropertyAndPropertyDescription() {
|
||||
public void formatTypeReference() {
|
||||
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<TestClass>() {
|
||||
});
|
||||
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",
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"someString" : {
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
}```
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formatTypeReferenceArray() {
|
||||
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<List<TestClass>>() {
|
||||
});
|
||||
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",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"someString" : {
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}```
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formatClassTypeWithAnnotations() {
|
||||
var converter = new BeanOutputConverter<>(TestClassWithJsonAnnotations.class);
|
||||
assertThat(converter.getFormat()).contains("""
|
||||
```{
|
||||
@@ -119,7 +197,25 @@ class BeanOutputConverterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizesLineEndings() {
|
||||
public void formatTypeReferenceWithAnnotations() {
|
||||
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<TestClassWithJsonAnnotations>() {
|
||||
});
|
||||
assertThat(converter.getFormat()).contains("""
|
||||
```{
|
||||
"$schema" : "https://json-schema.org/draft/2020-12/schema",
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"string_property" : {
|
||||
"type" : "string",
|
||||
"description" : "string_property_description"
|
||||
}
|
||||
}
|
||||
}```
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizesLineEndingsClassType() {
|
||||
var converter = new BeanOutputConverter<>(TestClass.class);
|
||||
|
||||
String formatOutput = converter.getFormat();
|
||||
@@ -128,6 +224,17 @@ class BeanOutputConverterTest {
|
||||
assertThat(formatOutput).contains(System.lineSeparator()).doesNotContain("\r\n").doesNotContain("\r");
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizesLineEndingsTypeReference() {
|
||||
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<TestClass>() {
|
||||
});
|
||||
|
||||
String formatOutput = converter.getFormat();
|
||||
|
||||
// validate that output contains \n line endings
|
||||
assertThat(formatOutput).contains(System.lineSeparator()).doesNotContain("\r\n").doesNotContain("\r");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TestClass {
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
/*
|
||||
* 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 com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Sebastian Ullrich
|
||||
* @author Kirk Lund
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TypeReferenceOutputConverterTest {
|
||||
|
||||
@Mock
|
||||
private ObjectMapper objectMapperMock;
|
||||
|
||||
@Test
|
||||
public void shouldHavePreConfiguredDefaultObjectMapper() {
|
||||
var converter = new ParameterizedTypeReferenceOutputConverter<>(new ParameterizedTypeReference<TestClass>() {
|
||||
});
|
||||
var objectMapper = converter.getObjectMapper();
|
||||
assertThat(objectMapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ParserTest {
|
||||
|
||||
@Test
|
||||
public void shouldParseFieldNamesFromString() {
|
||||
var converter = new ParameterizedTypeReferenceOutputConverter<>(
|
||||
new ParameterizedTypeReference<TestClass>() {
|
||||
});
|
||||
var testClass = converter.convert("{ \"someString\": \"some value\" }");
|
||||
assertThat(testClass.getSomeString()).isEqualTo("some value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldParseFieldNamesFromArrayString() {
|
||||
var converter = new ParameterizedTypeReferenceOutputConverter<>(
|
||||
new ParameterizedTypeReference<List<TestClass>>() {
|
||||
});
|
||||
List<TestClass> testClass = converter.convert("[{ \"someString\": \"some value\" }]");
|
||||
assertThat(testClass).hasSize(1);
|
||||
assertThat(testClass.get(0).getSomeString()).isEqualTo("some value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldParseJsonPropertiesFromString() {
|
||||
var converter = new ParameterizedTypeReferenceOutputConverter<>(
|
||||
new ParameterizedTypeReference<TestClassWithJsonAnnotations>() {
|
||||
});
|
||||
var testClass = converter.convert("{ \"string_property\": \"some value\" }");
|
||||
assertThat(testClass.getSomeString()).isEqualTo("some value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldParseJsonPropertiesFromArrayString() {
|
||||
var converter = new ParameterizedTypeReferenceOutputConverter<>(
|
||||
new ParameterizedTypeReference<List<TestClassWithJsonAnnotations>>() {
|
||||
});
|
||||
List<TestClassWithJsonAnnotations> testClass = converter
|
||||
.convert("[{ \"string_property\": \"some value\" }]");
|
||||
assertThat(testClass).hasSize(1);
|
||||
assertThat(testClass.get(0).getSomeString()).isEqualTo("some value");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FormatTest {
|
||||
|
||||
@Test
|
||||
public void shouldReturnFormatContainingResponseInstructionsAndJsonSchema() {
|
||||
var converter = new ParameterizedTypeReferenceOutputConverter<>(
|
||||
new ParameterizedTypeReference<TestClass>() {
|
||||
});
|
||||
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",
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"someString" : {
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
}```
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnFormatContainingJsonSchemaIncludingPropertyAndPropertyDescription() {
|
||||
var converter = new ParameterizedTypeReferenceOutputConverter<>(
|
||||
new ParameterizedTypeReference<TestClassWithJsonAnnotations>() {
|
||||
});
|
||||
assertThat(converter.getFormat()).contains("""
|
||||
```{
|
||||
"$schema" : "https://json-schema.org/draft/2020-12/schema",
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"string_property" : {
|
||||
"type" : "string",
|
||||
"description" : "string_property_description"
|
||||
}
|
||||
}
|
||||
}```
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizesLineEndings() {
|
||||
var converter = new ParameterizedTypeReferenceOutputConverter<>(
|
||||
new ParameterizedTypeReference<TestClass>() {
|
||||
});
|
||||
|
||||
String formatOutput = converter.getFormat();
|
||||
|
||||
// validate that output contains \n line endings
|
||||
assertThat(formatOutput).contains(System.lineSeparator()).doesNotContain("\r\n").doesNotContain("\r");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TestClass {
|
||||
|
||||
private String someString;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public TestClass() {
|
||||
}
|
||||
|
||||
public TestClass(String someString) {
|
||||
this.someString = someString;
|
||||
}
|
||||
|
||||
public String getSomeString() {
|
||||
return someString;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TestClassWithJsonAnnotations {
|
||||
|
||||
@JsonProperty("string_property")
|
||||
@JsonPropertyDescription("string_property_description")
|
||||
private String someString;
|
||||
|
||||
public TestClassWithJsonAnnotations() {
|
||||
}
|
||||
|
||||
public String getSomeString() {
|
||||
return someString;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 224 KiB |
@@ -86,8 +86,8 @@ Currently, Spring AI provides `AbstractConversionServiceOutputConverter`, `Abstr
|
||||
image::structured-output-hierarchy4.jpg[Structured Output Class Hierarchy, width=900, align="center"]
|
||||
|
||||
* `AbstractConversionServiceOutputConverter<T>` - Offers a pre-configured link:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/support/GenericConversionService.html[GenericConversionService] for transforming LLM output into the desired format. No default `FormatProvider` implementation is provided.
|
||||
* `AbstractMessageOutputConverter` - Supplies a pre-configured https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jms/support/converter/MessageConverter.html[MessageConverter] for converting LLM output into the desired format. No default `FormatProvider` implementation is provided.
|
||||
* `BeanOutputConverter` - Configured with a designated Java class (e.g., Bean), this converter employs a `FormatProvider` implementation that directs the AI Model to produce a JSON response compliant with a `DRAFT_2020_12`, `JSON Schema` derived from the specified Java class. Subsequently, it utilizes an `ObjectMapper` to deserialize the JSON output into a Java object instance of the target class.
|
||||
* `AbstractMessageOutputConverter<T>` - 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<T>` - Configured with a designated Java class (e.g., Bean) or a link:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/ParameterizedTypeReference.html[ParameterizedTypeReference], this converter employs a `FormatProvider` implementation that directs the AI Model to produce a JSON response compliant with a `DRAFT_2020_12`, `JSON Schema` derived from the specified Java class. Subsequently, it utilizes an `ObjectMapper` to deserialize the JSON output into a Java object instance of the target class.
|
||||
* `MapOutputConverter` - Extends the functionality of `AbstractMessageOutputConverter` with a `FormatProvider` implementation that guides the AI Model to generate an RFC8259 compliant JSON response. Additionally, it incorporates a converter implementation that utilizes the provided `MessageConverter` to translate the JSON payload into a `java.util.Map<String, Object>` instance.
|
||||
* `ListOutputConverter` - Extends the `AbstractConversionServiceOutputConverter` and includes a `FormatProvider` implementation tailored for comma-delimited list output. The converter implementation employs the provided `ConversionService` to transform the model text output into a `java.util.List`.
|
||||
|
||||
@@ -129,6 +129,29 @@ Generation generation = chatClient.call(
|
||||
ActorsFilms actorsFilms = beanOutputConverter.convert(generation.getOutput().getContent());
|
||||
----
|
||||
|
||||
==== Generic Bean Types
|
||||
|
||||
Use the `ParameterizedTypeReference` constructor to specify a more complex target class structure.
|
||||
For example, to represent a list of actors and their filmographies:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
BeanOutputConverter<List<ActorsFilmsRecord>> outputConverter = new BeanOutputConverter<>(
|
||||
new ParameterizedTypeReference<List<ActorsFilmsRecord>>() { });
|
||||
|
||||
String format = outputConverter.getFormat();
|
||||
String template = """
|
||||
Generate the filmography of 5 movies for Tom Hanks and Bill Murray.
|
||||
{format}
|
||||
""";
|
||||
|
||||
Prompt prompt = new Prompt(new PromptTemplate(template, Map.of("format", format)).createMessage());
|
||||
|
||||
Generation generation = chatClient.call(prompt).getResult();
|
||||
|
||||
List<ActorsFilmsRecord> actorsFilms = outputConverter.convert(generation.getOutput().getContent());
|
||||
----
|
||||
|
||||
=== Map Output Converter
|
||||
|
||||
Following sniped shows how to use `MapOutputConverter` to generate a list of numbers.
|
||||
|
||||
Reference in New Issue
Block a user