Skip to content

Commit 2c85792

Browse files
ThomasVitaletzolov
authored andcommitted
Advancing Tool Support - Part 6
* Completed new documentation for Tool Calling * Added deprecation notes and migration guide to documentation * Made “call” methods explicit in ToolCallback API * Consolidated naming: ToolCallExceptionConverter -> ToolExecutionExceptionProcessor * Consolidated naming: ToolCallResultConvert.apply() -> ToolCallResultConvert.convert() * Redraw diagrams for consistency Relates to gh-2049 Signed-off-by: Thomas Vitale <[email protected]>
1 parent c92f2d3 commit 2c85792

30 files changed

+1181
-204
lines changed

spring-ai-core/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
import org.springframework.ai.model.function.FunctionCallingOptions;
3131
import org.springframework.ai.tool.ToolCallback;
3232
import org.springframework.ai.tool.definition.ToolDefinition;
33-
import org.springframework.ai.tool.execution.DefaultToolCallExceptionConverter;
34-
import org.springframework.ai.tool.execution.ToolCallExceptionConverter;
33+
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
34+
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
3535
import org.springframework.ai.tool.execution.ToolExecutionException;
3636
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
3737
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
@@ -62,26 +62,26 @@ public class DefaultToolCallingManager implements ToolCallingManager {
6262
private static final ToolCallbackResolver DEFAULT_TOOL_CALLBACK_RESOLVER
6363
= new DelegatingToolCallbackResolver(List.of());
6464

65-
private static final ToolCallExceptionConverter DEFAULT_TOOL_CALL_EXCEPTION_CONVERTER
66-
= DefaultToolCallExceptionConverter.builder().build();
65+
private static final ToolExecutionExceptionProcessor DEFAULT_TOOL_EXECUTION_EXCEPTION_PROCESSOR
66+
= DefaultToolExecutionExceptionProcessor.builder().build();
6767

6868
// @formatter:on
6969

7070
private final ObservationRegistry observationRegistry;
7171

7272
private final ToolCallbackResolver toolCallbackResolver;
7373

74-
private final ToolCallExceptionConverter toolCallExceptionConverter;
74+
private final ToolExecutionExceptionProcessor toolExecutionExceptionProcessor;
7575

7676
public DefaultToolCallingManager(ObservationRegistry observationRegistry, ToolCallbackResolver toolCallbackResolver,
77-
ToolCallExceptionConverter toolCallExceptionConverter) {
77+
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor) {
7878
Assert.notNull(observationRegistry, "observationRegistry cannot be null");
7979
Assert.notNull(toolCallbackResolver, "toolCallbackResolver cannot be null");
80-
Assert.notNull(toolCallExceptionConverter, "toolCallExceptionConverter cannot be null");
80+
Assert.notNull(toolExecutionExceptionProcessor, "toolCallExceptionConverter cannot be null");
8181

8282
this.observationRegistry = observationRegistry;
8383
this.toolCallbackResolver = toolCallbackResolver;
84-
this.toolCallExceptionConverter = toolCallExceptionConverter;
84+
this.toolExecutionExceptionProcessor = toolExecutionExceptionProcessor;
8585
}
8686

8787
@Override
@@ -214,7 +214,7 @@ else if (toolCallback instanceof ToolCallback callback) {
214214
toolResult = toolCallback.call(toolInputArguments, toolContext);
215215
}
216216
catch (ToolExecutionException ex) {
217-
toolResult = toolCallExceptionConverter.convert(ex);
217+
toolResult = toolExecutionExceptionProcessor.process(ex);
218218
}
219219

220220
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, toolResult));
@@ -244,7 +244,7 @@ public static class Builder {
244244

245245
private ToolCallbackResolver toolCallbackResolver = DEFAULT_TOOL_CALLBACK_RESOLVER;
246246

247-
private ToolCallExceptionConverter toolCallExceptionConverter = DEFAULT_TOOL_CALL_EXCEPTION_CONVERTER;
247+
private ToolExecutionExceptionProcessor toolExecutionExceptionProcessor = DEFAULT_TOOL_EXECUTION_EXCEPTION_PROCESSOR;
248248

249249
private Builder() {
250250
}
@@ -259,13 +259,15 @@ public Builder toolCallbackResolver(ToolCallbackResolver toolCallbackResolver) {
259259
return this;
260260
}
261261

262-
public Builder toolCallExceptionConverter(ToolCallExceptionConverter toolCallExceptionConverter) {
263-
this.toolCallExceptionConverter = toolCallExceptionConverter;
262+
public Builder toolExecutionExceptionProcessor(
263+
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor) {
264+
this.toolExecutionExceptionProcessor = toolExecutionExceptionProcessor;
264265
return this;
265266
}
266267

267268
public DefaultToolCallingManager build() {
268-
return new DefaultToolCallingManager(observationRegistry, toolCallbackResolver, toolCallExceptionConverter);
269+
return new DefaultToolCallingManager(observationRegistry, toolCallbackResolver,
270+
toolExecutionExceptionProcessor);
269271
}
270272

271273
}

spring-ai-core/src/main/java/org/springframework/ai/model/tool/LegacyToolCallingManager.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
import org.springframework.ai.model.function.FunctionCallingOptions;
3030
import org.springframework.ai.tool.ToolCallback;
3131
import org.springframework.ai.tool.definition.ToolDefinition;
32-
import org.springframework.ai.tool.execution.DefaultToolCallExceptionConverter;
33-
import org.springframework.ai.tool.execution.ToolCallExceptionConverter;
32+
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
33+
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
3434
import org.springframework.ai.tool.execution.ToolExecutionException;
3535
import org.springframework.lang.Nullable;
3636
import org.springframework.util.Assert;
@@ -59,7 +59,8 @@ public class LegacyToolCallingManager implements ToolCallingManager {
5959

6060
private final Map<String, FunctionCallback> functionCallbacks = new HashMap<>();
6161

62-
private final ToolCallExceptionConverter toolCallExceptionConverter = DefaultToolCallExceptionConverter.builder()
62+
private final ToolExecutionExceptionProcessor toolExecutionExceptionProcessor = DefaultToolExecutionExceptionProcessor
63+
.builder()
6364
.build();
6465

6566
public LegacyToolCallingManager(@Nullable FunctionCallbackResolver functionCallbackResolver,
@@ -194,7 +195,7 @@ else if (prompt.getOptions() instanceof FunctionCallingOptions functionOptions)
194195
toolResult = toolCallback.call(toolInputArguments, toolContext);
195196
}
196197
catch (ToolExecutionException ex) {
197-
toolResult = toolCallExceptionConverter.convert(ex);
198+
toolResult = toolExecutionExceptionProcessor.process(ex);
198199
}
199200

200201
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, toolResult));

