Refactor auto-configurations

- Split model autoconfigurations based on the model

    - Change the autoconfiguration class into model specific autoconfigurations - chat, embedding, image etc.,
    - Update/add tests based on this change

 - Make sure the conditional logic to enable the model auto configuration is at the class level so that the configuration properties as well as the models are not enabled when the model is explicitly disabled. By default, the condition will allow enabling the beans if not explicitly overridden.

- Remove spring-ai-spring-boot-autoconfigure as a dedicated auto-configuration module

Signed-off-by: Ilayaperumal Gopinathan <ilayaperumal.gopinathan@broadcom.com>
This commit is contained in:
Ilayaperumal Gopinathan
2025-03-16 10:28:48 +00:00
committed by Soby Chacko
parent 32a2580635
commit d9c40652e9
375 changed files with 1769 additions and 22851 deletions

View File

@@ -39,21 +39,18 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- Boot dependencies -->

View File

@@ -56,9 +56,11 @@ import org.springframework.web.reactive.function.client.WebClient;
ToolCallingAutoConfiguration.class })
@EnableConfigurationProperties({ AnthropicChatProperties.class, AnthropicConnectionProperties.class })
@ConditionalOnClass(AnthropicApi.class)
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ANTHROPIC,
matchIfMissing = true)
@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
ToolCallingAutoConfiguration.class, WebClientAutoConfiguration.class })
public class AnthropicAutoConfiguration {
public class AnthropicChatAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@@ -74,8 +76,6 @@ public class AnthropicAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ANTHROPIC,
matchIfMissing = true)
public AnthropicChatModel anthropicChatModel(AnthropicApi anthropicApi, AnthropicChatProperties chatProperties,
RetryTemplate retryTemplate, ToolCallingManager toolCallingManager,
ObjectProvider<ObservationRegistry> observationRegistry,

View File

@@ -13,4 +13,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration
org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration

View File

@@ -39,13 +39,13 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".*")
public class AnthropicAutoConfigurationIT {
public class AnthropicChatAutoConfigurationIT {
private static final Log logger = LogFactory.getLog(AnthropicAutoConfigurationIT.class);
private static final Log logger = LogFactory.getLog(AnthropicChatAutoConfigurationIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"))
.withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(AnthropicChatAutoConfiguration.class));
@Test
void call() {

View File

@@ -25,7 +25,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit Tests for {@link AnthropicAutoConfiguration}'s conditional enabling of models.
* Unit Tests for {@link AnthropicChatAutoConfiguration}'s conditional enabling of models.
*
* @author Ilayaperumal Gopinathan
*/
@@ -33,7 +33,7 @@ public class AnthropicModelConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"))
.withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(AnthropicChatAutoConfiguration.class));
@Test
void chatModelActivation() {
@@ -42,7 +42,7 @@ public class AnthropicModelConfigurationTests {
});
this.contextRunner.withPropertyValues("spring.ai.model.chat=none").run(context -> {
assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isEmpty();
assertThat(context.getBeansOfType(AnthropicChatModel.class)).isEmpty();
});

View File

@@ -44,7 +44,7 @@ public class AnthropicPropertiesTests {
"spring.ai.anthropic.chat.options.temperature=0.55")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
RestClientAutoConfiguration.class, AnthropicChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(AnthropicChatProperties.class);
var connectionProperties = context.getBean(AnthropicConnectionProperties.class);
@@ -80,7 +80,7 @@ public class AnthropicPropertiesTests {
)
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
RestClientAutoConfiguration.class, AnthropicChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(AnthropicChatProperties.class);
var connectionProperties = context.getBean(AnthropicConnectionProperties.class);
@@ -104,7 +104,7 @@ public class AnthropicPropertiesTests {
// It is enabled by default
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
RestClientAutoConfiguration.class, AnthropicChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AnthropicChatModel.class)).isNotEmpty();
@@ -113,7 +113,7 @@ public class AnthropicPropertiesTests {
// Explicitly enable the chat auto-configuration.
new ApplicationContextRunner().withPropertyValues("spring.ai.anthropic.chat.enabled=true")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
RestClientAutoConfiguration.class, AnthropicChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AnthropicChatModel.class)).isNotEmpty();
@@ -122,7 +122,7 @@ public class AnthropicPropertiesTests {
// Explicitly disable the chat auto-configuration.
new ApplicationContextRunner().withPropertyValues("spring.ai.model.chat=none")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
RestClientAutoConfiguration.class, AnthropicChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(AnthropicChatModel.class)).isEmpty();
});

View File

