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"; + } + + } + +}