spring-ai-core/src/main/java/org/springframework/ai/tool/ToolCallback.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
package org.springframework.ai.tool;
1818

19+
import org.springframework.ai.chat.model.ToolContext;
1920
import org.springframework.ai.model.function.FunctionCallback;
2021
import org.springframework.ai.tool.definition.ToolDefinition;
2122
import org.springframework.ai.tool.metadata.ToolMetadata;
23+
import org.springframework.lang.Nullable;
2224

2325
/**
2426
* Represents a tool whose execution can be triggered by an AI model.
@@ -40,6 +42,23 @@ default ToolMetadata getToolMetadata() {
4042
return ToolMetadata.builder().build();
4143
}
4244

45+
/**
46+
* Execute tool with the given input and return the result to send back to the AI
47+
* model.
48+
*/
49+
String call(String toolInput);
50+
51+
/**
52+
* Execute tool with the given input and context, and return the result to send back
53+
* to the AI model.
54+
*/
55+
default String call(String toolInput, @Nullable ToolContext tooContext) {
56+
if (tooContext != null && !tooContext.getContext().isEmpty()) {
57+
throw new UnsupportedOperationException("Tool context is not supported!");
58+
}
59+
return call(toolInput);
60+
}
61+
4362
@Override
4463
@Deprecated // Call getToolDefinition().name() instead
4564
default String getName() {

spring-ai-core/src/main/java/org/springframework/ai/tool/execution/DefaultToolCallResultConverter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public final class DefaultToolCallResultConverter implements ToolCallResultConve
3535
private static final Logger logger = LoggerFactory.getLogger(DefaultToolCallResultConverter.class);
3636

3737
@Override
38-
public String apply(@Nullable Object result, @Nullable Type returnType) {
38+
public String convert(@Nullable Object result, @Nullable Type returnType) {
3939
if (returnType == Void.TYPE) {
4040
logger.debug("The tool has no return type. Converting to conventional response.");
4141
return "Done";
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,25 @@
2121
import org.springframework.util.Assert;
2222

2323
/**
24-
* Default implementation of {@link ToolCallExceptionConverter}.
24+
* Default implementation of {@link ToolExecutionExceptionProcessor}.
2525
*
2626
* @author Thomas Vitale
2727
* @since 1.0.0
2828
*/
29-
public class DefaultToolCallExceptionConverter implements ToolCallExceptionConverter {
29+
public class DefaultToolExecutionExceptionProcessor implements ToolExecutionExceptionProcessor {
3030

31-
private final static Logger logger = LoggerFactory.getLogger(DefaultToolCallExceptionConverter.class);
31+
private final static Logger logger = LoggerFactory.getLogger(DefaultToolExecutionExceptionProcessor.class);
3232

3333
private static final boolean DEFAULT_ALWAYS_THROW = false;
3434

3535
private final boolean alwaysThrow;
3636

37-
public DefaultToolCallExceptionConverter(boolean alwaysThrow) {
37+
public DefaultToolExecutionExceptionProcessor(boolean alwaysThrow) {
3838
this.alwaysThrow = alwaysThrow;
3939
}
4040

4141
@Override
42-
public String convert(ToolExecutionException exception) {
42+
public String process(ToolExecutionException exception) {
4343
Assert.notNull(exception, "exception cannot be null");
4444
if (alwaysThrow) {
4545
throw exception;
@@ -62,8 +62,8 @@ public Builder alwaysThrow(boolean alwaysThrow) {
6262
return this;
6363
}
6464

65-
public DefaultToolCallExceptionConverter build() {
66-
return new DefaultToolCallExceptionConverter(alwaysThrow);
65+
public DefaultToolExecutionExceptionProcessor build() {
66+
return new DefaultToolExecutionExceptionProcessor(alwaysThrow);
6767
}
6868

6969
}

spring-ai-core/src/main/java/org/springframework/ai/tool/execution/ToolCallResultConverter.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import org.springframework.lang.Nullable;
2020

2121
import java.lang.reflect.Type;
22-
import java.util.function.BiFunction;
2322

2423
/**
2524
* A functional interface to convert tool call results to a String that can be sent back
@@ -29,12 +28,12 @@
2928
* @since 1.0.0
3029
*/
3130
@FunctionalInterface
32-
public interface ToolCallResultConverter extends BiFunction<Object, Type, String> {
31+
public interface ToolCallResultConverter {
3332

3433
/**
3534
* Given an Object returned by a tool, convert it to a String compatible with the
3635
* given class type.
3736
*/
38-
String apply(@Nullable Object result, @Nullable Type returnType);
37+
String convert(@Nullable Object result, @Nullable Type returnType);
3938

4039
}

spring-ai-core/src/main/java/org/springframework/ai/tool/execution/ToolCallExceptionConverter.java renamed to spring-ai-core/src/main/java/org/springframework/ai/tool/execution/ToolExecutionExceptionProcessor.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,20 @@
1717
package org.springframework.ai.tool.execution;
1818

1919
/**
20-
* A functional interface to convert a tool call exception to a String that can be sent
21-
* back to the AI model.
20+
* A functional interface to process a {@link ToolExecutionException} by either converting
21+
* the error message to a String that can be sent back to the AI model or throwing an
22+
* exception to be handled by the caller.
2223
*
2324
* @author Thomas Vitale
2425
* @since 1.0.0
2526
*/
2627
@FunctionalInterface
27-
public interface ToolCallExceptionConverter {
28+
public interface ToolExecutionExceptionProcessor {
2829

2930
/**
3031
* Convert an exception thrown by a tool to a String that can be sent back to the AI
31-
* model.
32+
* model or throw an exception to be handled by the caller.
3233
*/
33-
String convert(ToolExecutionException exception);
34+
String process(ToolExecutionException exception);
3435

3536
}

spring-ai-core/src/main/java/org/springframework/ai/tool/function/FunctionToolCallback.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public String call(String toolInput, @Nullable ToolContext toolContext) {
102102

103103
logger.debug("Successful execution of tool: {}", toolDefinition.name());
104104

105-
return toolCallResultConverter.apply(response, null);
105+
return toolCallResultConverter.convert(response, null);
106106
}
107107

108108
@Override

spring-ai-core/src/main/java/org/springframework/ai/tool/method/MethodToolCallback.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import org.springframework.util.Assert;
3232
import org.springframework.util.ClassUtils;
3333
import org.springframework.util.CollectionUtils;
34-
import org.springframework.util.ReflectionUtils;
3534

3635
import java.lang.reflect.InvocationTargetException;
3736
import java.lang.reflect.Method;
@@ -112,7 +111,7 @@ public String call(String toolInput, @Nullable ToolContext toolContext) {
112111

113112
Type returnType = toolMethod.getGenericReturnType();
114113

115-
return toolCallResultConverter.apply(result, returnType);
114+
return toolCallResultConverter.convert(result, returnType);
116115
}
117116

118117
private void validateToolContextSupport(@Nullable ToolContext toolContext) {

spring-ai-core/src/main/java/org/springframework/ai/util/json/schema/JsonSchemaGenerator.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.ai.util.json.schema;
1818

1919
import com.fasterxml.jackson.annotation.JsonProperty;
20+
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
2021
import com.fasterxml.jackson.databind.JsonNode;
2122
import com.fasterxml.jackson.databind.node.ObjectNode;
2223
import com.github.victools.jsonschema.generator.Module;
@@ -54,6 +55,8 @@
5455
* <ul>
5556
* <li>{@code @ToolParam(required = ..., description = ...)}</li>
5657
* <li>{@code @JsonProperty(required = ...)}</li>
58+
* <li>{@code @JsonClassDescription(...)}</li>
59+
* <li>{@code @JsonPropertyDescription(...)}</li>
5760
* <li>{@code @Schema(required = ..., description = ...)}</li>
5861
* <li>{@code @Nullable}</li>
5962
* </ul>
@@ -165,13 +168,19 @@ private static void processSchemaOptions(SchemaOption[] schemaOptions, ObjectNod
165168
}
166169

167170
/**
168-
* Determines whether a property is required based on the presence of a series of
171+
* Determines whether a property is required based on the presence of a series of *
169172
* annotations.
173+
*
170174
* <p>
171-
* - {@code @ToolParam(required = ...)} - {@code @JsonProperty(required = ...)} -
172-
* {@code @Schema(required = ...)}
175+
* <ul>
176+
* <li>{@code @ToolParam(required = ...)}</li>
177+
* <li>{@code @JsonProperty(required = ...)}</li>
178+
* <li>{@code @Schema(required = ...)}</li>
179+
* <li>{@code @Nullable}</li>
180+
* </ul>
173181
* <p>
174-
* If none of these annotations are present, the default behavior is to consider the
182+
*
183+
* If none of these annotations are present, the default behavior is to consider the *
175184
* property as required.
176185
*/
177186
private static boolean isMethodParameterRequired(Method method, int index) {
@@ -201,6 +210,17 @@ private static boolean isMethodParameterRequired(Method method, int index) {
201210
return PROPERTY_REQUIRED_BY_DEFAULT;
202211
}
203212

213+
/**
214+
* Determines a property description based on the presence of a series of annotations.
215+
*
216+
* <p>
217+
* <ul>
218+
* <li>{@code @ToolParam(description = ...)}</li>
219+
* <li>{@code @JsonPropertyDescription(...)}</li>
220+
* <li>{@code @Schema(description = ...)}</li>
221+
* </ul>
222+
* <p>
223+
*/
204224
@Nullable
205225
private static String getMethodParameterDescription(Method method, int index) {
206226
Parameter parameter = method.getParameters()[index];
@@ -210,6 +230,11 @@ private static String getMethodParameterDescription(Method method, int index) {
210230
return toolParamAnnotation.description();
211231
}
212232

233+
var jacksonAnnotation = parameter.getAnnotation(JsonPropertyDescription.class);
234+
if (jacksonAnnotation != null && StringUtils.hasText(jacksonAnnotation.value())) {
235+
return jacksonAnnotation.value();
236+
}
237+
213238
var schemaAnnotation = parameter.getAnnotation(Schema.class);
214239
if (schemaAnnotation != null && StringUtils.hasText(schemaAnnotation.description())) {
215240
return schemaAnnotation.description();

0 commit comments

Comments
 (0)