@@ -27,13 +27,12 @@ import org.slf4j.LoggerFactory;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration;
import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration;
import org.springframework.ai.model.anthropic.autoconfigure.tool.MockWeatherService.Request;
import org.springframework.ai.model.anthropic.autoconfigure.tool.MockWeatherService.Response;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -50,7 +49,7 @@ class FunctionCallWithFunctionBeanIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"))
.withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(AnthropicChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@Test

View File

@@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration;
import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
@@ -43,7 +43,7 @@ public class FunctionCallWithPromptFunctionIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"))
.withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(AnthropicChatAutoConfiguration.class));
@Test
void functionCallTest() {

View File

@@ -39,35 +39,30 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-image-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- Boot dependencies -->

View File

@@ -0,0 +1,56 @@
/*
* 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.model.azure.openai.autoconfigure;
import com.azure.ai.openai.OpenAIClientBuilder;
import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* {@link AutoConfiguration Auto-configuration} for Azure OpenAI.
*
* @author Piotr Olaszewski
* @author Soby Chacko
* @author Manuel Andreo Garcia
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration
@ConditionalOnClass({ AzureOpenAiAudioTranscriptionModel.class })
@EnableConfigurationProperties(AzureOpenAiAudioTranscriptionProperties.class)
@ConditionalOnProperty(name = SpringAIModelProperties.AUDIO_TRANSCRIPTION_MODEL,
havingValue = SpringAIModels.AZURE_OPENAI, matchIfMissing = true)
@ImportAutoConfiguration(classes = AzureOpenAiClientBuilderAutoConfiguration.class)
public class AzureOpenAiAudioTranscriptionAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AzureOpenAiAudioTranscriptionModel azureOpenAiAudioTranscriptionModel(OpenAIClientBuilder openAIClient,
AzureOpenAiAudioTranscriptionProperties audioProperties) {
return new AzureOpenAiAudioTranscriptionModel(openAIClient.buildClient(), audioProperties.getOptions());
}
}

View File

@@ -1,195 +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.model.azure.openai.autoconfigure;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.credential.KeyCredential;
import com.azure.core.credential.TokenCredential;
import com.azure.core.util.ClientOptions;
import com.azure.core.util.Header;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
import org.springframework.ai.azure.openai.AzureOpenAiImageModel;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link AutoConfiguration Auto-configuration} for Azure OpenAI.
*
* @author Piotr Olaszewski
* @author Soby Chacko
* @author Manuel Andreo Garcia
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration(after = { ToolCallingAutoConfiguration.class })
@ConditionalOnClass({ OpenAIClientBuilder.class, AzureOpenAiChatModel.class })
@EnableConfigurationProperties({ AzureOpenAiChatProperties.class, AzureOpenAiEmbeddingProperties.class,
AzureOpenAiConnectionProperties.class, AzureOpenAiImageOptionsProperties.class,
AzureOpenAiAudioTranscriptionProperties.class })
@ImportAutoConfiguration(classes = { ToolCallingAutoConfiguration.class })
public class AzureOpenAiAutoConfiguration {
private static final String APPLICATION_ID = "spring-ai";
@Bean
@ConditionalOnMissingBean // ({ OpenAIClient.class, TokenCredential.class })
public OpenAIClientBuilder openAIClientBuilder(AzureOpenAiConnectionProperties connectionProperties,
ObjectProvider<AzureOpenAIClientBuilderCustomizer> customizers) {
if (StringUtils.hasText(connectionProperties.getApiKey())) {
Assert.hasText(connectionProperties.getEndpoint(), "Endpoint must not be empty");
Map<String, String> customHeaders = connectionProperties.getCustomHeaders();
List<Header> headers = customHeaders.entrySet()
.stream()
.map(entry -> new Header(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
ClientOptions clientOptions = new ClientOptions().setApplicationId(APPLICATION_ID).setHeaders(headers);
OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint(connectionProperties.getEndpoint())
.credential(new AzureKeyCredential(connectionProperties.getApiKey()))
.clientOptions(clientOptions);
applyOpenAIClientBuilderCustomizers(clientBuilder, customizers);
return clientBuilder;
}
// Connect to OpenAI (e.g. not the Azure OpenAI). The deploymentName property is
// used as OpenAI model name.
if (StringUtils.hasText(connectionProperties.getOpenAiApiKey())) {
OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint("https://api.openai.com/v1")
.credential(new KeyCredential(connectionProperties.getOpenAiApiKey()))
.clientOptions(new ClientOptions().setApplicationId(APPLICATION_ID));
applyOpenAIClientBuilderCustomizers(clientBuilder, customizers);
return clientBuilder;
}
throw new IllegalArgumentException("Either API key or OpenAI API key must not be empty");
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(TokenCredential.class)
public OpenAIClientBuilder openAIClientWithTokenCredential(AzureOpenAiConnectionProperties connectionProperties,
TokenCredential tokenCredential, ObjectProvider<AzureOpenAIClientBuilderCustomizer> customizers) {
Assert.notNull(tokenCredential, "TokenCredential must not be null");
Assert.hasText(connectionProperties.getEndpoint(), "Endpoint must not be empty");
OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint(connectionProperties.getEndpoint())
.credential(tokenCredential)
.clientOptions(new ClientOptions().setApplicationId(APPLICATION_ID));
applyOpenAIClientBuilderCustomizers(clientBuilder, customizers);
return clientBuilder;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.AZURE_OPENAI,
matchIfMissing = true)
public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder,
AzureOpenAiChatProperties chatProperties, ToolCallingManager toolCallingManager,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var chatModel = AzureOpenAiChatModel.builder()
.openAIClientBuilder(openAIClientBuilder)
.defaultOptions(chatProperties.getOptions())
.toolCallingManager(toolCallingManager)
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.build();
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.AZURE_OPENAI,
matchIfMissing = true)
public AzureOpenAiEmbeddingModel azureOpenAiEmbeddingModel(OpenAIClientBuilder openAIClient,
AzureOpenAiEmbeddingProperties embeddingProperties, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<EmbeddingModelObservationConvention> observationConvention) {
var embeddingModel = new AzureOpenAiEmbeddingModel(openAIClient.buildClient(),
embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(),
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(embeddingModel::setObservationConvention);
return embeddingModel;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.AZURE_OPENAI,
matchIfMissing = true)
public AzureOpenAiImageModel azureOpenAiImageModel(OpenAIClientBuilder openAIClientBuilder,
AzureOpenAiImageOptionsProperties imageProperties) {
return new AzureOpenAiImageModel(openAIClientBuilder.buildClient(), imageProperties.getOptions());
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.AUDIO_TRANSCRIPTION_MODEL,
havingValue = SpringAIModels.AZURE_OPENAI, matchIfMissing = true)
public AzureOpenAiAudioTranscriptionModel azureOpenAiAudioTranscriptionModel(OpenAIClientBuilder openAIClient,
AzureOpenAiAudioTranscriptionProperties audioProperties) {
return new AzureOpenAiAudioTranscriptionModel(openAIClient.buildClient(), audioProperties.getOptions());
}
private void applyOpenAIClientBuilderCustomizers(OpenAIClientBuilder clientBuilder,
ObjectProvider<AzureOpenAIClientBuilderCustomizer> customizers) {
customizers.orderedStream().forEach(customizer -> customizer.customize(clientBuilder));
}
@Bean
@ConditionalOnMissingBean
public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
manager.setApplicationContext(context);
return manager;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2024 the original author or authors.
* 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.
@@ -14,73 +14,59 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.converse;
package org.springframework.ai.model.azure.openai.autoconfigure;
import com.azure.ai.openai.OpenAIClientBuilder;
import io.micrometer.observation.ObservationRegistry;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.providers.AwsRegionProvider;
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeAsyncClient;
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionConfiguration;
import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
/**
* {@link AutoConfiguration Auto-configuration} for Bedrock Converse Proxy Chat Client.
* {@link AutoConfiguration Auto-configuration} for Azure OpenAI.
*
* Leverages the Spring Cloud AWS to resolve the {@link AwsCredentialsProvider}.
*
* @author Christian Tzolov
* @author Wei Jiang
* @author Piotr Olaszewski
* @author Soby Chacko
* @author Manuel Andreo Garcia
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration(after = { ToolCallingAutoConfiguration.class })
@EnableConfigurationProperties({ BedrockConverseProxyChatProperties.class, BedrockAwsConnectionConfiguration.class })
@ConditionalOnClass({ BedrockProxyChatModel.class, BedrockRuntimeClient.class, BedrockRuntimeAsyncClient.class })
@ConditionalOnProperty(prefix = BedrockConverseProxyChatProperties.CONFIG_PREFIX, name = "enabled",
havingValue = "true", matchIfMissing = true)
@Import(BedrockAwsConnectionConfiguration.class)
@ImportAutoConfiguration({ ToolCallingAutoConfiguration.class })
public class BedrockConverseProxyChatAutoConfiguration {
@ConditionalOnClass({ AzureOpenAiChatModel.class })
@EnableConfigurationProperties({ AzureOpenAiChatProperties.class })
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.AZURE_OPENAI,
matchIfMissing = true)
@ImportAutoConfiguration(
classes = { AzureOpenAiClientBuilderAutoConfiguration.class, ToolCallingAutoConfiguration.class })
public class AzureOpenAiChatAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
public BedrockProxyChatModel bedrockProxyChatModel(AwsCredentialsProvider credentialsProvider,
AwsRegionProvider regionProvider, BedrockAwsConnectionProperties connectionProperties,
BedrockConverseProxyChatProperties chatProperties, ToolCallingManager toolCallingManager,
public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder,
AzureOpenAiChatProperties chatProperties, ToolCallingManager toolCallingManager,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention,
ObjectProvider<BedrockRuntimeClient> bedrockRuntimeClient,
ObjectProvider<BedrockRuntimeAsyncClient> bedrockRuntimeAsyncClient) {
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var chatModel = BedrockProxyChatModel.builder()
.credentialsProvider(credentialsProvider)
.region(regionProvider.getRegion())
.timeout(connectionProperties.getTimeout())
var chatModel = AzureOpenAiChatModel.builder()
.openAIClientBuilder(openAIClientBuilder)
.defaultOptions(chatProperties.getOptions())
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.toolCallingManager(toolCallingManager)
.bedrockRuntimeClient(bedrockRuntimeClient.getIfAvailable())
.bedrockRuntimeAsyncClient(bedrockRuntimeAsyncClient.getIfAvailable())
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.build();
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;

View File

@@ -26,27 +26,13 @@ import com.azure.core.credential.KeyCredential;
import com.azure.core.credential.TokenCredential;
import com.azure.core.util.ClientOptions;
import com.azure.core.util.Header;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
import org.springframework.ai.azure.openai.AzureOpenAiImageModel;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -59,13 +45,10 @@ import org.springframework.util.StringUtils;
* @author Manuel Andreo Garcia
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration(after = { ToolCallingAutoConfiguration.class })
@ConditionalOnClass({ OpenAIClientBuilder.class, AzureOpenAiChatModel.class })
@EnableConfigurationProperties({ AzureOpenAiChatProperties.class, AzureOpenAiEmbeddingProperties.class,
AzureOpenAiConnectionProperties.class, AzureOpenAiImageOptionsProperties.class,
AzureOpenAiAudioTranscriptionProperties.class })
@ImportAutoConfiguration(classes = { ToolCallingAutoConfiguration.class })
public class AzureOpenAiAutoConfiguration {
@AutoConfiguration
@ConditionalOnClass({ OpenAIClientBuilder.class })
@EnableConfigurationProperties(AzureOpenAiConnectionProperties.class)
public class AzureOpenAiClientBuilderAutoConfiguration {
private static final String APPLICATION_ID = "spring-ai";
@@ -120,71 +103,6 @@ public class AzureOpenAiAutoConfiguration {
return clientBuilder;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = AzureOpenAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder,
AzureOpenAiChatProperties chatProperties, ToolCallingManager toolCallingManager,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var chatModel = AzureOpenAiChatModel.builder()
.openAIClientBuilder(openAIClientBuilder)
.defaultOptions(chatProperties.getOptions())
.toolCallingManager(toolCallingManager)
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.build();
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = AzureOpenAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled",
havingValue = "true", matchIfMissing = true)
public AzureOpenAiEmbeddingModel azureOpenAiEmbeddingModel(OpenAIClientBuilder openAIClient,
AzureOpenAiEmbeddingProperties embeddingProperties, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<EmbeddingModelObservationConvention> observationConvention) {
var embeddingModel = new AzureOpenAiEmbeddingModel(openAIClient.buildClient(),
embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(),
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(embeddingModel::setObservationConvention);
return embeddingModel;
}
@Bean
@ConditionalOnMissingBean
public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
manager.setApplicationContext(context);
return manager;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = AzureOpenAiImageOptionsProperties.CONFIG_PREFIX, name = "enabled",
havingValue = "true", matchIfMissing = true)
public AzureOpenAiImageModel azureOpenAiImageClient(OpenAIClientBuilder openAIClientBuilder,
AzureOpenAiImageOptionsProperties imageProperties) {
return new AzureOpenAiImageModel(openAIClientBuilder.buildClient(), imageProperties.getOptions());
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = AzureOpenAiAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled",
havingValue = "true", matchIfMissing = true)
public AzureOpenAiAudioTranscriptionModel azureOpenAiAudioTranscriptionModel(OpenAIClientBuilder openAIClient,
AzureOpenAiAudioTranscriptionProperties audioProperties) {
return new AzureOpenAiAudioTranscriptionModel(openAIClient.buildClient(), audioProperties.getOptions());
}
private void applyOpenAIClientBuilderCustomizers(OpenAIClientBuilder clientBuilder,
ObjectProvider<AzureOpenAIClientBuilderCustomizer> customizers) {
customizers.orderedStream().forEach(customizer -> customizer.customize(clientBuilder));

View File

@@ -14,16 +14,18 @@
* limitations under the License.
*/
package org.springframework.ai.model.transformers.autoconfigure;
package org.springframework.ai.model.azure.openai.autoconfigure;
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import ai.onnxruntime.OrtSession;
import com.azure.ai.openai.OpenAIClientBuilder;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.transformers.TransformersEmbeddingModel;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -31,41 +33,35 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean;
/**
* {@link AutoConfiguration Auto-configuration} for Transformers Embedding Model.
* {@link AutoConfiguration Auto-configuration} for Azure OpenAI.
*
* @author Christian Tzolov
* @author Piotr Olaszewski
* @author Soby Chacko
* @author Manuel Andreo Garcia
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration
@EnableConfigurationProperties({ TransformersEmbeddingModelProperties.class })
@ConditionalOnClass({ OrtSession.class, HuggingFaceTokenizer.class, TransformersEmbeddingModel.class })
public class TransformersEmbeddingModelAutoConfiguration {
@ConditionalOnClass({ AzureOpenAiEmbeddingModel.class })
@EnableConfigurationProperties({ AzureOpenAiEmbeddingProperties.class })
@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.AZURE_OPENAI,
matchIfMissing = true)
@ImportAutoConfiguration(classes = AzureOpenAiClientBuilderAutoConfiguration.class)
public class AzureOpenAiEmbeddingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = TransformersEmbeddingModelProperties.CONFIG_PREFIX, name = "enabled",
havingValue = "true", matchIfMissing = true)
public TransformersEmbeddingModel embeddingModel(TransformersEmbeddingModelProperties properties,
ObjectProvider<ObservationRegistry> observationRegistry,
public AzureOpenAiEmbeddingModel azureOpenAiEmbeddingModel(OpenAIClientBuilder openAIClient,
AzureOpenAiEmbeddingProperties embeddingProperties, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<EmbeddingModelObservationConvention> observationConvention) {
TransformersEmbeddingModel embeddingModel = new TransformersEmbeddingModel(properties.getMetadataMode(),
var embeddingModel = new AzureOpenAiEmbeddingModel(openAIClient.buildClient(),
embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(),
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
embeddingModel.setDisableCaching(!properties.getCache().isEnabled());
embeddingModel.setResourceCacheDirectory(properties.getCache().getDirectory());
embeddingModel.setTokenizerResource(properties.getTokenizer().getUri());
embeddingModel.setTokenizerOptions(properties.getTokenizer().getOptions());
embeddingModel.setModelResource(properties.getOnnx().getModelUri());
embeddingModel.setGpuDeviceId(properties.getOnnx().getGpuDeviceId());
embeddingModel.setModelOutputName(properties.getOnnx().getModelOutputName());
observationConvention.ifAvailable(embeddingModel::setObservationConvention);
return embeddingModel;
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.model.azure.openai.autoconfigure;
import com.azure.ai.openai.OpenAIClientBuilder;
import org.springframework.ai.azure.openai.AzureOpenAiImageModel;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* {@link AutoConfiguration Auto-configuration} for Azure OpenAI.
*
* @author Piotr Olaszewski
* @author Soby Chacko
* @author Manuel Andreo Garcia
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration
@ConditionalOnClass(AzureOpenAiImageModel.class)
@ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.AZURE_OPENAI,
matchIfMissing = true)
@EnableConfigurationProperties(AzureOpenAiImageOptionsProperties.class)
@ImportAutoConfiguration(classes = AzureOpenAiClientBuilderAutoConfiguration.class)
public class AzureOpenAiImageAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AzureOpenAiImageModel azureOpenAiImageModel(OpenAIClientBuilder openAIClientBuilder,
AzureOpenAiImageOptionsProperties imageProperties) {
return new AzureOpenAiImageModel(openAIClientBuilder.buildClient(), imageProperties.getOptions());
}
}

View File

@@ -13,4 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration
org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration
org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration
org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiImageAutoConfiguration
org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAudioTranscriptionAutoConfiguration

View File

@@ -35,11 +35,8 @@ import com.azure.core.http.HttpResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAIClientBuilderCustomizer;
import reactor.core.publisher.Flux;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
@@ -86,7 +83,7 @@ class AzureOpenAiAutoConfigurationIT {
"spring.ai.azure.openai.embedding.options.deployment-name=" + EMBEDDING_MODEL_NAME,
"spring.ai.azure.openai.audio.transcription.options.deployment-name=" + System.getenv("AZURE_OPENAI_TRANSCRIPTION_DEPLOYMENT_NAME")
// @formatter:on
).withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class));
);
private final Message systemMessage = new SystemPromptTemplate("""
You are a helpful AI assistant. Your name is {name}.
@@ -100,16 +97,17 @@ class AzureOpenAiAutoConfigurationIT {
@Test
void chatCompletion() {
this.contextRunner.run(context -> {
AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
ChatResponse response = chatModel.call(new Prompt(List.of(this.userMessage, this.systemMessage)));
assertThat(response.getResult().getOutput().getText()).contains("Blackbeard");
});
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.run(context -> {
AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
ChatResponse response = chatModel.call(new Prompt(List.of(this.userMessage, this.systemMessage)));
assertThat(response.getResult().getOutput().getText()).contains("Blackbeard");
});
}
@Test
void httpRequestContainsUserAgentAndCustomHeaders() {
this.contextRunner
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.withPropertyValues("spring.ai.azure.openai.custom-headers.foo=bar",
"spring.ai.azure.openai.custom-headers.fizz=buzz")
.run(context -> {
@@ -136,100 +134,137 @@ class AzureOpenAiAutoConfigurationIT {
@Test
void chatCompletionStreaming() {
this.contextRunner.run(context -> {
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.run(context -> {
AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
Flux<ChatResponse> response = chatModel.stream(new Prompt(List.of(this.userMessage, this.systemMessage)));
Flux<ChatResponse> response = chatModel
.stream(new Prompt(List.of(this.userMessage, this.systemMessage)));
List<ChatResponse> responses = response.collectList().block();
assertThat(responses.size()).isGreaterThan(10);
List<ChatResponse> responses = response.collectList().block();
assertThat(responses.size()).isGreaterThan(10);
String stitchedResponseContent = responses.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getText)
.collect(Collectors.joining());
String stitchedResponseContent = responses.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getText)
.collect(Collectors.joining());
assertThat(stitchedResponseContent).contains("Blackbeard");
});
assertThat(stitchedResponseContent).contains("Blackbeard");
});
}
@Test
void embedding() {
this.contextRunner.run(context -> {
AzureOpenAiEmbeddingModel embeddingModel = context.getBean(AzureOpenAiEmbeddingModel.class);
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class))
.run(context -> {
AzureOpenAiEmbeddingModel embeddingModel = context.getBean(AzureOpenAiEmbeddingModel.class);
EmbeddingResponse embeddingResponse = embeddingModel
.embedForResponse(List.of("Hello World", "World is big and salvation is near"));
assertThat(embeddingResponse.getResults()).hasSize(2);
assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
EmbeddingResponse embeddingResponse = embeddingModel
.embedForResponse(List.of("Hello World", "World is big and salvation is near"));
assertThat(embeddingResponse.getResults()).hasSize(2);
assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
assertThat(embeddingModel.dimensions()).isEqualTo(1536);
});
assertThat(embeddingModel.dimensions()).isEqualTo(1536);
});
}
@Test
@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_TRANSCRIPTION_DEPLOYMENT_NAME", matches = ".+")
void transcribe() {
this.contextRunner.run(context -> {
AzureOpenAiAudioTranscriptionModel transcriptionModel = context
.getBean(AzureOpenAiAudioTranscriptionModel.class);
Resource audioFile = new ClassPathResource("/speech/jfk.flac");
String response = transcriptionModel.call(audioFile);
assertThat(response).isEqualTo(
"And so my fellow Americans, ask not what your country can do for you, ask what you can do for your country.");
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class))
.run(context -> {
AzureOpenAiAudioTranscriptionModel transcriptionModel = context
.getBean(AzureOpenAiAudioTranscriptionModel.class);
Resource audioFile = new ClassPathResource("/speech/jfk.flac");
String response = transcriptionModel.call(audioFile);
assertThat(response).isEqualTo(
"And so my fellow Americans, ask not what your country can do for you, ask what you can do for your country.");
});
}
@Test
void chatActivation() {
// Disable the chat auto-configuration.
this.contextRunner.withPropertyValues("spring.ai.azure.openai.chat.enabled=false")
.run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty());
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=none")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty();
});
// The chat auto-configuration is enabled by default.
this.contextRunner.run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty());
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty();
});
// Explicitly enable the chat auto-configuration.
this.contextRunner.withPropertyValues("spring.ai.azure.openai.chat.enabled=true")
.run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty());
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=azure-openai")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty();
});
}
@Test
void embeddingActivation() {
// Disable the embedding auto-configuration.
this.contextRunner.withPropertyValues("spring.ai.azure.openai.embedding.enabled=false")
.run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty());
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.embedding=none")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isEmpty();
});
// The embedding auto-configuration is enabled by default.
this.contextRunner
.run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty());
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty();
});
// Explicitly enable the embedding auto-configuration.
this.contextRunner.withPropertyValues("spring.ai.azure.openai.embedding.enabled=true")
.run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty());
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.embedding=azure-openai")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty();
});
}
@Test
void audioTranscriptionActivation() {
// Disable the transcription auto-configuration.
this.contextRunner.withPropertyValues("spring.ai.azure.openai.audio.transcription.enabled=false")
.run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty());
this.contextRunner
.withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class))
.withPropertyValues("spring.ai.model.audio.transcription=none")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isEmpty();
});
// The transcription auto-configuration is enabled by default.
this.contextRunner
.withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class))
.run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty());
// Explicitly enable the transcription auto-configuration.
this.contextRunner.withPropertyValues("spring.ai.azure.openai.audio.transcription.enabled=true")
this.contextRunner
.withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class))
.withPropertyValues("spring.ai.model.audio.transcription=azure-openai")
.run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty());
}
@@ -237,7 +272,7 @@ class AzureOpenAiAutoConfigurationIT {
void openAIClientBuilderCustomizer() {
AtomicBoolean firstCustomizationApplied = new AtomicBoolean(false);
AtomicBoolean secondCustomizationApplied = new AtomicBoolean(false);
this.contextRunner
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.withBean("first", AzureOpenAIClientBuilderCustomizer.class,
() -> clientBuilder -> firstCustomizationApplied.set(true))
.withBean("second", AzureOpenAIClientBuilderCustomizer.class,

View File

@@ -18,10 +18,6 @@ package org.springframework.ai.model.azure.openai.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatProperties;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiConnectionProperties;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -40,7 +36,7 @@ public class AzureOpenAiAutoConfigurationPropertyTests {
.withPropertyValues("spring.ai.azure.openai.api-key=TEST_API_KEY",
"spring.ai.azure.openai.endpoint=TEST_ENDPOINT",
"spring.ai.azure.openai.embedding.options.deployment-name=MODEL_XYZ")
.withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(AzureOpenAiEmbeddingProperties.class);
var connectionProperties = context.getBean(AzureOpenAiConnectionProperties.class);
@@ -72,7 +68,8 @@ public class AzureOpenAiAutoConfigurationPropertyTests {
"spring.ai.azure.openai.chat.options.user=userXYZ"
)
// @formatter:on
.withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class,
AzureOpenAiEmbeddingAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(AzureOpenAiChatProperties.class);
var connectionProperties = context.getBean(AzureOpenAiConnectionProperties.class);

View File

@@ -24,7 +24,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import reactor.core.publisher.Flux;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
import org.springframework.ai.chat.messages.AssistantMessage;
@@ -60,7 +59,9 @@ public class AzureOpenAiDirectOpenAiAutoConfigurationIT {
"spring.ai.azure.openai.chat.options.maxTokens=123",
"spring.ai.azure.openai.embedding.options.deployment-name=" + EMBEDDING_MODEL_NAME
// @formatter:on
).withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class));
)
.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class,
AzureOpenAiEmbeddingAutoConfiguration.class));
private final Message systemMessage = new SystemPromptTemplate("""
You are a helpful AI assistant. Your name is {name}.

View File

@@ -29,38 +29,42 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit Tests for {@link AzureOpenAiAutoConfiguration}'s conditional enabling of models.
* Unit Tests for Azure OpenAI auto-configurations conditional enabling of models.
*
* @author Ilayaperumal Gopinathan
*/
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
public class AzureOpenAiModelConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.azure.openai.openai-api-key=" + System.getenv("OPENAI_API_KEY"),
"spring.ai.openai.base-url=TEST_BASE_URL")
.withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class));
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
"spring.ai.azure.openai.openai-api-key=" + System.getenv("OPENAI_API_KEY"),
"spring.ai.openai.base-url=TEST_BASE_URL");
@Test
void chatModelActivation() {
this.contextRunner.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.chat=none").run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=none")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.chat=azure-openai").run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=azure-openai")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty();
});
this.contextRunner
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=azure-openai", "spring.ai.model.embedding=none",
"spring.ai.model.image=none", "spring.ai.model.audio.speech=none",
"spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none")
@@ -74,24 +78,29 @@ public class AzureOpenAiModelConfigurationTests {
@Test
void embeddingModelActivation() {
this.contextRunner.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.embedding=none")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.embedding=azure-openai").run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.embedding=azure-openai")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=azure-openai",
"spring.ai.model.image=none", "spring.ai.model.audio.speech=none",
"spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none")
@@ -105,24 +114,29 @@ public class AzureOpenAiModelConfigurationTests {
@Test
void imageModelActivation() {
this.contextRunner.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiImageAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.image=none").run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiImageOptionsProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiImageAutoConfiguration.class))
.withPropertyValues("spring.ai.model.image=none")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiImageOptionsProperties.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.image=azure-openai").run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiImageOptionsProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiImageAutoConfiguration.class))
.withPropertyValues("spring.ai.model.image=azure-openai")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiImageOptionsProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty();
});
this.contextRunner
this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiImageAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none",
"spring.ai.model.image=azure-openai", "spring.ai.model.audio.speech=none",
"spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none")
@@ -136,24 +150,33 @@ public class AzureOpenAiModelConfigurationTests {
@Test
void audioTranscriptionModelActivation() {
this.contextRunner.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.audio.transcription=none").run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.audio.transcription=azure-openai").run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty();
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty();
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class))
.withPropertyValues("spring.ai.model.audio.transcription=none")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty();
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class))
.withPropertyValues("spring.ai.model.audio.transcription=azure-openai")
.run(context -> {
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty();
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none",
"spring.ai.model.image=none", "spring.ai.model.audio.speech=none",
"spring.ai.model.audio.transcription=azure-openai", "spring.ai.model.moderation=none")

View File

@@ -24,13 +24,13 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -51,7 +51,7 @@ class FunctionCallWithFunctionBeanIT {
"spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"),
"spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT"))
// @formatter:onn
.withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@Test

