Skip to content

Commit 3ef0599

Browse files
committed
add spring-ai-autoconfigure-tool module
Signed-off-by: jitokim <[email protected]>
1 parent 144ae1e commit 3ef0599

File tree

7 files changed

+342
-0
lines changed

7 files changed

+342
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>org.springframework.ai</groupId>
8+
<artifactId>spring-ai-parent</artifactId>
9+
<version>1.0.0-SNAPSHOT</version>
10+
<relativePath>../../../pom.xml</relativePath>
11+
</parent>
12+
<artifactId>spring-ai-autoconfigure-tool</artifactId>
13+
<packaging>jar</packaging>
14+
<name>Spring AI Tool Auto Configuration</name>
15+
<description>Spring AI Tool Auto Configuration</description>
16+
<url>https://github.com/spring-projects/spring-ai</url>
17+
18+
<scm>
19+
<url>https://github.com/spring-projects/spring-ai</url>
20+
<connection>git://github.com/spring-projects/spring-ai.git</connection>
21+
<developerConnection>[email protected]:spring-projects/spring-ai.git</developerConnection>
22+
</scm>
23+
24+
25+
<dependencies>
26+
27+
<dependency>
28+
<groupId>org.springframework.ai</groupId>
29+
<artifactId>spring-ai-core</artifactId>
30+
<version>${project.parent.version}</version>
31+
</dependency>
32+
33+
<!-- Boot dependencies -->
34+
<dependency>
35+
<groupId>org.springframework.boot</groupId>
36+
<artifactId>spring-boot-starter</artifactId>
37+
</dependency>
38+
39+
<dependency>
40+
<groupId>org.springframework.boot</groupId>
41+
<artifactId>spring-boot-configuration-processor</artifactId>
42+
<optional>true</optional>
43+
</dependency>
44+
45+
<dependency>
46+
<groupId>org.springframework.boot</groupId>
47+
<artifactId>spring-boot-autoconfigure-processor</artifactId>
48+
<optional>true</optional>
49+
</dependency>
50+
51+
<!-- Test dependencies -->
52+
<dependency>
53+
<groupId>org.springframework.ai</groupId>
54+
<artifactId>spring-ai-test</artifactId>
55+
<version>${project.parent.version}</version>
56+
<scope>test</scope>
57+
</dependency>
58+
59+
<dependency>
60+
<groupId>org.springframework.boot</groupId>
61+
<artifactId>spring-boot-starter-test</artifactId>
62+
<scope>test</scope>
63+
</dependency>
64+
65+
<dependency>
66+
<groupId>org.mockito</groupId>
67+
<artifactId>mockito-core</artifactId>
68+
<scope>test</scope>
69+
</dependency>
70+
</dependencies>
71+
72+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package org.springframework.ai.tool.autoconfigure;
2+
3+
4+
import org.springframework.ai.tool.autoconfigure.annotation.EnableToolCallbackAutoRegistration;
5+
import org.springframework.beans.factory.config.BeanDefinition;
6+
import org.springframework.beans.factory.config.ConstructorArgumentValues;
7+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
8+
import org.springframework.beans.factory.support.BeanNameGenerator;
9+
import org.springframework.beans.factory.support.GenericBeanDefinition;
10+
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
11+
import org.springframework.core.type.AnnotationMetadata;
12+
import org.springframework.lang.NonNull;
13+
14+
import java.util.HashSet;
15+
import java.util.Map;
16+
import java.util.Set;
17+
18+
/**
19+
* {@link ImportBeanDefinitionRegistrar} implementation that registers a {@link ToolCallbackBeanRegistrar}
20+
* bean based on the metadata from {@link EnableToolCallbackAutoRegistration}.
21+
*
22+
* <p>This registrar extracts package scanning information from the annotation attributes
23+
* and registers a {@link ToolCallbackBeanRegistrar} to process beans containing {@code @Tool}-annotated methods.
24+
*
25+
* @see EnableToolCallbackAutoRegistration
26+
* @see ToolCallbackBeanRegistrar
27+
*/
28+
public class AutoToolCallbacksRegistrar implements ImportBeanDefinitionRegistrar {
29+
30+
@Override
31+
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
32+
Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(EnableToolCallbackAutoRegistration.class.getName());
33+
34+
if (attributes == null) {
35+
return;
36+
}
37+
38+
Set<String> basePackages = getBasePackages(attributes);
39+
40+
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
41+
beanDefinition.setBeanClass(ToolCallbackBeanRegistrar.class);
42+
beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
43+
44+
ConstructorArgumentValues args = new ConstructorArgumentValues();
45+
args.addGenericArgumentValue(basePackages);
46+
beanDefinition.setConstructorArgumentValues(args);
47+
48+
registry.registerBeanDefinition("toolScannerConfigurer", beanDefinition);
49+
}
50+
51+
/**
52+
* Extracts the base packages to scan from the {@code @EnableToolCallbackAutoRegistration} annotation attributes.
53+
*
54+
* <p>Supports the following attributes:
55+
* <ul>
56+
* <li>{@code value} - Shorthand for base packages</li>
57+
* <li>{@code basePackages} - Explicit list of packages</li>
58+
* <li>{@code basePackageClasses} - Infers packages from class types</li>
59+
* </ul>
60+
*
61+
* @param attributes the annotation attributes
62+
* @return a set of base package names to scan
63+
*/
64+
private Set<String> getBasePackages(@NonNull Map<String, Object> attributes) {
65+
Set<String> basePackages = new HashSet<>();
66+
67+
Object[] valuePackages = (Object[]) attributes.get("value");
68+
if (valuePackages == null) valuePackages = new Object[0];
69+
70+
for (Object obj : valuePackages) {
71+
if (obj instanceof String str && !str.isEmpty()) {
72+
basePackages.add(str);
73+
}
74+
}
75+
76+
Object[] basePackagesAttr = (Object[]) attributes.get("basePackages");
77+
if (basePackagesAttr == null) basePackagesAttr = new Object[0];
78+
79+
for (Object obj : basePackagesAttr) {
80+
if (obj instanceof String str && !str.isEmpty()) {
81+
basePackages.add(str);
82+
}
83+
}
84+
85+
Object[] basePackageClasses = (Object[]) attributes.get("basePackageClasses");
86+
if (basePackageClasses == null) basePackageClasses = new Object[0];
87+
88+
for (Object obj : basePackageClasses) {
89+
if (obj instanceof Class<?>) {
90+
// type casting instead of
91+
// if (obj instanceof Class<?> clazz)
92+
// for kotlin classes
93+
Class<?> clazz = (Class<?>) obj;
94+
basePackages.add(clazz.getPackage().getName());
95+
}
96+
}
97+
98+
return basePackages;
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package org.springframework.ai.tool.autoconfigure;
2+
3+
import org.springframework.ai.tool.ToolCallbackProvider;
4+
import org.springframework.ai.tool.annotation.Tool;
5+
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
6+
import org.springframework.beans.BeansException;
7+
import org.springframework.beans.factory.config.BeanPostProcessor;
8+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
9+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
10+
import org.springframework.context.ApplicationContext;
11+
import org.springframework.context.ApplicationContextAware;
12+
import org.springframework.context.annotation.Bean;
13+
14+
import java.util.Arrays;
15+
import java.util.HashSet;
16+
import java.util.Set;
17+
18+
/**
19+
* {@code ToolCallbackBeanRegistrar} scans beans after initialization and collects
20+
* those that have methods annotated with {@link Tool} within the specified base packages.
21+
*
22+
* The collected beans are then registered with a {@link ToolCallbackProvider}.
23+
*/
24+
@ConditionalOnClass(Tool.class)
25+
public class ToolCallbackBeanRegistrar implements ApplicationContextAware, BeanPostProcessor {
26+
27+
private final Set<String> basePackages;
28+
29+
private ApplicationContext applicationContext;
30+
31+
private Set<Object> toolBeans = new HashSet<>();
32+
33+
34+
public ToolCallbackBeanRegistrar(Set<String> basePackages) {
35+
this.basePackages = basePackages;
36+
}
37+
38+
@Override
39+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
40+
this.applicationContext = applicationContext;
41+
}
42+
43+
@Override
44+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
45+
Class<?> beanClass = bean.getClass();
46+
47+
if (isInBasePackage(beanClass.getPackageName())) {
48+
49+
if (hasToolAnnotatedMethod(beanClass)) {
50+
toolBeans.add(bean);
51+
}
52+
}
53+
54+
return bean;
55+
}
56+
57+
/**
58+
* Checks whether the given package name starts with any of the configured base packages.
59+
*
60+
* @param packageName the package name to check
61+
* @return {@code true} if it is within a configured base package; {@code false} otherwise
62+
*/
63+
private boolean isInBasePackage(String packageName) {
64+
return basePackages.stream().anyMatch(packageName::startsWith);
65+
}
66+
67+
/**
68+
* Checks if the specified class has any method annotated with {@link Tool}.
69+
*
70+
* @param clazz the class to inspect
71+
* @return {@code true} if at least one method is annotated with {@link Tool}; {@code false} otherwise
72+
*/
73+
private boolean hasToolAnnotatedMethod(Class<?> clazz) {
74+
return Arrays.stream(clazz.getMethods()).anyMatch(method -> method.isAnnotationPresent(Tool.class));
75+
}
76+
77+
/**
78+
* Creates and registers a {@link MethodToolCallbackProvider} bean, containing all collected
79+
* tool beans.
80+
*
81+
* @return the configured {@link MethodToolCallbackProvider}
82+
*/
83+
@Bean
84+
@ConditionalOnMissingBean
85+
public MethodToolCallbackProvider methodToolCallbackProvider() {
86+
MethodToolCallbackProvider.Builder builder = MethodToolCallbackProvider.builder();
87+
if (!toolBeans.isEmpty()) {
88+
builder.toolObjects(toolBeans.toArray());
89+
}
90+
91+
return builder.build();
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.springframework.ai.tool.autoconfigure.annotation;
2+
3+
import org.springframework.ai.tool.ToolCallback;
4+
import org.springframework.ai.tool.ToolCallbackProvider;
5+
import org.springframework.ai.tool.annotation.Tool;
6+
import org.springframework.ai.tool.autoconfigure.AutoToolCallbacksRegistrar;
7+
import org.springframework.context.annotation.Import;
8+
9+
import java.lang.annotation.*;
10+
11+
/**
12+
* Enables automatic registration of {@link Tool}-annotated methods as {@link ToolCallback}s.
13+
*
14+
* <p>When this annotation is used on a configuration class, it imports the
15+
* {@link AutoToolCallbacksRegistrar}, which scans the specified packages for Spring beans
16+
* containing {@code @Tool}-annotated methods.
17+
* These beans are then registered as {@link ToolCallbackProvider}s.
18+
*
19+
* <p><b>Usage example:</b></p>
20+
* <pre>
21+
* {@code
22+
* @Configuration
23+
* @EnableToolCallbackAutoRegistration(basePackages = "com.example.tools")
24+
* public class MyToolConfig {
25+
* }
26+
* }
27+
* </pre>
28+
*
29+
* <p>You can specify packages to scan in one of three ways:
30+
* <ul>
31+
* <li>{@code basePackages} - Explicit list of package names</li>
32+
* <li>{@code value} - Alias for {@code basePackages}</li>
33+
* <li>{@code basePackageClasses} - Package names inferred from provided classes</li>
34+
* </ul>
35+
*
36+
* @see Tool
37+
* @see ToolCallback
38+
* @see ToolCallbackProvider
39+
* @see AutoToolCallbacksRegistrar
40+
*/
41+
42+
@Target({ElementType.TYPE, ElementType.METHOD})
43+
@Retention(RetentionPolicy.RUNTIME)
44+
@Documented
45+
@Import(AutoToolCallbacksRegistrar.class)
46+
public @interface EnableToolCallbackAutoRegistration {
47+
48+
String[] basePackages() default {};
49+
50+
String[] value() default {};
51+
52+
Class<?>[] basePackageClasses() default {};
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#
2+
# Copyright 2025-2025 the original author or authors.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
org.springframework.ai.tool.autoconfigure.SpringAiToolAutoConfiguration

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181

8282

8383
<module>auto-configurations/common/spring-ai-autoconfigure-retry</module>
84+
<module>auto-configurations/common/spring-ai-autoconfigure-tool</module>
8485

8586
<module>auto-configurations/models/tool/spring-ai-autoconfigure-model-tool</module>
8687

spring-ai-bom/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,13 @@
460460
<version>${project.version}</version>
461461
</dependency>
462462

463+
<!-- Spring AI Tool autoconfiguration-->
464+
<dependency>
465+
<groupId>org.springframework.ai</groupId>
466+
<artifactId>spring-ai-autoconfigure-tool</artifactId>
467+
<version>${project.version}</version>
468+
</dependency>
469+
463470
<!-- Spring AI Chat client autoconfiguration -->
464471

465472
<dependency>

0 commit comments

Comments
 (0)