diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/ToolCallbackFilters.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/ToolCallbackFilters.java
new file mode 100644
index 00000000000..84277cb348d
--- /dev/null
+++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/ToolCallbackFilters.java
@@ -0,0 +1,185 @@
+/*
+ * 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.tool;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Utility class for filtering {@link ToolCallback} instances based on metadata and other
+ * criteria. This class provides various predicate-based filters that can be used to
+ * select tools dynamically based on their metadata properties.
+ *
+ *
+ * Example usage:
+ *
+ *
+ *
+ * // Filter tools by type
+ * List<ToolCallback> filteredTools = ToolCallbackFilters.filterByType(allTools, "RealTimeAnalysis");
+ *
+ * // Filter tools by multiple criteria
+ * Predicate<ToolCallback> filter = ToolCallbackFilters.byType("RealTimeAnalysis")
+ * .and(ToolCallbackFilters.byMinPriority(7));
+ * List<ToolCallback> filtered = allTools.stream().filter(filter).collect(Collectors.toList());
+ *
+ *
+ */
+public final class ToolCallbackFilters {
+
+ private ToolCallbackFilters() {
+ }
+
+ /**
+ * Creates a predicate that filters tools by their type metadata.
+ * @param type the required type
+ * @return a predicate that tests if a tool has the specified type
+ */
+ public static Predicate byType(String type) {
+ return toolCallback -> {
+ Map metadata = toolCallback.getToolDefinition().metadata();
+ Object typeValue = metadata.get("type");
+ return typeValue != null && type.equals(typeValue.toString());
+ };
+ }
+
+ /**
+ * Creates a predicate that filters tools by their category metadata.
+ * @param category the required category
+ * @return a predicate that tests if a tool has the specified category
+ */
+ public static Predicate byCategory(String category) {
+ return toolCallback -> {
+ Map metadata = toolCallback.getToolDefinition().metadata();
+ Object categoryValue = metadata.get("category");
+ return categoryValue != null && category.equals(categoryValue.toString());
+ };
+ }
+
+ /**
+ * Creates a predicate that filters tools by their priority metadata. Only tools with
+ * priority greater than or equal to the specified minimum are included.
+ * @param minPriority the minimum priority
+ * @return a predicate that tests if a tool's priority meets the threshold
+ */
+ public static Predicate byMinPriority(int minPriority) {
+ return toolCallback -> {
+ Map metadata = toolCallback.getToolDefinition().metadata();
+ Object priorityValue = metadata.get("priority");
+ if (priorityValue != null) {
+ try {
+ int priority = priorityValue instanceof Number ? ((Number) priorityValue).intValue()
+ : Integer.parseInt(priorityValue.toString());
+ return priority >= minPriority;
+ }
+ catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ return false;
+ };
+ }
+
+ /**
+ * Creates a predicate that filters tools by their tags metadata. Tools must have at
+ * least one of the specified tags.
+ * @param tags the required tags
+ * @return a predicate that tests if a tool has any of the specified tags
+ */
+ public static Predicate byTags(String... tags) {
+ Set tagSet = Set.of(tags);
+ return toolCallback -> {
+ Map metadata = toolCallback.getToolDefinition().metadata();
+ Object tagsValue = metadata.get("tags");
+ if (tagsValue instanceof List) {
+ List> toolTags = (List>) tagsValue;
+ return toolTags.stream().anyMatch(tag -> tagSet.contains(tag.toString()));
+ }
+ return false;
+ };
+ }
+
+ /**
+ * Creates a predicate that filters tools by a custom metadata field.
+ * @param key the metadata key
+ * @param expectedValue the expected value
+ * @return a predicate that tests if a tool has the specified metadata value
+ */
+ public static Predicate byMetadata(String key, Object expectedValue) {
+ return toolCallback -> {
+ Map metadata = toolCallback.getToolDefinition().metadata();
+ Object value = metadata.get(key);
+ return expectedValue.equals(value);
+ };
+ }
+
+ /**
+ * Filters a list of tool callbacks by type.
+ * @param toolCallbacks the list of tool callbacks to filter
+ * @param type the required type
+ * @return a filtered list containing only tools with the specified type
+ */
+ public static List filterByType(List toolCallbacks, String type) {
+ return toolCallbacks.stream().filter(byType(type)).collect(Collectors.toList());
+ }
+
+ /**
+ * Filters an array of tool callbacks by type.
+ * @param toolCallbacks the array of tool callbacks to filter
+ * @param type the required type
+ * @return a filtered array containing only tools with the specified type
+ */
+ public static ToolCallback[] filterByType(ToolCallback[] toolCallbacks, String type) {
+ return Arrays.stream(toolCallbacks).filter(byType(type)).toArray(ToolCallback[]::new);
+ }
+
+ /**
+ * Filters a list of tool callbacks by category.
+ * @param toolCallbacks the list of tool callbacks to filter
+ * @param category the required category
+ * @return a filtered list containing only tools with the specified category
+ */
+ public static List filterByCategory(List toolCallbacks, String category) {
+ return toolCallbacks.stream().filter(byCategory(category)).collect(Collectors.toList());
+ }
+
+ /**
+ * Filters a list of tool callbacks by minimum priority.
+ * @param toolCallbacks the list of tool callbacks to filter
+ * @param minPriority the minimum priority
+ * @return a filtered list containing only tools with priority >= minPriority
+ */
+ public static List filterByMinPriority(List toolCallbacks, int minPriority) {
+ return toolCallbacks.stream().filter(byMinPriority(minPriority)).collect(Collectors.toList());
+ }
+
+ /**
+ * Filters a list of tool callbacks by tags.
+ * @param toolCallbacks the list of tool callbacks to filter
+ * @param tags the required tags (tool must have at least one)
+ * @return a filtered list containing only tools with at least one of the specified
+ * tags
+ */
+ public static List filterByTags(List toolCallbacks, String... tags) {
+ return toolCallbacks.stream().filter(byTags(tags)).collect(Collectors.toList());
+ }
+
+}
diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/ToolMetadata.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/ToolMetadata.java
new file mode 100644
index 00000000000..13b3e6ec18f
--- /dev/null
+++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/ToolMetadata.java
@@ -0,0 +1,82 @@
+/*
+ * 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.tool.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to add metadata to a tool method. Can be used in conjunction with
+ * {@link Tool} annotation. Metadata can be used for filtering, categorization, and other
+ * purposes when managing large sets of tools.
+ *
+ *
+ * Example usage:
+ *
+ *
+ *
+ * @Tool(description = "Analyzes real-time market data")
+ * @ToolMetadata(type = "RealTimeAnalysis", category = "market", priority = 8)
+ * public String analyzeMarketData(String symbol) {
+ * // implementation
+ * }
+ *
+ *
+ */
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface ToolMetadata {
+
+ /**
+ * Additional metadata entries in key=value format. Example:
+ * {"environment=production", "version=1.0"}
+ * @return array of metadata key-value pairs
+ */
+ String[] value() default {};
+
+ /**
+ * Tool category for classification purposes. Can be used to group related tools
+ * together.
+ * @return the category name
+ */
+ String category() default "";
+
+ /**
+ * Tool type for filtering purposes. Useful for distinguishing between different kinds
+ * of operations (e.g., "RealTimeAnalysis", "HistoricalAnalysis").
+ * @return the type identifier
+ */
+ String type() default "";
+
+ /**
+ * Priority level for tool selection (1-10, where 10 is highest priority). Can be used
+ * to influence tool selection when multiple tools are available.
+ * @return the priority level
+ */
+ int priority() default 5;
+
+ /**
+ * Tags associated with the tool. Useful for flexible categorization and filtering.
+ * @return array of tags
+ */
+ String[] tags() default {};
+
+}
diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/DefaultToolDefinition.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/DefaultToolDefinition.java
index cafd1a70364..0b7ed335912 100644
--- a/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/DefaultToolDefinition.java
+++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/DefaultToolDefinition.java
@@ -16,6 +16,10 @@
package org.springframework.ai.tool.definition;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
import org.springframework.ai.util.ParsingUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -26,12 +30,21 @@
* @author Thomas Vitale
* @since 1.0.0
*/
-public record DefaultToolDefinition(String name, String description, String inputSchema) implements ToolDefinition {
+public record DefaultToolDefinition(String name, String description, String inputSchema,
+ Map metadata) implements ToolDefinition {
public DefaultToolDefinition {
Assert.hasText(name, "name cannot be null or empty");
Assert.hasText(description, "description cannot be null or empty");
Assert.hasText(inputSchema, "inputSchema cannot be null or empty");
+ metadata = Map.copyOf(metadata);
+ }
+
+ /**
+ * Constructor for backward compatibility without metadata.
+ */
+ public DefaultToolDefinition(String name, String description, String inputSchema) {
+ this(name, description, inputSchema, Collections.emptyMap());
}
public static Builder builder() {
@@ -46,6 +59,8 @@ public static final class Builder {
private String inputSchema;
+ private Map metadata = new HashMap<>();
+
private Builder() {
}
@@ -64,12 +79,22 @@ public Builder inputSchema(String inputSchema) {
return this;
}
+ public Builder metadata(Map metadata) {
+ this.metadata = new HashMap<>(metadata);
+ return this;
+ }
+
+ public Builder addMetadata(String key, Object value) {
+ this.metadata.put(key, value);
+ return this;
+ }
+
public ToolDefinition build() {
if (!StringUtils.hasText(this.description)) {
Assert.hasText(this.name, "toolName cannot be null or empty");
this.description = ParsingUtils.reConcatenateCamelCase(this.name, " ");
}
- return new DefaultToolDefinition(this.name, this.description, this.inputSchema);
+ return new DefaultToolDefinition(this.name, this.description, this.inputSchema, this.metadata);
}
}
diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/ToolDefinition.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/ToolDefinition.java
index 517a0061712..1db512d2762 100644
--- a/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/ToolDefinition.java
+++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/ToolDefinition.java
@@ -16,6 +16,9 @@
package org.springframework.ai.tool.definition;
+import java.util.Collections;
+import java.util.Map;
+
/**
* Definition used by the AI model to determine when and how to call the tool.
*
@@ -39,6 +42,16 @@ public interface ToolDefinition {
*/
String inputSchema();
+ /**
+ * The metadata associated with the tool. This can be used for filtering,
+ * categorization, and other purposes. Default implementation returns an empty map.
+ * @return an unmodifiable map of metadata key-value pairs
+ * @since 1.0.0
+ */
+ default Map metadata() {
+ return Collections.emptyMap();
+ }
+
/**
* Create a default {@link ToolDefinition} builder.
*/
diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolDefinitions.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolDefinitions.java
index 68d4646333a..1022ccbf902 100644
--- a/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolDefinitions.java
+++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolDefinitions.java
@@ -49,7 +49,8 @@ public static DefaultToolDefinition.Builder builder(Method method) {
return DefaultToolDefinition.builder()
.name(ToolUtils.getToolName(method))
.description(ToolUtils.getToolDescription(method))
- .inputSchema(JsonSchemaGenerator.generateForMethodInput(method));
+ .inputSchema(JsonSchemaGenerator.generateForMethodInput(method))
+ .metadata(ToolUtils.getToolMetadata(method));
}
/**
diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java
index baa03b830cb..061ce4c2b76 100644
--- a/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java
+++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java
@@ -18,6 +18,7 @@
import java.lang.reflect.Method;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
@@ -28,6 +29,7 @@
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolMetadata;
import org.springframework.ai.tool.execution.DefaultToolCallResultConverter;
import org.springframework.ai.tool.execution.ToolCallResultConverter;
import org.springframework.ai.util.ParsingUtils;
@@ -120,6 +122,52 @@ public static List getDuplicateToolNames(ToolCallback... toolCallbacks)
return getDuplicateToolNames(Arrays.asList(toolCallbacks));
}
+ /**
+ * Extracts metadata from a method annotated with {@link ToolMetadata}.
+ * @param method the method to extract metadata from
+ * @return a map of metadata key-value pairs, or an empty map if no metadata is
+ * present
+ */
+ public static Map getToolMetadata(Method method) {
+ Assert.notNull(method, "method cannot be null");
+ var toolMetadata = AnnotatedElementUtils.findMergedAnnotation(method, ToolMetadata.class);
+ if (toolMetadata == null) {
+ return Map.of();
+ }
+
+ Map metadata = new HashMap<>();
+
+ if (StringUtils.hasText(toolMetadata.type())) {
+ metadata.put("type", toolMetadata.type());
+ }
+
+ if (StringUtils.hasText(toolMetadata.category())) {
+ metadata.put("category", toolMetadata.category());
+ }
+
+ metadata.put("priority", toolMetadata.priority());
+
+ if (toolMetadata.tags().length > 0) {
+ metadata.put("tags", Arrays.asList(toolMetadata.tags()));
+ }
+
+ for (String entry : toolMetadata.value()) {
+ String[] parts = entry.split("=", 2);
+ if (parts.length == 2) {
+ String key = parts[0].trim();
+ String value = parts[1].trim();
+ if (StringUtils.hasText(key) && StringUtils.hasText(value)) {
+ metadata.put(key, value);
+ }
+ }
+ else {
+ logger.warn("Invalid metadata entry format: '{}'. Expected format is 'key=value'.", entry);
+ }
+ }
+
+ return metadata;
+ }
+
/**
* Validates that a tool name follows recommended naming conventions. Logs a warning
* if the tool name contains characters that may not be compatible with some LLMs.
diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/ToolCallbackFiltersTest.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/ToolCallbackFiltersTest.java
new file mode 100644
index 00000000000..603fe3a8b7b
--- /dev/null
+++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/ToolCallbackFiltersTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.tool;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolMetadata;
+import org.springframework.ai.tool.method.MethodToolCallback;
+import org.springframework.ai.tool.support.ToolDefinitions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ToolCallbackFilters}.
+ *
+ */
+class ToolCallbackFiltersTest {
+
+ private List allTools;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ TestToolsService service = new TestToolsService();
+
+ // Create tool callbacks for test methods
+ ToolCallback realTimeAnalysis = MethodToolCallback.builder()
+ .toolDefinition(ToolDefinitions.from(TestToolsService.class.getMethod("realTimeAnalysis", String.class)))
+ .toolMethod(TestToolsService.class.getMethod("realTimeAnalysis", String.class))
+ .toolObject(service)
+ .build();
+
+ ToolCallback historicalAnalysis = MethodToolCallback.builder()
+ .toolDefinition(ToolDefinitions.from(TestToolsService.class.getMethod("historicalAnalysis", String.class)))
+ .toolMethod(TestToolsService.class.getMethod("historicalAnalysis", String.class))
+ .toolObject(service)
+ .build();
+
+ ToolCallback reportGeneration = MethodToolCallback.builder()
+ .toolDefinition(ToolDefinitions.from(TestToolsService.class.getMethod("reportGeneration", String.class)))
+ .toolMethod(TestToolsService.class.getMethod("reportGeneration", String.class))
+ .toolObject(service)
+ .build();
+
+ ToolCallback dataValidation = MethodToolCallback.builder()
+ .toolDefinition(ToolDefinitions.from(TestToolsService.class.getMethod("dataValidation", String.class)))
+ .toolMethod(TestToolsService.class.getMethod("dataValidation", String.class))
+ .toolObject(service)
+ .build();
+
+ this.allTools = List.of(realTimeAnalysis, historicalAnalysis, reportGeneration, dataValidation);
+ }
+
+ @Test
+ void shouldFilterByType() {
+ List filtered = ToolCallbackFilters.filterByType(this.allTools, "RealTimeAnalysis");
+
+ assertThat(filtered).hasSize(1);
+ assertThat(filtered.get(0).getToolDefinition().name()).isEqualTo("realTimeAnalysis");
+ }
+
+ @Test
+ void shouldFilterByCategory() {
+ List filtered = ToolCallbackFilters.filterByCategory(this.allTools, "analytics");
+
+ assertThat(filtered).hasSize(2);
+ assertThat(filtered).extracting(tc -> tc.getToolDefinition().name())
+ .containsExactlyInAnyOrder("realTimeAnalysis", "historicalAnalysis");
+ }
+
+ @Test
+ void shouldFilterByMinPriority() {
+ List filtered = ToolCallbackFilters.filterByMinPriority(this.allTools, 7);
+
+ assertThat(filtered).hasSize(2);
+ assertThat(filtered).extracting(tc -> tc.getToolDefinition().name())
+ .containsExactlyInAnyOrder("realTimeAnalysis", "reportGeneration");
+ }
+
+ @Test
+ void shouldFilterByTags() {
+ List filtered = ToolCallbackFilters.filterByTags(this.allTools, "critical");
+
+ assertThat(filtered).hasSize(2);
+ assertThat(filtered).extracting(tc -> tc.getToolDefinition().name())
+ .containsExactlyInAnyOrder("realTimeAnalysis", "dataValidation");
+ }
+
+ @Test
+ void shouldFilterByCustomMetadata() {
+ List filtered = this.allTools.stream()
+ .filter(ToolCallbackFilters.byMetadata("environment", "production"))
+ .toList();
+
+ assertThat(filtered).hasSize(1);
+ assertThat(filtered.get(0).getToolDefinition().name()).isEqualTo("realTimeAnalysis");
+ }
+
+ @Test
+ void shouldCombineMultipleFilters() {
+ List filtered = this.allTools.stream()
+ .filter(ToolCallbackFilters.byCategory("analytics").and(ToolCallbackFilters.byMinPriority(7)))
+ .toList();
+
+ assertThat(filtered).hasSize(1);
+ assertThat(filtered.get(0).getToolDefinition().name()).isEqualTo("realTimeAnalysis");
+ }
+
+ @Test
+ void shouldReturnEmptyListWhenNoMatch() {
+ List filtered = ToolCallbackFilters.filterByType(this.allTools, "NonExistentType");
+
+ assertThat(filtered).isEmpty();
+ }
+
+ @Test
+ void shouldFilterArrayByType() {
+ ToolCallback[] array = this.allTools.toArray(new ToolCallback[0]);
+ ToolCallback[] filtered = ToolCallbackFilters.filterByType(array, "RealTimeAnalysis");
+
+ assertThat(filtered).hasSize(1);
+ assertThat(filtered[0].getToolDefinition().name()).isEqualTo("realTimeAnalysis");
+ }
+
+ // Test tools service
+ static class TestToolsService {
+
+ @Tool(description = "Analyzes real-time market data")
+ @ToolMetadata(type = "RealTimeAnalysis", category = "analytics", priority = 8,
+ tags = { "critical", "realtime" }, value = { "environment=production" })
+ public String realTimeAnalysis(String symbol) {
+ return "Real-time analysis for " + symbol;
+ }
+
+ @Tool(description = "Analyzes historical trends")
+ @ToolMetadata(type = "HistoricalAnalysis", category = "analytics", priority = 6, tags = { "historical" })
+ public String historicalAnalysis(String period) {
+ return "Historical analysis for " + period;
+ }
+
+ @Tool(description = "Generates reports")
+ @ToolMetadata(type = "Reporting", category = "reporting", priority = 7, tags = { "reporting" })
+ public String reportGeneration(String type) {
+ return "Report: " + type;
+ }
+
+ @Tool(description = "Validates data")
+ @ToolMetadata(type = "Validation", category = "quality", priority = 5, tags = { "critical", "validation" })
+ public String dataValidation(String data) {
+ return "Validation result for " + data;
+ }
+
+ }
+
+}
diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/annotation/ToolMetadataTest.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/annotation/ToolMetadataTest.java
new file mode 100644
index 00000000000..7a8435c118a
--- /dev/null
+++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/annotation/ToolMetadataTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.tool.annotation;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.tool.definition.ToolDefinition;
+import org.springframework.ai.tool.support.ToolDefinitions;
+import org.springframework.ai.tool.support.ToolUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ToolMetadata} annotation and metadata extraction.
+ *
+ */
+class ToolMetadataTest {
+
+ @Test
+ void shouldExtractMetadataFromAnnotation() throws Exception {
+ Method method = TestTools.class.getMethod("realTimeAnalysis", String.class);
+ Map metadata = ToolUtils.getToolMetadata(method);
+
+ assertThat(metadata).isNotEmpty();
+ assertThat(metadata.get("type")).isEqualTo("RealTimeAnalysis");
+ assertThat(metadata.get("category")).isEqualTo("market");
+ assertThat(metadata.get("priority")).isEqualTo(8);
+ }
+
+ @Test
+ void shouldExtractTagsFromAnnotation() throws Exception {
+ Method method = TestTools.class.getMethod("historicalAnalysis", String.class);
+ Map metadata = ToolUtils.getToolMetadata(method);
+
+ assertThat(metadata).isNotEmpty();
+ assertThat(metadata.get("tags")).isInstanceOf(List.class);
+ @SuppressWarnings("unchecked")
+ List tags = (List) metadata.get("tags");
+ assertThat(tags).containsExactlyInAnyOrder("historical", "longterm");
+ }
+
+ @Test
+ void shouldExtractMultipleMetadataFields() throws Exception {
+ Method method = TestTools.class.getMethod("customTool");
+ Map metadata = ToolUtils.getToolMetadata(method);
+
+ assertThat(metadata).isNotEmpty();
+ assertThat(metadata.get("type")).isEqualTo("CustomType");
+ assertThat(metadata.get("category")).isEqualTo("custom");
+ }
+
+ @Test
+ void shouldReturnEmptyMapWhenNoMetadata() throws Exception {
+ Method method = TestTools.class.getMethod("noMetadataTool");
+ Map metadata = ToolUtils.getToolMetadata(method);
+
+ assertThat(metadata).isEmpty();
+ }
+
+ @Test
+ void shouldIncludeMetadataInToolDefinition() throws Exception {
+ Method method = TestTools.class.getMethod("realTimeAnalysis", String.class);
+ ToolDefinition toolDefinition = ToolDefinitions.from(method);
+
+ assertThat(toolDefinition.metadata()).isNotEmpty();
+ assertThat(toolDefinition.metadata().get("type")).isEqualTo("RealTimeAnalysis");
+ assertThat(toolDefinition.metadata().get("category")).isEqualTo("market");
+ assertThat(toolDefinition.metadata().get("priority")).isEqualTo(8);
+ }
+
+ @Test
+ void shouldHandleDefaultPriority() throws Exception {
+ Method method = TestTools.class.getMethod("defaultPriorityTool");
+ Map metadata = ToolUtils.getToolMetadata(method);
+
+ assertThat(metadata.get("priority")).isEqualTo(5); // Default priority
+ }
+
+ @Test
+ void shouldHandlePartialMetadata() throws Exception {
+ Method method = TestTools.class.getMethod("partialMetadataTool");
+ Map metadata = ToolUtils.getToolMetadata(method);
+
+ assertThat(metadata).isNotEmpty();
+ assertThat(metadata.get("type")).isEqualTo("PartialType");
+ assertThat(metadata.get("category")).isNull();
+ assertThat(metadata.get("priority")).isEqualTo(5); // Default
+ }
+
+ // Test tools class
+ static class TestTools {
+
+ @Tool(description = "Analyzes real-time market data")
+ @ToolMetadata(type = "RealTimeAnalysis", category = "market", priority = 8)
+ public String realTimeAnalysis(String symbol) {
+ return "Real-time analysis for " + symbol;
+ }
+
+ @Tool(description = "Analyzes historical trends")
+ @ToolMetadata(type = "HistoricalAnalysis", category = "market", priority = 6,
+ tags = { "historical", "longterm" })
+ public String historicalAnalysis(String period) {
+ return "Historical analysis for " + period;
+ }
+
+ @Tool(description = "Custom tool with additional metadata")
+ @ToolMetadata(type = "CustomType", category = "custom")
+ public String customTool() {
+ return "Custom result";
+ }
+
+ @Tool(description = "Tool without metadata")
+ public String noMetadataTool() {
+ return "No metadata";
+ }
+
+ @Tool(description = "Tool with default priority")
+ @ToolMetadata(category = "test")
+ public String defaultPriorityTool() {
+ return "Default priority";
+ }
+
+ @Tool(description = "Tool with partial metadata")
+ @ToolMetadata(type = "PartialType")
+ public String partialMetadataTool() {
+ return "Partial metadata";
+ }
+
+ }
+
+}