diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiImageModel.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiImageModel.java index 40fde38b28..7d2b3dae38 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiImageModel.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiImageModel.java @@ -10,7 +10,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.databind.json.JsonMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.azure.openai.metadata.AzureOpenAiImageGenerationMetadata; @@ -22,6 +22,7 @@ import org.springframework.ai.image.ImageResponse; import org.springframework.ai.image.ImageResponseMetadata; import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.util.JacksonUtils; import org.springframework.util.Assert; import java.util.List; @@ -33,6 +34,7 @@ * {@link OpenAIClient}. * * @author Benoit Moussaud + * @author Sebastien Deleuze * @see ImageModel * @see com.azure.ai.openai.OpenAIClient * @since 1.0.0 @@ -47,6 +49,8 @@ public class AzureOpenAiImageModel implements ImageModel { private final AzureOpenAiImageOptions defaultOptions; + private final ObjectMapper objectMapper; + public AzureOpenAiImageModel(OpenAIClient openAIClient) { this(openAIClient, AzureOpenAiImageOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build()); } @@ -56,6 +60,11 @@ public AzureOpenAiImageModel(OpenAIClient microsoftOpenAiClient, AzureOpenAiImag Assert.notNull(options, "AzureOpenAiChatOptions must not be null"); this.openAIClient = microsoftOpenAiClient; this.defaultOptions = options; + this.objectMapper = JsonMapper.builder() + .addModules(JacksonUtils.instantiateAvailableModules()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .build(); } public AzureOpenAiImageOptions getDefaultOptions() { @@ -88,11 +97,8 @@ public ImageResponse call(ImagePrompt imagePrompt) { } private String toPrettyJson(Object object) { - ObjectMapper objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - .registerModule(new JavaTimeModule()); try { - return objectMapper.writeValueAsString(object); + return this.objectMapper.writeValueAsString(object); } catch (JsonProcessingException e) { return "JsonProcessingException:" + e + " [" + object.toString() + "]"; diff --git a/models/spring-ai-vertex-ai-palm2/src/main/java/org/springframework/ai/vertexai/palm2/api/VertexAiPaLm2Api.java b/models/spring-ai-vertex-ai-palm2/src/main/java/org/springframework/ai/vertexai/palm2/api/VertexAiPaLm2Api.java index 4e590fbf79..2ad7c827bf 100644 --- a/models/spring-ai-vertex-ai-palm2/src/main/java/org/springframework/ai/vertexai/palm2/api/VertexAiPaLm2Api.java +++ b/models/spring-ai-vertex-ai-palm2/src/main/java/org/springframework/ai/vertexai/palm2/api/VertexAiPaLm2Api.java @@ -25,7 +25,9 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import org.springframework.ai.util.JacksonUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; @@ -116,6 +118,8 @@ public class VertexAiPaLm2Api { private final String embeddingModel; + private final ObjectMapper objectMapper; + /** * Create a new chat completion api. * @param apiKey vertex apiKey. @@ -138,6 +142,7 @@ public VertexAiPaLm2Api(String baseUrl, String apiKey, String model, String embe this.chatModel = model; this.embeddingModel = embeddingModel; this.apiKey = apiKey; + this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); Consumer jsonContentHeaders = headers -> { headers.setAccept(List.of(MediaType.APPLICATION_JSON)); @@ -154,7 +159,7 @@ public boolean hasError(ClientHttpResponse response) throws IOException { public void handleError(ClientHttpResponse response) throws IOException { if (response.getStatusCode().isError()) { throw new RuntimeException(String.format("%s - %s", response.getStatusCode().value(), - new ObjectMapper().readValue(response.getBody(), ResponseError.class))); + objectMapper.readValue(response.getBody(), ResponseError.class))); } } }; 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 index c3736801a7..224a96fc3a 100644 --- 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 @@ -21,8 +21,11 @@ import java.lang.reflect.Type; import java.util.Objects; +import com.fasterxml.jackson.databind.json.JsonMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import org.springframework.ai.util.JacksonUtils; import org.springframework.core.ParameterizedTypeReference; import org.springframework.lang.NonNull; @@ -34,7 +37,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.github.victools.jsonschema.generator.Option; import com.github.victools.jsonschema.generator.SchemaGenerator; import com.github.victools.jsonschema.generator.SchemaGeneratorConfig; @@ -54,6 +56,7 @@ * @author Sebastian Ullrich * @author Kirk Lund * @author Josh Long + * @author Sebastien Deleuze */ public class BeanOutputConverter implements StructuredOutputConverter { @@ -65,12 +68,10 @@ public class BeanOutputConverter implements StructuredOutputConverter { /** * The target class type reference to which the output will be converted. */ - @SuppressWarnings({ "FieldMayBeFinal" }) - private TypeReference typeRef; + private final TypeReference typeRef; /** The object mapper used for deserialization and other JSON operations. */ - @SuppressWarnings("FieldMayBeFinal") - private ObjectMapper objectMapper; + private final ObjectMapper objectMapper; /** * Constructor to initialize with the target type's class. @@ -149,7 +150,7 @@ private void generateSchema() { SchemaGeneratorConfig config = configBuilder.build(); SchemaGenerator generator = new SchemaGenerator(config); JsonNode jsonNode = generator.generateSchema(this.typeRef.getType()); - ObjectWriter objectWriter = new ObjectMapper().writer(new DefaultPrettyPrinter() + ObjectWriter objectWriter = this.objectMapper.writer(new DefaultPrettyPrinter() .withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator()))); try { this.jsonSchema = objectWriter.writeValueAsString(jsonNode); @@ -201,10 +202,10 @@ public T convert(@NonNull String text) { * @return Configured object mapper. */ protected ObjectMapper getObjectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.registerModule(new JavaTimeModule()); - return mapper; + return JsonMapper.builder() + .addModules(JacksonUtils.instantiateAvailableModules()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .build(); } /** diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java b/spring-ai-core/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java index 809b1cc82a..8a6cc7d0da 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java @@ -26,6 +26,13 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import org.springframework.ai.util.JacksonUtils; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -33,9 +40,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.github.victools.jsonschema.generator.Option; import com.github.victools.jsonschema.generator.OptionPreset; import com.github.victools.jsonschema.generator.SchemaGenerator; @@ -46,12 +53,6 @@ import com.github.victools.jsonschema.module.jackson.JacksonOption; import com.github.victools.jsonschema.module.swagger2.Swagger2Module; -import org.springframework.beans.BeanWrapper; -import org.springframework.beans.BeanWrapperImpl; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; - /** * Utility class for manipulating {@link ModelOptions} objects. * @@ -61,10 +62,11 @@ */ public abstract class ModelOptionsUtils { - public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + public static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - .registerModule(new JavaTimeModule()); + .addModules(JacksonUtils.instantiateAvailableModules()) + .build(); private static final List BEAN_MERGE_FIELD_EXCISIONS = List.of("class"); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java index 7524399408..960292cf8e 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java @@ -21,18 +21,22 @@ import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.model.function.FunctionCallbackContext.SchemaType; +import org.springframework.ai.util.JacksonUtils; import org.springframework.util.Assert; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.databind.json.JsonMapper; /** * Note that the underlying function is responsible for converting the output into format * that can be consumed by the Model. The default implementation converts the output into * String before sending it to the Model. Provide a custom function responseConverter * implementation to override this. + * + * @author Christian Tzolov + * @author Sebastien Deleuze * */ public class FunctionCallbackWrapper extends AbstractFunctionCallback { @@ -90,10 +94,7 @@ public Builder(Function function) { private String inputTypeSchema; - private ObjectMapper objectMapper = new ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - .registerModule(new JavaTimeModule()); + private ObjectMapper objectMapper; public Builder withName(String name) { Assert.hasText(name, "Name must not be empty"); @@ -142,7 +143,14 @@ public FunctionCallbackWrapper build() { Assert.hasText(this.name, "Name must not be empty"); Assert.hasText(this.description, "Description must not be empty"); Assert.notNull(this.responseConverter, "ResponseConverter must not be null"); - Assert.notNull(this.objectMapper, "ObjectMapper must not be null"); + + if (this.objectMapper == null) { + this.objectMapper = JsonMapper.builder() + .addModules(JacksonUtils.instantiateAvailableModules()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .build(); + } if (this.inputType == null) { if (this.function != null) { 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 edd4722202..625dee2154 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 @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.github.victools.jsonschema.generator.SchemaGenerator; import com.github.victools.jsonschema.generator.SchemaGeneratorConfig; import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; @@ -30,6 +31,8 @@ import java.util.Map; import java.util.Objects; +import org.springframework.ai.util.JacksonUtils; + import static com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON; import static com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12; @@ -91,7 +94,7 @@ private void generateSchema() { SchemaGeneratorConfig config = configBuilder.build(); SchemaGenerator generator = new SchemaGenerator(config); JsonNode jsonNode = generator.generateSchema(this.clazz); - ObjectWriter objectWriter = new ObjectMapper().writer(new DefaultPrettyPrinter() + ObjectWriter objectWriter = this.objectMapper.writer(new DefaultPrettyPrinter() .withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator()))); try { this.jsonSchema = objectWriter.writeValueAsString(jsonNode); @@ -142,9 +145,10 @@ private String jsonSchemaToInstance(String text) { * @return Configured object mapper. */ protected ObjectMapper getObjectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - return mapper; + return JsonMapper.builder() + .addModules(JacksonUtils.instantiateAvailableModules()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .build(); } /** diff --git a/spring-ai-core/src/main/java/org/springframework/ai/util/JacksonUtils.java b/spring-ai-core/src/main/java/org/springframework/ai/util/JacksonUtils.java new file mode 100644 index 0000000000..80176631b3 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/util/JacksonUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 - 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.util; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.databind.Module; + +import org.springframework.beans.BeanUtils; +import org.springframework.core.KotlinDetector; +import org.springframework.util.ClassUtils; + +/** + * Utility methods for Jackson. + * + * @author Sebastien Deleuze + */ +public abstract class JacksonUtils { + + /** + * Instantiate well-known Jackson modules available in the classpath. + *

+ * Supports the follow-modules: Jdk8Module, JavaTimeModule, + * ParameterNamesModule and KotlinModule. + * @return The list of instantiated modules. + */ + @SuppressWarnings("unchecked") + public static List instantiateAvailableModules() { + List modules = new ArrayList<>(); + try { + Class jdk8ModuleClass = (Class) ClassUtils + .forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", null); + com.fasterxml.jackson.databind.Module jdk8Module = BeanUtils.instantiateClass(jdk8ModuleClass); + modules.add(jdk8Module); + } + catch (ClassNotFoundException ex) { + // jackson-datatype-jdk8 not available + } + + try { + Class javaTimeModuleClass = (Class) ClassUtils + .forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", null); + com.fasterxml.jackson.databind.Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass); + modules.add(javaTimeModule); + } + catch (ClassNotFoundException ex) { + // jackson-datatype-jsr310 not available + } + + try { + Class parameterNamesModuleClass = (Class) ClassUtils + .forName("com.fasterxml.jackson.module.paramnames.ParameterNamesModule", null); + com.fasterxml.jackson.databind.Module parameterNamesModule = BeanUtils + .instantiateClass(parameterNamesModuleClass); + modules.add(parameterNamesModule); + } + catch (ClassNotFoundException ex) { + // jackson-module-parameter-names not available + } + + // Kotlin present? + if (KotlinDetector.isKotlinPresent()) { + try { + Class kotlinModuleClass = (Class) ClassUtils + .forName("com.fasterxml.jackson.module.kotlin.KotlinModule", null); + Module kotlinModule = BeanUtils.instantiateClass(kotlinModuleClass); + modules.add(kotlinModule); + } + catch (ClassNotFoundException ex) { + // jackson-module-kotlin not available + } + } + return modules; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java index 338327e819..724c0395d5 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java @@ -32,12 +32,14 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import com.fasterxml.jackson.databind.json.JsonMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.observation.conventions.VectorStoreProvider; import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric; +import org.springframework.ai.util.JacksonUtils; import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore; import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention; @@ -64,11 +66,14 @@ * @author Dingmeng Xue * @author Mark Pollack * @author Christian Tzolov + * @author Sebastien Deleuze */ public class SimpleVectorStore extends AbstractObservationVectorStore { private static final Logger logger = LoggerFactory.getLogger(SimpleVectorStore.class); + private final ObjectMapper objectMapper; + protected Map store = new ConcurrentHashMap<>(); protected EmbeddingModel embeddingModel; @@ -84,6 +89,7 @@ public SimpleVectorStore(EmbeddingModel embeddingModel, ObservationRegistry obse Objects.requireNonNull(embeddingModel, "EmbeddingModel must not be null"); this.embeddingModel = embeddingModel; + this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); } @Override @@ -172,9 +178,8 @@ public void save(File file) { public void load(File file) { TypeReference> typeRef = new TypeReference<>() { }; - ObjectMapper objectMapper = new ObjectMapper(); try { - Map deserializedMap = objectMapper.readValue(file, typeRef); + Map deserializedMap = this.objectMapper.readValue(file, typeRef); this.store = deserializedMap; } catch (IOException ex) { @@ -189,9 +194,8 @@ public void load(File file) { public void load(Resource resource) { TypeReference> typeRef = new TypeReference<>() { }; - ObjectMapper objectMapper = new ObjectMapper(); try { - Map deserializedMap = objectMapper.readValue(resource.getInputStream(), typeRef); + Map deserializedMap = this.objectMapper.readValue(resource.getInputStream(), typeRef); this.store = deserializedMap; } catch (IOException ex) { @@ -200,8 +204,7 @@ public void load(Resource resource) { } private String getVectorDbAsJson() { - ObjectMapper objectMapper = new ObjectMapper(); - ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter(); + ObjectWriter objectWriter = this.objectMapper.writerWithDefaultPrettyPrinter(); String json; try { json = objectWriter.writeValueAsString(this.store); diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/anthropic/BedrockAnthropicChatAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/anthropic/BedrockAnthropicChatAutoConfiguration.java index 4cb042e562..5d3f2d5fb0 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/anthropic/BedrockAnthropicChatAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/anthropic/BedrockAnthropicChatAutoConfiguration.java @@ -52,9 +52,9 @@ public class BedrockAnthropicChatAutoConfiguration { @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) public AnthropicChatBedrockApi anthropicApi(AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider, BedrockAnthropicChatProperties properties, - BedrockAwsConnectionProperties awsProperties) { + BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) { return new AnthropicChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - new ObjectMapper(), awsProperties.getTimeout()); + objectMapper, awsProperties.getTimeout()); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/anthropic3/BedrockAnthropic3ChatAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/anthropic3/BedrockAnthropic3ChatAutoConfiguration.java index 853b6e137a..ac4f788355 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/anthropic3/BedrockAnthropic3ChatAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/anthropic3/BedrockAnthropic3ChatAutoConfiguration.java @@ -52,9 +52,9 @@ public class BedrockAnthropic3ChatAutoConfiguration { @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) public Anthropic3ChatBedrockApi anthropic3Api(AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider, BedrockAnthropic3ChatProperties properties, - BedrockAwsConnectionProperties awsProperties) { + BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) { return new Anthropic3ChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - new ObjectMapper(), awsProperties.getTimeout()); + objectMapper, awsProperties.getTimeout()); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereChatAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereChatAutoConfiguration.java index 946b9a8222..706474ca35 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereChatAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereChatAutoConfiguration.java @@ -50,9 +50,9 @@ public class BedrockCohereChatAutoConfiguration { @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) public CohereChatBedrockApi cohereChatApi(AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider, BedrockCohereChatProperties properties, - BedrockAwsConnectionProperties awsProperties) { + BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) { return new CohereChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - new ObjectMapper(), awsProperties.getTimeout()); + objectMapper, awsProperties.getTimeout()); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingAutoConfiguration.java index ad28904ea9..27b1edd44a 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingAutoConfiguration.java @@ -51,9 +51,9 @@ public class BedrockCohereEmbeddingAutoConfiguration { @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) public CohereEmbeddingBedrockApi cohereEmbeddingApi(AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider, BedrockCohereEmbeddingProperties properties, - BedrockAwsConnectionProperties awsProperties) { + BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) { return new CohereEmbeddingBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - new ObjectMapper(), awsProperties.getTimeout()); + objectMapper, awsProperties.getTimeout()); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/jurrasic2/BedrockAi21Jurassic2ChatAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/jurrasic2/BedrockAi21Jurassic2ChatAutoConfiguration.java index 1daec88b97..ac46a66f6a 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/jurrasic2/BedrockAi21Jurassic2ChatAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/jurrasic2/BedrockAi21Jurassic2ChatAutoConfiguration.java @@ -52,9 +52,9 @@ public class BedrockAi21Jurassic2ChatAutoConfiguration { @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) public Ai21Jurassic2ChatBedrockApi ai21Jurassic2ChatBedrockApi(AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider, BedrockAi21Jurassic2ChatProperties properties, - BedrockAwsConnectionProperties awsProperties) { + BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) { return new Ai21Jurassic2ChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - new ObjectMapper(), awsProperties.getTimeout()); + objectMapper, awsProperties.getTimeout()); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/llama/BedrockLlamaChatAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/llama/BedrockLlamaChatAutoConfiguration.java index cac69ce298..59341b7f17 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/llama/BedrockLlamaChatAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/llama/BedrockLlamaChatAutoConfiguration.java @@ -52,9 +52,10 @@ public class BedrockLlamaChatAutoConfiguration { @ConditionalOnMissingBean @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) public LlamaChatBedrockApi llamaApi(AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider, - BedrockLlamaChatProperties properties, BedrockAwsConnectionProperties awsProperties) { + BedrockLlamaChatProperties properties, BedrockAwsConnectionProperties awsProperties, + ObjectMapper objectMapper) { return new LlamaChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - new ObjectMapper(), awsProperties.getTimeout()); + objectMapper, awsProperties.getTimeout()); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanChatAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanChatAutoConfiguration.java index 0115967fe5..639085cb75 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanChatAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanChatAutoConfiguration.java @@ -50,9 +50,9 @@ public class BedrockTitanChatAutoConfiguration { @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) public TitanChatBedrockApi titanChatBedrockApi(AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider, BedrockTitanChatProperties properties, - BedrockAwsConnectionProperties awsProperties) { + BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) { return new TitanChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - new ObjectMapper(), awsProperties.getTimeout()); + objectMapper, awsProperties.getTimeout()); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanEmbeddingAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanEmbeddingAutoConfiguration.java index bfca436bf1..37b63e1ab6 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanEmbeddingAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanEmbeddingAutoConfiguration.java @@ -51,9 +51,9 @@ public class BedrockTitanEmbeddingAutoConfiguration { @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) public TitanEmbeddingBedrockApi titanEmbeddingBedrockApi(AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider, BedrockTitanEmbeddingProperties properties, - BedrockAwsConnectionProperties awsProperties) { + BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) { return new TitanEmbeddingBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - new ObjectMapper(), awsProperties.getTimeout()); + objectMapper, awsProperties.getTimeout()); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfiguration.java index c6d031ec1b..441cb04add 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfiguration.java @@ -39,6 +39,7 @@ * @author Christian Tzolov * @author EddĂș MelĂ©ndez * @author Soby Chacko + * @author Sebastien Deleuze */ @AutoConfiguration @ConditionalOnClass({ EmbeddingModel.class, RestClient.class, ChromaVectorStore.class, ObjectMapper.class }) @@ -54,12 +55,13 @@ PropertiesChromaConnectionDetails chromaConnectionDetails(ChromaApiProperties pr @Bean @ConditionalOnMissingBean public ChromaApi chromaApi(ChromaApiProperties apiProperties, - ObjectProvider restClientBuilderProvider, ChromaConnectionDetails connectionDetails) { + ObjectProvider restClientBuilderProvider, ChromaConnectionDetails connectionDetails, + ObjectMapper objectMapper) { String chromaUrl = String.format("%s:%s", connectionDetails.getHost(), connectionDetails.getPort()); var chromaApi = new ChromaApi(chromaUrl, restClientBuilderProvider.getIfAvailable(RestClient::builder), - new ObjectMapper()); + objectMapper); if (StringUtils.hasText(connectionDetails.getKeyToken())) { chromaApi.withKeyToken(connectionDetails.getKeyToken()); diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactoryTest.java index 1086c6bf1e..2bf14e3de3 100644 --- a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactoryTest.java +++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactoryTest.java @@ -15,6 +15,7 @@ */ package org.springframework.ai.testcontainers.service.connection.chroma; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaVectorStoreAutoConfiguration; import org.springframework.ai.document.Document; @@ -83,6 +84,11 @@ public void addAndSearchWithFilters() { @ImportAutoConfiguration(ChromaVectorStoreAutoConfiguration.class) static class Config { + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + @Bean public EmbeddingModel embeddingModel() { return new TransformersEmbeddingModel(); diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithToken2ContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithToken2ContainerConnectionDetailsFactoryTest.java index a2c568df11..1c7daf6423 100644 --- a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithToken2ContainerConnectionDetailsFactoryTest.java +++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithToken2ContainerConnectionDetailsFactoryTest.java @@ -15,6 +15,7 @@ */ package org.springframework.ai.testcontainers.service.connection.chroma; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaVectorStoreAutoConfiguration; import org.springframework.ai.document.Document; @@ -87,6 +88,11 @@ public void addAndSearchWithFilters() { @ImportAutoConfiguration(ChromaVectorStoreAutoConfiguration.class) static class Config { + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + @Bean public EmbeddingModel embeddingModel() { return new TransformersEmbeddingModel(); diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithTokenContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithTokenContainerConnectionDetailsFactoryTest.java index a0c6e40a60..34460d6fb3 100644 --- a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithTokenContainerConnectionDetailsFactoryTest.java +++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithTokenContainerConnectionDetailsFactoryTest.java @@ -15,6 +15,7 @@ */ package org.springframework.ai.testcontainers.service.connection.chroma; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaVectorStoreAutoConfiguration; import org.springframework.ai.document.Document; @@ -85,6 +86,11 @@ public void addAndSearchWithFilters() { @ImportAutoConfiguration(ChromaVectorStoreAutoConfiguration.class) static class Config { + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + @Bean public EmbeddingModel embeddingModel() { return new TransformersEmbeddingModel(); diff --git a/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/vectorstore/ChromaVectorStore.java b/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/vectorstore/ChromaVectorStore.java index 1203688b53..f777868065 100644 --- a/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/vectorstore/ChromaVectorStore.java +++ b/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/vectorstore/ChromaVectorStore.java @@ -32,6 +32,7 @@ import org.springframework.ai.embedding.EmbeddingOptionsBuilder; import org.springframework.ai.embedding.TokenCountBatchingStrategy; import org.springframework.ai.observation.conventions.VectorStoreProvider; +import org.springframework.ai.util.JacksonUtils; import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore; import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; @@ -42,6 +43,9 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; import io.micrometer.observation.ObservationRegistry; /** @@ -50,10 +54,11 @@ * their similarity to a query, using the {@link ChromaApi} and {@link EmbeddingModel} for * embedding calculations. For more information about how it does this, see the official * Chroma website. - * + * * @author Christian Tzolov * @author Fu Cheng - * + * @author Sebastien Deleuze + * */ public class ChromaVectorStore extends AbstractObservationVectorStore implements InitializingBean { @@ -79,6 +84,8 @@ public class ChromaVectorStore extends AbstractObservationVectorStore implements private final BatchingStrategy batchingStrategy; + private final ObjectMapper objectMapper; + public ChromaVectorStore(EmbeddingModel embeddingModel, ChromaApi chromaApi, boolean initializeSchema) { this(embeddingModel, chromaApi, DEFAULT_COLLECTION_NAME, initializeSchema); } @@ -101,6 +108,7 @@ public ChromaVectorStore(EmbeddingModel embeddingModel, ChromaApi chromaApi, Str this.initializeSchema = initializeSchema; this.filterExpressionConverter = new ChromaFilterExpressionConverter(); this.batchingStrategy = batchingStrategy; + this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); } public void setFilterExpressionConverter(FilterExpressionConverter filterExpressionConverter) { @@ -152,8 +160,8 @@ public List doSimilaritySearch(SearchRequest request) { Assert.notNull(query, "Query string must not be null"); float[] embedding = this.embeddingModel.embed(query); - Map where = (StringUtils.hasText(nativeFilterExpression)) - ? JsonUtils.jsonToMap(nativeFilterExpression) : Map.of(); + Map where = (StringUtils.hasText(nativeFilterExpression)) ? jsonToMap(nativeFilterExpression) + : Map.of(); var queryRequest = new ChromaApi.QueryRequest(embedding, request.getTopK(), where); var queryResponse = this.chromaApi.queryCollection(this.collectionId, queryRequest); var embeddings = this.chromaApi.toEmbeddingResponseList(queryResponse); @@ -179,6 +187,16 @@ public List doSimilaritySearch(SearchRequest request) { return responseDocuments; } + @SuppressWarnings("unchecked") + private Map jsonToMap(String jsonText) { + try { + return (Map) this.objectMapper.readValue(jsonText, Map.class); + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + public String getCollectionName() { return this.collectionName; } diff --git a/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/vectorstore/JsonUtils.java b/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/vectorstore/JsonUtils.java deleted file mode 100644 index 81860ccd86..0000000000 --- a/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/vectorstore/JsonUtils.java +++ /dev/null @@ -1,79 +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.vectorstore; - -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Utility class for JSON processing. Provides methods for converting JSON strings to maps - * and lists, and for converting between lists of different numeric types. - * - * @author Christian Tzolov - */ -public class JsonUtils { - - /** - * Converts a JSON string to a map. - * @param jsonText the JSON string to convert - * @return the map representation of the JSON string - * @throws RuntimeException if an error occurs during the conversion - */ - public static Map jsonToMap(String jsonText) { - try { - return (Map) new ObjectMapper().readValue(jsonText, Map.class); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - // /** - // * Converts a list of doubles to a list of floats. - // * @param embeddingDouble the list of doubles to convert - // * @return the list of floats - // */ - // public static List toFloatList(List embeddingDouble) { - // return embeddingDouble.stream().map(Number::floatValue).toList(); - // } - - /** - * Converts a list of doubles to a float array. - * @param embedding the list of doubles to convert - * @return the float array - */ - public static float[] toFloatArray(List embedding) { - float[] embeddingFloat = new float[embedding.size()]; - int i = 0; - for (Float d : embedding) { - embeddingFloat[i++] = d.floatValue(); - } - return embeddingFloat; - } - - /** - * Converts a list of floats to a list of doubles. - * @param floats the list of floats to convert - * @return the list of doubles - */ - public static List toDouble(List floats) { - return floats.stream().map(f -> f.doubleValue()).toList(); - } - -} diff --git a/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/GemFireVectorStore.java b/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/GemFireVectorStore.java index 05e6a01cff..416e550532 100644 --- a/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/GemFireVectorStore.java +++ b/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/GemFireVectorStore.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Optional; +import com.fasterxml.jackson.databind.json.JsonMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.document.Document; @@ -32,6 +33,7 @@ import org.springframework.ai.embedding.EmbeddingOptionsBuilder; import org.springframework.ai.embedding.TokenCountBatchingStrategy; import org.springframework.ai.observation.conventions.VectorStoreProvider; +import org.springframework.ai.util.JacksonUtils; import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore; import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention; @@ -62,6 +64,7 @@ * @author Christian Tzolov * @author Thomas Vitale * @author Soby Chacko + * @author Sebastien Deleuze */ public class GemFireVectorStore extends AbstractObservationVectorStore implements InitializingBean { @@ -81,6 +84,8 @@ public class GemFireVectorStore extends AbstractObservationVectorStore implement private final BatchingStrategy batchingStrategy; + private final ObjectMapper objectMapper; + /** * Configures and initializes a GemFireVectorStore instance based on the provided * configuration. @@ -127,6 +132,7 @@ public GemFireVectorStore(GemFireVectorStoreConfig config, EmbeddingModel embedd .toString(); this.client = WebClient.create(base); this.batchingStrategy = batchingStrategy; + this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); } // Create Index Parameters @@ -419,10 +425,9 @@ public void doAdd(List documents) { document.getContent(), document.getMetadata())) .toList()); - ObjectMapper objectMapper = new ObjectMapper(); String embeddingsJson = null; try { - String embeddingString = objectMapper.writeValueAsString(upload); + String embeddingString = this.objectMapper.writeValueAsString(upload); embeddingsJson = embeddingString.substring("{\"embeddings\":".length()); } catch (JsonProcessingException e) { @@ -498,8 +503,7 @@ public void createIndex() throws JsonProcessingException { createRequest.setVectorSimilarityFunction(vectorSimilarityFunction); createRequest.setFields(fields); - ObjectMapper objectMapper = new ObjectMapper(); - String index = objectMapper.writeValueAsString(createRequest); + String index = this.objectMapper.writeValueAsString(createRequest); client.post() .contentType(MediaType.APPLICATION_JSON) diff --git a/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaCloudVectorStore.java b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaCloudVectorStore.java index 66e6d29a0c..d420a206d7 100644 --- a/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaCloudVectorStore.java +++ b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaCloudVectorStore.java @@ -17,6 +17,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; import io.micrometer.observation.ObservationRegistry; import org.slf4j.Logger; @@ -26,6 +28,7 @@ import org.springframework.ai.model.EmbeddingUtils; import org.springframework.ai.observation.conventions.VectorStoreProvider; import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric; +import org.springframework.ai.util.JacksonUtils; import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore; import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext.Builder; @@ -64,6 +67,7 @@ * * @author Rahul Mittal * @author Christian Tzolov + * @author Sebastien Deleuze * @see SAP * HANA Database Vector Engine Guide @@ -79,6 +83,8 @@ public class HanaCloudVectorStore extends AbstractObservationVectorStore { private final HanaCloudVectorStoreConfig config; + private final ObjectMapper objectMapper; + public HanaCloudVectorStore(HanaVectorRepository repository, EmbeddingModel embeddingModel, HanaCloudVectorStoreConfig config) { @@ -94,6 +100,7 @@ public HanaCloudVectorStore(HanaVectorRepository rep this.repository = repository; this.embeddingModel = embeddingModel; this.config = config; + this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); } @Override @@ -142,7 +149,7 @@ public List doSimilaritySearch(SearchRequest request) { return searchResult.stream().map(c -> { try { - return new Document(c.get_id(), c.toJson(), Collections.emptyMap()); + return new Document(c.get_id(), this.objectMapper.writeValueAsString(c), Collections.emptyMap()); } catch (JsonProcessingException e) { throw new RuntimeException(e); diff --git a/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaVectorEntity.java b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaVectorEntity.java index 375316f0c5..439b4f88f8 100644 --- a/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaVectorEntity.java +++ b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaVectorEntity.java @@ -15,8 +15,6 @@ */ package org.springframework.ai.vectorstore; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.Column; import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; @@ -40,10 +38,6 @@ public abstract class HanaVectorEntity { public HanaVectorEntity() { } - public String toJson() throws JsonProcessingException { - return new ObjectMapper().writeValueAsString(this); - } - public String get_id() { return _id; } diff --git a/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java b/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java index 697960f15d..486902fb8f 100644 --- a/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java +++ b/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.pgvector.PGvector; import io.micrometer.observation.ObservationRegistry; import org.postgresql.util.PGobject; @@ -29,6 +30,7 @@ import org.springframework.ai.embedding.TokenCountBatchingStrategy; import org.springframework.ai.observation.conventions.VectorStoreProvider; import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric; +import org.springframework.ai.util.JacksonUtils; import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore; import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; @@ -60,6 +62,7 @@ * @author Muthukumaran Navaneethakrishnan * @author Thomas Vitale * @author Soby Chacko + * @author Sebastien Deleuze * @since 1.0.0 */ public class PgVectorStore extends AbstractObservationVectorStore implements InitializingBean { @@ -100,7 +103,7 @@ public class PgVectorStore extends AbstractObservationVectorStore implements Ini private final PgDistanceType distanceType; - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; private final boolean removeExistingVectorStoreTable; @@ -154,6 +157,8 @@ private PgVectorStore(String schemaName, String vectorTableName, boolean vectorT super(observationRegistry, customObservationConvention); + this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); + this.vectorTableName = (null == vectorTableName || vectorTableName.isEmpty()) ? DEFAULT_TABLE_NAME : vectorTableName.trim(); logger.info("Using the vector table name: {}. Is empty: {}", this.vectorTableName, @@ -231,7 +236,7 @@ public int getBatchSize() { private String toJson(Map map) { try { - return objectMapper.writeValueAsString(map); + return this.objectMapper.writeValueAsString(map); } catch (JsonProcessingException e) { throw new RuntimeException(e);