View File

@@ -23,12 +23,12 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -49,7 +49,7 @@ public class FunctionCallWithFunctionWrapperIT {
"spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"),
"spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT"))
// @formatter:onn
.withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@Test

View File

@@ -23,12 +23,12 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -46,7 +46,7 @@ public class FunctionCallWithPromptFunctionIT {
"spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"),
"spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT"))
// @formatter:onn
.withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class));
@Test
void functionCallTest() {

View File

@@ -46,35 +46,30 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-image-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- Boot dependencies -->

View File

@@ -1,64 +0,0 @@
/*
* 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.model.bedrock.autoconfigure.converse;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.util.Assert;
/**
* Configuration properties for Bedrock Converse.
*
* @author Christian Tzolov
* @since 1.0.0
*/
@ConfigurationProperties(BedrockConverseProxyChatProperties.CONFIG_PREFIX)
public class BedrockConverseProxyChatProperties {
public static final String CONFIG_PREFIX = "spring.ai.bedrock.converse.chat";
/**
* Enable Bedrock Converse chat model.
*/
private boolean enabled = true;
@NestedConfigurationProperty
private ToolCallingChatOptions options = ToolCallingChatOptions.builder()
.temperature(0.7)
.maxTokens(300)
.topK(10)
.build();
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public ToolCallingChatOptions getOptions() {
return this.options;
}
public void setOptions(ToolCallingChatOptions options) {
Assert.notNull(options, "FunctionCallingOptions must not be null");
this.options = options;
}
}

View File

@@ -1,78 +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.model.bedrock.autoconfigure.titan;
import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel.InputType;
import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi.TitanEmbeddingModel;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Bedrock Titan Embedding autoconfiguration properties.
*
* @author Christian Tzolov
* @since 0.8.0
*/
@ConfigurationProperties(BedrockTitanEmbeddingProperties.CONFIG_PREFIX)
public class BedrockTitanEmbeddingProperties {
public static final String CONFIG_PREFIX = "spring.ai.bedrock.titan.embedding";
/**
* Enable Bedrock Titan Embedding Model. False by default.
*/
private boolean enabled = false;
/**
* Bedrock Titan Embedding generative name. Defaults to 'amazon.titan-embed-image-v1'.
*/
private String model = TitanEmbeddingModel.TITAN_EMBED_IMAGE_V1.id();
/**
* Titan Embedding API input types. Could be either text or image (encoded in base64).
* Defaults to {@link InputType#IMAGE}.
*/
private InputType inputType = InputType.IMAGE;
public static String getConfigPrefix() {
return CONFIG_PREFIX;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getModel() {
return this.model;
}
public void setModel(String model) {
this.model = model;
}
public InputType getInputType() {
return this.inputType;
}
public void setInputType(InputType inputType) {
this.inputType = inputType;
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.cohere;
package org.springframework.ai.model.bedrock.cohere.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.cohere;
package org.springframework.ai.model.bedrock.cohere.autoconfigure;
import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingOptions;
import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingModel;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.converse;
package org.springframework.ai.model.bedrock.converse.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.converse;
package org.springframework.ai.model.bedrock.converse.autoconfigure;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.titan;
package org.springframework.ai.model.bedrock.titan.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.titan;
package org.springframework.ai.model.bedrock.titan.autoconfigure;
import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel.InputType;
import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi.TitanEmbeddingModel;

View File

@@ -13,6 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
cohere.org.springframework.ai.model.bedrock.autoconfigure.BedrockCohereEmbeddingAutoConfiguration
titan.org.springframework.ai.model.bedrock.autoconfigure.BedrockTitanEmbeddingAutoConfiguration
converse.org.springframework.ai.model.bedrock.autoconfigure.BedrockConverseProxyChatAutoConfiguration
org.springframework.ai.model.bedrock.cohere.autoconfigure.BedrockCohereEmbeddingAutoConfiguration
org.springframework.ai.model.bedrock.titan.autoconfigure.BedrockTitanEmbeddingAutoConfiguration
org.springframework.ai.model.bedrock.converse.autoconfigure.BedrockConverseProxyChatAutoConfiguration

View File

@@ -1,130 +0,0 @@
/*
* Copyright 2023-2025 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.model.bedrock.autoconfigure.converse.tool;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils;
import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials;
import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration;
import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import static org.assertj.core.api.Assertions.assertThat;
@RequiresAwsCredentials
class FunctionCallWithFunctionBeanIT {
private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionBeanIT.class);
private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner()
.withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@Test
void functionCallTest() {
this.contextRunner
.withPropertyValues(
"spring.ai.bedrock.converse.chat.options.model=" + "anthropic.claude-3-5-sonnet-20240620-v1:0")
.run(context -> {
BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class);
var userMessage = new UserMessage(
"What's the weather like in San Francisco, in Paris, France and in Tokyo, Japan? Return the temperature in Celsius.");
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
ToolCallingChatOptions.builder().toolNames("weatherFunction").build()));
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
response = chatModel.call(new Prompt(List.of(userMessage),
ToolCallingChatOptions.builder().toolNames("weatherFunction3").build()));
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
});
}
@Test
void functionStreamTest() {
this.contextRunner
.withPropertyValues(
"spring.ai.bedrock.converse.chat.options.model=" + "anthropic.claude-3-5-sonnet-20240620-v1:0")
.run(context -> {
BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class);
var userMessage = new UserMessage(
"What's the weather like in San Francisco, in Paris, France and in Tokyo, Japan? Return the temperature in Celsius.");
Flux<ChatResponse> responses = chatModel.stream(new Prompt(List.of(userMessage),
ToolCallingChatOptions.builder().toolNames("weatherFunction").build()));
String content = responses.collectList()
.block()
.stream()
.filter(cr -> cr.getResult() != null)
.map(cr -> cr.getResult().getOutput().getText())
.collect(Collectors.joining());
logger.info("Response: {}", content);
assertThat(content).contains("30", "10", "15");
});
}
@Configuration
static class Config {
@Bean
@Description("Get the weather in location. Return temperature in 36°F or 36°C format.")
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction() {
return new MockWeatherService();
}
// Relies on the Request's JsonClassDescription annotation to provide the
// function description.
@Bean
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction3() {
MockWeatherService weatherService = new MockWeatherService();
return (weatherService::apply);
}
}
}

View File

@@ -1,95 +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.model.bedrock.autoconfigure.converse.tool;
import java.util.function.Function;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
/**
* Mock 3rd party weather service.
*
* @author Christian Tzolov
*/
public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> {
@Override
public Response apply(Request request) {
double temperature = 0;
if (request.location().contains("Paris")) {
temperature = 15;
}
else if (request.location().contains("Tokyo")) {
temperature = 10;
}
else if (request.location().contains("San Francisco")) {
temperature = 30;
}
return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
}
/**
* Temperature units.
*/
public enum Unit {
/**
* Celsius.
*/
C("metric"),
/**
* Fahrenheit.
*/
F("imperial");
/**
* Human readable unit name.
*/
public final String unitName;
Unit(String text) {
this.unitName = text;
}
}
/**
* Weather Function request.
*/
@JsonInclude(Include.NON_NULL)
@JsonClassDescription("Weather API request")
public record Request(@JsonProperty(required = true,
value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
@JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
}
/**
* Weather Function response.
*/
public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
Unit unit) {
}
}

View File

@@ -14,15 +14,12 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.cohere;
package org.springframework.ai.model.bedrock.cohere.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingModel;
import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel;
import org.springframework.ai.model.bedrock.autoconfigure.titan.BedrockTitanEmbeddingAutoConfiguration;
import org.springframework.ai.model.bedrock.autoconfigure.titan.BedrockTitanEmbeddingProperties;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.converse;
package org.springframework.ai.model.bedrock.converse.autoconfigure;
import org.junit.jupiter.api.Test;
@@ -25,9 +25,8 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit Tests for
* {@link org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration}'s
* conditional enabling of models.
* Unit Tests for {@link BedrockConverseProxyChatAutoConfiguration}'s conditional enabling
* of models.
*
* @author Ilayaperumal Gopinathan
*/

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.converse;
package org.springframework.ai.model.bedrock.converse.autoconfigure;
import java.util.List;
import java.util.stream.Collectors;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.converse.tool;
package org.springframework.ai.model.bedrock.converse.autoconfigure.tool;
import java.util.List;
import java.util.function.Function;
@@ -27,7 +27,7 @@ import reactor.core.publisher.Flux;
import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils;
import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials;
import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration;
import org.springframework.ai.model.bedrock.converse.autoconfigure.BedrockConverseProxyChatAutoConfiguration;
import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.converse.tool;
package org.springframework.ai.model.bedrock.converse.autoconfigure.tool;
import java.util.List;
@@ -24,7 +24,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils;
import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials;
import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration;
import org.springframework.ai.model.bedrock.converse.autoconfigure.BedrockConverseProxyChatAutoConfiguration;
import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.anthropic.autoconfigure.tool;
package org.springframework.ai.model.bedrock.converse.autoconfigure.tool;
import java.util.function.Function;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.titan;
package org.springframework.ai.model.bedrock.titan.autoconfigure;
import java.util.Base64;
import java.util.List;

View File

@@ -14,15 +14,12 @@
* limitations under the License.
*/
package org.springframework.ai.model.bedrock.autoconfigure.titan;
package org.springframework.ai.model.bedrock.titan.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel;
import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration;
import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatProperties;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

View File

@@ -39,7 +39,6 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- Boot dependencies -->

View File

@@ -29,12 +29,12 @@ import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnClass(HuggingfaceChatModel.class)
@EnableConfigurationProperties(HuggingfaceChatProperties.class)
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.HUGGINGFACE,
matchIfMissing = true)
public class HuggingfaceChatAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.HUGGINGFACE,
matchIfMissing = true)
public HuggingfaceChatModel huggingfaceChatModel(HuggingfaceChatProperties huggingfaceChatProperties) {
return new HuggingfaceChatModel(huggingfaceChatProperties.getApiKey(), huggingfaceChatProperties.getUrl());
}

View File

@@ -42,7 +42,7 @@ public class HuggingfaceModelConfigurationTests {
});
this.contextRunner.withPropertyValues("spring.ai.model.chat=none").run(context -> {
assertThat(context.getBeansOfType(HuggingfaceChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(HuggingfaceChatProperties.class)).isEmpty();
assertThat(context.getBeansOfType(HuggingfaceChatModel.class)).isEmpty();
});

View File

@@ -39,28 +39,24 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- Boot dependencies -->

View File

@@ -20,17 +20,15 @@ import java.util.List;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -47,20 +45,20 @@ import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
/**
* {@link AutoConfiguration Auto-configuration} for MiniMax Chat and Embedding Models.
* {@link AutoConfiguration Auto-configuration} for MiniMax Chat Model.
*
* @author Geng Rong
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@ConditionalOnClass(MiniMaxApi.class)
@EnableConfigurationProperties({ MiniMaxConnectionProperties.class, MiniMaxChatProperties.class,
MiniMaxEmbeddingProperties.class })
public class MiniMaxAutoConfiguration {
@EnableConfigurationProperties({ MiniMaxConnectionProperties.class, MiniMaxChatProperties.class })
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MINIMAX,
matchIfMissing = true)
public class MiniMaxChatAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MINIMAX,
matchIfMissing = true)
public MiniMaxChatModel miniMaxChatModel(MiniMaxConnectionProperties commonProperties,
MiniMaxChatProperties chatProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider,
List<FunctionCallback> toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver,
@@ -79,29 +77,6 @@ public class MiniMaxAutoConfiguration {
return chatModel;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.MINIMAX,
matchIfMissing = true)
public MiniMaxEmbeddingModel miniMaxEmbeddingModel(MiniMaxConnectionProperties commonProperties,
MiniMaxEmbeddingProperties embeddingProperties,
ObjectProvider<RestClient.Builder> restClientBuilderProvider, RetryTemplate retryTemplate,
ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<EmbeddingModelObservationConvention> observationConvention) {
var miniMaxApi = miniMaxApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(),
embeddingProperties.getApiKey(), commonProperties.getApiKey(),
restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
var embeddingModel = new MiniMaxEmbeddingModel(miniMaxApi, embeddingProperties.getMetadataMode(),
embeddingProperties.getOptions(), retryTemplate,
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(embeddingModel::setObservationConvention);
return embeddingModel;
}
private MiniMaxApi miniMaxApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {

View File

@@ -16,19 +16,14 @@
package org.springframework.ai.model.minimax.autoconfigure;
import java.util.List;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -36,7 +31,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
@@ -45,42 +39,20 @@ import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
/**
* {@link AutoConfiguration Auto-configuration} for MiniMax Chat and Embedding Models.
* {@link AutoConfiguration Auto-configuration} for MiniMax Embedding Model.
*
* @author Geng Rong
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@ConditionalOnClass(MiniMaxApi.class)
@EnableConfigurationProperties({ MiniMaxConnectionProperties.class, MiniMaxChatProperties.class,
MiniMaxEmbeddingProperties.class })
public class MiniMaxAutoConfiguration {
@EnableConfigurationProperties({ MiniMaxConnectionProperties.class, MiniMaxEmbeddingProperties.class })
@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.MINIMAX,
matchIfMissing = true)
public class MiniMaxEmbeddingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = MiniMaxChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public MiniMaxChatModel miniMaxChatModel(MiniMaxConnectionProperties commonProperties,
MiniMaxChatProperties chatProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider,
List<FunctionCallback> toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var miniMaxApi = miniMaxApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
chatProperties.getApiKey(), commonProperties.getApiKey(),
restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
var chatModel = new MiniMaxChatModel(miniMaxApi, chatProperties.getOptions(), functionCallbackResolver,
toolFunctionCallbacks, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = MiniMaxEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public MiniMaxEmbeddingModel miniMaxEmbeddingModel(MiniMaxConnectionProperties commonProperties,
MiniMaxEmbeddingProperties embeddingProperties,
ObjectProvider<RestClient.Builder> restClientBuilderProvider, RetryTemplate retryTemplate,
@@ -112,12 +84,4 @@ public class MiniMaxAutoConfiguration {
return new MiniMaxApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler);
}
@Bean
@ConditionalOnMissingBean
public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
manager.setApplicationContext(context);
return manager;
}
}

View File

@@ -13,4 +13,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.springframework.ai.model.minimax.autoconfigure.MiniMaxAutoConfiguration
org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration
org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration

View File

@@ -25,7 +25,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
@@ -34,6 +33,7 @@ import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -51,7 +51,7 @@ public class FunctionCallbackInPromptIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class));
RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class));
@Test
void functionCallTest() {

View File

@@ -55,7 +55,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
// FIXME: multiple function calls may stop prematurely due to model performance

View File

@@ -47,51 +47,58 @@ public class MiniMaxAutoConfigurationIT {
private static final Log logger = LogFactory.getLog(MiniMaxAutoConfigurationIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class));
.withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY"));
@Test
void generate() {
this.contextRunner.run(context -> {
MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
String response = chatModel.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class))
.run(context -> {
MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
String response = chatModel.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
@Test
void generateStreaming() {
this.contextRunner.run(context -> {
MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
Flux<ChatResponse> responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
String response = responseFlux.collectList()
.block()
.stream()
.map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
.collect(Collectors.joining());
this.contextRunner
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class))
.run(context -> {
MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
Flux<ChatResponse> responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
String response = responseFlux.collectList()
.block()
.stream()
.map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
.collect(Collectors.joining());
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
@Test
void embedding() {
this.contextRunner.run(context -> {
MiniMaxEmbeddingModel embeddingModel = context.getBean(MiniMaxEmbeddingModel.class);
this.contextRunner
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class))
.run(context -> {
MiniMaxEmbeddingModel embeddingModel = context.getBean(MiniMaxEmbeddingModel.class);
EmbeddingResponse embeddingResponse = embeddingModel
.embedForResponse(List.of("Hello World", "World is big and salvation is near"));
assertThat(embeddingResponse.getResults()).hasSize(2);
assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
EmbeddingResponse embeddingResponse = embeddingModel
.embedForResponse(List.of("Hello World", "World is big and salvation is near"));
assertThat(embeddingResponse.getResults()).hasSize(2);
assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
assertThat(embeddingModel.dimensions()).isEqualTo(1536);
});
assertThat(embeddingModel.dimensions()).isEqualTo(1536);
});
}
}

View File

@@ -25,7 +25,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
@@ -34,6 +33,7 @@ import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -53,7 +53,7 @@ public class MiniMaxFunctionCallbackIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@Test

View File

@@ -50,7 +50,7 @@ public class MiniMaxPropertiesTests {
"spring.ai.minimax.chat.options.temperature=0.55")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(MiniMaxChatProperties.class);
var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
@@ -79,7 +79,7 @@ public class MiniMaxPropertiesTests {
"spring.ai.minimax.chat.options.temperature=0.55")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(MiniMaxChatProperties.class);
var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
@@ -105,7 +105,7 @@ public class MiniMaxPropertiesTests {
"spring.ai.minimax.embedding.options.model=MODEL_XYZ")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class))
.run(context -> {
var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class);
var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
@@ -132,7 +132,7 @@ public class MiniMaxPropertiesTests {
"spring.ai.minimax.embedding.options.model=MODEL_XYZ")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class))
.run(context -> {
var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class);
var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
@@ -199,17 +199,14 @@ public class MiniMaxPropertiesTests {
)
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(MiniMaxChatProperties.class);
var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class);
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("embo-01");
assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5);
assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
@@ -248,7 +245,7 @@ public class MiniMaxPropertiesTests {
)
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class))
.run(context -> {
var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class);
@@ -267,16 +264,16 @@ public class MiniMaxPropertiesTests {
.withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL",
"spring.ai.model.embedding=none")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty();
});
new ApplicationContextRunner()
.withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty();
@@ -286,7 +283,7 @@ public class MiniMaxPropertiesTests {
.withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL",
"spring.ai.minimax.embedding.enabled=true")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty();
@@ -299,16 +296,16 @@ public class MiniMaxPropertiesTests {
.withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL",
"spring.ai.model.chat=none")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isEmpty();
assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isEmpty();
});
new ApplicationContextRunner()
.withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty();
@@ -318,7 +315,7 @@ public class MiniMaxPropertiesTests {
.withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL",
"spring.ai.model.chat=minimax")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty();

View File

@@ -27,57 +27,59 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit Tests for {@link MiniMaxAutoConfiguration}'s conditional enabling of models.
* Unit Tests for MiniMax auto-configurations' conditional enabling of models.
*
* @author Ilayaperumal Gopinathan
*/
public class MinimaxModelConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MiniMaxAutoConfiguration.class, SpringAiRetryAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class))
.withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL");
@Test
void chatModelActivation() {
this.contextRunner.run(context -> {
this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxChatAutoConfiguration.class)).run(context -> {
assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none")
this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxChatAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none")
.run(context -> {
assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isEmpty();
assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.chat=minimax", "spring.ai.model.embedding=none")
this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxChatAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=minimax", "spring.ai.model.embedding=none")
.run(context -> {
assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty();
});
}
@Test
void embeddingModelActivation() {
this.contextRunner.run(context -> {
assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxEmbeddingAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> {
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.embedding=none")
.run(context -> {
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.embedding=minimax").run(context -> {
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.embedding=minimax")
.run(context -> {
assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty();
});
}
}

View File

@@ -39,35 +39,30 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-image-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- Boot dependencies -->

View File

@@ -18,18 +18,16 @@ package org.springframework.ai.model.mistralai.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.mistralai.MistralAiChatModel;
import org.springframework.ai.mistralai.api.MistralAiApi;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.mistralai.MistralAiChatModel;
import org.springframework.ai.mistralai.MistralAiEmbeddingModel;
import org.springframework.ai.mistralai.api.MistralAiApi;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
@@ -47,49 +45,26 @@ import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
/**
* {@link AutoConfiguration Auto-configuration} for Mistral AI.
* Chat {@link AutoConfiguration Auto-configuration} for Mistral AI.
*
* @author Ricken Bazolo
* @author Christian Tzolov
* @author Thomas Vitale
* @author Ilayaperumal Gopinathan
* @since 0.8.1
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
ToolCallingAutoConfiguration.class })
@EnableConfigurationProperties({ MistralAiEmbeddingProperties.class, MistralAiCommonProperties.class,
MistralAiChatProperties.class })
@EnableConfigurationProperties({ MistralAiCommonProperties.class, MistralAiChatProperties.class })
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MISTRAL,
matchIfMissing = true)
@ConditionalOnClass(MistralAiApi.class)
@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
ToolCallingAutoConfiguration.class })
public class MistralAiAutoConfiguration {
public class MistralAiChatAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.MISTRAL,
matchIfMissing = true)
public MistralAiEmbeddingModel mistralAiEmbeddingModel(MistralAiCommonProperties commonProperties,
MistralAiEmbeddingProperties embeddingProperties,
ObjectProvider<RestClient.Builder> restClientBuilderProvider, RetryTemplate retryTemplate,
ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<EmbeddingModelObservationConvention> observationConvention) {
var mistralAiApi = mistralAiApi(embeddingProperties.getApiKey(), commonProperties.getApiKey(),
embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(),
restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
var embeddingModel = new MistralAiEmbeddingModel(mistralAiApi, embeddingProperties.getMetadataMode(),
embeddingProperties.getOptions(), retryTemplate,
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(embeddingModel::setObservationConvention);
return embeddingModel;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MISTRAL,
matchIfMissing = true)
public MistralAiChatModel mistralAiChatModel(MistralAiCommonProperties commonProperties,
MistralAiChatProperties chatProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider,
ToolCallingManager toolCallingManager, RetryTemplate retryTemplate,

View File

@@ -18,16 +18,12 @@ package org.springframework.ai.model.mistralai.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.mistralai.MistralAiChatModel;
import org.springframework.ai.mistralai.MistralAiEmbeddingModel;
import org.springframework.ai.mistralai.api.MistralAiApi;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
@@ -36,7 +32,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
@@ -45,26 +40,24 @@ import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
/**
* {@link AutoConfiguration Auto-configuration} for Mistral AI.
* Embedding {@link AutoConfiguration Auto-configuration} for Mistral AI.
*
* @author Ricken Bazolo
* @author Christian Tzolov
* @author Thomas Vitale
* @author Ilayaperumal Gopinathan
* @since 0.8.1
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
ToolCallingAutoConfiguration.class })
@EnableConfigurationProperties({ MistralAiEmbeddingProperties.class, MistralAiCommonProperties.class,
MistralAiChatProperties.class })
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@EnableConfigurationProperties({ MistralAiCommonProperties.class, MistralAiEmbeddingProperties.class })
@ConditionalOnClass(MistralAiApi.class)
@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
ToolCallingAutoConfiguration.class })
public class MistralAiAutoConfiguration {
@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.MISTRAL,
matchIfMissing = true)
@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class })
public class MistralAiEmbeddingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = MistralAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public MistralAiEmbeddingModel mistralAiEmbeddingModel(MistralAiCommonProperties commonProperties,
MistralAiEmbeddingProperties embeddingProperties,
ObjectProvider<RestClient.Builder> restClientBuilderProvider, RetryTemplate retryTemplate,
@@ -84,33 +77,6 @@ public class MistralAiAutoConfiguration {
return embeddingModel;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = MistralAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public MistralAiChatModel mistralAiChatModel(MistralAiCommonProperties commonProperties,
MistralAiChatProperties chatProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider,
ToolCallingManager toolCallingManager, RetryTemplate retryTemplate,
ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var mistralAiApi = mistralAiApi(chatProperties.getApiKey(), commonProperties.getApiKey(),
chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
var chatModel = MistralAiChatModel.builder()
.mistralAiApi(mistralAiApi)
.defaultOptions(chatProperties.getOptions())
.toolCallingManager(toolCallingManager)
.retryTemplate(retryTemplate)
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.build();
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
private MistralAiApi mistralAiApi(String apiKey, String commonApiKey, String baseUrl, String commonBaseUrl,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
@@ -123,12 +89,4 @@ public class MistralAiAutoConfiguration {
return new MistralAiApi(resoledBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler);
}
@Bean
@ConditionalOnMissingBean
public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
manager.setApplicationContext(context);
return manager;
}
}

View File

@@ -13,4 +13,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration
org.springframework.ai.model.mistralai.autoconfigure.MistralAiChatAutoConfiguration
org.springframework.ai.model.mistralai.autoconfigure.MistralAiEmbeddingAutoConfiguration

View File

@@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
* @author Ilayaperumal Gopinathan
* @since 0.8.1
*/
@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*")
@@ -46,50 +47,52 @@ public class MistralAiAutoConfigurationIT {
private static final Log logger = LogFactory.getLog(MistralAiAutoConfigurationIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"))
.withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class));
.withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"));
@Test
void generate() {
this.contextRunner.run(context -> {
MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class);
String response = chatModel.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class))
.run(context -> {
MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class);
String response = chatModel.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
@Test
void generateStreaming() {
this.contextRunner.run(context -> {
MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class);
Flux<ChatResponse> responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
String response = responseFlux.collectList()
.block()
.stream()
.map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
.collect(Collectors.joining());
this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class))
.run(context -> {
MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class);
Flux<ChatResponse> responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
String response = responseFlux.collectList()
.block()
.stream()
.map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
.collect(Collectors.joining());
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
@Test
void embedding() {
this.contextRunner.run(context -> {
MistralAiEmbeddingModel embeddingModel = context.getBean(MistralAiEmbeddingModel.class);
this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiEmbeddingAutoConfiguration.class))
.run(context -> {
MistralAiEmbeddingModel embeddingModel = context.getBean(MistralAiEmbeddingModel.class);
EmbeddingResponse embeddingResponse = embeddingModel
.embedForResponse(List.of("Hello World", "World is big and salvation is near"));
assertThat(embeddingResponse.getResults()).hasSize(2);
assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
EmbeddingResponse embeddingResponse = embeddingModel
.embedForResponse(List.of("Hello World", "World is big and salvation is near"));
assertThat(embeddingResponse.getResults()).hasSize(2);
assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
assertThat(embeddingModel.dimensions()).isEqualTo(1024);
});
assertThat(embeddingModel.dimensions()).isEqualTo(1024);
});
}
}

View File

@@ -38,7 +38,7 @@ public class MistralAiPropertiesTests {
.withPropertyValues("spring.ai.mistralai.base-url=TEST_BASE_URL", "spring.ai.mistralai.api-key=abc123",
"spring.ai.mistralai.embedding.options.model=MODEL_XYZ")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class))
RestClientAutoConfiguration.class, MistralAiEmbeddingAutoConfiguration.class))
.run(context -> {
var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class);
var connectionProperties = context.getBean(MistralAiCommonProperties.class);
@@ -87,7 +87,7 @@ public class MistralAiPropertiesTests {
"spring.ai.mistralai.api-key=abc123", "spring.ai.mistralai.embedding.base-url=TEST_BASE_URL2",
"spring.ai.mistralai.embedding.api-key=456", "spring.ai.mistralai.embedding.options.model=MODEL_XYZ")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class))
RestClientAutoConfiguration.class, MistralAiChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(MistralAiChatProperties.class);
@@ -108,7 +108,7 @@ public class MistralAiPropertiesTests {
"spring.ai.mistralai.api-key=abc123", "spring.ai.mistralai.embedding.base-url=TEST_BASE_URL2",
"spring.ai.mistralai.embedding.api-key=456", "spring.ai.mistralai.embedding.options.model=MODEL_XYZ")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class))
RestClientAutoConfiguration.class, MistralAiEmbeddingAutoConfiguration.class))
.run(context -> {
var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class);
var connectionProperties = context.getBean(MistralAiCommonProperties.class);
@@ -132,7 +132,7 @@ public class MistralAiPropertiesTests {
"spring.ai.mistralai.embedding.options.model=MODEL_XYZ",
"spring.ai.mistralai.embedding.options.encodingFormat=MyEncodingFormat")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class))
RestClientAutoConfiguration.class, MistralAiEmbeddingAutoConfiguration.class))
.run(context -> {
var connectionProperties = context.getBean(MistralAiCommonProperties.class);
var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class);

View File

@@ -26,57 +26,64 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit Tests for {@link MistralAiAutoConfiguration}'s conditional enabling of models.
* Unit Tests for Mistral AI auto-configurations conditional enabling of models.
*
* @author Ilayaperumal Gopinathan
*/
public class MistralModelConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class))
.withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"));
@Test
void chatModelActivation() {
this.contextRunner.run(context -> {
assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none")
.run(context -> {
assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiChatModel.class)).isEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.chat=mistral", "spring.ai.model.embedding=none")
this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none")
.run(context -> {
assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isEmpty();
assertThat(context.getBeansOfType(MistralAiChatModel.class)).isEmpty();
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class,
MistralAiEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=mistral", "spring.ai.model.embedding=none")
.run(context -> {
assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty();
});
}
@Test
void embeddingModelActivation() {
this.contextRunner.run(context -> {
assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiEmbeddingAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> {
assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.embedding=none")
.run(context -> {
assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.embedding=mistral").run(context -> {
assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.embedding=mistral")
.run(context -> {
assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty();
});
}
}

View File

@@ -26,13 +26,13 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.mistralai.MistralAiChatModel;
import org.springframework.ai.mistralai.MistralAiChatOptions;
import org.springframework.ai.mistralai.api.MistralAiApi;
import org.springframework.ai.model.mistralai.autoconfigure.MistralAiChatAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
@@ -53,7 +53,7 @@ class PaymentStatusBeanIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"))
.withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@Test

View File

@@ -25,13 +25,13 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.mistralai.MistralAiChatModel;
import org.springframework.ai.mistralai.MistralAiChatOptions;
import org.springframework.ai.mistralai.api.MistralAiApi;
import org.springframework.ai.model.mistralai.autoconfigure.MistralAiChatAutoConfiguration;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -51,7 +51,7 @@ public class PaymentStatusPromptIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"))
.withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class));
@Test
void functionCallTest() {

View File

@@ -27,9 +27,6 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration;
import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Request;
import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Response;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
@@ -37,6 +34,9 @@ import org.springframework.ai.mistralai.MistralAiChatModel;
import org.springframework.ai.mistralai.MistralAiChatOptions;
import org.springframework.ai.mistralai.api.MistralAiApi;
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice;
import org.springframework.ai.model.mistralai.autoconfigure.MistralAiChatAutoConfiguration;
import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Request;
import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Response;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -56,7 +56,7 @@ public class WeatherServicePromptIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.mistralai.api-key=" + System.getenv("MISTRAL_AI_API_KEY"))
.withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class));
@Test
void promptFunctionCall() {

View File

@@ -39,21 +39,18 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -48,19 +48,17 @@ import org.springframework.web.client.RestClient;
* {@link AutoConfiguration Auto-configuration} for Moonshot Chat Model.
*
* @author Geng Rong
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@EnableConfigurationProperties({ MoonshotCommonProperties.class, MoonshotChatProperties.class })
@ConditionalOnClass(MoonshotApi.class)
public class MoonshotAutoConfiguration {
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MOONSHOT,
matchIfMissing = true)
public class MoonshotChatAutoConfiguration {
@Bean
@ConditionalOnMissingBean
// @ConditionalOnProperty(prefix = MoonshotChatProperties.CONFIG_PREFIX, name =
// "enabled", havingValue = "true",
// matchIfMissing = true)
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MOONSHOT,
matchIfMissing = true)
public MoonshotChatModel moonshotChatModel(MoonshotCommonProperties commonProperties,
MoonshotChatProperties chatProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider,
List<FunctionCallback> toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver,

View File

@@ -13,4 +13,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration
org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration

View File

@@ -1,77 +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.model.moonshot.autoconfigure;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import reactor.core.publisher.Flux;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
*/
@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*")
public class MoonshotAutoConfigurationIT {
private static final Log logger = LogFactory.getLog(MoonshotAutoConfigurationIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class));
@Test
void generate() {
this.contextRunner.run(context -> {
MoonshotChatModel client = context.getBean(MoonshotChatModel.class);
String response = client.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
@Test
void generateStreaming() {
this.contextRunner.run(context -> {
MoonshotChatModel client = context.getBean(MoonshotChatModel.class);
Flux<ChatResponse> responseFlux = client.stream(new Prompt(new UserMessage("Hello")));
String response = Objects.requireNonNull(responseFlux.collectList().block())
.stream()
.map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
.collect(Collectors.joining());
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
}

View File

@@ -40,14 +40,14 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Geng Rong
*/
@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*")
public class MoonshotAutoConfigurationIT {
public class MoonshotChatAutoConfigurationIT {
private static final Log logger = LogFactory.getLog(MoonshotAutoConfigurationIT.class);
private static final Log logger = LogFactory.getLog(MoonshotChatAutoConfigurationIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class));
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class));
@Test
void generate() {

View File

@@ -42,7 +42,7 @@ public class MoonshotPropertiesTests {
"spring.ai.moonshot.chat.options.temperature=0.55")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(MoonshotChatProperties.class);
var connectionProperties = context.getBean(MoonshotCommonProperties.class);
@@ -71,7 +71,7 @@ public class MoonshotPropertiesTests {
"spring.ai.moonshot.chat.options.temperature=0.55")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(MoonshotChatProperties.class);
var connectionProperties = context.getBean(MoonshotCommonProperties.class);
@@ -110,7 +110,7 @@ public class MoonshotPropertiesTests {
)
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(MoonshotChatProperties.class);
var connectionProperties = context.getBean(MoonshotCommonProperties.class);
@@ -137,16 +137,16 @@ public class MoonshotPropertiesTests {
.withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL",
"spring.ai.model.chat=none")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isEmpty();
assertThat(context.getBeansOfType(MoonshotChatModel.class)).isEmpty();
});
new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MoonshotChatModel.class)).isNotEmpty();
@@ -156,7 +156,7 @@ public class MoonshotPropertiesTests {
.withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL",
"spring.ai.model.chat=moonshot")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MoonshotChatModel.class)).isNotEmpty();

View File

@@ -25,7 +25,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration;
import org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
@@ -53,7 +53,7 @@ public class FunctionCallbackInPromptIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class));
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class));
@Test
void functionCallTest() {

View File

@@ -26,7 +26,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration;
import org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
@@ -57,7 +57,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@Test

View File

@@ -26,7 +26,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration;
import org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
@@ -56,7 +56,7 @@ public class MoonshotFunctionCallbackIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@Test

View File

@@ -46,14 +46,12 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- Boot dependencies -->

View File

@@ -1,115 +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.model.oci.genai.autoconfigure;
import java.io.IOException;
import com.oracle.bmc.ClientConfiguration;
import com.oracle.bmc.Region;
import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider;
import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider;
import com.oracle.bmc.auth.SimplePrivateKeySupplier;
import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider;
import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient;
import com.oracle.bmc.retrier.RetryConfiguration;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.oci.OCIEmbeddingModel;
import org.springframework.ai.oci.cohere.OCICohereChatModel;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.util.StringUtils;
/**
* {@link AutoConfiguration Auto-configuration} for Oracle Cloud Infrastructure Generative
* AI.
*
* @author Anders Swanson
*/
@AutoConfiguration
@ConditionalOnClass({ GenerativeAiInferenceClient.class, OCIEmbeddingModel.class })
@EnableConfigurationProperties({ OCIConnectionProperties.class, OCIEmbeddingModelProperties.class,
OCICohereChatModelProperties.class })
public class OCIGenAiAutoConfiguration {
private static BasicAuthenticationDetailsProvider authenticationProvider(OCIConnectionProperties properties)
throws IOException {
return switch (properties.getAuthenticationType()) {
case FILE -> new ConfigFileAuthenticationDetailsProvider(properties.getFile(), properties.getProfile());
case INSTANCE_PRINCIPAL -> InstancePrincipalsAuthenticationDetailsProvider.builder().build();
case WORKLOAD_IDENTITY -> OkeWorkloadIdentityAuthenticationDetailsProvider.builder().build();
case SIMPLE -> SimpleAuthenticationDetailsProvider.builder()
.userId(properties.getUserId())
.tenantId(properties.getTenantId())
.fingerprint(properties.getFingerprint())
.privateKeySupplier(new SimplePrivateKeySupplier(properties.getPrivateKey()))
.passPhrase(properties.getPassPhrase())
.region(Region.valueOf(properties.getRegion()))
.build();
};
}
@ConditionalOnMissingBean
@Bean
public GenerativeAiInferenceClient generativeAiInferenceClient(OCIConnectionProperties properties)
throws IOException {
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.retryConfiguration(RetryConfiguration.SDK_DEFAULT_RETRY_CONFIGURATION)
.build();
GenerativeAiInferenceClient.Builder builder = GenerativeAiInferenceClient.builder()
.configuration(clientConfiguration);
if (StringUtils.hasText(properties.getRegion())) {
builder.region(Region.valueOf(properties.getRegion()));
}
if (StringUtils.hasText(properties.getEndpoint())) {
builder.endpoint(properties.getEndpoint());
}
return builder.build(authenticationProvider(properties));
}
@Bean
@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.OCI_GENAI,
matchIfMissing = true)
public OCIEmbeddingModel ociEmbeddingModel(GenerativeAiInferenceClient generativeAiClient,
OCIEmbeddingModelProperties properties) {
return new OCIEmbeddingModel(generativeAiClient, properties.getEmbeddingOptions());
}
@Bean
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OCI_GENAI,
matchIfMissing = true)
public OCICohereChatModel ociChatModel(GenerativeAiInferenceClient generativeAiClient,
OCICohereChatModelProperties properties, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var chatModel = new OCICohereChatModel(generativeAiClient, properties.getOptions(),
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2023-2025 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.model.oci.genai.autoconfigure;
import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.oci.cohere.OCICohereChatModel;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* Chat {@link AutoConfiguration Auto-configuration} for Oracle Cloud Infrastructure
* Generative AI.
*
* @author Anders Swanson
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration
@ConditionalOnClass(OCICohereChatModel.class)
@EnableConfigurationProperties(OCICohereChatModelProperties.class)
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OCI_GENAI,
matchIfMissing = true)
@ImportAutoConfiguration(OCIGenAiInferenceClientAutoConfiguration.class)
public class OCIGenAiChatAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public OCICohereChatModel ociChatModel(GenerativeAiInferenceClient generativeAiClient,
OCICohereChatModelProperties properties, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var chatModel = new OCICohereChatModel(generativeAiClient, properties.getOptions(),
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2023-2025 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.model.oci.genai.autoconfigure;
import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.oci.OCIEmbeddingModel;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* Embedding {@link AutoConfiguration Auto-configuration} for Oracle Cloud Infrastructure
* Generative AI.
*
* @author Anders Swanson
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration
@ConditionalOnClass(OCIEmbeddingModel.class)
@EnableConfigurationProperties(OCIEmbeddingModelProperties.class)
@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.OCI_GENAI,
matchIfMissing = true)
@ImportAutoConfiguration(OCIGenAiInferenceClientAutoConfiguration.class)
public class OCIGenAiEmbeddingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public OCIEmbeddingModel ociEmbeddingModel(GenerativeAiInferenceClient generativeAiClient,
OCIEmbeddingModelProperties properties) {
return new OCIEmbeddingModel(generativeAiClient, properties.getEmbeddingOptions());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 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.
@@ -28,31 +28,43 @@ import com.oracle.bmc.auth.SimplePrivateKeySupplier;
import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider;
import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient;
import com.oracle.bmc.retrier.RetryConfiguration;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.oci.OCIEmbeddingModel;
import org.springframework.ai.oci.cohere.OCICohereChatModel;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.util.StringUtils;
/**
* {@link AutoConfiguration Auto-configuration} for Oracle Cloud Infrastructure Generative
* AI.
* AI Inference Client.
*
* @author Anders Swanson
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration
@ConditionalOnClass({ GenerativeAiInferenceClient.class, OCIEmbeddingModel.class })
@EnableConfigurationProperties({ OCIConnectionProperties.class, OCIEmbeddingModelProperties.class,
OCICohereChatModelProperties.class })
public class OCIGenAiAutoConfiguration {
@ConditionalOnClass(GenerativeAiInferenceClient.class)
@EnableConfigurationProperties(OCIConnectionProperties.class)
public class OCIGenAiInferenceClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GenerativeAiInferenceClient generativeAiInferenceClient(OCIConnectionProperties properties)
throws IOException {
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.retryConfiguration(RetryConfiguration.SDK_DEFAULT_RETRY_CONFIGURATION)
.build();
GenerativeAiInferenceClient.Builder builder = GenerativeAiInferenceClient.builder()
.configuration(clientConfiguration);
if (StringUtils.hasText(properties.getRegion())) {
builder.region(Region.valueOf(properties.getRegion()));
}
if (StringUtils.hasText(properties.getEndpoint())) {
builder.endpoint(properties.getEndpoint());
}
return builder.build(authenticationProvider(properties));
}
private static BasicAuthenticationDetailsProvider authenticationProvider(OCIConnectionProperties properties)
throws IOException {
@@ -71,43 +83,4 @@ public class OCIGenAiAutoConfiguration {
};
}
@ConditionalOnMissingBean
@Bean
public GenerativeAiInferenceClient generativeAiInferenceClient(OCIConnectionProperties properties)
throws IOException {
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.retryConfiguration(RetryConfiguration.SDK_DEFAULT_RETRY_CONFIGURATION)
.build();
GenerativeAiInferenceClient.Builder builder = GenerativeAiInferenceClient.builder()
.configuration(clientConfiguration);
if (StringUtils.hasText(properties.getRegion())) {
builder.region(Region.valueOf(properties.getRegion()));
}
if (StringUtils.hasText(properties.getEndpoint())) {
builder.endpoint(properties.getEndpoint());
}
return builder.build(authenticationProvider(properties));
}
@Bean
@ConditionalOnProperty(prefix = OCIEmbeddingModelProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public OCIEmbeddingModel ociEmbeddingModel(GenerativeAiInferenceClient generativeAiClient,
OCIEmbeddingModelProperties properties) {
return new OCIEmbeddingModel(generativeAiClient, properties.getEmbeddingOptions());
}
@Bean
@ConditionalOnProperty(prefix = OCICohereChatModelProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public OCICohereChatModel ociChatModel(GenerativeAiInferenceClient generativeAiClient,
OCICohereChatModelProperties properties, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var chatModel = new OCICohereChatModel(generativeAiClient, properties.getOptions(),
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
}

View File

@@ -13,4 +13,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.springframework.ai.model.oci.genai.autoconfigure.OCIGenAiAutoConfiguration
org.springframework.ai.model.oci.genai.autoconfigure.OCIGenAiChatAutoConfiguration
org.springframework.ai.model.oci.genai.autoconfigure.OCIGenAiEmbeddingAutoConfiguration

View File

@@ -56,7 +56,7 @@ class OCIGenAIAutoConfigurationTest {
"spring.ai.oci.genai.cohere.chat.options.frequencyPenalty=0.1",
"spring.ai.oci.genai.cohere.chat.options.presencePenalty=0.2"
// @formatter:on
).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class));
).withConfiguration(AutoConfigurations.of(OCIGenAiChatAutoConfiguration.class));
contextRunner.run(context -> {
OCICohereChatModel chatModel = context.getBean(OCICohereChatModel.class);

View File

@@ -52,7 +52,7 @@ public class OCIGenAiAutoConfigurationIT {
"spring.ai.oci.genai.embedding.servingMode=on-demand",
"spring.ai.oci.genai.embedding.model=cohere.embed-english-light-v2.0"
// @formatter:on
).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class));
).withConfiguration(AutoConfigurations.of(OCIGenAiEmbeddingAutoConfiguration.class));
private final ApplicationContextRunner cohereChatContextRunner = new ApplicationContextRunner().withPropertyValues(
// @formatter:off
@@ -62,7 +62,7 @@ public class OCIGenAiAutoConfigurationIT {
"spring.ai.oci.genai.cohere.chat.options.servingMode=on-demand",
"spring.ai.oci.genai.cohere.chat.options.model=" + this.CHAT_MODEL_ID
// @formatter:on
).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class));
).withConfiguration(AutoConfigurations.of(OCIGenAiChatAutoConfiguration.class));
@Test
void embeddings() {

View File

@@ -38,28 +38,18 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-image-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- Boot dependencies -->

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2023-2025 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.model.ollama.autoconfigure;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
/**
* {@link AutoConfiguration Auto-configuration} for Ollama API.
*
* @author Christian Tzolov
* @author Eddú Meléndez
* @author Thomas Vitale
* @author Ilayaperumal Gopinathan
* @since 0.8.0
*/
@AutoConfiguration
@ConditionalOnClass(OllamaApi.class)
@EnableConfigurationProperties(OllamaConnectionProperties.class)
public class OllamaApiAutoConfiguration {
@Bean
@ConditionalOnMissingBean(OllamaConnectionDetails.class)
public PropertiesOllamaConnectionDetails ollamaConnectionDetails(OllamaConnectionProperties properties) {
return new PropertiesOllamaConnectionDetails(properties);
}
@Bean
@ConditionalOnMissingBean
public OllamaApi ollamaApi(OllamaConnectionDetails connectionDetails,
ObjectProvider<RestClient.Builder> restClientBuilderProvider,
ObjectProvider<WebClient.Builder> webClientBuilderProvider) {
return new OllamaApi(connectionDetails.getBaseUrl(),
restClientBuilderProvider.getIfAvailable(RestClient::builder),
webClientBuilderProvider.getIfAvailable(WebClient::builder));
}
static class PropertiesOllamaConnectionDetails implements OllamaConnectionDetails {
private final OllamaConnectionProperties properties;
PropertiesOllamaConnectionDetails(OllamaConnectionProperties properties) {
this.properties = properties;
}
@Override
public String getBaseUrl() {
return this.properties.getBaseUrl();
}
}
}

View File

@@ -1,153 +0,0 @@
/*
* Copyright 2023-2025 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.model.ollama.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.management.ModelManagementOptions;
import org.springframework.ai.ollama.management.PullModelStrategy;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
/**
* {@link AutoConfiguration Auto-configuration} for Ollama Chat Client.
*
* @author Christian Tzolov
* @author Eddú Meléndez
* @author Thomas Vitale
* @since 0.8.0
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class })
@ConditionalOnClass(OllamaApi.class)
@EnableConfigurationProperties({ OllamaChatProperties.class, OllamaEmbeddingProperties.class,
OllamaConnectionProperties.class, OllamaInitializationProperties.class })
@ImportAutoConfiguration(classes = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class,
WebClientAutoConfiguration.class })
public class OllamaAutoConfiguration {
@Bean
@ConditionalOnMissingBean(OllamaConnectionDetails.class)
public PropertiesOllamaConnectionDetails ollamaConnectionDetails(OllamaConnectionProperties properties) {
return new PropertiesOllamaConnectionDetails(properties);
}
@Bean
@ConditionalOnMissingBean
public OllamaApi ollamaApi(OllamaConnectionDetails connectionDetails,
ObjectProvider<RestClient.Builder> restClientBuilderProvider,
ObjectProvider<WebClient.Builder> webClientBuilderProvider) {
return new OllamaApi(connectionDetails.getBaseUrl(),
restClientBuilderProvider.getIfAvailable(RestClient::builder),
webClientBuilderProvider.getIfAvailable(WebClient::builder));
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OLLAMA,
matchIfMissing = true)
public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi, OllamaChatProperties properties,
OllamaInitializationProperties initProperties, ToolCallingManager toolCallingManager,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var chatModelPullStrategy = initProperties.getChat().isInclude() ? initProperties.getPullModelStrategy()
: PullModelStrategy.NEVER;
var chatModel = OllamaChatModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(properties.getOptions())
.toolCallingManager(toolCallingManager)
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.modelManagementOptions(
new ModelManagementOptions(chatModelPullStrategy, initProperties.getChat().getAdditionalModels(),
initProperties.getTimeout(), initProperties.getMaxRetries()))
.build();
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.OLLAMA,
matchIfMissing = true)
public OllamaEmbeddingModel ollamaEmbeddingModel(OllamaApi ollamaApi, OllamaEmbeddingProperties properties,
OllamaInitializationProperties initProperties, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<EmbeddingModelObservationConvention> observationConvention) {
var embeddingModelPullStrategy = initProperties.getEmbedding().isInclude()
? initProperties.getPullModelStrategy() : PullModelStrategy.NEVER;
var embeddingModel = OllamaEmbeddingModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(properties.getOptions())
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.modelManagementOptions(new ModelManagementOptions(embeddingModelPullStrategy,
initProperties.getEmbedding().getAdditionalModels(), initProperties.getTimeout(),
initProperties.getMaxRetries()))
.build();
observationConvention.ifAvailable(embeddingModel::setObservationConvention);
return embeddingModel;
}
@Bean
@ConditionalOnMissingBean
public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
manager.setApplicationContext(context);
return manager;
}
static class PropertiesOllamaConnectionDetails implements OllamaConnectionDetails {
private final OllamaConnectionProperties properties;
PropertiesOllamaConnectionDetails(OllamaConnectionProperties properties) {
this.properties = properties;
}
@Override
public String getBaseUrl() {
return this.properties.getBaseUrl();
}
}
}

View File

@@ -14,18 +14,21 @@
* limitations under the License.
*/
package org.springframework.ai.model.anthropic.autoconfigure;
package org.springframework.ai.model.ollama.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.management.ModelManagementOptions;
import org.springframework.ai.ollama.management.PullModelStrategy;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
@@ -37,53 +40,42 @@ import org.springframework.boot.autoconfigure.web.reactive.function.client.WebCl
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
/**
* {@link AutoConfiguration Auto-configuration} for Anthropic Chat Model.
* {@link AutoConfiguration Auto-configuration} for Ollama Chat model.
*
* @author Christian Tzolov
* @author Eddú Meléndez
* @author Thomas Vitale
* @since 1.0.0
* @author Ilayaperumal Gopinathan
* @since 0.8.0
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
ToolCallingAutoConfiguration.class })
@EnableConfigurationProperties({ AnthropicChatProperties.class, AnthropicConnectionProperties.class })
@ConditionalOnClass(AnthropicApi.class)
@ConditionalOnProperty(prefix = AnthropicChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
@AutoConfiguration(after = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class })
@ConditionalOnClass(OllamaChatModel.class)
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OLLAMA,
matchIfMissing = true)
@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
@EnableConfigurationProperties({ OllamaChatProperties.class, OllamaInitializationProperties.class })
@ImportAutoConfiguration(classes = { OllamaApiAutoConfiguration.class, RestClientAutoConfiguration.class,
ToolCallingAutoConfiguration.class, WebClientAutoConfiguration.class })
public class AnthropicAutoConfiguration {
public class OllamaChatAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AnthropicApi anthropicApi(AnthropicConnectionProperties connectionProperties,
ObjectProvider<RestClient.Builder> restClientBuilderProvider,
ObjectProvider<WebClient.Builder> webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) {
return new AnthropicApi(connectionProperties.getBaseUrl(), connectionProperties.getApiKey(),
connectionProperties.getVersion(), restClientBuilderProvider.getIfAvailable(RestClient::builder),
webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler,
connectionProperties.getBetaVersion());
}
@Bean
@ConditionalOnMissingBean
public AnthropicChatModel anthropicChatModel(AnthropicApi anthropicApi, AnthropicChatProperties chatProperties,
RetryTemplate retryTemplate, ToolCallingManager toolCallingManager,
public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi, OllamaChatProperties properties,
OllamaInitializationProperties initProperties, ToolCallingManager toolCallingManager,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var chatModelPullStrategy = initProperties.getChat().isInclude() ? initProperties.getPullModelStrategy()
: PullModelStrategy.NEVER;
var chatModel = AnthropicChatModel.builder()
.anthropicApi(anthropicApi)
.defaultOptions(chatProperties.getOptions())
var chatModel = OllamaChatModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(properties.getOptions())
.toolCallingManager(toolCallingManager)
.retryTemplate(retryTemplate)
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.modelManagementOptions(
new ModelManagementOptions(chatModelPullStrategy, initProperties.getChat().getAdditionalModels(),
initProperties.getTimeout(), initProperties.getMaxRetries()))
.build();
observationConvention.ifAvailable(chatModel::setObservationConvention);

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2023-2025 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.model.ollama.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.management.ModelManagementOptions;
import org.springframework.ai.ollama.management.PullModelStrategy;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* {@link AutoConfiguration Auto-configuration} for Ollama Chat Client.
*
* @author Christian Tzolov
* @author Eddú Meléndez
* @author Thomas Vitale
* @author Ilayaperumal Gopinathan
* @since 0.8.0
*/
@AutoConfiguration(after = RestClientAutoConfiguration.class)
@ConditionalOnClass(OllamaEmbeddingModel.class)
@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.OLLAMA,
matchIfMissing = true)
@EnableConfigurationProperties({ OllamaEmbeddingProperties.class, OllamaInitializationProperties.class })
@ImportAutoConfiguration(classes = { OllamaApiAutoConfiguration.class, RestClientAutoConfiguration.class,
WebClientAutoConfiguration.class })
public class OllamaEmbeddingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public OllamaEmbeddingModel ollamaEmbeddingModel(OllamaApi ollamaApi, OllamaEmbeddingProperties properties,
OllamaInitializationProperties initProperties, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<EmbeddingModelObservationConvention> observationConvention) {
var embeddingModelPullStrategy = initProperties.getEmbedding().isInclude()
? initProperties.getPullModelStrategy() : PullModelStrategy.NEVER;
var embeddingModel = OllamaEmbeddingModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(properties.getOptions())
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.modelManagementOptions(new ModelManagementOptions(embeddingModelPullStrategy,
initProperties.getEmbedding().getAdditionalModels(), initProperties.getTimeout(),
initProperties.getMaxRetries()))
.build();
observationConvention.ifAvailable(embeddingModel::setObservationConvention);
return embeddingModel;
}
}

View File

@@ -13,4 +13,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration
org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration
org.springframework.ai.model.ollama.autoconfigure.OllamaEmbeddingAutoConfiguration

View File

@@ -55,7 +55,7 @@ public class OllamaChatAutoConfigurationIT extends BaseOllamaIT {
"spring.ai.ollama.chat.options.temperature=0.5",
"spring.ai.ollama.chat.options.topK=10")
// @formatter:on
.withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class));
private final UserMessage userMessage = new UserMessage("What's the capital of Denmark?");

View File

@@ -41,7 +41,8 @@ public class OllamaChatAutoConfigurationTests {
"spring.ai.ollama.chat.options.topP=0.56",
"spring.ai.ollama.chat.options.topK=123")
// @formatter:on
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class))
.withConfiguration(
AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(OllamaChatProperties.class);
var connectionProperties = context.getBean(OllamaConnectionProperties.class);

View File

@@ -45,7 +45,8 @@ public class OllamaEmbeddingAutoConfigurationIT extends BaseOllamaIT {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.ollama.embedding.options.model=" + MODEL_NAME,
"spring.ai.ollama.base-url=" + getBaseUrl())
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class));
.withConfiguration(
AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaEmbeddingAutoConfiguration.class));
@BeforeAll
public static void beforeAll() throws IOException, InterruptedException {

View File

@@ -41,7 +41,8 @@ public class OllamaEmbeddingAutoConfigurationTests {
"spring.ai.ollama.embedding.options.topK=13"
// @formatter:on
)
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class))
.withConfiguration(
AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaEmbeddingAutoConfiguration.class))
.run(context -> {
var embeddingProperties = context.getBean(OllamaEmbeddingProperties.class);
var connectionProperties = context.getBean(OllamaConnectionProperties.class);

View File

@@ -26,56 +26,59 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit Tests for {@link OllamaAutoConfiguration}'s conditional enabling of models.
* Unit Tests for Ollama auto-configurations conditional enabling of models.
*
* @author Ilayaperumal Gopinathan
*/
public class OllamaModelConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class));
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
@Test
void chatModelActivation() {
this.contextRunner.run(context -> {
this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class)).run(context -> {
assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none")
this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=none")
.run(context -> {
assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaChatProperties.class)).isEmpty();
assertThat(context.getBeansOfType(OllamaChatModel.class)).isEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.chat=ollama", "spring.ai.model.embedding=none")
this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class))
.withPropertyValues("spring.ai.model.chat=ollama")
.run(context -> {
assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaChatModel.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty();
});
}
@Test
void embeddingModelActivation() {
this.contextRunner.run(context -> {
assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaEmbeddingAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> {
assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.embedding=none")
.run(context -> {
assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty();
});
this.contextRunner.withPropertyValues("spring.ai.model.embedding=ollama").run(context -> {
assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty();
});
this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaEmbeddingAutoConfiguration.class))
.withPropertyValues("spring.ai.model.embedding=ollama")
.run(context -> {
assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty();
});
}
}

