Skip to content

Commit d75885d

Browse files
academeyilayaperumalg
authored andcommitted
Add validation for Tool annotation names
- Add regex pattern validation for tool names in ToolUtils - Tool names can only contain alphanumeric characters, underscores, hyphens, and dots - Add comprehensive unit tests for tool name validation - Update @tool annotation documentation with naming constraints - Throw IllegalArgumentException for invalid tool names with clear error message This prevents runtime failures when LLMs (like GPT 4.1-mini) encounter tool names with spaces or special characters that they cannot process. Fixes #3832 Signed-off-by: Hyunjoon Park <academey@gmail.com> Change Tool name validation from exception to warning Based on feedback, the validation has been changed from throwing an exception to logging a warning. This approach is more flexible as the Tool annotation is generic and not specific to any particular LLM. Changes: - Modified ToolUtils to log warnings instead of throwing exceptions - Updated Tool annotation JavaDoc to recommend naming conventions - Adjusted tests to verify tool names are returned (no exceptions thrown) - Added test for unicode characters to support non-English contexts The warning message guides users toward compatible naming while allowing flexibility for different LLMs and use cases. Fixes #3832 Signed-off-by: Hyunjoon Park <academey@gmail.com>
1 parent b059cdf commit d75885d

File tree

3 files changed

+212
-2
lines changed

3 files changed

+212
-2
lines changed

spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/Tool.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@
3838

3939
/**
4040
* The name of the tool. If not provided, the method name will be used.
41+
* <p>
42+
* For maximum compatibility across different LLMs, it is recommended to use only
43+
* alphanumeric characters, underscores, hyphens, and dots in tool names. Using spaces
44+
* or special characters may cause issues with some LLMs (e.g., OpenAI).
45+
* </p>
46+
* <p>
47+
* Examples of recommended names: "get_weather", "search-docs", "tool.v1"
48+
* </p>
49+
* <p>
50+
* Examples of names that may cause compatibility issues: "get weather" (contains
51+
* space), "tool()" (contains parentheses)
52+
* </p>
4153
*/
4254
String name() default "";
4355

spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020
import java.util.Arrays;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.regex.Pattern;
2324
import java.util.stream.Collectors;
2425

26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
2529
import org.springframework.ai.tool.ToolCallback;
2630
import org.springframework.ai.tool.annotation.Tool;
2731
import org.springframework.ai.tool.execution.DefaultToolCallResultConverter;
@@ -38,16 +42,30 @@
3842
*/
3943
public final class ToolUtils {
4044

45+
private static final Logger logger = LoggerFactory.getLogger(ToolUtils.class);
46+
47+
/**
48+
* Regular expression pattern for recommended tool names. Tool names should contain
49+
* only alphanumeric characters, underscores, hyphens, and dots for maximum
50+
* compatibility across different LLMs.
51+
*/
52+
private static final Pattern RECOMMENDED_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_\\.-]+$");
53+
4154
private ToolUtils() {
4255
}
4356

4457
public static String getToolName(Method method) {
4558
Assert.notNull(method, "method cannot be null");
4659
var tool = AnnotatedElementUtils.findMergedAnnotation(method, Tool.class);
60+
String toolName;
4761
if (tool == null) {
48-
return method.getName();
62+
toolName = method.getName();
63+
}
64+
else {
65+
toolName = StringUtils.hasText(tool.name()) ? tool.name() : method.getName();
4966
}
50-
return StringUtils.hasText(tool.name()) ? tool.name() : method.getName();
67+
validateToolName(toolName);
68+
return toolName;
5169
}
5270

5371
public static String getToolDescriptionFromName(String toolName) {
@@ -102,4 +120,17 @@ public static List<String> getDuplicateToolNames(ToolCallback... toolCallbacks)
102120
return getDuplicateToolNames(Arrays.asList(toolCallbacks));
103121
}
104122

