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 <ThomasVitale@users.noreply.github.com>
This commit is contained in:
Thomas Vitale
2025-02-05 01:13:00 +01:00
committed by Christian Tzolov
parent c92f2d3096
commit 2c8579278a
30 changed files with 1190 additions and 213 deletions

View File

@@ -30,8 +30,8 @@ import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.execution.DefaultToolCallExceptionConverter;
import org.springframework.ai.tool.execution.ToolCallExceptionConverter;
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
import org.springframework.ai.tool.execution.ToolExecutionException;
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
@@ -62,8 +62,8 @@ public class DefaultToolCallingManager implements ToolCallingManager {
private static final ToolCallbackResolver DEFAULT_TOOL_CALLBACK_RESOLVER
= new DelegatingToolCallbackResolver(List.of());
private static final ToolCallExceptionConverter DEFAULT_TOOL_CALL_EXCEPTION_CONVERTER
= DefaultToolCallExceptionConverter.builder().build();
private static final ToolExecutionExceptionProcessor DEFAULT_TOOL_EXECUTION_EXCEPTION_PROCESSOR
= DefaultToolExecutionExceptionProcessor.builder().build();
// @formatter:on
@@ -71,17 +71,17 @@ public class DefaultToolCallingManager implements ToolCallingManager {
private final ToolCallbackResolver toolCallbackResolver;
private final ToolCallExceptionConverter toolCallExceptionConverter;
private final ToolExecutionExceptionProcessor toolExecutionExceptionProcessor;
public DefaultToolCallingManager(ObservationRegistry observationRegistry, ToolCallbackResolver toolCallbackResolver,
ToolCallExceptionConverter toolCallExceptionConverter) {
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor) {
Assert.notNull(observationRegistry, "observationRegistry cannot be null");
Assert.notNull(toolCallbackResolver, "toolCallbackResolver cannot be null");
Assert.notNull(toolCallExceptionConverter, "toolCallExceptionConverter cannot be null");
Assert.notNull(toolExecutionExceptionProcessor, "toolCallExceptionConverter cannot be null");
this.observationRegistry = observationRegistry;
this.toolCallbackResolver = toolCallbackResolver;
this.toolCallExceptionConverter = toolCallExceptionConverter;
this.toolExecutionExceptionProcessor = toolExecutionExceptionProcessor;
}
@Override
@@ -214,7 +214,7 @@ public class DefaultToolCallingManager implements ToolCallingManager {
toolResult = toolCallback.call(toolInputArguments, toolContext);
}
catch (ToolExecutionException ex) {
toolResult = toolCallExceptionConverter.convert(ex);
toolResult = toolExecutionExceptionProcessor.process(ex);
}
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, toolResult));
@@ -244,7 +244,7 @@ public class DefaultToolCallingManager implements ToolCallingManager {
private ToolCallbackResolver toolCallbackResolver = DEFAULT_TOOL_CALLBACK_RESOLVER;
private ToolCallExceptionConverter toolCallExceptionConverter = DEFAULT_TOOL_CALL_EXCEPTION_CONVERTER;
private ToolExecutionExceptionProcessor toolExecutionExceptionProcessor = DEFAULT_TOOL_EXECUTION_EXCEPTION_PROCESSOR;
private Builder() {
}
@@ -259,13 +259,15 @@ public class DefaultToolCallingManager implements ToolCallingManager {
return this;
}
public Builder toolCallExceptionConverter(ToolCallExceptionConverter toolCallExceptionConverter) {
this.toolCallExceptionConverter = toolCallExceptionConverter;
public Builder toolExecutionExceptionProcessor(
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor) {
this.toolExecutionExceptionProcessor = toolExecutionExceptionProcessor;
return this;
}
public DefaultToolCallingManager build() {
return new DefaultToolCallingManager(observationRegistry, toolCallbackResolver, toolCallExceptionConverter);
return new DefaultToolCallingManager(observationRegistry, toolCallbackResolver,
toolExecutionExceptionProcessor);
}
}

View File

@@ -29,8 +29,8 @@ import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.execution.DefaultToolCallExceptionConverter;
import org.springframework.ai.tool.execution.ToolCallExceptionConverter;
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
import org.springframework.ai.tool.execution.ToolExecutionException;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -59,7 +59,8 @@ public class LegacyToolCallingManager implements ToolCallingManager {
private final Map<String, FunctionCallback> functionCallbacks = new HashMap<>();
private final ToolCallExceptionConverter toolCallExceptionConverter = DefaultToolCallExceptionConverter.builder()
private final ToolExecutionExceptionProcessor toolExecutionExceptionProcessor = DefaultToolExecutionExceptionProcessor
.builder()
.build();
public LegacyToolCallingManager(@Nullable FunctionCallbackResolver functionCallbackResolver,
@@ -194,7 +195,7 @@ public class LegacyToolCallingManager implements ToolCallingManager {
toolResult = toolCallback.call(toolInputArguments, toolContext);
}
catch (ToolExecutionException ex) {
toolResult = toolCallExceptionConverter.convert(ex);
toolResult = toolExecutionExceptionProcessor.process(ex);
}
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, toolResult));

View File

@@ -16,9 +16,11 @@
package org.springframework.ai.tool;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.metadata.ToolMetadata;
import org.springframework.lang.Nullable;
/**
* Represents a tool whose execution can be triggered by an AI model.
@@ -40,6 +42,23 @@ public interface ToolCallback extends FunctionCallback {
return ToolMetadata.builder().build();
}
/**
* Execute tool with the given input and return the result to send back to the AI
* model.
*/
String call(String toolInput);
/**
* Execute tool with the given input and context, and return the result to send back
* to the AI model.
*/
default String call(String toolInput, @Nullable ToolContext tooContext) {
if (tooContext != null && !tooContext.getContext().isEmpty()) {
throw new UnsupportedOperationException("Tool context is not supported!");
}
return call(toolInput);
}
@Override
@Deprecated // Call getToolDefinition().name() instead
default String getName() {

View File

@@ -35,7 +35,7 @@ public final class DefaultToolCallResultConverter implements ToolCallResultConve
private static final Logger logger = LoggerFactory.getLogger(DefaultToolCallResultConverter.class);
@Override
public String apply(@Nullable Object result, @Nullable Type returnType) {
public String convert(@Nullable Object result, @Nullable Type returnType) {
if (returnType == Void.TYPE) {
logger.debug("The tool has no return type. Converting to conventional response.");
return "Done";

View File

@@ -21,25 +21,25 @@ import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
/**
* Default implementation of {@link ToolCallExceptionConverter}.
* Default implementation of {@link ToolExecutionExceptionProcessor}.
*
* @author Thomas Vitale
* @since 1.0.0
*/
public class DefaultToolCallExceptionConverter implements ToolCallExceptionConverter {
public class DefaultToolExecutionExceptionProcessor implements ToolExecutionExceptionProcessor {
private final static Logger logger = LoggerFactory.getLogger(DefaultToolCallExceptionConverter.class);
private final static Logger logger = LoggerFactory.getLogger(DefaultToolExecutionExceptionProcessor.class);
private static final boolean DEFAULT_ALWAYS_THROW = false;
private final boolean alwaysThrow;
public DefaultToolCallExceptionConverter(boolean alwaysThrow) {
public DefaultToolExecutionExceptionProcessor(boolean alwaysThrow) {
this.alwaysThrow = alwaysThrow;
}
@Override
public String convert(ToolExecutionException exception) {
public String process(ToolExecutionException exception) {
Assert.notNull(exception, "exception cannot be null");
if (alwaysThrow) {
throw exception;
@@ -62,8 +62,8 @@ public class DefaultToolCallExceptionConverter implements ToolCallExceptionConve
return this;
}
public DefaultToolCallExceptionConverter build() {
return new DefaultToolCallExceptionConverter(alwaysThrow);
public DefaultToolExecutionExceptionProcessor build() {
return new DefaultToolExecutionExceptionProcessor(alwaysThrow);
}
}

View File

@@ -19,7 +19,6 @@ package org.springframework.ai.tool.execution;
import org.springframework.lang.Nullable;
import java.lang.reflect.Type;
import java.util.function.BiFunction;
/**
* A functional interface to convert tool call results to a String that can be sent back
@@ -29,12 +28,12 @@ import java.util.function.BiFunction;
* @since 1.0.0
*/
@FunctionalInterface
public interface ToolCallResultConverter extends BiFunction<Object, Type, String> {
public interface ToolCallResultConverter {
/**
* Given an Object returned by a tool, convert it to a String compatible with the
* given class type.
*/
String apply(@Nullable Object result, @Nullable Type returnType);
String convert(@Nullable Object result, @Nullable Type returnType);
}

View File

@@ -17,19 +17,20 @@
package org.springframework.ai.tool.execution;
/**
* A functional interface to convert a tool call exception to a String that can be sent
* back to the AI model.
* A functional interface to process a {@link ToolExecutionException} by either converting
* the error message to a String that can be sent back to the AI model or throwing an
* exception to be handled by the caller.
*
* @author Thomas Vitale
* @since 1.0.0
*/
@FunctionalInterface
public interface ToolCallExceptionConverter {
public interface ToolExecutionExceptionProcessor {
/**
* Convert an exception thrown by a tool to a String that can be sent back to the AI
* model.
* model or throw an exception to be handled by the caller.
*/
String convert(ToolExecutionException exception);
String process(ToolExecutionException exception);
}

View File

@@ -102,7 +102,7 @@ public class FunctionToolCallback<I, O> implements ToolCallback {
logger.debug("Successful execution of tool: {}", toolDefinition.name());
return toolCallResultConverter.apply(response, null);
return toolCallResultConverter.convert(response, null);
}
@Override

View File

@@ -31,7 +31,6 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -112,7 +111,7 @@ public class MethodToolCallback implements ToolCallback {
Type returnType = toolMethod.getGenericReturnType();
return toolCallResultConverter.apply(result, returnType);
return toolCallResultConverter.convert(result, returnType);
}
private void validateToolContextSupport(@Nullable ToolContext toolContext) {

View File

@@ -17,6 +17,7 @@
package org.springframework.ai.util.json.schema;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.victools.jsonschema.generator.Module;
@@ -54,6 +55,8 @@ import java.util.stream.Stream;
* <ul>
* <li>{@code @ToolParam(required = ..., description = ...)}</li>
* <li>{@code @JsonProperty(required = ...)}</li>
* <li>{@code @JsonClassDescription(...)}</li>
* <li>{@code @JsonPropertyDescription(...)}</li>
* <li>{@code @Schema(required = ..., description = ...)}</li>
* <li>{@code @Nullable}</li>
* </ul>
@@ -165,13 +168,19 @@ public final class JsonSchemaGenerator {
}
/**
* Determines whether a property is required based on the presence of a series of
* Determines whether a property is required based on the presence of a series of *
* annotations.
*
* <p>
* - {@code @ToolParam(required = ...)} - {@code @JsonProperty(required = ...)} -
* {@code @Schema(required = ...)}
* <ul>
* <li>{@code @ToolParam(required = ...)}</li>
* <li>{@code @JsonProperty(required = ...)}</li>
* <li>{@code @Schema(required = ...)}</li>
* <li>{@code @Nullable}</li>
* </ul>
* <p>
* If none of these annotations are present, the default behavior is to consider the
*
* If none of these annotations are present, the default behavior is to consider the *
* property as required.
*/
private static boolean isMethodParameterRequired(Method method, int index) {
@@ -201,6 +210,17 @@ public final class JsonSchemaGenerator {
return PROPERTY_REQUIRED_BY_DEFAULT;
}
/**
* Determines a property description based on the presence of a series of annotations.
*
* <p>
* <ul>
* <li>{@code @ToolParam(description = ...)}</li>
* <li>{@code @JsonPropertyDescription(...)}</li>
* <li>{@code @Schema(description = ...)}</li>
* </ul>
* <p>
*/
@Nullable
private static String getMethodParameterDescription(Method method, int index) {
Parameter parameter = method.getParameters()[index];
@@ -210,6 +230,11 @@ public final class JsonSchemaGenerator {
return toolParamAnnotation.description();
}
var jacksonAnnotation = parameter.getAnnotation(JsonPropertyDescription.class);
if (jacksonAnnotation != null && StringUtils.hasText(jacksonAnnotation.value())) {
return jacksonAnnotation.value();
}
var schemaAnnotation = parameter.getAnnotation(Schema.class);
if (schemaAnnotation != null && StringUtils.hasText(schemaAnnotation.description())) {
return schemaAnnotation.description();

View File

@@ -26,7 +26,7 @@ import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.execution.ToolCallExceptionConverter;
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
import org.springframework.ai.tool.execution.ToolExecutionException;
import org.springframework.ai.tool.metadata.ToolMetadata;
import org.springframework.ai.tool.resolution.StaticToolCallbackResolver;
@@ -59,7 +59,7 @@ class DefaultToolCallingManagerTests {
assertThatThrownBy(() -> DefaultToolCallingManager.builder()
.observationRegistry(null)
.toolCallbackResolver(mock(ToolCallbackResolver.class))
.toolCallExceptionConverter(mock(ToolCallExceptionConverter.class))
.toolExecutionExceptionProcessor(mock(ToolExecutionExceptionProcessor.class))
.build()).isInstanceOf(IllegalArgumentException.class).hasMessage("observationRegistry cannot be null");
}
@@ -68,7 +68,7 @@ class DefaultToolCallingManagerTests {
assertThatThrownBy(() -> DefaultToolCallingManager.builder()
.observationRegistry(mock(ObservationRegistry.class))
.toolCallbackResolver(null)
.toolCallExceptionConverter(mock(ToolCallExceptionConverter.class))
.toolExecutionExceptionProcessor(mock(ToolExecutionExceptionProcessor.class))
.build()).isInstanceOf(IllegalArgumentException.class).hasMessage("toolCallbackResolver cannot be null");
}
@@ -77,7 +77,7 @@ class DefaultToolCallingManagerTests {
assertThatThrownBy(() -> DefaultToolCallingManager.builder()
.observationRegistry(mock(ObservationRegistry.class))
.toolCallbackResolver(mock(ToolCallbackResolver.class))
.toolCallExceptionConverter(null)
.toolExecutionExceptionProcessor(null)
.build()).isInstanceOf(IllegalArgumentException.class)
.hasMessage("toolCallExceptionConverter cannot be null");
}

View File

@@ -18,32 +18,32 @@ class DefaultToolCallResultConverterTests {
@Test
void convertWithNullReturnTypeShouldReturn() {
String result = converter.apply(null, null);
String result = converter.convert(null, null);
assertThat(result).isEqualTo("null");
}
@Test
void convertVoidReturnTypeShouldReturnDone() {
String result = converter.apply(null, void.class);
String result = converter.convert(null, void.class);
assertThat(result).isEqualTo("Done");
}
@Test
void convertStringReturnTypeShouldReturnJson() {
String result = converter.apply("test", String.class);
String result = converter.convert("test", String.class);
assertThat(result).isEqualTo("\"test\"");
}
@Test
void convertNullReturnValueShouldReturnNullJson() {
String result = converter.apply(null, String.class);
String result = converter.convert(null, String.class);
assertThat(result).isEqualTo("null");
}
@Test
void convertObjectReturnTypeShouldReturnJson() {
TestObject testObject = new TestObject("test", 42);
String result = converter.apply(testObject, TestObject.class);
String result = converter.convert(testObject, TestObject.class);
assertThat(result).containsIgnoringWhitespaces("""
"name": "test"
""").containsIgnoringWhitespaces("""
@@ -54,7 +54,7 @@ class DefaultToolCallResultConverterTests {
@Test
void convertCollectionReturnTypeShouldReturnJson() {
List<String> testList = List.of("one", "two", "three");
String result = converter.apply(testList, List.class);
String result = converter.convert(testList, List.class);
assertThat(result).isEqualTo("""
["one","two","three"]
""".trim());
@@ -63,7 +63,7 @@ class DefaultToolCallResultConverterTests {
@Test
void convertMapReturnTypeShouldReturnJson() {
Map<String, Integer> testMap = Map.of("one", 1, "two", 2);
String result = converter.apply(testMap, Map.class);
String result = converter.convert(testMap, Map.class);
assertThat(result).containsIgnoringWhitespaces("""
"one": 1
""").containsIgnoringWhitespaces("""

View File

@@ -24,34 +24,38 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link DefaultToolCallExceptionConverter}.
* Unit tests for {@link DefaultToolExecutionExceptionProcessor}.
*
* @author Thomas Vitale
*/
class DefaultToolCallExceptionConverterTests {
class DefaultToolExecutionExceptionProcessorTests {
@Test
void whenDefaultThenReturnMessage() {
ToolCallExceptionConverter converter = DefaultToolCallExceptionConverter.builder().build();
ToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder().build();
ToolExecutionException exception = new ToolExecutionException(generateTestDefinition(),
new RuntimeException("Test"));
assertThat(converter.convert(exception)).isEqualTo("Test");
assertThat(processor.process(exception)).isEqualTo("Test");
}
@Test
void whenNotAlwaysThrowThenReturnMessage() {
ToolCallExceptionConverter converter = DefaultToolCallExceptionConverter.builder().alwaysThrow(false).build();
ToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder()
.alwaysThrow(false)
.build();
ToolExecutionException exception = new ToolExecutionException(generateTestDefinition(),
new RuntimeException("Test"));
assertThat(converter.convert(exception)).isEqualTo("Test");
assertThat(processor.process(exception)).isEqualTo("Test");
}
@Test
void whenAlwaysThrowThenThrow() {
ToolCallExceptionConverter converter = DefaultToolCallExceptionConverter.builder().alwaysThrow(true).build();
ToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder()
.alwaysThrow(true)
.build();
ToolExecutionException exception = new ToolExecutionException(generateTestDefinition(),
new RuntimeException("Test"));
assertThatThrownBy(() -> converter.convert(exception)).isInstanceOf(ToolExecutionException.class);
assertThatThrownBy(() -> processor.process(exception)).isInstanceOf(ToolExecutionException.class);
}
private ToolDefinition generateTestDefinition() {

View File

@@ -182,7 +182,7 @@ class ToolUtilsTests {
public static class CustomToolCallResultConverter implements ToolCallResultConverter {
@Override
public String apply(Object result, Type returnType) {
public String convert(Object result, Type returnType) {
return returnType == null ? "null" : returnType.getTypeName();
}
@@ -195,7 +195,7 @@ class ToolUtilsTests {
}
@Override
public String apply(Object result, Type returnType) {
public String convert(Object result, Type returnType) {
return returnType == null ? "null" : returnType.getTypeName();
}

View File

@@ -16,7 +16,9 @@
package org.springframework.ai.util.json;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -168,7 +170,8 @@ class JsonSchemaGeneratorTests {
"type": "object",
"properties": {
"username": {
"type": "string"
"type": "string",
"description": "The username of the customer"
},
"password": {
"type": "string"
@@ -296,7 +299,8 @@ class JsonSchemaGeneratorTests {
"description": "Even more special name"
}
},
"required": [ "id", "name" ]
"required": [ "id", "name" ],
"description" : "Much more data"
}
},
"required": [ "items", "data", "moreData" ],
@@ -640,7 +644,9 @@ class JsonSchemaGeneratorTests {
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String password) {
}
public void jacksonMethod(@JsonProperty String username, @JsonProperty(required = true) String password) {
public void jacksonMethod(
@JsonProperty @JsonPropertyDescription("The username of the customer") String username,
@JsonProperty(required = true) String password) {
}
public void nullableMethod(@Nullable String username, String password) {
@@ -657,6 +663,7 @@ class JsonSchemaGeneratorTests {
record TestData(int id, @ToolParam(description = "The special name") String name) {
}
@JsonClassDescription("Much more data")
record MoreTestData(int id, @Schema(description = "Even more special name") String name) {
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

View File

@@ -96,7 +96,8 @@
* xref:api/structured-output-converter.adoc[Structured Output]
* xref:api/tools.adoc[Tool Calling]
* xref:api/functions.adoc[Function Calling (Deprecated)]
** xref:api/function-callback.adoc[FunctionCallback API]
** xref:api/function-callback.adoc[FunctionCallback API (Deprecated)]
** xref:api/tools-migration.adoc[Migrating to ToolCallback API]
* xref:api/multimodality.adoc[Multimodality]
* xref:api/etl-pipeline.adoc[]
* xref:api/testing.adoc[AI Model Evaluation]

View File

@@ -1,4 +1,6 @@
= Ollama Function Calling
= Ollama Function Calling (Deprecated)
WARNING: This page describes the previous version of the Function Calling API, which has been deprecated and marked for remove in the next release. The current version is available at xref:api/tools.adoc[Tool Calling]. See the xref:api/tools-migration.adoc[Migration Guide] for more information.
TIP: You need Ollama 0.2.8 or newer to use the functional calling capabilities and Ollama 0.4.6 or newer to use them in streaming mode.

View File

@@ -1,5 +1,7 @@
= FunctionCallback
WARNING: This page describes the previous version of the Function Calling API, which has been deprecated and marked for remove in the next release. The current version is available at xref:api/tools.adoc[Tool Calling]. See the xref:api/tools-migration.adoc[Migration Guide] for more information.
The `FunctionCallback` interface in Spring AI provides a standardized way to implement Large Language Model (LLM) function calling capabilities. It allows developers to register custom functions that can be called by AI models when specific conditions or intents are detected in the prompts.
The `FunctionCallback` interface defines several key methods:

View File

@@ -1,6 +1,8 @@
[[Function]]
= Function Calling API
WARNING: This page describes the previous version of the Function Calling API, which has been deprecated and marked for remove in the next release. The current version is available at xref:api/tools.adoc[Tool Calling]. See the xref:api/tools-migration.adoc[Migration Guide] for more information.
The integration of function support in AI models, permits the model to request the execution of client-side functions, thereby accessing necessary information or performing tasks dynamically as required.
Spring AI currently supports function invocation for the following AI Models:

View File

@@ -1,44 +1,50 @@
# Migrating from FunctionCallback to ToolCallback API
= Migrating from FunctionCallback to ToolCallback API
This guide helps you migrate from the deprecated FunctionCallback API to the new ToolCallback API in Spring AI.
This guide helps you migrate from the deprecated `FunctionCallback` API to the new `ToolCallback` API in Spring AI. For more information about the new APIs, check out the xref:api/tools.adoc[Tools Calling] documentation.
## Overview of Changes
== Overview of Changes
The Spring AI project is moving from "functions" to "tools" terminology to better align with industry standards. This involves several API changes while maintaining backward compatibility through deprecated methods.
These changes are part of a broader effort to improve and extend the tool calling capabilities in Spring AI. Among the other things, the new API moves from "functions" to "tools" terminology to better align with industry conventions. This involves several API changes while maintaining backward compatibility through deprecated methods.
## Key Changes
== Key Changes
1. `FunctionCallback` → `ToolCallback`
2. `FunctionCallback.builder().functions()` → `FunctionToolCallback.builder()`
2. `FunctionCallback.builder().function()` → `FunctionToolCallback.builder()`
3. `FunctionCallback.builder().method()` → `MethodToolCallback.builder()`
4. `FunctionCallingOptions` → `ToolCallingChatOptions`
5. Method names from `functions()` → `tools()`
5. `ChatClient.builder().defaultFunctions()` → `ChatClient.builder().defaultTools()`
6. `ChatClient.functions()` → `ChatClient.tools()`
7. `FunctionCallingOptions.builder().functions()` → `ToolCallingChatOptions.builder().toolNames()`
8. `FunctionCallingOptions.builder().functionCallbacks()` → `ToolCallingChatOptions.builder().toolCallbacks()`
## Migration Examples
== Migration Examples
### 1. Basic Function Callback
=== 1. Basic Function Callback
Before:
```java
[source,java]
----
FunctionCallback.builder()
.function("getCurrentWeather", new MockWeatherService())
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build()
```
----
After:
```java
[source,java]
----
FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build()
```
----
### 2. ChatClient Usage
=== 2. ChatClient Usage
Before:
```java
[source,java]
----
String response = ChatClient.create(chatModel)
.prompt()
.user("What's the weather like in San Francisco?")
@@ -49,10 +55,11 @@ String response = ChatClient.create(chatModel)
.build())
.call()
.content();
```
----
After:
```java
[source,java]
----
String response = ChatClient.create(chatModel)
.prompt()
.user("What's the weather like in San Francisco?")
@@ -62,23 +69,24 @@ String response = ChatClient.create(chatModel)
.build())
.call()
.content();
```
----
### 3. Method-Based Function Callbacks
=== 3. Method-Based Function Callbacks
Before:
```java
[source,java]
----
FunctionCallback.builder()
.method("getWeatherInLocation", String.class, Unit.class)
.description("Get the weather in location")
.targetClass(TestFunctionClass.class)
.build()
```
----
After:
```java
var toolMethod = ReflectionUtils.findMethod(
TestFunctionClass.class, "getWeatherInLocation", String.class, Unit.class);
[source,java]
----
var toolMethod = ReflectionUtils.findMethod(TestFunctionClass.class, "getWeatherInLocation");
MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder(toolMethod)
@@ -86,11 +94,25 @@ MethodToolCallback.builder()
.build())
.toolMethod(toolMethod)
.build()
```
----
Or with the declarative approach:
[source,java]
----
class WeatherTools {
@Tool(description = "Get the weather in location")
public void getWeatherInLocation(String location, Unit unit) {
// ...
}
}
----
And you can use the same `ChatClient#tools()` API to register method-based tool callbackes:
```java
[source,java]
----
String response = ChatClient.create(chatModel)
.prompt()
.user("What's the weather like in San Francisco?")
@@ -102,30 +124,45 @@ String response = ChatClient.create(chatModel)
.build())
.call()
.content();
```
----
### 4. Options Configuration
Or with the declarative approach:
[source,java]
----
String response = ChatClient.create(chatModel)
.prompt()
.user("What's the weather like in San Francisco?")
.tools(new WeatherTools())
.call()
.content();
----
=== 4. Options Configuration
Before:
```java
[source,java]
----
FunctionCallingOptions.builder()
.model(modelName)
.function("weatherFunction")
.build()
```
----
After:
```java
[source,java]
----
ToolCallingChatOptions.builder()
.model(modelName)
.tools("weatherFunction")
.toolNames("weatherFunction")
.build()
```
----
### 5. Default Functions in ChatClient Builder
=== 5. Default Functions in ChatClient Builder
Before:
```java
[source,java]
----
ChatClient.builder(chatModel)
.defaultFunctions(FunctionCallback.builder()
.function("getCurrentWeather", new MockWeatherService())
@@ -133,22 +170,24 @@ ChatClient.builder(chatModel)
.inputType(MockWeatherService.Request.class)
.build())
.build()
```
----
After:
```java
[source,java]
----
ChatClient.builder(chatModel)
.defaultTools(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build())
.build()
```
----
### 6. Spring Bean Configuration
=== 6. Spring Bean Configuration
Before:
```java
[source,java]
----
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallback.builder()
@@ -157,10 +196,11 @@ public FunctionCallback weatherFunctionInfo() {
.inputType(MockWeatherService.Request.class)
.build();
}
```
----
After:
```java
[source,java]
----
@Bean
public ToolCallback weatherFunctionInfo() {
return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService())
@@ -168,16 +208,17 @@ public ToolCallback weatherFunctionInfo() {
.inputType(MockWeatherService.Request.class)
.build();
}
```
----
## Breaking Changes
== Breaking Changes
1. The `method()` configuration in function callbacks has been replaced with a more explicit method tool configuration using `ToolDefinition` and `MethodToolCallback`.
2. When using method-based callbacks, you now need to explicitly find the method using `ReflectionUtils` and provide it to the builder.
2. When using method-based callbacks, you now need to explicitly find the method using `ReflectionUtils` and provide it to the builder. Alternatively, you can use the declarative approach with the `@Tool` annotation.
3. For non-static methods, you must now provide both the method and the target object:
```java
[source,java]
----
MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder(toolMethod)
.description("Description")
@@ -185,9 +226,9 @@ MethodToolCallback.builder()
.toolMethod(toolMethod)
.toolObject(targetObject)
.build()
```
----
## Deprecated Methods
== Deprecated Methods
The following methods are deprecated and will be removed in a future release:
@@ -197,38 +238,35 @@ The following methods are deprecated and will be removed in a future release:
Use their `tools` counterparts instead.
## @Tool tool definition path.
== Declarative Specification with @Tool
Now you can use the method-level annothation (`@Tool`) to register tools with Spring AI
Now you can use the method-level annotation (`@Tool`) to register tools with Spring AI:
```java
public class Home {
[source,java]
----
class Home {
@Tool(description = "Turn light On or Off in a room.")
public void turnLight(String roomName, boolean on) {
void turnLight(String roomName, boolean on) {
// ...
logger.info("Turn light in room: {} to: {}", roomName, on);
}
}
Home homeAutomation = new HomeAutomation();
String response = ChatClient.create(this.chatModel).prompt()
.user("Turn the light in the living room On.")
.tools(homeAutomation)
.tools(new Home())
.call()
.content();
----
```
## Additional Notes
== Additional Notes
1. The new API provides better separation between tool definition and implementation.
2. Tool definitions can be reused across different implementations.
3. The builder pattern has been simplified for common use cases.
4. Better support for method-based tools with improved error handling.
## Timeline
== Timeline
The deprecated methods will be maintained for backward compatibility in the current major version but will be removed in the next major release. It's recommended to migrate to the new API as soon as possible.
The deprecated methods will be maintained for backward compatibility in the current milestone version but will be removed in the next milestone release. It's recommended to migrate to the new API as soon as possible.

View File

@@ -20,8 +20,8 @@ import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.tool.execution.DefaultToolCallExceptionConverter;
import org.springframework.ai.tool.execution.ToolCallExceptionConverter;
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
import org.springframework.ai.tool.resolution.StaticToolCallbackResolver;
@@ -59,19 +59,19 @@ public class ToolCallingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ToolCallExceptionConverter toolCallExceptionConverter() {
return new DefaultToolCallExceptionConverter(false);
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() {
return new DefaultToolExecutionExceptionProcessor(false);
}
@Bean
@ConditionalOnMissingBean
ToolCallingManager toolCallingManager(ToolCallbackResolver toolCallbackResolver,
ToolCallExceptionConverter toolCallExceptionConverter,
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor,
ObjectProvider<ObservationRegistry> observationRegistry) {
return ToolCallingManager.builder()
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.toolCallbackResolver(toolCallbackResolver)
.toolCallExceptionConverter(toolCallExceptionConverter)
.toolExecutionExceptionProcessor(toolExecutionExceptionProcessor)
.build();
}

View File

@@ -19,8 +19,8 @@ package org.springframework.ai.autoconfigure.chat.model;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.tool.DefaultToolCallingManager;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.tool.execution.DefaultToolCallExceptionConverter;
import org.springframework.ai.tool.execution.ToolCallExceptionConverter;
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -42,8 +42,8 @@ class ToolCallingAutoConfigurationTests {
var toolCallbackResolver = context.getBean(ToolCallbackResolver.class);
assertThat(toolCallbackResolver).isInstanceOf(DelegatingToolCallbackResolver.class);
var toolCallExceptionConverter = context.getBean(ToolCallExceptionConverter.class);
assertThat(toolCallExceptionConverter).isInstanceOf(DefaultToolCallExceptionConverter.class);
var toolExecutionExceptionProcessor = context.getBean(ToolExecutionExceptionProcessor.class);
assertThat(toolExecutionExceptionProcessor).isInstanceOf(DefaultToolExecutionExceptionProcessor.class);
var toolCallingManager = context.getBean(ToolCallingManager.class);
assertThat(toolCallingManager).isInstanceOf(DefaultToolCallingManager.class);