View File

@@ -25,13 +25,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT;
import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT;
import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.tool.function.FunctionToolCallback;
@@ -53,7 +53,7 @@ public class FunctionCallbackInPromptIT extends BaseOllamaIT {
"spring.ai.ollama.chat.options.temperature=0.5",
"spring.ai.ollama.chat.options.topK=10")
// @formatter:on
.withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class));
@BeforeAll
public static void beforeAll() {

View File

@@ -25,13 +25,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT;
import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT;
import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaOptions;
@@ -57,7 +57,7 @@ public class OllamaFunctionCallbackIT extends BaseOllamaIT {
"spring.ai.ollama.chat.options.temperature=0.5",
"spring.ai.ollama.chat.options.topK=10")
// @formatter:on
.withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@BeforeAll

View File

@@ -26,13 +26,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT;
import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT;
import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaOptions;
@@ -64,7 +64,7 @@ public class OllamaFunctionToolBeanIT extends BaseOllamaIT {
"spring.ai.ollama.chat.options.temperature=0.5",
"spring.ai.ollama.chat.options.topK=10")
// @formatter:on
.withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@BeforeAll

View File

@@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test
import org.slf4j.LoggerFactory
import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT
import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration
import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration
import org.springframework.ai.chat.messages.UserMessage
import org.springframework.ai.chat.prompt.Prompt
import org.springframework.ai.model.function.FunctionCallingOptions
@@ -56,7 +56,7 @@ class FunctionCallbackResolverKotlinIT : BaseOllamaIT() {
"spring.ai.ollama.chat.options.temperature=0.5",
"spring.ai.ollama.chat.options.topK=10"
)
.withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration::class.java))
.withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration::class.java))
.withUserConfiguration(Config::class.java)
@Test

Some files were not shown because too many files have changed in this diff Show More