123+
/**
124+
* Validates that a tool name follows recommended naming conventions. Logs a warning
125+
* if the tool name contains characters that may not be compatible with some LLMs.
126+
* @param toolName the tool name to validate
127+
*/
128+
private static void validateToolName(String toolName) {
129+
Assert.hasText(toolName, "Tool name cannot be null or empty");
130+
if (!RECOMMENDED_NAME_PATTERN.matcher(toolName).matches()) {
131+
logger.warn("Tool name '{}' may not be compatible with some LLMs (e.g., OpenAI). "
132+
+ "Consider using only alphanumeric characters, underscores, hyphens, and dots.", toolName);
133+
}
134+
}
135+
105136
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright 2023-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+
17+
package org.springframework.ai.tool.support;
18+
19+
import java.lang.reflect.Method;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.ai.tool.annotation.Tool;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
/**
28+
* Unit tests for {@link ToolUtils}.
29+
*
30+
* @author Hyunjoon Park
31+
* @since 1.0.0
32+
*/
33+
class ToolUtilsTests {
34+
35+
@Test
36+
void getToolNameFromMethodWithoutAnnotation() throws NoSuchMethodException {
37+
Method method = TestTools.class.getMethod("simpleMethod");
38+
String toolName = ToolUtils.getToolName(method);
39+
assertThat(toolName).isEqualTo("simpleMethod");
40+
}
41+
42+
@Test
43+
void getToolNameFromMethodWithAnnotationButNoName() throws NoSuchMethodException {
44+
Method method = TestTools.class.getMethod("annotatedMethodWithoutName");
45+
String toolName = ToolUtils.getToolName(method);
46+
assertThat(toolName).isEqualTo("annotatedMethodWithoutName");
47+
}
48+
49+
@Test
50+
void getToolNameFromMethodWithValidName() throws NoSuchMethodException {
51+
Method method = TestTools.class.getMethod("methodWithValidName");
52+
String toolName = ToolUtils.getToolName(method);
53+
assertThat(toolName).isEqualTo("valid_tool-name.v1");
54+
}
55+
56+
@Test
57+
void getToolNameFromMethodWithNameContainingSpaces() throws NoSuchMethodException {
58+
// Tool names with spaces are now allowed but will generate a warning log
59+
Method method = TestTools.class.getMethod("methodWithSpacesInName");
60+
String toolName = ToolUtils.getToolName(method);
61+
assertThat(toolName).isEqualTo("invalid tool name");
62+
}
63+
64+
@Test
65+
void getToolNameFromMethodWithNameContainingSpecialChars() throws NoSuchMethodException {
66+
// Tool names with special characters are now allowed but will generate a warning
67+
// log
68+
Method method = TestTools.class.getMethod("methodWithSpecialCharsInName");
69+
String toolName = ToolUtils.getToolName(method);
70+
assertThat(toolName).isEqualTo("tool@name!");
71+
}
72+
73+
@Test
74+
void getToolNameFromMethodWithNameContainingParentheses() throws NoSuchMethodException {
75+
// Tool names with parentheses are now allowed but will generate a warning log
76+
Method method = TestTools.class.getMethod("methodWithParenthesesInName");
77+
String toolName = ToolUtils.getToolName(method);
78+
assertThat(toolName).isEqualTo("tool()");
79+
}
80+
81+
@Test
82+
void getToolNameFromMethodWithEmptyName() throws NoSuchMethodException {
83+
Method method = TestTools.class.getMethod("methodWithEmptyName");
84+
// When name is empty, it falls back to method name which is valid
85+
String toolName = ToolUtils.getToolName(method);
86+
assertThat(toolName).isEqualTo("methodWithEmptyName");
87+
}
88+
89+
@Test
90+
void getToolDescriptionFromMethodWithoutAnnotation() throws NoSuchMethodException {
91+
Method method = TestTools.class.getMethod("simpleMethod");
92+
String description = ToolUtils.getToolDescription(method);
93+
assertThat(description).isEqualTo("simple method");
94+
}
95+
96+
@Test
97+
void getToolDescriptionFromMethodWithAnnotationButNoDescription() throws NoSuchMethodException {
98+
Method method = TestTools.class.getMethod("annotatedMethodWithoutName");
99+
String description = ToolUtils.getToolDescription(method);
100+
assertThat(description).isEqualTo("annotatedMethodWithoutName");
101+
}
102+
103+
@Test
104+
void getToolDescriptionFromMethodWithDescription() throws NoSuchMethodException {
105+
Method method = TestTools.class.getMethod("methodWithDescription");
106+
String description = ToolUtils.getToolDescription(method);
107+
assertThat(description).isEqualTo("This is a tool description");
108+
}
109+
110+
@Test
111+
void getToolNameFromMethodWithUnicodeCharacters() throws NoSuchMethodException {
112+
// Tool names with unicode characters should be allowed for non-English contexts
113+
Method method = TestTools.class.getMethod("methodWithUnicodeName");
114+
String toolName = ToolUtils.getToolName(method);
115+
assertThat(toolName).isEqualTo("获取天气");
116+
}
117+
118+
// Test helper class with various tool methods
119+
public static class TestTools {
120+
121+
public void simpleMethod() {
122+
// Method without @Tool annotation
123+
}
124+
125+
@Tool
126+
public void annotatedMethodWithoutName() {
127+
// Method with @Tool but no name specified
128+
}
129+
130+
@Tool(name = "valid_tool-name.v1")
131+
public void methodWithValidName() {
132+
// Method with valid tool name
133+
}
134+
135+
@Tool(name = "invalid tool name")
136+
public void methodWithSpacesInName() {
137+
// Method with spaces in tool name (invalid)
138+
}
139+
140+
@Tool(name = "tool@name!")
141+
public void methodWithSpecialCharsInName() {
142+
// Method with special characters in tool name (invalid)
143+
}
144+
145+
@Tool(name = "tool()")
146+
public void methodWithParenthesesInName() {
147+
// Method with parentheses in tool name (invalid)
148+
}
149+
150+
@Tool(name = "")
151+
public void methodWithEmptyName() {
152+
// Method with empty name (falls back to method name)
153+
}
154+
155+
@Tool(description = "This is a tool description")
156+
public void methodWithDescription() {
157+
// Method with description
158+
}
159+
160+
@Tool(name = "获取天气")
161+
public void methodWithUnicodeName() {
162+
// Method with unicode characters in tool name (Chinese: "get weather")
163+
}
164+
165+
}
166+
167+
}

0 commit comments

Comments
 (0)