Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions contrib/spring-ai/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@
<artifactId>spring-ai-ollama</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-openai</artifactId>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: The test also imports ToolCallingAutoConfiguration directly, but gets it only transitively; would declaring its module as a test dependency be clearer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declared spring-ai-autoconfigure-model-tool explicitly.

<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
<scope>test</scope>
</dependency>

<!-- Testcontainers for local testing -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,24 @@
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
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.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;

/**
* Auto-configuration for Spring AI integration with ADK.
*
* <p>This auto-configuration automatically creates SpringAI beans when Spring AI ChatModel beans
* are available in the application context. It supports both regular ChatModel and
* StreamingChatModel instances.
* <p>This auto-configuration automatically creates SpringAI beans when a Spring AI ChatModel bean
* is available in the application context as a single autowire candidate (or is marked
* {@code @Primary}). It supports both regular ChatModel and StreamingChatModel instances. When
* several model beans are present without a primary, the auto-configuration backs off and the
* application should define its own beans.
*
* <p>The auto-configuration can be disabled by setting:
*
Expand All @@ -59,6 +63,9 @@
* </pre>
*/
@AutoConfiguration
// Sorts after provider auto-configurations; configs ordered after this class need LOWEST_PRECEDENCE
// too.
@AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE)
@ConditionalOnClass({SpringAI.class, ChatModel.class})
@ConditionalOnProperty(
prefix = "adk.spring-ai.auto-configuration",
Expand All @@ -71,7 +78,8 @@ public class SpringAIAutoConfiguration {
private static final Logger logger = LoggerFactory.getLogger(SpringAIAutoConfiguration.class);

/**
* Creates a SpringAI bean when both ChatModel and StreamingChatModel are available.
* Creates a SpringAI bean when a single-candidate ChatModel is available. Every ChatModel is also
* a StreamingChatModel, so one bean serves both paths.
*
* @param chatModel the Spring AI ChatModel
* @param streamingChatModel the Spring AI StreamingChatModel
Expand All @@ -81,7 +89,7 @@ public class SpringAIAutoConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean(SpringAI.class)
@ConditionalOnBean({ChatModel.class, StreamingChatModel.class})
@ConditionalOnSingleCandidate(ChatModel.class)
public SpringAI springAIWithBothModels(
ChatModel chatModel, StreamingChatModel streamingChatModel, SpringAIProperties properties) {

Expand All @@ -95,15 +103,15 @@ public SpringAI springAIWithBothModels(
}

/**
* Creates a SpringAI bean when only ChatModel is available.
* Creates a SpringAI bean when only a single-candidate ChatModel is available.
*
* @param chatModel the Spring AI ChatModel
* @param properties the ADK Spring AI properties
* @return configured SpringAI instance
*/
@Bean
@ConditionalOnMissingBean(SpringAI.class)
@ConditionalOnBean(ChatModel.class)
@ConditionalOnSingleCandidate(ChatModel.class)
public SpringAI springAIWithChatModel(ChatModel chatModel, SpringAIProperties properties) {

String modelName = determineModelName(chatModel, properties);
Expand All @@ -114,15 +122,15 @@ public SpringAI springAIWithChatModel(ChatModel chatModel, SpringAIProperties pr
}

/**
* Creates a SpringAI bean when only StreamingChatModel is available.
* Creates a SpringAI bean when only a single-candidate StreamingChatModel is available.
*
* @param streamingChatModel the Spring AI StreamingChatModel
* @param properties the ADK Spring AI properties
* @return configured SpringAI instance
*/
@Bean
@ConditionalOnMissingBean({SpringAI.class, ChatModel.class})
@ConditionalOnBean(StreamingChatModel.class)
@ConditionalOnSingleCandidate(StreamingChatModel.class)
public SpringAI springAIWithStreamingModel(
StreamingChatModel streamingChatModel, SpringAIProperties properties) {

Expand All @@ -134,15 +142,15 @@ public SpringAI springAIWithStreamingModel(
}

/**
* Creates a SpringAIEmbedding bean when EmbeddingModel is available.
* Creates a SpringAIEmbedding bean when a single-candidate EmbeddingModel is available.
*
* @param embeddingModel the Spring AI EmbeddingModel
* @param properties the ADK Spring AI properties
* @return configured SpringAIEmbedding instance
*/
@Bean
@ConditionalOnMissingBean(SpringAIEmbedding.class)
@ConditionalOnBean(EmbeddingModel.class)
@ConditionalOnSingleCandidate(EmbeddingModel.class)
public SpringAIEmbedding springAIEmbedding(
EmbeddingModel embeddingModel, SpringAIProperties properties) {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2026 Google LLC
*
* 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
*
* http://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 com.google.adk.models.springai.autoconfigure;

import static org.assertj.core.api.Assertions.assertThat;

import com.google.adk.models.springai.SpringAI;
import com.google.adk.models.springai.SpringAIEmbedding;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

/**
* SpringAIAutoConfiguration sorts after provider auto-configurations via @AutoConfigureOrder, so
* its order-sensitive conditions can see the provider beans.
*/
class SpringAIAutoConfigurationOrderingTest {

// ToolCallingAutoConfiguration provides the ToolCallingManager OpenAiChatAutoConfiguration needs.
private final ApplicationContextRunner providerRunner =
new ApplicationContextRunner()
.withPropertyValues("spring.ai.openai.api-key=dummy")
.withConfiguration(
AutoConfigurations.of(
SpringAIAutoConfiguration.class,
ToolCallingAutoConfiguration.class,
OpenAiChatAutoConfiguration.class,
OpenAiEmbeddingAutoConfiguration.class));

@Test
void registersSpringAI_whenProviderAutoConfigurationsAreProcessedFirst() {
providerRunner.run(context -> assertThat(context).hasSingleBean(SpringAI.class));
}

@Test
void registersSpringAIEmbedding_whenEmbeddingAutoConfigurationIsProcessedFirst() {
providerRunner.run(context -> assertThat(context).hasSingleBean(SpringAIEmbedding.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@
package com.google.adk.models.springai.autoconfigure;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

import com.google.adk.models.springai.SpringAI;
import com.google.adk.models.springai.SpringAIEmbedding;
import com.google.adk.models.springai.properties.SpringAIProperties;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -164,6 +167,44 @@ void testDefaultConfiguration() {
});
}

@Test
void testBacksOffWithMultipleChatModelBeans() {
contextRunner
.withBean("chatModelOne", ChatModel.class, () -> mock(ChatModel.class))
.withBean("chatModelTwo", ChatModel.class, () -> mock(ChatModel.class))
.run(context -> assertThat(context).doesNotHaveBean(SpringAI.class));
}

@Test
void testBacksOffWithMultipleStreamingChatModelBeans() {
contextRunner
.withBean(
"streamingModelOne", StreamingChatModel.class, () -> mock(StreamingChatModel.class))
.withBean(
"streamingModelTwo", StreamingChatModel.class, () -> mock(StreamingChatModel.class))
.run(context -> assertThat(context).doesNotHaveBean(SpringAI.class));
}

@Test
void testBacksOffWithMultipleEmbeddingModelBeans() {
contextRunner
.withBean("embeddingModelOne", EmbeddingModel.class, () -> mock(EmbeddingModel.class))
.withBean("embeddingModelTwo", EmbeddingModel.class, () -> mock(EmbeddingModel.class))
.run(context -> assertThat(context).doesNotHaveBean(SpringAIEmbedding.class));
}

@Test
void testRegistersSpringAIWithPrimaryChatModelBean() {
contextRunner
.withBean(
"primaryChatModel",
ChatModel.class,
() -> mock(ChatModel.class),
beanDefinition -> beanDefinition.setPrimary(true))
.withBean("chatModelTwo", ChatModel.class, () -> mock(ChatModel.class))
.run(context -> assertThat(context).hasSingleBean(SpringAI.class));
}

@Configuration
static class TestConfigurationWithBothModels {
@Bean
Expand Down
Loading