Remove deprecated FunctionCallback references

- Remove the deprecated FunctionCallback related classes, references
 - Replace them with the corresponding ToolCallback usage

Signed-off-by: Ilayaperumal Gopinathan <ilayaperumal.gopinathan@broadcom.com>
This commit is contained in:
Ilayaperumal Gopinathan
2025-04-28 10:13:06 +01:00
committed by Mark Pollack
parent 22c778f8cc
commit f0702c343b
55 changed files with 102 additions and 3216 deletions

View File

@@ -44,7 +44,7 @@ public class BedrockConverseProxyChatProperties {
}
public void setOptions(ToolCallingChatOptions options) {
Assert.notNull(options, "FunctionCallingOptions must not be null");
Assert.notNull(options, "ToolCallingChatOptions must not be null");
this.options = options;
}

View File

@@ -32,7 +32,6 @@ import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.boot.autoconfigure.AutoConfigurations;

View File

@@ -33,7 +33,6 @@ import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;

View File

@@ -32,7 +32,6 @@ import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.tool.function.FunctionToolCallback;

View File

@@ -59,7 +59,7 @@ class FunctionCallbackResolverKotlinIT : BaseOllamaIT() {
.withUserConfiguration(Config::class.java)
@Test
fun functionCallTest() {
fun toolCallTest() {
this.contextRunner.run {context ->
val chatModel = context.getBean(OllamaChatModel::class.java)

View File

@@ -33,7 +33,7 @@ import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Description
class FunctionCallbackKotlinIT : BaseOllamaIT() {
class ToolCallbackKotlinIT : BaseOllamaIT() {
companion object {
@@ -46,7 +46,7 @@ class FunctionCallbackKotlinIT : BaseOllamaIT() {
}
}
private val logger = LoggerFactory.getLogger(FunctionCallbackKotlinIT::class.java)
private val logger = LoggerFactory.getLogger(ToolCallbackKotlinIT::class.java)
private val contextRunner = ApplicationContextRunner()
.withPropertyValues(
@@ -59,7 +59,7 @@ class FunctionCallbackKotlinIT : BaseOllamaIT() {
.withUserConfiguration(Config::class.java)
@Test
fun functionCallTest() {
fun toolCallTest() {
this.contextRunner.run {context ->
val chatModel = context.getBean(OllamaChatModel::class.java)

View File

@@ -113,16 +113,4 @@ public class VertexAiGeminiChatAutoConfiguration {
return chatModel;
}
/**
* Because of the OPEN_API_SCHEMA type, the FunctionCallbackResolver instance must
* different from the other JSON schema types.
*/
// private FunctionCallbackResolver springAiFunctionManager(ApplicationContext
// context) {
// DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
// manager.setSchemaType(SchemaType.OPEN_API_SCHEMA);
// manager.setApplicationContext(context);
// return manager;
// }
}

View File

@@ -27,7 +27,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiChatAutoConfiguration;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
@@ -109,7 +109,7 @@ class FunctionCallWithFunctionBeanIT {
""");
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
FunctionCallingOptions.builder().function("weatherFunction").build()));
ToolCallingChatOptions.builder().toolNames("weatherFunction").build()));
logger.info("Response: {}", response);

View File

@@ -30,7 +30,6 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.tool.function.FunctionToolCallback;

View File

@@ -31,7 +31,6 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;

View File

@@ -30,7 +30,6 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.tool.ToolCallback;

View File

@@ -75,24 +75,26 @@ class ToolCallingAutoConfigurationTests {
assertThat(toolCallbackResolver).isInstanceOf(DelegatingToolCallbackResolver.class);
assertThat(toolCallbackResolver.resolve("getForecast")).isNotNull();
assertThat(toolCallbackResolver.resolve("getForecast").getName()).isEqualTo("getForecast");
assertThat(toolCallbackResolver.resolve("getForecast").getToolDefinition().name())
.isEqualTo("getForecast");
assertThat(toolCallbackResolver.resolve("getAlert")).isNotNull();
assertThat(toolCallbackResolver.resolve("getAlert").getName()).isEqualTo("getAlert");
assertThat(toolCallbackResolver.resolve("getAlert").getToolDefinition().name()).isEqualTo("getAlert");
assertThat(toolCallbackResolver.resolve("weatherFunction1")).isNotNull();
assertThat(toolCallbackResolver.resolve("weatherFunction1").getName()).isEqualTo("weatherFunction1");
assertThat(toolCallbackResolver.resolve("weatherFunction1").getToolDefinition().name())
.isEqualTo("weatherFunction1");
assertThat(toolCallbackResolver.resolve("getCurrentWeather3")).isNotNull();
assertThat(toolCallbackResolver.resolve("getCurrentWeather3").getName())
assertThat(toolCallbackResolver.resolve("getCurrentWeather3").getToolDefinition().name())
.isEqualTo("getCurrentWeather3");
assertThat(toolCallbackResolver.resolve("getCurrentWeather4")).isNotNull();
assertThat(toolCallbackResolver.resolve("getCurrentWeather4").getName())
assertThat(toolCallbackResolver.resolve("getCurrentWeather4").getToolDefinition().name())
.isEqualTo("getCurrentWeather4");
assertThat(toolCallbackResolver.resolve("getCurrentWeather5")).isNotNull();
assertThat(toolCallbackResolver.resolve("getCurrentWeather5").getName())
assertThat(toolCallbackResolver.resolve("getCurrentWeather5").getToolDefinition().name())
.isEqualTo("getCurrentWeather5");
});
}

View File

@@ -223,10 +223,10 @@ public class VertexAiGeminiPaymentTransactionMethodIT {
List<ToolCallbackProvider> tcps, List<ToolCallback> toolCallbacks,
ObjectProvider<ObservationRegistry> observationRegistry) {
List<ToolCallback> allFunctionCallbacks = new ArrayList(toolCallbacks);
tcps.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionCallbacks::addAll);
List<ToolCallback> allToolCallbacks = new ArrayList(toolCallbacks);
tcps.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allToolCallbacks::addAll);
var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionCallbacks);
var staticToolCallbackResolver = new StaticToolCallbackResolver(allToolCallbacks);
var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
.applicationContext(applicationContext)

View File

@@ -20,9 +20,7 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.ai.zhipuai.api.MockWeatherService;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi;

View File

@@ -48,7 +48,6 @@ import org.springframework.ai.content.Media;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import org.springframework.ai.zhipuai.ZhiPuAiTestConfiguration;

View File

@@ -248,7 +248,7 @@ public class ChatClientTest {
var fco = (ToolCallingChatOptions) prompt.getOptions();
assertThat(fco.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2");
assertThat(fco.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
assertThat(fco.getToolCallbacks().iterator().next().getToolDefinition().name()).isEqualTo("fun3");
// Streaming
content = join(chatClient.prompt().stream().content());
@@ -270,7 +270,7 @@ public class ChatClientTest {
fco = (ToolCallingChatOptions) prompt.getOptions();
assertThat(fco.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2");
assertThat(fco.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
assertThat(fco.getToolCallbacks().iterator().next().getToolDefinition().name()).isEqualTo("fun3");
// mutate builder
// @formatter:off
@@ -300,7 +300,7 @@ public class ChatClientTest {
fco = (ToolCallingChatOptions) prompt.getOptions();
assertThat(fco.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2", "fun4");
assertThat(fco.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
assertThat(fco.getToolCallbacks().iterator().next().getToolDefinition().name()).isEqualTo("fun3");
// Streaming
content = join(chatClient.prompt().stream().content());
@@ -322,7 +322,7 @@ public class ChatClientTest {
fco = (ToolCallingChatOptions) prompt.getOptions();
assertThat(fco.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2", "fun4");
assertThat(fco.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
assertThat(fco.getToolCallbacks().iterator().next().getToolDefinition().name()).isEqualTo("fun3");
}
@@ -385,7 +385,7 @@ public class ChatClientTest {
var tco = (ToolCallingChatOptions) prompt.getOptions();
assertThat(tco.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2", "fun5");
assertThat(tco.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
assertThat(tco.getToolCallbacks().iterator().next().getToolDefinition().name()).isEqualTo("fun3");
// Streaming
// @formatter:off
@@ -416,7 +416,7 @@ public class ChatClientTest {
var tcoptions = (ToolCallingChatOptions) prompt.getOptions();
assertThat(tcoptions.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2", "fun5");
assertThat(tcoptions.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
assertThat(tcoptions.getToolCallbacks().iterator().next().getToolDefinition().name()).isEqualTo("fun3");
}
@Test

View File

@@ -1491,8 +1491,6 @@ class DefaultChatClientTests {
assertThat(defaultSpec.getToolCallbacks()).contains(toolCallback);
}
// FunctionCallback.builder().description("description").function(null,input->"hello").inputType(String.class).build()
@Test
void whenFunctionNameIsNullThenThrow() {
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
@@ -1544,7 +1542,8 @@ class DefaultChatClientTests {
.description("description")
.build());
DefaultChatClient.DefaultChatClientRequestSpec defaultSpec = (DefaultChatClient.DefaultChatClientRequestSpec) spec;
assertThat(defaultSpec.getToolCallbacks()).anyMatch(callback -> callback.getName().equals("name"));
assertThat(defaultSpec.getToolCallbacks())
.anyMatch(callback -> callback.getToolDefinition().name().equals("name"));
}
@Test
@@ -1556,7 +1555,8 @@ class DefaultChatClientTests {
.description("description")
.build());
DefaultChatClient.DefaultChatClientRequestSpec defaultSpec = (DefaultChatClient.DefaultChatClientRequestSpec) spec;
assertThat(defaultSpec.getToolCallbacks()).anyMatch(callback -> callback.getName().equals("name"));
assertThat(defaultSpec.getToolCallbacks())
.anyMatch(callback -> callback.getToolDefinition().name().equals("name"));
}
@Test
@@ -1610,7 +1610,8 @@ class DefaultChatClientTests {
.inputType(String.class)
.build());
DefaultChatClient.DefaultChatClientRequestSpec defaultSpec = (DefaultChatClient.DefaultChatClientRequestSpec) spec;
assertThat(defaultSpec.getToolCallbacks()).anyMatch(callback -> callback.getName().equals("name"));
assertThat(defaultSpec.getToolCallbacks())
.anyMatch(callback -> callback.getToolDefinition().name().equals("name"));
}
@Test
@@ -1632,7 +1633,7 @@ class DefaultChatClientTests {
}
@Test
void whenFunctionCallbacksElementIsNullThenThrow() {
void whenFunctionToolCallbacksElementIsNullThenThrow() {
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
assertThatThrownBy(() -> spec.tools(mock(FunctionToolCallback.class), null))
@@ -1641,7 +1642,7 @@ class DefaultChatClientTests {
}
@Test
void whenFunctionCallbacksThenReturn() {
void whenFunctionToolCallbacksThenReturn() {
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
FunctionToolCallback functionToolCallback = mock(FunctionToolCallback.class);

View File

@@ -93,7 +93,7 @@ class AdvisedRequestTests {
}
@Test
void whenFunctionCallbacksIsNullThenThrows() {
void whenToolCallbacksIsNullThenThrows() {
assertThatThrownBy(() -> new AdvisedRequest(mock(ChatModel.class), "user", null, null, List.of(), List.of(),
null, List.of(), Map.of(), Map.of(), List.of(), Map.of(), Map.of(), Map.of()))
.isInstanceOf(IllegalArgumentException.class)

View File

@@ -33,7 +33,6 @@
**** xref:api/chat/functions/openai-chat-functions.adoc[OpenAI Function Calling (Deprecated)]
*** xref:api/chat/qianfan-chat.adoc[QianFan]
*** xref:api/chat/zhipuai-chat.adoc[ZhiPu AI]
// **** xref:api/chat/functions/zhipuai-chat-functions.adoc[Function Calling]
*** xref:api/chat/watsonx-ai-chat.adoc[watsonx.AI]
** xref:api/embeddings.adoc[Embedding Models]
*** xref:api/bedrock.adoc[Amazon Bedrock]
@@ -107,7 +106,6 @@
** xref:api/prompt.adoc[]
** xref:api/chat/prompt-engineering-patterns.adoc[]
* xref:api/testing.adoc[AI Model Evaluation]
* xref:api/functions.adoc[Function Calling (Deprecated)]
* Service Connections

View File

@@ -20,7 +20,7 @@ Your function can in turn invoke other 3rd party services to provide the results
Spring AI makes this as easy as defining a `@Bean` definition that returns a `java.util.Function` and supplying the bean name as an option when invoking the `ChatModel`.
Under the hood, Spring wraps your POJO (the function) with the appropriate adapter code that enables interaction with the AI Model, saving you from writing tedious boilerplate code.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/tool/ToolCallback.java[ToolCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
== How it works
@@ -72,7 +72,7 @@ We start with describing the most POJO friendly options.
In this approach you define `@Beans` in your application context as you would any other Spring managed object.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `ToolCallback` that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
@@ -117,9 +117,9 @@ It is a best practice to annotate the request object with information such that
The link:https://github.com/spring-projects/spring-ai/blob/main/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java[FunctionCallWithFunctionBeanIT.java] demonstrates this approach.
==== FunctionCallback
==== ToolCallback
Another way to register a function is to create a `FunctionCallback` instance like this:
Another way to register a function is to create a `ToolCallback` instance like this:
[source,java]
----

View File

@@ -19,7 +19,7 @@ Your function can in turn invoke other 3rd party services to provide the results
Spring AI makes this as easy as defining a `@Bean` definition that returns a `java.util.Function` and supplying the bean name as an option when invoking the `ChatModel`.
Under the hood, Spring wraps your POJO (the function) with the appropriate adapter code that enables interaction with the AI Model, saving you from writing tedious boilerplate code.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/tool/ToolCallback.java[ToolCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
== How it works
@@ -70,7 +70,7 @@ We start with describing the most POJO friendly options.
In this approach you define `@Beans` in your application context as you would any other Spring managed object.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` instance that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `ToolCallback` instance that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
@@ -113,9 +113,9 @@ It is a best practice to annotate the request object with information such that
The link:https://github.com/spring-projects/spring-ai/blob/main/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java[FunctionCallWithFunctionBeanIT.java] demonstrates this approach.
==== FunctionCallback Wrapper
==== ToolCallback Wrapper
Another way to register a function is to create a `FunctionCallback` instance like this:
Another way to register a function is to create a `ToolCallback` instance like this:
[source,java]
----

View File

@@ -14,7 +14,7 @@ As a developer, you need to implement a function that takes the function call ar
Spring AI makes this as easy as defining a `@Bean` definition that returns a `java.util.Function` and supplying the bean name as an option when invoking the `ChatModel`.
Under the hood, Spring wraps your POJO (the function) with the appropriate adapter code that enables interaction with the AI Model, saving you from writing tedious boilerplate code.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/tool/ToolCallback.java[ToolCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
// Additionally, the Auto-Configuration provides a way to auto-register any Function<I, O> beans definition as function calling candidates in the `ChatModel`.
@@ -71,7 +71,7 @@ We start with describing the most POJO friendly options.
In this approach you define `@Beans` in your application context as you would any other Spring managed object.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` instance that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `ToolCallback` instance that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
@@ -115,9 +115,9 @@ It is a best practice to annotate the request object with information such that
The link:https://github.com/spring-projects/spring-ai/blob/main/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java[FunctionCallbackWithPlainFunctionBeanIT.java] demonstrates this approach.
==== FunctionCallback Wrapper
==== ToolCallback Wrapper
Another way register a function is to create `FunctionCallback` instance like this:
Another way register a function is to create `ToolCallback` instance like this:
[source,java]
----
@@ -125,9 +125,9 @@ Another way register a function is to create `FunctionCallback` instance like th
static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
public FunctionToolCallback weatherFunctionInfo() {
return FunctionCallback.builder()
return FunctionToolCallback.builder()
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.description("Get the weather in location") // (2) function description
.inputType(MockWeatherService.Request.class) // (3) function signature
@@ -142,7 +142,7 @@ It also provides a description (2) and the function signature (3) to let the mod
NOTE: By default, the response converter does a JSON serialization of the Response object.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
NOTE: The `FunctionToolCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Specifying functions in Chat Options
@@ -185,8 +185,7 @@ MiniMaxChatModel chatModel = ...
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
var promptOptions = MiniMaxChatOptions.builder()
.functionCallbacks(List.of(FunctionCallback.builder()
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.description("Get the weather in location") // (2) function description
.inputType(MockWeatherService.Request.class) // (3) function signature
.build())) // function code

View File

@@ -18,7 +18,7 @@ Your function can in turn invoke other 3rd party services to provide the results
Spring AI makes this as easy as defining a `@Bean` definition that returns a `java.util.Function` and supplying the bean name as an option when invoking the `ChatModel`.
Under the hood, Spring wraps your POJO (the function) with the appropriate adapter code that enables interaction with the AI Model, saving you from writing tedious boilerplate code.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/tool/ToolCallback.java[ToolCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
== How it works
@@ -70,7 +70,7 @@ We start by describing the most POJO-friendly options.
In this approach, you define a `@Bean` in your application context as you would any other Spring managed object.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `ToolCallback` that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
[source,java]
@@ -115,7 +115,7 @@ The link:https://github.com/spring-projects/spring-ai/blob/main/auto-configurati
TIP: The Mistral AI link:https://github.com/spring-projects/spring-ai/blob/main/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java[PaymentStatusBeanOpenAiIT] implements the same function using the OpenAI API.
Mistral AI is almost identical to OpenAI in this regard.
==== FunctionCallback Wrapper
==== ToolCallback Wrapper
Another way to register a function is to create a `FunctionToolCallback` like this:
@@ -141,7 +141,7 @@ It also provides a description (2) and the function signature (3) to let the mod
NOTE: By default, the response converter performs a JSON serialization of the Response object.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
NOTE: The `FunctionToolCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Specifying functions in Chat Options

View File

@@ -23,7 +23,7 @@ Your function can in turn invoke other 3rd party services to provide the results
Spring AI makes this as easy as defining a `@Bean` definition that returns a `java.util.Function` and supplying the bean name as an option when invoking the `ChatModel`.
Under the hood, Spring wraps your POJO (the function) with the appropriate adapter code that enables interaction with the AI Model, saving you from writing tedious boilerplate code.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/tool/ToolCallback.java[ToolCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
== How it works
@@ -78,7 +78,7 @@ We start by describing the most POJO-friendly options.
In this approach, you define a `@Bean` in your application context as you would any other Spring managed object.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `ToolCallback` that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
[source,java]
@@ -117,9 +117,9 @@ public record Request(String location, Unit unit) {}
It is a best practice to annotate the request object with information such that the generated JSON schema of that function is as descriptive as possible to help the AI model pick the correct function to invoke.
==== FunctionCallback
==== ToolCallback
Another way to register a function is to create a `FunctionCallback` like this:
Another way to register a function is to create a `ToolCallback` like this:
[source,java]
----

View File

@@ -16,7 +16,7 @@ As a developer, you need to implement a function that takes the function call ar
Spring AI makes this as easy as defining a `@Bean` definition that returns a `java.util.Function` and supplying the bean name as an option when invoking the `ChatModel`.
Under the hood, Spring wraps your POJO (the function) with the appropriate adapter code that enables interaction with the AI Model, saving you from writing tedious boilerplate code.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/tool/ToolCallback.java[ToolCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
// Additionally, the Auto-Configuration provides a way to auto-register any Function<I, O> beans definition as function calling candidates in the `ChatModel`.
@@ -71,7 +71,7 @@ We start by describing the most POJO-friendly options.
In this approach, you define a `@Bean` in your application context as you would any other Spring managed object.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `ToolCallback` that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
[source,java]
@@ -112,9 +112,9 @@ It is a best practice to annotate the request object with information such that
The link:https://github.com/spring-projects/spring-ai/blob/main/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java[FunctionCallbackWithPlainFunctionBeanIT.java] demonstrates this approach.
==== FunctionCallback Wrapper
==== FunctionToolCallback Wrapper
Another way to register a function is to create a `FunctionCallback` like this:
Another way to register a function is to create a `toolCallback` like this:
[source,java]
----

View File

@@ -14,7 +14,7 @@ As a developer, you need to implement a function that takes the function call ar
Spring AI makes this as easy as defining a `@Bean` definition that returns a `java.util.Function` and supplying the bean name as an option when invoking the `ChatModel`.
Under the hood, Spring wraps your POJO (the function) with the appropriate adapter code that enables interaction with the AI Model, saving you from writing tedious boilerplate code.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/tool/ToolCallback.java[ToolCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
// Additionally, the Auto-Configuration provides a way to auto-register any Function<I, O> beans definition as function calling candidates in the `ChatModel`.
@@ -71,7 +71,7 @@ We start with describing the most POJO friendly options.
In this approach you define `@Beans` in your application context as you would any other Spring managed object.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` instance that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `ToolCallback` instance that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
@@ -115,9 +115,9 @@ It is a best practice to annotate the request object with information such that
The link:https://github.com/spring-projects/spring-ai/blob/main/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java[FunctionCallbackWithPlainFunctionBeanIT.java] demonstrates this approach.
==== FunctionCallback Wrapper
==== FunctionToolCallback Wrapper
Another way register a function is to create `FunctionCallback` instance like this:
Another way register a function is to create `ToolCallback` instance like this:
[source,java]
----
@@ -125,10 +125,9 @@ Another way register a function is to create `FunctionCallback` instance like th
static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
public FunctionToolCallback weatherFunctionInfo() {
return FunctionCallback.builder()
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
return FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.description("Get the weather in location") // (2) function description
.inputType(MockWeatherService.Request.class) // (3) function signature
.build();
@@ -142,7 +141,7 @@ It also provides a description (2) and the input type (3) used to generate the J
NOTE: By default, the response converter does a JSON serialization of the Response object.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
NOTE: The `FunctionToolCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Specifying functions in Chat Options
@@ -185,8 +184,7 @@ ZhiPuAiChatModel chatModel = ...
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
var promptOptions = ZhiPuAiChatOptions.builder()
.functionCallbacks(List.of(FunctionCallback.builder()
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.description("Get the weather in location") // (2) function description
.inputType(MockWeatherService.Request.class) // (3) function signature
.build())) // function code

View File

@@ -1,258 +0,0 @@
= 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:
* `getName()`: Returns the unique function name within the AI model context
* `getDescription()`: Provides a description that helps the model decide when to invoke the function
* `getInputTypeSchema()`: Defines the JSON schema for the function's input parameters
* `call(String functionArguments)`: Handles the actual function execution
* `call(String functionArguments, ToolContext toolContext)`: Extended version that supports additional xref:api/functions.adoc#Tool-Context[tool context]
== Builder Pattern
Spring AI provides a fluent builder API for creating `FunctionCallback` implementations.
This is particularly useful for defining function callbacks that you can register, pragmatically, on the fly, with your `ChatClient` or `ChatModel` model calls.
Use the `FunctionCallback.builder()` method to create a new builder instance and chain the configuration methods to set the function name, description, input type, and other properties.
The `FunctionCallback.Builder` is a hierarchical with the following structure:
- FunctionCallback.Builder - The root builder interface used for configuring the xref:_common_configurations[shared properties].
- FunctionInvokingSpec - The xref:Function-Invoking[function invoking] builder interface.
- MethodInvokingSpec - The xref:Method-Invoking[method invoking] builder interface.
[[Function-Invoking]]
== Function-Invoking Approach
Converts any `java.util.function.Function`, `BiFunction`, `Supplier` or `Consumer` into a `FunctionCallback` that can be called by the AI model.
NOTE: You can use lambda expressions or method references to define the function logic but you must provide the input type of the function using the `inputType(TYPE)`.
=== Function<I, O>
[source,java]
----
FunctionCallback callback = FunctionCallback.builder()
.function("processOrder", (Order order) -> processOrderLogic(order))
.description("Process a new order")
.inputType(Order.class)
.build();
----
=== BiFunction<I, ToolContext, O>
Using Function with input type <I> and additional xref:api/functions.adoc#Tool-Context[ToolContext] parameter:
[source,java]
----
FunctionCallback callback = FunctionCallback.builder()
.function("processOrder", (Order order, ToolContext context) ->
processOrderWithContext(order, context))
.description("Process a new order with context")
.inputType(Order.class)
.build();
----
=== Supplier<O>
Use `java.util.Supplier<O>` or `java.util.function.Function<Void, O>` to define functions that don't take any input:
[source,java]
----
FunctionCallback.builder()
.function("turnsLight", () -> state.put("Light", "ON"))
.description("Turns light on in the living room")
.inputType(Void.class)
.build();
----
=== Consumer<I>
Use `java.util.Consumer<I>` or `java.util.function.Function<I, Void>` to define functions that don't produce output:
[source,java]
----
record LightInfo(String roomName, boolean isOn) {}
FunctionCallback.builder()
.function("turnsLight", (LightInfo lightInfo) -> {
logger.info("Turning light to [" + lightInfo.isOn + "] in " + lightInfo.roomName());
})
.description("Turns light on/off in a selected room")
.inputType(LightInfo.class)
.build();
----
=== Generics Input Type
Use the `ParameterizedTypeReference` to define functions with generic input types:
[source,java]
----
record TrainSearchRequest<T>(T data) {}
record TrainSearchSchedule(String from, String to, String date) {}
record TrainSearchScheduleResponse(String from, String to, String date, String trainNumber) {}
FunctionCallback.builder()
.function("trainSchedule", (TrainSearchRequest<TrainSearchSchedule> request) -> {
logger.info("Schedule: " + request.data().from() + " to " + request.data().to());
return new TrainSearchScheduleResponse(request.data().from(), request. data().to(), "", "123");
})
.description("Schedule a train reservation")
.inputType(new ParameterizedTypeReference<TrainSearchRequest<TrainSearchSchedule>>() {})
.build();
----
[[Method-Invoking]]
== Method Invoking Approach
Enables method invocation through reflection while automatically handling JSON schema generation and parameter conversion. Its particularly useful for integrating Java methods as callable functions within AI model interactions.
The method invoking implements the `FunctionCallback` interface and provides:
- Automatic JSON schema generation for method parameters
- Support for both static and instance methods
- Any number of parameters (including none) and return values (including void)
- Any parameter/return types (primitives, objects, collections)
- Special handling for xref:api/functions.adoc#Tool-Context[ToolContext] parameters
=== Static Method Invocation
You can refer to a static method in a class by providing the method name, parameter types, and the target class.
[source,java]
----
public class WeatherService {
public static String getWeather(String city, TemperatureUnit unit) {
return "Temperature in " + city + ": 20" + unit;
}
}
FunctionCallback callback = FunctionCallback.builder()
.method("getWeather", String.class, TemperatureUnit.class)
.description("Get weather information for a city")
.targetClass(WeatherService.class)
.build();
----
=== Object instance Method Invocation
You can refer to an instance method in a class by providing the method name, parameter types, and the target object instance.
[source,java]
----
public class DeviceController {
public void setDeviceState(String deviceId, boolean state, ToolContext context) {
Map<String, Object> contextData = context.getContext();
// Implementation using context data
}
}
DeviceController controller = new DeviceController();
String response = ChatClient.create(chatModel).prompt()
.user("Turn on the living room lights")
.functions(FunctionCallback.builder()
.method("setDeviceState", String.class,boolean.class,ToolContext.class)
.description("Control device state")
.targetObject(controller)
.build())
.toolContext(Map.of("location", "home"))
.call()
.content();
----
TIP: Optionally, using the `.name()`, you can set a custom function name different from the method name.
== Common Configurations
There are several common configurations that you can use to customize the function callbacks.
=== Schema Type
The framework supports different schema types for generating the schemas for the input parameters:
* JSON Schema (default)
* OpenAPI Schema (for Vertex AI compatibility)
[source,java]
----
FunctionCallback.builder()
.schemaType(SchemaType.OPEN_API_SCHEMA)
// ... other configuration
.build();
----
=== Custom Response Handling
You can provide a custom response converter to format the function response before sending it back to the AI model.
Most AI Models expect an text response, so it is your responsibility to convert the function response to a text format.
By default, the response is converted to a String.
TIP: Many models cope well with JSON responses, so you can return a JSON string.
[source,java]
----
FunctionCallback.builder()
.responseConverter(response ->
customResponseFormatter.format(response))
// ... other configuration
.build();
----
=== Custom Object Mapping
Spring AI uses ObjectMapper for JSON serialization and deserialization.
You can provide a custom ObjectMapper to handle custom object mapping:
[source,java]
----
FunctionCallback.builder()
.objectMapper(customObjectMapper)
// ... other configuration
.build();
----
== Best Practices
=== Descriptive Names and Descriptions
* Provide unique function names
* Write comprehensive descriptions to help the model understand when to invoke the function
=== Input Type & Schema
* For the function invoking approach, define input types explicitly and use `ParameterizedTypeReference` for generic types.
* Consider using custom schema when auto-generated ones don't meet requirements.
=== Error Handling
* Implement proper error handling in function implementations and return the error message in the response
* You can use the ToolContext to provide additional error context when needed
=== Tool Context Usage
* Use xref:api/functions.adoc#Tool-Context[ToolContext] when additional state or context is required that is provided from the User and not part of the function input generated by the AI model.
* Use `BiFunction<I, ToolContext, O>` to access the ToolContext in the function invocation approach and add `ToolContext` parameter in the method invoking approach.
=== Notes on Schema Generation
* The framework automatically generates JSON schemas from Java types
* For function invoking, the schema is generated based on the input type for the function that needs to be set using `inputType(TYPE)`. Use `ParameterizedTypeReference` for generic types.
* Generated schemas respect Jackson annotations on model classes
* You can bypass the automatic generation by providing custom schemas using `inputTypeSchema()`
=== Common Pitfalls to Avoid
==== Lack of Description
* Always provide explicit descriptions instead of relying on auto-generated ones
* Clear descriptions improve model's function selection accuracy
==== Schema Mismatches
* Ensure input types match the Function's input parameter types.
* Use `ParameterizedTypeReference` for generic types.

View File

@@ -1,470 +0,0 @@
[[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 tool/function calling for the following AI Models:
* Anthropic Claude: Refer to the xref:api/chat/functions/anthropic-chat-functions.adoc[Anthropic Claude tool/function calling].
* Azure OpenAI: Refer to the xref:api/chat/functions/azure-open-ai-chat-functions.adoc[Azure OpenAI tool/function calling].
* Amazon Bedrock Converse: Refer to the xref:api/chat/bedrock-converse.adoc#_tool_calling[Amazon Bedrock Converse tool/function calling].
* Google VertexAI Gemini: Refer to the xref:api/chat/vertexai-gemini-chat.adoc#_tool_calling[Vertex AI Gemini tool/function calling].
* Groq: Refer to the xref:api/chat/groq-chat.adoc#_function_calling[Groq tool/function calling].
* Mistral AI: Refer to the xref:api/chat/functions/mistralai-chat-functions.adoc[Mistral AI tool/function calling].
// * MiniMax : Refer to the xref:api/chat/functions/minimax-chat-functions.adoc[MiniMax function invocation docs].
* Ollama: Refer to the xref:api/chat/functions/ollama-chat-functions.adoc[Ollama tool/function calling]
* OpenAI: Refer to the xref:api/chat/functions/openai-chat-functions.adoc[OpenAI tool/function calling].
// * ZhiPu AI : Refer to the xref:api/chat/functions/zhipuai-chat-functions.adoc[ZhiPu AI function invocation docs].
image::function-calling-basic-flow.jpg[Function calling, width=700, align="center"]
You can register custom Java functions with the `ChatClient` and have the AI model intelligently choose to output a JSON object containing arguments to call one or many of the registered functions.
This allows you to connect the LLM capabilities with external tools and APIs.
The AI models are trained to detect when a function should be called and to respond with JSON that adheres to the function signature.
The API does not call the function directly; instead, the model generates JSON that you can use to call the function in your code and return the result back to the model to complete the conversation.
Spring AI provides flexible and user-friendly ways to register and call custom functions.
In general, the custom functions need to provide a function `name`, `description`, and the function call `signature` (as JSON schema) to let the model know what arguments the function expects. The `description` helps the model to understand when to call the function.
As a developer, you need to implement a function that takes the function call arguments sent from the AI model, and responds with the result back to the model. Your function can in turn invoke other 3rd party services to provide the results.
Spring AI makes this as easy as defining a `@Bean` definition that returns a `java.util.Function` and supplying the bean name as an option when invoking the `ChatClient` or registering the function dynamically in your prompt request.
Under the hood, Spring wraps your POJO (the function) with the appropriate adapter code that enables interaction with the AI Model, saving you from writing tedious boilerplate code.
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/tool/function/FunctionToolCallback.java[FunctionToolCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
== How it works
Suppose we want the AI model to respond with information that it does not have, for example, the current temperature at a given location.
We can provide the AI model with metadata about our own functions that it can use to retrieve that information as it processes your prompt.
For example, if during the processing of a prompt, the AI Model determines that it needs additional information about the temperature in a given location, it will start a server-side generated request/response interaction.
Instead of returning the final response message, the AI Model returns at special Tool Call request, providing the function name and arguments (as JSON).
It is the responsibility of the client to process this message and execute the named function and return the response
as Tool Response message back to the AI Model.
Spring AI greatly simplifies the code you need to write to support function invocation.
It brokers the function invocation conversation for you.
You can simply provide your function definition as a `@Bean` and then provide the bean name of the function in your prompt options or pass the function directly as a parameter in your prompt request options.
You can also reference multiple function bean names in your prompt.
=== Example Use Case
Lets define a simple use case that we can use as an example to explain how function invocation works.
Let's create a chatbot that answer questions by calling our own function.
To support the response of the chatbot, we will register our own function that takes a location and returns the current weather in that location.
When the model needs to answer a question such as `"Whats the weather like in Boston?"` the AI model will invoke the client providing the location value as an argument to be passed to the function. This RPC-like data is passed as JSON.
Our function calls some SaaS-based weather service API and returns the weather response back to the model to complete the conversation. In this example, we will use a simple implementation named `MockWeatherService` that hard-codes the temperature for various locations.
The following `MockWeatherService` class represents the weather service API:
--
[tabs]
======
Java::
+
[source,java]
----
public class MockWeatherService implements Function<Request, Response> {
public enum Unit { C, F }
public record Request(String location, Unit unit) {}
public record Response(double temp, Unit unit) {}
public Response apply(Request request) {
return new Response(30.0, Unit.C);
}
}
----
Kotlin::
+
[source,kotlin]
----
class MockWeatherService : Function1<Request, Response> {
override fun invoke(request: Request) = Response(30.0, Unit.C)
}
enum class Unit { C, F }
data class Request(val location: String, val unit: Unit) {}
data class Response(val temp: Double, val unit: Unit) {}
----
======
--
== Server-Side Registration
=== Functions as Beans
Spring AI provides multiple ways to register custom functions as beans in the Spring context.
We start by describing the most POJO-friendly options.
==== Plain Functions
In this approach, you define a `@Bean` in your application context as you would any other Spring managed object.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionToolCallback` that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is used function name.
--
[tabs]
======
Java::
+
[source,java]
----
@Configuration
static class Config {
@Bean
@Description("Get the weather in location") // function description
public Function<MockWeatherService.Request, MockWeatherService.Response> currentWeather() {
return new MockWeatherService();
}
}
----
Kotlin::
+
[source,kotlin]
----
@Configuration
class Config {
@Bean
@Description("Get the weather in location") // function description
fun currentWeather(): (Request) -> Response = MockWeatherService()
}
----
======
--
The `@Description` annotation is optional and provides a function description that helps the model understand when to call the function. It is an important property to set to help the AI model determine what client side function to invoke.
Another option for providing the description of the function is to use the `@JsonClassDescription` annotation on the `MockWeatherService.Request`:
--
[tabs]
======
Java::
+
[source,java]
----
@Configuration
static class Config {
@Bean
public Function<Request, Response> currentWeather() { // bean name as function name
return new MockWeatherService();
}
}
@JsonClassDescription("Get the weather in location") // function description
public record Request(String location, Unit unit) {}
----
Kotlin::
+
[source,kotlin]
----
@Configuration
class Config {
@Bean
fun currentWeather(): (Request) -> Response { // bean name as function name
return MockWeatherService()
}
}
@JsonClassDescription("Get the weather in location") // function description
data class Request(val location: String, val unit: Unit)
----
======
--
It is a best practice to annotate the request object with information such that the generated JSON schema of that function is as descriptive as possible to help the AI model pick the correct function to invoke.
==== FunctionToolCallback
Another way to register a function is to create a `FunctionToolCallback` like this:
--
[tabs]
======
Java::
+
[source,java]
----
@Configuration
static class Config {
@Bean
public FunctionToolCallback weatherFunctionInfo() {
return FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.description("Get the weather in location") // (2) function description
.inputType(MockWeatherService.Request.class) // (3) input type to build the JSON schema
.build();
}
}
----
Kotlin::
+
[source,kotlin]
----
import org.springframework.ai.model.function.withInputType
@Configuration
class Config {
@Bean
fun weatherFunctionInfo(): FunctionToolCallback {
return FunctionToolCallback.builder("CurrentWeather", MockWeatherService()) // (1) function name and instance
.description("Get the weather in location") // (2) function description
// (3) Required due to Kotlin SAM conversion being an opaque lambda
.inputType<MockWeatherService.Request>()
.build();
}
}
----
======
--
It wraps the 3rd party `MockWeatherService` function and registers it as a `CurrentWeather` function with the `ChatClient`.
It also provides a description (2) and an optional response converter to convert the response into a text as expected by the model.
NOTE: By default, the response converter performs a JSON serialization of the Response object.
NOTE: The `FunctionToolCallback.Builder` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Enable functions by bean name
To let the model know and call your `CurrentWeather` function you need to enable it in your prompt requests:
[source,java]
----
ChatClient chatClient = ...
ChatResponse response = this.chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
.tools("CurrentWeather") // Enable the function
.call().
chatResponse();
logger.info("Response: {}", response);
----
The above user question will trigger 3 calls to the `CurrentWeather` function (one for each city) and the final response will be something like this:
----
Here is the current weather for the requested cities:
- San Francisco, CA: 30.0°C
- Tokyo, Japan: 10.0°C
- Paris, France: 15.0°C
----
The link:https://github.com/spring-projects/spring-ai/blob/main/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java[FunctionCallbackWithPlainFunctionBeanIT.java] test demo this approach.
== Client-Side Registration
In addition to the auto-configuration, you can register callback functions, dynamically.
You can use either the function invoking or method invoking approaches to register functions with your `ChatClient` or `ChatModel` requests.
The client-side registration enables you to register functions by default.
=== Function Invoking
[source,java]
----
ChatClient chatClient = ...
ChatResponse response = this.chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
.tools(FunctionToolCallback.builder("currentWeather", (Request request) -> new Response(30.0, Unit.C)) // (1) function name and instance
.description("Get the weather in location") // (2) function description
.inputType(MockWeatherService.Request.class) // (3) input type to build the JSON schema
.build())
.call()
.chatResponse();
----
NOTE: The on the fly functions are enabled by default for the duration of this request.
This approach allows to choose dynamically different functions to be called based on the user input.
The https://github.com/spring-projects/spring-ai/blob/main/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java[FunctionCallbackInPromptIT.java] integration test provides a complete example of how to register a function with the `ChatClient` and use it in a prompt request.
=== Method Invoking
The `MethodInvokingFunctionCallback` enables method invocation through reflection while automatically handling JSON schema generation and parameter conversion.
It's particularly useful for integrating Java methods as callable functions within AI model interactions.
The `MethodInvokingFunctionCallback` implements the `FunctionCallback` interface and provides:
- Automatic JSON schema generation for method parameters
- Support for both static and instance methods
- Any number of parameters (including none) and return values (including void)
- Any parameter/return types (primitives, objects, collections)
- Special handling for `ToolContext` parameters
You need the `MethodToolCallback.Builder` to create `MethodInvokingFunctionCallback` like this:
[source,java]
----
// Create using builder pattern
FunctionCallback methodInvokingCallback = FunctionCallback.builder()
.method("MethodName", Class<?>...argumentTypes) // The method to invoke and its argument types
.description("Function calling description") // Hints the AI to know when to call this method
.targetObject(targetObject) // Required instance methods for static methods use targetClass
.build();
----
Here are a few usage examples:
[tabs]
======
Static Method Invocation::
+
[source,java]
----
public class WeatherService {
public static String getWeather(String city, TemperatureUnit unit) {
return "Temperature in " + city + ": 20" + unit;
}
}
// Usage
var toolMethod = ReflectionUtils.findMethod(WeatherService.class, "getWeather", String.class, TemperatureUnit.class);
MethodToolCallback callback = MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder(toolMethod)
.description("Get the weather in location")
.build())
.toolMethod(toolMethod)
.toolObject(targetObject)
.build();
----
Instance Method with ToolContext::
+
[source,java]
----
public class DeviceController {
public void setDeviceState(String deviceId, boolean state, ToolContext context) {
Map<String, Object> contextData = context.getContext();
// Implementation using context data
}
}
// Usage
DeviceController controller = new DeviceController();
var toolMethod = ReflectionUtils.findMethod(
DeviceController.class, "setDeviceState", String.class, Boolean.class, ToolContext.class);
String response = ChatClient.create(chatModel).prompt()
.user("Turn on the living room lights")
.tools(MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder(toolMethod)
.description("Control device state")
.build())
.toolMethod(toolMethod)
.toolObject(controller)
.build())
.toolContext(Map.of("location", "home"))
.call()
.content();
----
======
The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMethodInvokingFunctionCallbackIT.java[OpenAiChatClientMethodInvokingFunctionCallbackIT]
integration test provides additional examples of how to use the FunctionCallback.Builder to create method invocation FunctionCallbacks.
[[Tool-Context]]
== Tool Context
Spring AI now supports passing additional contextual information to function callbacks through a tool context.
This feature allows you to provide extra, user provided, data that can be used within the function execution along with the function arguments passed by the AI model.
image::function-calling-tool-context.jpg[Function calling with Tool Context, width=700, align="center"]
The https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/chat/model/ToolContext.java[ToolContext] class provides a way to pass additional context information.
=== Using Tool Context
In case of function-invoking, the context information that is passed in as the second argument of a `java.util.BiFunction`.
For method-invoking, the context information is passed as a method argument of type `ToolContext`.
==== Function Invoking
You can set the tool context when building your chat options and use a BiFunction for your callback:
[source,java]
----
BiFunction<MockWeatherService.Request, ToolContext, MockWeatherService.Response> weatherFunction =
(request, toolContext) -> {
String sessionId = (String) toolContext.getContext().get("sessionId");
String userId = (String) toolContext.getContext().get("userId");
// Use sessionId and userId in your function logic
double temperature = 0;
if (request.location().contains("Paris")) {
temperature = 15;
}
else if (request.location().contains("Tokyo")) {
temperature = 10;
}
else if (request.location().contains("San Francisco")) {
temperature = 30;
}
return new MockWeatherService.Response(temperature, 15, 20, 2, 53, 45, MockWeatherService.Unit.C);
};
ChatResponse response = chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
.tools(FunctionToolCallback.builder("getCurrentWeather", this.weatherFunction)
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build())
.toolContext(Map.of("sessionId", "1234", "userId", "5678"))
.call()
.chatResponse();
----
In this example, the `weatherFunction` is defined as a BiFunction that takes both the request and the tool context as parameters. This allows you to access the context directly within the function logic.
This approach allows you to pass session-specific or user-specific information to your functions, enabling more contextual and personalized responses.
==== Method Invoking
[source,java]
----
public class DeviceController {
public void setDeviceState(String deviceId, boolean state, ToolContext context) {
Map<String, Object> contextData = context.getContext();
// Implementation using context data
}
}
// Usage
DeviceController controller = new DeviceController();
var toolMethod = ReflectionUtils.findMethod(
DeviceController.class, "setDeviceState", String.class, Boolean.class, ToolContext.class);
String response = ChatClient.create(chatModel).prompt()
.user("Turn on the living room lights")
.tools(MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder(toolMethod)
.description("Control device state")
.build())
.toolMethod(toolMethod)
.toolObject(controller)
.build())
.toolContext(Map.of("location", "home"))
.call()
.content();
----

View File

@@ -16,7 +16,6 @@
package org.springframework.ai.aot;
import java.lang.reflect.Method;
import java.util.Set;
import org.springframework.ai.chat.messages.AbstractMessage;
@@ -26,15 +25,13 @@ import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.ToolResponseMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.core.io.ClassPathResource;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
public class SpringAiCoreRuntimeHints implements RuntimeHintsRegistrar {
@@ -42,18 +39,16 @@ public class SpringAiCoreRuntimeHints implements RuntimeHintsRegistrar {
public void registerHints(@NonNull RuntimeHints hints, @Nullable ClassLoader classLoader) {
var chatTypes = Set.of(AbstractMessage.class, AssistantMessage.class, ToolResponseMessage.class, Message.class,
MessageType.class, UserMessage.class, SystemMessage.class, DefaultFunctionCallbackResolver.class,
FunctionCallback.class);
MessageType.class, UserMessage.class, SystemMessage.class);
for (var c : chatTypes) {
hints.reflection().registerType(c);
}
Method getDescription = ReflectionUtils.findMethod(FunctionCallback.class, "getDescription");
hints.reflection().registerMethod(getDescription, ExecutableMode.INVOKE);
Method getInputTypeSchema = ReflectionUtils.findMethod(FunctionCallback.class, "getInputTypeSchema");
hints.reflection().registerMethod(getInputTypeSchema, ExecutableMode.INVOKE);
Method getName = ReflectionUtils.findMethod(FunctionCallback.class, "getName");
hints.reflection().registerMethod(getName, ExecutableMode.INVOKE);
// Register tool-related types for reflection
var toolTypes = Set.of(ToolCallback.class, ToolDefinition.class);
for (var c : toolTypes) {
hints.reflection().registerType(c);
}
for (var r : Set.of("embedding/embedding-model-dimensions.properties")) {
hints.resources().registerResource(new ClassPathResource(r));

View File

@@ -1,297 +0,0 @@
/*
* 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.chat.model;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.ToolResponseMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Abstract base class for tool call support. Provides functionality for handling function
* callbacks and executing functions.
*
* @author Christian Tzolov
* @author Grogdunn
* @author Thomas Vitale
* @author Jihoon Kim
* @since 1.0.0
* @deprecated Use {@link ToolCallingManager} instead.
*/
@Deprecated
public abstract class AbstractToolCallSupport {
protected static final boolean IS_RUNTIME_CALL = true;
/**
* The function callback register is used to resolve the function callbacks by name.
*/
protected final Map<String, FunctionCallback> functionCallbackRegister = new ConcurrentHashMap<>();
/**
* The function callback resolver is used to resolve the function callbacks by name
* from the Spring context. It is optional and usually used with Spring
* auto-configuration.
*/
protected final FunctionCallbackResolver functionCallbackResolver;
@Deprecated
protected AbstractToolCallSupport(FunctionCallbackResolver functionCallbackResolver) {
this(functionCallbackResolver, FunctionCallingOptions.builder().build(), List.of());
}
@Deprecated
protected AbstractToolCallSupport(FunctionCallbackResolver functionCallbackResolver,
FunctionCallingOptions functionCallingOptions, List<FunctionCallback> toolFunctionCallbacks) {
this.functionCallbackResolver = functionCallbackResolver;
List<FunctionCallback> defaultFunctionCallbacks = merge(functionCallingOptions, toolFunctionCallbacks);
if (!CollectionUtils.isEmpty(defaultFunctionCallbacks)) {
this.functionCallbackRegister.putAll(defaultFunctionCallbacks.stream()
.collect(ConcurrentHashMap::new, (m, v) -> m.put(v.getName(), v), ConcurrentHashMap::putAll));
}
}
private static List<FunctionCallback> merge(FunctionCallingOptions functionOptions,
List<FunctionCallback> toolFunctionCallbacks) {
List<FunctionCallback> toolFunctionCallbacksCopy = new ArrayList<>();
if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) {
toolFunctionCallbacksCopy.addAll(toolFunctionCallbacks);
}
if (!CollectionUtils.isEmpty(functionOptions.getFunctionCallbacks())) {
toolFunctionCallbacksCopy.addAll(functionOptions.getFunctionCallbacks());
// Make sure that that function callbacks are registered directly to the
// functionCallbackRegister and not passed in the default options.
functionOptions.setFunctionCallbacks(List.of());
}
return toolFunctionCallbacksCopy;
}
@Deprecated
public Map<String, FunctionCallback> getFunctionCallbackRegister() {
return this.functionCallbackRegister;
}
/**
* Handle the runtime function callback configurations. Register the function
* callbacks
* @param runtimeFunctionOptions FunctionCallingOptions to handle.
* @return Set of function names to call.
*/
@Deprecated
protected Set<String> runtimeFunctionCallbackConfigurations(FunctionCallingOptions runtimeFunctionOptions) {
Set<String> enabledFunctionsToCall = new HashSet<>();
if (runtimeFunctionOptions != null) {
// Add the explicitly enabled functions.
if (!CollectionUtils.isEmpty(runtimeFunctionOptions.getFunctions())) {
enabledFunctionsToCall.addAll(runtimeFunctionOptions.getFunctions());
}
// Add the function callbacks to the register and automatically enable them.
if (!CollectionUtils.isEmpty(runtimeFunctionOptions.getFunctionCallbacks())) {
runtimeFunctionOptions.getFunctionCallbacks().stream().forEach(functionCallback -> {
// Register the tool callback.
this.functionCallbackRegister.put(functionCallback.getName(), functionCallback);
// Automatically enable the function, usually from prompt callback.
enabledFunctionsToCall.add(functionCallback.getName());
});
}
}
return enabledFunctionsToCall;
}
@Deprecated
protected List<Message> handleToolCalls(Prompt prompt, ChatResponse response) {
Optional<Generation> toolCallGeneration = response.getResults()
.stream()
.filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
.findFirst();
if (toolCallGeneration.isEmpty()) {
throw new IllegalStateException("No tool call generation found in the response!");
}
AssistantMessage assistantMessage = toolCallGeneration.get().getOutput();
Map<String, Object> toolContextMap = Map.of();
if (prompt.getOptions() instanceof FunctionCallingOptions functionCallOptions
&& !CollectionUtils.isEmpty(functionCallOptions.getToolContext())) {
toolContextMap = new HashMap<>(functionCallOptions.getToolContext());
List<Message> toolCallHistory = new ArrayList<>(prompt.copy().getInstructions());
toolCallHistory.add(new AssistantMessage(assistantMessage.getText(), assistantMessage.getMetadata(),
assistantMessage.getToolCalls()));
toolContextMap.put(ToolContext.TOOL_CALL_HISTORY, toolCallHistory);
}
ToolResponseMessage toolMessageResponse = this.executeFunctions(assistantMessage,
new ToolContext(toolContextMap));
List<Message> toolConversationHistory = this.buildToolCallConversation(prompt.getInstructions(),
assistantMessage, toolMessageResponse);
return toolConversationHistory;
}
@Deprecated
protected List<Message> buildToolCallConversation(List<Message> previousMessages, AssistantMessage assistantMessage,
ToolResponseMessage toolResponseMessage) {
List<Message> messages = new ArrayList<>(previousMessages);
messages.add(assistantMessage);
messages.add(toolResponseMessage);
return messages;
}
/**
* Resolve the function callbacks by name. Retrieve them from the registry or try to
* resolve them from the Application Context.
* @param functionNames Name of function callbacks to retrieve.
* @return list of resolved FunctionCallbacks.
*/
@Deprecated
protected List<FunctionCallback> resolveFunctionCallbacks(Set<String> functionNames) {
List<FunctionCallback> retrievedFunctionCallbacks = new ArrayList<>();
for (String functionName : functionNames) {
if (!this.functionCallbackRegister.containsKey(functionName)) {
if (this.functionCallbackResolver != null) {
FunctionCallback functionCallback = this.functionCallbackResolver.resolve(functionName);
if (functionCallback != null) {
this.functionCallbackRegister.put(functionName, functionCallback);
}
else {
throw new IllegalStateException(
"No function callback [" + functionName + "] found in tht FunctionCallbackRegister");
}
}
else {
throw new IllegalStateException("No function callback found for name: " + functionName);
}
}
FunctionCallback functionCallback = this.functionCallbackRegister.get(functionName);
retrievedFunctionCallbacks.add(functionCallback);
}
return retrievedFunctionCallbacks;
}
@Deprecated
protected ToolResponseMessage executeFunctions(AssistantMessage assistantMessage, ToolContext toolContext) {
List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
var functionName = toolCall.name();
String functionArguments = toolCall.arguments();
if (!this.functionCallbackRegister.containsKey(functionName)) {
throw new IllegalStateException("No function callback found for function name: " + functionName);
}
String functionResponse = this.functionCallbackRegister.get(functionName)
.call(functionArguments, toolContext);
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), functionName, functionResponse));
}
return new ToolResponseMessage(toolResponses, Map.of());
}
@Deprecated
protected boolean isToolCall(ChatResponse chatResponse, Set<String> toolCallFinishReasons) {
Assert.isTrue(!CollectionUtils.isEmpty(toolCallFinishReasons), "Tool call finish reasons cannot be empty!");
if (chatResponse == null) {
return false;
}
var generations = chatResponse.getResults();
if (CollectionUtils.isEmpty(generations)) {
return false;
}
return generations.stream().anyMatch(g -> isToolCall(g, toolCallFinishReasons));
}
/**
* Check if the generation is a tool call. The tool call finish reasons are used to
* determine if the generation is a tool call.
* @param generation the generation to check.
* @param toolCallFinishReasons the tool call finish reasons to check.
* @return true if the generation is a tool call, false otherwise.
*/
@Deprecated
protected boolean isToolCall(Generation generation, Set<String> toolCallFinishReasons) {
var finishReason = (generation.getMetadata().getFinishReason() != null)
? generation.getMetadata().getFinishReason() : "";
return generation.getOutput().hasToolCalls() && toolCallFinishReasons.stream()
.map(s -> s.toLowerCase())
.toList()
.contains(finishReason.toLowerCase());
}
/**
* Check if the proxyToolCalls is enabled for the given prompt or the default tool
* call options. The prompt options take precedence over the default options. When the
* proxyToolCalls is enabled the ChatModel implementation will not handle the function
* calling internally. The tool call and tool response messages are exposed outside
* the ChatModel implementation.
* @param prompt the prompt to check.
* @param defaultOptions the default tool call options to check.
* @return true if the proxyToolCalls is enabled, false otherwise.
*/
@Deprecated
protected boolean isProxyToolCalls(Prompt prompt, FunctionCallingOptions defaultOptions) {
if (prompt.getOptions() instanceof FunctionCallingOptions functionCallOptions
&& functionCallOptions.getProxyToolCalls() != null) {
return functionCallOptions.getProxyToolCalls();
}
else if (defaultOptions.getProxyToolCalls() != null) {
return defaultOptions.getProxyToolCalls();
}
return false;
}
}

View File

@@ -33,7 +33,7 @@ import org.springframework.ai.chat.messages.Message;
*
* <p>
* The context is typically populated from the {@code toolContext} field of
* {@code FunctionCallingOptions} and is used in the function execution process.
* {@code ToolCallingChatOptions} and is used in the function execution process.
* </p>
*
* <p>

View File

@@ -1,153 +0,0 @@
/*
* 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.model.function;
import java.lang.reflect.Type;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.util.Assert;
/**
* Abstract implementation of the {@link FunctionCallback} for interacting with the
* Model's function calling protocol and a {@link Function} wrapping the interaction with
* the 3rd party service/function.
*
* Implement the {@code O apply(I request) } method to implement the interaction with the
* 3rd party service/function.
*
* The {@link #responseConverter} function is responsible to convert the 3rd party
* function's output type into a string expected by the LLM model.
*
* @param <I> the 3rd party service input type.
* @param <O> the 3rd party service output type.
* @author Christian Tzolov
* @deprecated in favor of {@link FunctionToolCallback}.
*/
@Deprecated
abstract class AbstractFunctionCallback<I, O> implements BiFunction<I, ToolContext, O>, FunctionCallback {
private final String name;
private final String description;
private final Type inputType;
private final String inputTypeSchema;
private final ObjectMapper objectMapper;
private final Function<O, String> responseConverter;
/**
* Constructs a new {@link AbstractFunctionCallback} with the given name, description,
* input type and default object mapper.
* @param name Function name. Should be unique within the ChatModel's function
* registry.
* @param description Function description. Used as a "system prompt" by the model to
* decide if the function should be called.
* @param inputTypeSchema Used to compute, the argument's Schema (such as JSON Schema
* or OpenAPI Schema)required by the Model's function calling protocol.
* @param inputType Used to compute, the argument's JSON schema required by the
* Model's function calling protocol.
* @param responseConverter Used to convert the function's output type to a string.
* @param objectMapper Used to convert the function's input and output types to and
* from JSON.
*/
protected AbstractFunctionCallback(String name, String description, String inputTypeSchema, Type inputType,
Function<O, String> responseConverter, ObjectMapper objectMapper) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(description, "Description must not be null");
Assert.notNull(inputType, "InputType must not be null");
Assert.notNull(inputTypeSchema, "InputTypeSchema must not be null");
Assert.notNull(responseConverter, "ResponseConverter must not be null");
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.name = name;
this.description = description;
this.inputType = inputType;
this.inputTypeSchema = inputTypeSchema;
this.responseConverter = responseConverter;
this.objectMapper = objectMapper;
}
@Override
public String getName() {
return this.name;
}
@Override
public String getDescription() {
return this.description;
}
@Override
public String getInputTypeSchema() {
return this.inputTypeSchema;
}
@Override
public String call(String functionInput, ToolContext toolContext) {
I request = fromJson(functionInput, this.inputType);
O response = this.apply(request, toolContext);
return this.responseConverter.apply(response);
}
@Override
public String call(String functionArguments) {
// Convert the tool calls JSON arguments into a Java function request object.
I request = fromJson(functionArguments, this.inputType);
// extend conversation with function response.
return this.andThen(this.responseConverter).apply(request, null);
}
private <T> T fromJson(String json, Type targetType) {
try {
return this.objectMapper.readValue(json, this.objectMapper.constructType(targetType));
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@Override
public int hashCode() {
return Objects.hash(this.name, this.description, this.inputType);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
AbstractFunctionCallback other = (AbstractFunctionCallback) obj;
return Objects.equals(this.name, other.name) && Objects.equals(this.description, other.description)
&& Objects.equals(this.inputType, other.inputType);
}
}

View File

@@ -1,138 +0,0 @@
/*
* Copyright 2024-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.model.function;
import java.util.function.Function;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.springframework.ai.model.function.FunctionCallback.CommonCallbackInvokingSpec;
import org.springframework.ai.model.function.FunctionCallback.SchemaType;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.ai.tool.method.MethodToolCallback;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.util.Assert;
/**
* @deprecated Use specific builder for the type of tool you need, e.g.
* {@link FunctionToolCallback.Builder} and {@link MethodToolCallback.Builder}.
*/
@Deprecated
public class DefaultCommonCallbackInvokingSpec<B extends CommonCallbackInvokingSpec<B>>
implements CommonCallbackInvokingSpec<B> {
/**
* The description of the function callback. Used to hint the LLM model about the
* tool's purpose and when to use it.
*/
protected String description;
/**
* The schema type to use for the input type schema generation. The default is JSON
* Schema. Note: Vertex AI requires the input type schema to be in Open API schema
*/
protected SchemaType schemaType = SchemaType.JSON_SCHEMA;
/**
* The function to convert the response object to a string. The default is to convert
* the response to a JSON string.
*/
protected Function<Object, String> responseConverter = response -> (response instanceof String) ? "" + response
: this.toJsonString(response);
/**
* (Optional) Instead of generating the input type schema from the input type or
* method argument types, you can provide the schema directly. This will override the
* generated schema.
*/
protected String inputTypeSchema;
protected ObjectMapper objectMapper = JsonMapper.builder()
.addModules(JacksonUtils.instantiateAvailableModules())
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.build();
private String toJsonString(Object object) {
try {
return this.objectMapper.writeValueAsString(object);
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@Override
public B description(String description) {
Assert.hasText(description, "Description must not be empty");
this.description = description;
return (B) this;
}
@Override
public B schemaType(SchemaType schemaType) {
Assert.notNull(schemaType, "SchemaType must not be null");
this.schemaType = schemaType;
return (B) this;
}
@Override
public B responseConverter(Function<Object, String> responseConverter) {
Assert.notNull(responseConverter, "ResponseConverter must not be null");
this.responseConverter = responseConverter;
return (B) this;
}
@Override
public B inputTypeSchema(String inputTypeSchema) {
Assert.hasText(inputTypeSchema, "InputTypeSchema must not be empty");
this.inputTypeSchema = inputTypeSchema;
return (B) this;
}
@Override
public B objectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper;
return (B) this;
}
public String getDescription() {
return this.description;
}
public SchemaType getSchemaType() {
return this.schemaType;
}
public Function<Object, String> getResponseConverter() {
return this.responseConverter;
}
public String getInputTypeSchema() {
return this.inputTypeSchema;
}
public ObjectMapper getObjectMapper() {
return this.objectMapper;
}
}

View File

@@ -1,227 +0,0 @@
/*
* 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.model.function;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.model.function.FunctionCallback.FunctionInvokingSpec;
import org.springframework.ai.model.function.FunctionCallback.MethodInvokingSpec;
import org.springframework.ai.model.function.FunctionCallback.SchemaType;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.ai.tool.method.MethodToolCallback;
import org.springframework.ai.util.ParsingUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* Default implementation of the {@link FunctionCallback.Builder}.
*
* @author Christian Tzolov
* @since 1.0.0
* @deprecated Use specific builder for the type of tool you need, e.g.
* {@link FunctionToolCallback.Builder} and {@link MethodToolCallback.Builder}.
*/
@Deprecated
public class DefaultFunctionCallbackBuilder implements FunctionCallback.Builder {
private static final Logger logger = LoggerFactory.getLogger(DefaultFunctionCallbackBuilder.class);
@Override
public <I, O> FunctionInvokingSpec<I, O> function(String name, Function<I, O> function) {
return new DefaultFunctionInvokingSpec<>(name, function);
}
@Override
public <I, O> FunctionInvokingSpec<I, O> function(String name, BiFunction<I, ToolContext, O> biFunction) {
return new DefaultFunctionInvokingSpec<>(name, biFunction);
}
@Override
public <O> FunctionInvokingSpec<Void, O> function(String name, Supplier<O> supplier) {
Function<Void, O> function = input -> supplier.get();
return new DefaultFunctionInvokingSpec<>(name, function).inputType(Void.class);
}
public <I> FunctionInvokingSpec<I, Void> function(String name, Consumer<I> consumer) {
Function<I, Void> function = (I input) -> {
consumer.accept(input);
return null;
};
return new DefaultFunctionInvokingSpec<>(name, function);
}
@Override
public MethodInvokingSpec method(String methodName, Class<?>... argumentTypes) {
return new DefaultMethodInvokingSpec(methodName, argumentTypes);
}
private String generateDescription(String fromName) {
String generatedDescription = ParsingUtils.reConcatenateCamelCase(fromName, " ");
logger.info("Description is not set! A best effort attempt to generate a description:'{}' from the:'{}'",
generatedDescription, fromName);
logger.info("It is recommended to set the Description explicitly! Use the 'description()' method!");
return generatedDescription;
}
final class DefaultFunctionInvokingSpec<I, O> extends DefaultCommonCallbackInvokingSpec<FunctionInvokingSpec<I, O>>
implements FunctionInvokingSpec<I, O> {
private final String name;
private Type inputType;
private final BiFunction<I, ToolContext, O> biFunction;
private final Function<I, O> function;
private DefaultFunctionInvokingSpec(String name, BiFunction<I, ToolContext, O> biFunction) {
Assert.hasText(name, "Name must not be empty");
Assert.notNull(biFunction, "BiFunction must not be null");
this.name = name;
this.biFunction = biFunction;
this.function = null;
}
private DefaultFunctionInvokingSpec(String name, Function<I, O> function) {
Assert.hasText(name, "Name must not be empty");
Assert.notNull(function, "Function must not be null");
this.name = name;
this.biFunction = null;
this.function = function;
}
@Override
public FunctionInvokingSpec<I, O> inputType(Class<?> inputType) {
Assert.notNull(inputType, "InputType must not be null");
this.inputType = inputType;
return this;
}
@Override
public FunctionInvokingSpec<I, O> inputType(ParameterizedTypeReference<?> inputType) {
Assert.notNull(inputType, "InputType must not be null");
this.inputType = inputType.getType();
return this;
}
@Override
public FunctionCallback build() {
Assert.notNull(this.getObjectMapper(), "ObjectMapper must not be null");
Assert.hasText(this.name, "Name must not be empty");
Assert.notNull(this.getResponseConverter(), "ResponseConverter must not be null");
Assert.notNull(this.inputType, "InputType must not be null");
if (this.getInputTypeSchema() == null) {
boolean upperCaseTypeValues = this.schemaType == SchemaType.OPEN_API_SCHEMA;
this.inputTypeSchema = ModelOptionsUtils.getJsonSchema(this.inputType, upperCaseTypeValues);
}
BiFunction<I, ToolContext, O> finalBiFunction = (this.biFunction != null) ? this.biFunction
: (request, context) -> this.function.apply(request);
return new FunctionInvokingFunctionCallback<>(this.name, this.getDescriptionExt(),
this.getInputTypeSchema(), this.inputType, (Function<O, String>) this.getResponseConverter(),
this.getObjectMapper(), finalBiFunction);
}
private String getDescriptionExt() {
if (StringUtils.hasText(this.getDescription())) {
return this.getDescription();
}
return generateDescription(this.name);
}
}
final class DefaultMethodInvokingSpec extends DefaultCommonCallbackInvokingSpec<MethodInvokingSpec>
implements MethodInvokingSpec {
private String name;
private final String methodName;
private Class<?> targetClass;
private Object targetObject;
private final Class<?>[] argumentTypes;
private DefaultMethodInvokingSpec(String methodName, Class<?>... argumentTypes) {
Assert.hasText(methodName, "Method name must not be null");
Assert.notNull(argumentTypes, "Argument types must not be null");
this.methodName = methodName;
this.argumentTypes = argumentTypes;
}
public MethodInvokingSpec name(String name) {
Assert.hasText(name, "Name must not be empty");
this.name = name;
return this;
}
public MethodInvokingSpec targetClass(Class<?> targetClass) {
Assert.notNull(targetClass, "Target class must not be null");
this.targetClass = targetClass;
return this;
}
@Override
public MethodInvokingSpec targetObject(Object methodObject) {
Assert.notNull(methodObject, "Method object must not be null");
this.targetObject = methodObject;
this.targetClass = methodObject.getClass();
return this;
}
@Override
public FunctionCallback build() {
Assert.isTrue(this.targetClass != null || this.targetObject != null,
"Target class or object must not be null");
var method = ReflectionUtils.findMethod(this.targetClass, this.methodName, this.argumentTypes);
Assert.notNull(method, "Method: '" + this.methodName + "' with arguments:"
+ Arrays.toString(this.argumentTypes) + " not found!");
return new MethodInvokingFunctionCallback(this.targetObject, method, this.getDescriptionExt(),
this.getObjectMapper(), this.name, this.getResponseConverter());
}
private String getDescriptionExt() {
if (StringUtils.hasText(this.getDescription())) {
return this.getDescription();
}
return generateDescription(StringUtils.hasText(this.name) ? this.name : this.methodName);
}
}
}

View File

@@ -1,215 +0,0 @@
/*
* Copyright 2023-2024 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.model.function;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.functions.Function2;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.model.function.FunctionCallback.SchemaType;
import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
import org.springframework.ai.tool.resolution.TypeResolverHelper;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Description;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.KotlinDetector;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
/**
* A Spring {@link ApplicationContextAware} implementation that provides a way to retrieve
* a {@link Function} from the Spring context and wrap it into a {@link FunctionCallback}.
* <p>
* The name of the function is determined by the bean name.
* <p>
* The description of the function is determined by the following rules:
* <ul>
* <li>Provided as a default description</li>
* <li>Provided as a {@code @Description} annotation on the bean</li>
* <li>Provided as a {@code @JsonClassDescription} annotation on the input class</li>
* </ul>
*
* @author Christian Tzolov
* @author Christopher Smith
* @author Sebastien Deleuze
* @deprecated Use {@link SpringBeanToolCallbackResolver} instead.
*/
@Deprecated
public class DefaultFunctionCallbackResolver implements ApplicationContextAware, FunctionCallbackResolver {
private GenericApplicationContext applicationContext;
private SchemaType schemaType = SchemaType.JSON_SCHEMA;
public void setSchemaType(SchemaType schemaType) {
this.schemaType = schemaType;
}
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = (GenericApplicationContext) applicationContext;
}
@Override
public FunctionCallback resolve(@NonNull String beanName) {
ResolvableType functionType = TypeResolverHelper.resolveBeanType(this.applicationContext, beanName);
ResolvableType functionInputType = (ResolvableType.forType(Supplier.class).isAssignableFrom(functionType))
? ResolvableType.forType(Void.class) : TypeResolverHelper.getFunctionArgumentType(functionType, 0);
String functionDescription = resolveFunctionDescription(beanName, functionInputType.toClass());
Object bean = this.applicationContext.getBean(beanName);
return buildFunctionCallback(beanName, functionType, functionInputType, functionDescription, bean);
}
private String resolveFunctionDescription(String beanName, Class<?> functionInputClass) {
String functionDescription = "";
if (!StringUtils.hasText(functionDescription)) {
Description descriptionAnnotation = this.applicationContext.findAnnotationOnBean(beanName,
Description.class);
if (descriptionAnnotation != null) {
functionDescription = descriptionAnnotation.value();
}
if (!StringUtils.hasText(functionDescription)) {
JsonClassDescription jsonClassDescriptionAnnotation = functionInputClass
.getAnnotation(JsonClassDescription.class);
if (jsonClassDescriptionAnnotation != null) {
functionDescription = jsonClassDescriptionAnnotation.value();
}
}
if (!StringUtils.hasText(functionDescription)) {
throw new IllegalStateException("Could not determine function description. "
+ "Please provide a description either as a default parameter, via @Description annotation on the bean "
+ "or @JsonClassDescription annotation on the input class.");
}
}
return functionDescription;
}
private FunctionCallback buildFunctionCallback(String beanName, ResolvableType functionType,
ResolvableType functionInputType, String functionDescription, Object bean) {
if (KotlinDetector.isKotlinPresent()) {
if (KotlinDelegate.isKotlinFunction(functionType.toClass())) {
return FunctionCallback.builder()
.function(beanName, KotlinDelegate.wrapKotlinFunction(bean))
.schemaType(this.schemaType)
.description(functionDescription)
.inputType(ParameterizedTypeReference.forType(functionInputType.getType()))
.build();
}
if (KotlinDelegate.isKotlinBiFunction(functionType.toClass())) {
return FunctionCallback.builder()
.function(beanName, KotlinDelegate.wrapKotlinBiFunction(bean))
.description(functionDescription)
.schemaType(this.schemaType)
.inputType(ParameterizedTypeReference.forType(functionInputType.getType()))
.build();
}
if (KotlinDelegate.isKotlinSupplier(functionType.toClass())) {
return FunctionCallback.builder()
.function(beanName, KotlinDelegate.wrapKotlinSupplier(bean))
.description(functionDescription)
.schemaType(this.schemaType)
.inputType(ParameterizedTypeReference.forType(functionInputType.getType()))
.build();
}
}
if (bean instanceof Function<?, ?> function) {
return FunctionCallback.builder()
.function(beanName, function)
.schemaType(this.schemaType)
.description(functionDescription)
.inputType(ParameterizedTypeReference.forType(functionInputType.getType()))
.build();
}
if (bean instanceof BiFunction<?, ?, ?>) {
return FunctionCallback.builder()
.function(beanName, (BiFunction<?, ToolContext, ?>) bean)
.description(functionDescription)
.schemaType(this.schemaType)
.inputType(ParameterizedTypeReference.forType(functionInputType.getType()))
.build();
}
if (bean instanceof Supplier<?> supplier) {
return FunctionCallback.builder()
.function(beanName, supplier)
.description(functionDescription)
.schemaType(this.schemaType)
.inputType(ParameterizedTypeReference.forType(functionInputType.getType()))
.build();
}
if (bean instanceof Consumer<?> consumer) {
return FunctionCallback.builder()
.function(beanName, consumer)
.description(functionDescription)
.schemaType(this.schemaType)
.inputType(ParameterizedTypeReference.forType(functionInputType.getType()))
.build();
}
throw new IllegalStateException("Unsupported function type");
}
private static final class KotlinDelegate {
public static boolean isKotlinSupplier(Class<?> clazz) {
return Function0.class.isAssignableFrom(clazz);
}
@SuppressWarnings("unchecked")
public static Supplier<?> wrapKotlinSupplier(Object function) {
return () -> ((Function0<Object>) function).invoke();
}
public static boolean isKotlinFunction(Class<?> clazz) {
return Function1.class.isAssignableFrom(clazz);
}
@SuppressWarnings("unchecked")
public static Function<?, ?> wrapKotlinFunction(Object function) {
return t -> ((Function1<Object, Object>) function).invoke(t);
}
public static boolean isKotlinBiFunction(Class<?> clazz) {
return Function2.class.isAssignableFrom(clazz);
}
@SuppressWarnings("unchecked")
public static BiFunction<?, ToolContext, ?> wrapKotlinBiFunction(Object function) {
return (t, u) -> ((Function2<Object, ToolContext, Object>) function).invoke(t, u);
}
}
}

View File

@@ -1,254 +0,0 @@
/*
* Copyright 2024-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.model.function;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.model.tool.DefaultToolCallingChatOptions;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* Default implementation of {@link FunctionCallingOptions}.
*
* @author Christian Tzolov
* @author Thomas Vitale
* @author Ilayaperumal Gopinathan
* @deprecated in favor of {@link DefaultToolCallingChatOptions}.
*/
@Deprecated
public class DefaultFunctionCallingOptions implements FunctionCallingOptions {
private List<FunctionCallback> functionCallbacks = new ArrayList<>();
private Set<String> functions = new HashSet<>();
private Boolean proxyToolCalls = false;
private Map<String, Object> context = new HashMap<>();
private String model;
private Double frequencyPenalty;
private Integer maxTokens;
private Double presencePenalty;
private List<String> stopSequences;
private Double temperature;
private Integer topK;
private Double topP;
@Override
public List<FunctionCallback> getFunctionCallbacks() {
return Collections.unmodifiableList(this.functionCallbacks);
}
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
Assert.notNull(functionCallbacks, "FunctionCallbacks must not be null");
this.functionCallbacks = new ArrayList<>(functionCallbacks);
}
@Override
public Set<String> getFunctions() {
return Collections.unmodifiableSet(this.functions);
}
public void setFunctions(Set<String> functions) {
Assert.notNull(functions, "Functions must not be null");
this.functions = new HashSet<>(functions);
}
@Override
public Boolean getProxyToolCalls() {
return this.proxyToolCalls;
}
public void setProxyToolCalls(Boolean proxyToolCalls) {
this.proxyToolCalls = proxyToolCalls;
}
public Map<String, Object> getToolContext() {
return Collections.unmodifiableMap(this.context);
}
public void setToolContext(Map<String, Object> context) {
Assert.notNull(context, "Context must not be null");
this.context = new HashMap<>(context);
}
@Override
public String getModel() {
return this.model;
}
public void setModel(String model) {
this.model = model;
}
@Override
public Double getFrequencyPenalty() {
return this.frequencyPenalty;
}
public void setFrequencyPenalty(Double frequencyPenalty) {
this.frequencyPenalty = frequencyPenalty;
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
public void setMaxTokens(Integer maxTokens) {
this.maxTokens = maxTokens;
}
@Override
public Double getPresencePenalty() {
return this.presencePenalty;
}
public void setPresencePenalty(Double presencePenalty) {
this.presencePenalty = presencePenalty;
}
@Override
public List<String> getStopSequences() {
return this.stopSequences != null ? Collections.unmodifiableList(this.stopSequences) : null;
}
public void setStopSequences(List<String> stopSequences) {
this.stopSequences = stopSequences;
}
@Override
public Double getTemperature() {
return this.temperature;
}
public void setTemperature(Double temperature) {
this.temperature = temperature;
}
@Override
public Integer getTopK() {
return this.topK;
}
public void setTopK(Integer topK) {
this.topK = topK;
}
@Override
public Double getTopP() {
return this.topP;
}
public void setTopP(Double topP) {
this.topP = topP;
}
@Override
@SuppressWarnings("unchecked")
public <T extends ChatOptions> T copy() {
DefaultFunctionCallingOptions copy = new DefaultFunctionCallingOptions();
copy.setModel(this.getModel());
copy.setFrequencyPenalty(this.getFrequencyPenalty());
copy.setMaxTokens(this.getMaxTokens());
copy.setPresencePenalty(this.getPresencePenalty());
copy.setStopSequences(this.getStopSequences() != null ? new ArrayList<>(this.getStopSequences()) : null);
copy.setTemperature(this.getTemperature());
copy.setTopK(this.getTopK());
copy.setTopP(this.getTopP());
copy.setFunctions(new HashSet<>(this.functions));
copy.setFunctionCallbacks(new ArrayList<>(this.functionCallbacks));
copy.setProxyToolCalls(this.proxyToolCalls);
copy.setToolContext(new HashMap<>(this.getToolContext()));
return (T) copy;
}
public FunctionCallingOptions merge(ChatOptions options) {
Builder builder = FunctionCallingOptions.builder();
builder.model(StringUtils.hasText(options.getModel()) ? options.getModel() : this.getModel())
.frequencyPenalty(
options.getFrequencyPenalty() != null ? options.getFrequencyPenalty() : this.getFrequencyPenalty())
.maxTokens(options.getMaxTokens() != null ? options.getMaxTokens() : this.getMaxTokens())
.presencePenalty(
options.getPresencePenalty() != null ? options.getPresencePenalty() : this.getPresencePenalty())
.stopSequences(options.getStopSequences() != null ? options.getStopSequences() : this.getStopSequences())
.temperature(options.getTemperature() != null ? options.getTemperature() : this.getTemperature())
.topK(options.getTopK() != null ? options.getTopK() : this.getTopK())
.topP(options.getTopP() != null ? options.getTopP() : this.getTopP());
// Try to get function-specific properties if options is a FunctionCallingOptions
if (options instanceof FunctionCallingOptions functionOptions) {
builder.proxyToolCalls(functionOptions.getProxyToolCalls() != null ? functionOptions.getProxyToolCalls()
: this.proxyToolCalls);
Set<String> functions = new HashSet<>();
if (!CollectionUtils.isEmpty(this.functions)) {
functions.addAll(this.functions);
}
if (!CollectionUtils.isEmpty(functionOptions.getFunctions())) {
functions.addAll(functionOptions.getFunctions());
}
builder.functions(functions);
List<FunctionCallback> functionCallbacks = new ArrayList<>();
if (!CollectionUtils.isEmpty(this.functionCallbacks)) {
functionCallbacks.addAll(this.functionCallbacks);
}
if (!CollectionUtils.isEmpty(functionOptions.getFunctionCallbacks())) {
functionCallbacks.addAll(functionOptions.getFunctionCallbacks());
}
builder.functionCallbacks(functionCallbacks);
Map<String, Object> context = new HashMap<>();
if (!CollectionUtils.isEmpty(this.context)) {
context.putAll(this.context);
}
if (!CollectionUtils.isEmpty(functionOptions.getToolContext())) {
context.putAll(functionOptions.getToolContext());
}
builder.toolContext(context);
}
else {
// If not a FunctionCallingOptions, preserve current function-specific
// properties
builder.proxyToolCalls(this.proxyToolCalls);
builder.functions(new HashSet<>(this.functions));
builder.functionCallbacks(new ArrayList<>(this.functionCallbacks));
builder.toolContext(new HashMap<>(this.context));
}
return builder.build();
}
}

View File

@@ -1,153 +0,0 @@
/*
* Copyright 2024-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.model.function;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.ai.model.tool.DefaultToolCallingChatOptions;
import org.springframework.util.Assert;
/**
* Default implementation of {@link FunctionCallingOptions.Builder}.
*
* @author Christian Tzolov
* @author Thomas Vitale
* @author Ilayaperumal Gopinathan
* @deprecated in favor of {@link DefaultToolCallingChatOptions.Builder}.
*/
@Deprecated
public class DefaultFunctionCallingOptionsBuilder implements FunctionCallingOptions.Builder {
private final DefaultFunctionCallingOptions options;
public DefaultFunctionCallingOptionsBuilder() {
this.options = new DefaultFunctionCallingOptions();
}
// Function calling specific methods
@Override
public FunctionCallingOptions.Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
this.options.setFunctionCallbacks(functionCallbacks);
return this;
}
@Override
public FunctionCallingOptions.Builder functionCallbacks(FunctionCallback... functionCallbacks) {
Assert.notNull(functionCallbacks, "FunctionCallbacks must not be null");
this.options.setFunctionCallbacks(List.of(functionCallbacks));
return this;
}
@Override
public FunctionCallingOptions.Builder functions(Set<String> functions) {
this.options.setFunctions(functions);
return this;
}
@Override
public FunctionCallingOptions.Builder function(String function) {
Assert.notNull(function, "Function must not be null");
var set = new HashSet<>(this.options.getFunctions());
set.add(function);
this.options.setFunctions(set);
return this;
}
@Override
public FunctionCallingOptions.Builder proxyToolCalls(Boolean proxyToolCalls) {
this.options.setProxyToolCalls(proxyToolCalls);
return this;
}
@Override
public FunctionCallingOptions.Builder toolContext(Map<String, Object> context) {
Assert.notNull(context, "Tool context must not be null");
Map<String, Object> newContext = new HashMap<>(this.options.getToolContext());
newContext.putAll(context);
this.options.setToolContext(newContext);
return this;
}
@Override
public FunctionCallingOptions.Builder toolContext(String key, Object value) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
Map<String, Object> newContext = new HashMap<>(this.options.getToolContext());
newContext.put(key, value);
this.options.setToolContext(newContext);
return this;
}
// ChatOptions.Builder methods with covariant return type
@Override
public FunctionCallingOptions.Builder model(String model) {
this.options.setModel(model);
return this;
}
@Override
public FunctionCallingOptions.Builder frequencyPenalty(Double frequencyPenalty) {
this.options.setFrequencyPenalty(frequencyPenalty);
return this;
}
@Override
public FunctionCallingOptions.Builder maxTokens(Integer maxTokens) {
this.options.setMaxTokens(maxTokens);
return this;
}
@Override
public FunctionCallingOptions.Builder presencePenalty(Double presencePenalty) {
this.options.setPresencePenalty(presencePenalty);
return this;
}
@Override
public FunctionCallingOptions.Builder stopSequences(List<String> stop) {
this.options.setStopSequences(stop);
return this;
}
@Override
public FunctionCallingOptions.Builder temperature(Double temperature) {
this.options.setTemperature(temperature);
return this;
}
@Override
public FunctionCallingOptions.Builder topK(Integer topK) {
this.options.setTopK(topK);
return this;
}
@Override
public FunctionCallingOptions.Builder topP(Double topP) {
this.options.setTopP(topP);
return this;
}
@Override
public FunctionCallingOptions build() {
return this.options.copy();
}
}

View File

@@ -1,252 +0,0 @@
/*
* 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.model.function;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.ai.tool.method.MethodToolCallback;
import org.springframework.core.ParameterizedTypeReference;
/**
* Represents a model function call handler. Implementations are registered with the
* Models and called on prompts that trigger the function call.
*
* @author Christian Tzolov
* @deprecated in favor of {@link ToolCallback}.
*/
@Deprecated
public interface FunctionCallback {
/**
* @return Returns the Function name. Unique within the model.
*/
String getName();
/**
* @return Returns the function description. This description is used by the model do
* decide if the function should be called or not.
*/
String getDescription();
/**
* @return Returns the JSON schema of the function input type.
*/
String getInputTypeSchema();
/**
* Called when a model detects and triggers a function call. The model is responsible
* to pass the function arguments in the pre-configured JSON schema format.
* @param functionInput JSON string with the function arguments to be passed to the
* function. The arguments are defined as JSON schema usually registered with the
* model.
* @return String containing the function call response.
*/
String call(String functionInput);
/**
* Called when a model detects and triggers a function call. The model is responsible
* to pass the function arguments in the pre-configured JSON schema format.
* Additionally, the model can pass a context map to the function if available. The
* context is used to pass additional user provided state in addition to the arguments
* provided by the AI model.
* @param functionInput JSON string with the function arguments to be passed to the
* function. The arguments are defined as JSON schema usually registered with the
* model. Arguments are provided by the AI model.
* @param toolContext Map with the function context. The context is used to pass
* additional user provided state in addition to the arguments provided by the AI
* model.
* @return String containing the function call response.
*/
default String call(String functionInput, ToolContext toolContext) {
if (toolContext != null && !toolContext.getContext().isEmpty()) {
throw new UnsupportedOperationException("Function context is not supported!");
}
return call(functionInput);
}
/**
* Creates a new {@link Builder} instance used to build a default
* {@link FunctionCallback} instance.
* @return Returns a new {@link Builder} instance.
*/
static Builder builder() {
return new DefaultFunctionCallbackBuilder();
}
/**
* Describes the type of the schema used to describe the input parameters of the
* function.
*/
enum SchemaType {
/**
* JSON schema
*/
JSON_SCHEMA,
/**
* Open API schema
*/
OPEN_API_SCHEMA
}
/**
* Builder for creating a {@link FunctionCallback} instance. This is a hierarchical
* builder with the following structure:
* <ul>
* <li>{@link Builder} - The root builder interface.
* <li>{@link FunctionInvokingSpec} - The function invoking builder interface.
* <li>{@link MethodInvokingSpec} - The method invoking builder interface.
* </ul>
*
* @deprecated Use specific builder for the type of tool you need, e.g.
* {@link FunctionToolCallback.Builder} and {@link MethodToolCallback.Builder}.
*/
@Deprecated
interface Builder {
/**
* Builds a {@link Function} invoking {@link FunctionCallback} instance.
*/
<I, O> FunctionInvokingSpec<I, O> function(String name, Function<I, O> function);
/**
* Builds a {@link BiFunction} invoking {@link FunctionCallback} instance.
*/
<I, O> FunctionInvokingSpec<I, O> function(String name, BiFunction<I, ToolContext, O> biFunction);
/**
* Builds a {@link Supplier} invoking {@link FunctionCallback} instance.
*/
<O> FunctionInvokingSpec<Void, O> function(String name, Supplier<O> supplier);
/**
* Builds a {@link Consumer} invoking {@link FunctionCallback} instance.
*/
<I> FunctionInvokingSpec<I, Void> function(String name, Consumer<I> consumer);
/**
* Builds a Method invoking {@link FunctionCallback} instance.
*/
MethodInvokingSpec method(String methodName, Class<?>... argumentTypes);
}
interface CommonCallbackInvokingSpec<B extends CommonCallbackInvokingSpec<B>> {
/**
* Function description. This description is used by the model to decide if the
* function should be called or not.
*/
B description(String description);
/**
* Specifies what {@link SchemaType} is used by the AI model to validate the
* function input arguments. Most models use JSON Schema, except Vertex AI that
* uses OpenAPI types.
*/
B schemaType(SchemaType schemaType);
/**
* Function response converter. The default implementation converts the output
* into String before sending it to the Model. Provide a custom function
* responseConverter implementation to override this.
*/
B responseConverter(Function<Object, String> responseConverter);
/**
* You can provide the Input Type Schema directly. In this case it won't be
* generated from the inputType.
*/
B inputTypeSchema(String inputTypeSchema);
/**
* Custom object mapper for JSON operations.
*/
B objectMapper(ObjectMapper objectMapper);
}
/**
* {@link Function} invoking builder interface.
*
* @param <I> Function input type.
* @param <O> Function output type.
*/
interface FunctionInvokingSpec<I, O> extends CommonCallbackInvokingSpec<FunctionInvokingSpec<I, O>> {
/**
* Function input type. The input type is used to validate the function input
* arguments.
* @see #inputType(ParameterizedTypeReference)
*/
FunctionInvokingSpec<I, O> inputType(Class<?> inputType);
/**
* Function input type retaining generic types. The input type is used to validate
* the function input arguments.
*/
FunctionInvokingSpec<I, O> inputType(ParameterizedTypeReference<?> inputType);
/**
* Builds the {@link FunctionCallback} instance.
*/
FunctionCallback build();
}
/**
* Method invoking builder interface.
*/
interface MethodInvokingSpec extends CommonCallbackInvokingSpec<MethodInvokingSpec> {
/**
* Optional function name. If not provided the method name is used as the
* function.
* @param name Function name. Unique within the model.
*/
MethodInvokingSpec name(String name);
/**
* For non-static objects the target object is used to invoke the method.
* @param methodObject target object where the method is defined.
*/
MethodInvokingSpec targetObject(Object methodObject);
/**
* Target class where the method is defined. Used for static methods. For
* non-static methods the target object is used.
* @param targetClass method target class.
*/
MethodInvokingSpec targetClass(Class<?> targetClass);
/**
* Builds the {@link FunctionCallback} instance.
*/
FunctionCallback build();
}
}

View File

@@ -1,39 +0,0 @@
/*
* Copyright 2024-2024 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.model.function;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.lang.NonNull;
/**
* Strategy interface for resolving {@link FunctionCallback} instances.
*
* @author Christian Tzolov
* @since 1.0.0
* @deprecated Use {@link ToolCallbackResolver} instead.
*/
@Deprecated
public interface FunctionCallbackResolver {
/**
* Resolve the {@link FunctionCallback} instance by its name.
* @param name the name of the function to resolve
* @return the {@link FunctionCallback} instance
*/
FunctionCallback resolve(@NonNull String name);
}

View File

@@ -1,180 +0,0 @@
/*
* 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.model.function;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
/**
* FunctionCallingOptions is a set of options that can be used to configure the function
* calling behavior of the ChatModel.
*
* @author Christian Tzolov
* @author Ilayaperumal Gopinathan
* @deprecated in favor of {@link ToolCallingChatOptions}.
*/
@Deprecated
public interface FunctionCallingOptions extends ChatOptions {
/**
* @return Returns {@link DefaultFunctionCallingOptionsBuilder} to create a new
* instance of {@link FunctionCallingOptions}.
*/
static Builder builder() {
return new DefaultFunctionCallingOptionsBuilder();
}
/**
* Function Callbacks to be registered with the ChatModel. For Prompt Options the
* toolCallbacks are automatically enabled for the duration of the prompt execution.
* For Default Options the FunctionCallbacks are registered but disabled by default.
* You have to use "functions" property to list the function names from the ChatModel
* registry to be used in the chat completion requests.
* @return Return the Function Callbacks to be registered with the ChatModel.
*/
List<FunctionCallback> getFunctionCallbacks();
/**
* Set the Function Callbacks to be registered with the ChatModel.
* @param functionCallbacks the Function Callbacks to be registered with the
* ChatModel.
*/
void setFunctionCallbacks(List<FunctionCallback> functionCallbacks);
/**
* @return {@link Set} of function names from the ChatModel registry to be used in the
* next chat completion requests.
*/
Set<String> getFunctions();
/**
* Set the list of function names from the ChatModel registry to be used in the next
* chat completion requests.
* @param functions the list of function names from the ChatModel registry to be used
* in the next chat completion requests.
*/
void setFunctions(Set<String> functions);
default Boolean getProxyToolCalls() {
return false;
}
default void setProxyToolCalls(Boolean proxyToolCalls) {
if (proxyToolCalls != null) {
throw new UnsupportedOperationException("Setting Proxy Tool Calls are not supported!");
}
}
Map<String, Object> getToolContext();
void setToolContext(Map<String, Object> tooContext);
/**
* Builder for creating {@link FunctionCallingOptions} instance.
*/
interface Builder extends ChatOptions.Builder {
/**
* The list of Function Callbacks to be registered with the Chat model.
* @param functionCallbacks the list of Function Callbacks.
* @return the FunctionCallOptions Builder.
*/
Builder functionCallbacks(List<FunctionCallback> functionCallbacks);
/**
* The Function Callbacks to be registered with the Chat model.
* @param functionCallbacks the function callbacks.
* @return the FunctionCallOptions Builder.
*/
Builder functionCallbacks(FunctionCallback... functionCallbacks);
/**
* {@link Set} of function names to be registered with the Chat model.
* @param functions the {@link Set} of function names
* @return the FunctionCallOptions Builder.
*/
Builder functions(Set<String> functions);
/**
* The function name to be registered with the chat model.
* @param function the name of the function.
* @return the FunctionCallOptions Builder.
*/
Builder function(String function);
/**
* Boolean flag to indicate if the proxy ToolCalls is enabled.
* @param proxyToolCalls boolean value to enable proxy ToolCalls.
* @return the FunctionCallOptions Builder.
*/
Builder proxyToolCalls(Boolean proxyToolCalls);
/**
* Add a {@link Map} of context values into tool context.
* @param context the map representing the tool context.
* @return the FunctionCallOptions Builder.
*/
Builder toolContext(Map<String, Object> context);
/**
* Add a specific key/value pair to the tool context.
* @param key the key to use.
* @param value the corresponding value.
* @return the FunctionCallOptions Builder.
*/
Builder toolContext(String key, Object value);
/**
* Builds the {@link FunctionCallingOptions}.
* @return the FunctionCalling options.
*/
@Override
FunctionCallingOptions build();
// Override all ChatOptions.Builder methods to return
// FunctionCallingOptions.Builder
@Override
Builder model(String model);
@Override
Builder frequencyPenalty(Double frequencyPenalty);
@Override
Builder maxTokens(Integer maxTokens);
@Override
Builder presencePenalty(Double presencePenalty);
@Override
Builder stopSequences(List<String> stopSequences);
@Override
Builder temperature(Double temperature);
@Override
Builder topK(Integer topK);
@Override
Builder topP(Double topP);
}
}

View File

@@ -1,57 +0,0 @@
/*
* 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.model.function;
import java.lang.reflect.Type;
import java.util.function.BiFunction;
import java.util.function.Function;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.util.Assert;
/**
* Note that the underlying function is responsible for converting the output into format
* that can be consumed by the Model. The default implementation converts the output into
* String before sending it to the Model. Provide a custom function responseConverter
* implementation to override this.
*
* @param <I> the input type
* @param <O> the output type
* @author Christian Tzolov
* @deprecated in favor of {@link FunctionToolCallback}.
*/
@Deprecated
public final class FunctionInvokingFunctionCallback<I, O> extends AbstractFunctionCallback<I, O> {
private final BiFunction<I, ToolContext, O> biFunction;
FunctionInvokingFunctionCallback(String name, String description, String inputTypeSchema, Type inputType,
Function<O, String> responseConverter, ObjectMapper objectMapper, BiFunction<I, ToolContext, O> function) {
super(name, description, inputTypeSchema, inputType, responseConverter, objectMapper);
Assert.notNull(function, "Function must not be null");
this.biFunction = function;
}
@Override
public O apply(I input, ToolContext context) {
return this.biFunction.apply(input, context);
}
}

View File

@@ -1,277 +0,0 @@
/*
* 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.model.function;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.tool.method.MethodToolCallback;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
/**
* A {@link FunctionCallback} that invokes methods on objects via reflection, supporting:
* <ul>
* <li>Static and non-static methods</li>
* <li>Any number of parameters (including none)</li>
* <li>Any parameter/return types (primitives, objects, collections)</li>
* <li>Special handling for {@link ToolContext} parameters</li>
* </ul>
* Automatically infers the input parameters JSON schema from method's argument types.
*
* @author Christian Tzolov
* @since 1.0.0
* @deprecated in favor of {@link MethodToolCallback}.
*/
@Deprecated
public class MethodInvokingFunctionCallback implements FunctionCallback {
private static final Logger logger = LoggerFactory.getLogger(MethodInvokingFunctionCallback.class);
/**
* Object instance that contains the method to be invoked. If the method is static
* this object can be null.
*/
private final Object functionObject;
/**
* The method to be invoked.
*/
private final Method method;
/**
* Description to help the LLM model to understand worth the method does and when to
* use it.
*/
private final String description;
/**
* Internal ObjectMapper used to serialize/deserialize the method input and output.
*/
private final ObjectMapper mapper;
/**
* The JSON schema generated from the method input parameters.
*/
private final String inputSchema;
/**
* Flag indicating if the method accepts a {@link ToolContext} as input parameter.
*/
private boolean isToolContextMethod = false;
/**
* Optional function name. If not provided the method name is used as the function.
*/
private final String name;
/**
*
*/
private final Function<Object, String> responseConverter;
MethodInvokingFunctionCallback(Object functionObject, Method method, String description, ObjectMapper mapper,
String name, Function<Object, String> responseConverter) {
Assert.notNull(method, "Method must not be null");
Assert.notNull(mapper, "ObjectMapper must not be null");
Assert.hasText(description, "Description must not be empty");
Assert.notNull(responseConverter, "Response converter must not be null");
this.method = method;
this.description = description;
this.mapper = mapper;
this.functionObject = functionObject;
this.name = name;
this.responseConverter = responseConverter;
Assert.isTrue(this.functionObject != null || Modifier.isStatic(this.method.getModifiers()),
"Function object must be provided for non-static methods!");
// Generate the JSON schema from the method input parameters
Map<String, Class<?>> methodParameters = Stream.of(method.getParameters())
.collect(Collectors.toMap(param -> param.getName(), param -> param.getType()));
this.inputSchema = this.generateJsonSchema(methodParameters);
logger.debug("Generated JSON Schema: {}", this.inputSchema);
}
@Override
public String getName() {
return org.springframework.util.StringUtils.hasText(this.name) ? this.name : this.method.getName();
}
@Override
public String getDescription() {
return this.description;
}
@Override
public String getInputTypeSchema() {
return this.inputSchema;
}
@Override
public String call(String functionInput) {
return this.call(functionInput, null);
}
@Override
public String call(String functionInput, ToolContext toolContext) {
try {
// If the toolContext is not empty but the method does not accept ToolContext
// as
// input parameter then throw an exception.
if (toolContext != null && !CollectionUtils.isEmpty(toolContext.getContext())
&& !this.isToolContextMethod) {
throw new IllegalArgumentException("Configured method does not accept ToolContext as input parameter!");
}
@SuppressWarnings("unchecked")
Map<String, Object> map = this.mapper.readValue(functionInput, Map.class);
// ReflectionUtils.findMethod
Object[] methodArgs = Stream.of(this.method.getParameters()).map(parameter -> {
Class<?> type = parameter.getType();
if (ClassUtils.isAssignable(type, ToolContext.class)) {
return toolContext;
}
Object rawValue = map.get(parameter.getName());
return this.toJavaType(rawValue, type);
}).toArray();
Object response = ReflectionUtils.invokeMethod(this.method, this.functionObject, methodArgs);
var returnType = this.method.getReturnType();
if (returnType == Void.TYPE) {
return "Done";
}
else if (returnType == Class.class || returnType.isRecord() || returnType == List.class
|| returnType == Map.class) {
return ModelOptionsUtils.toJsonString(response);
}
return this.responseConverter.apply(response);
}
catch (Exception e) {
ReflectionUtils.handleReflectionException(e);
return null;
}
}
/**
* Generates a JSON schema from the given named classes.
* @param namedClasses The named classes to generate the schema from.
* @return The generated JSON schema.
*/
protected String generateJsonSchema(Map<String, Class<?>> namedClasses) {
try {
JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(this.mapper);
ObjectNode rootNode = this.mapper.createObjectNode();
rootNode.put("$schema", "https://json-schema.org/draft/2020-12/schema");
rootNode.put("type", "object");
ObjectNode propertiesNode = rootNode.putObject("properties");
for (Map.Entry<String, Class<?>> entry : namedClasses.entrySet()) {
String className = entry.getKey();
Class<?> clazz = entry.getValue();
if (ClassUtils.isAssignable(clazz, ToolContext.class)) {
// Skip the ToolContext class from the schema generation.
this.isToolContextMethod = true;
continue;
}
JsonSchema schema = schemaGen.generateSchema(clazz);
JsonNode schemaNode = this.mapper.valueToTree(schema);
propertiesNode.set(className, schemaNode);
}
return this.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Converts the given value to the specified Java type.
* @param value The value to convert.
* @param javaType The Java type to convert to.
* @return Returns the converted value.
*/
protected Object toJavaType(Object value, Class<?> javaType) {
if (value == null) {
return null;
}
javaType = ClassUtils.resolvePrimitiveIfNecessary(javaType);
if (javaType == String.class) {
return value.toString();
}
else if (javaType == Integer.class) {
return Integer.parseInt(value.toString());
}
else if (javaType == Long.class) {
return Long.parseLong(value.toString());
}
else if (javaType == Double.class) {
return Double.parseDouble(value.toString());
}
else if (javaType == Float.class) {
return Float.parseFloat(value.toString());
}
else if (javaType == Boolean.class) {
return Boolean.parseBoolean(value.toString());
}
else if (javaType.isEnum()) {
return Enum.valueOf((Class<Enum>) javaType, value.toString());
}
try {
String json = this.mapper.writeValueAsString(value);
return this.mapper.readValue(json, javaType);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -33,7 +33,6 @@ import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
@@ -93,7 +92,9 @@ public class DefaultToolCallingManager implements ToolCallingManager {
// Skip the tool if it is already present in the request toolCallbacks.
// That might happen if a tool is defined in the options
// both as a ToolCallback and as a tool name.
if (chatOptions.getToolCallbacks().stream().anyMatch(tool -> tool.getName().equals(toolName))) {
if (chatOptions.getToolCallbacks()
.stream()
.anyMatch(tool -> tool.getToolDefinition().name().equals(toolName))) {
continue;
}
ToolCallback toolCallback = this.toolCallbackResolver.resolve(toolName);
@@ -163,9 +164,7 @@ public class DefaultToolCallingManager implements ToolCallingManager {
}
/**
* Execute the tool call and return the response message. To ensure backward
* compatibility, both {@link ToolCallback} and {@link FunctionCallback} are
* supported.
* Execute the tool call and return the response message.
*/
private InternalToolExecutionResult executeToolCall(Prompt prompt, AssistantMessage assistantMessage,
ToolContext toolContext) {
@@ -186,7 +185,7 @@ public class DefaultToolCallingManager implements ToolCallingManager {
String toolInputArguments = toolCall.arguments();
ToolCallback toolCallback = toolCallbacks.stream()
.filter(tool -> toolName.equals(tool.getName()))
.filter(tool -> toolName.equals(tool.getToolDefinition().name()))
.findFirst()
.orElseGet(() -> this.toolCallbackResolver.resolve(toolName));

View File

@@ -20,7 +20,6 @@ import java.util.function.Function;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.util.Assert;
/**
@@ -69,10 +68,6 @@ public interface ToolExecutionEligibilityChecker extends Function<ChatResponse,
internalToolExecutionEnabled = Boolean.TRUE
.equals(toolCallingChatOptions.getInternalToolExecutionEnabled());
}
else if (chatOptions instanceof FunctionCallingOptions functionCallingOptions
&& functionCallingOptions.getProxyToolCalls() != null) {
internalToolExecutionEnabled = Boolean.TRUE.equals(!functionCallingOptions.getProxyToolCalls());
}
else {
internalToolExecutionEnabled = true;
}

View File

@@ -31,14 +31,14 @@ import org.springframework.util.Assert;
*
* <p>
* Example usage: <pre>{@code
* FunctionCallback callback1 = new MyFunctionCallback();
* FunctionCallback callback2 = new AnotherFunctionCallback();
* ToolCallback callback1 = new MyFunctionCallback();
* ToolCallback callback2 = new AnotherFunctionCallback();
*
* // Create provider with varargs constructor
* ToolCallbackProvider provider1 = new StaticToolCallbackProvider(callback1, callback2);
*
* // Or create provider with List constructor
* List<FunctionCallback> callbacks = Arrays.asList(callback1, callback2);
* List<ToolCallback> callbacks = Arrays.asList(callback1, callback2);
* ToolCallbackProvider provider2 = new StaticToolCallbackProvider(callbacks);
* }</pre>
*

View File

@@ -17,7 +17,6 @@
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;
@@ -28,7 +27,7 @@ import org.springframework.lang.Nullable;
* @author Thomas Vitale
* @since 1.0.0
*/
public interface ToolCallback extends FunctionCallback {
public interface ToolCallback {
/**
* Definition used by the AI model to determine when and how to call the tool.
@@ -59,22 +58,4 @@ public interface ToolCallback extends FunctionCallback {
return call(toolInput);
}
@Override
@Deprecated // Call getToolDefinition().name() instead
default String getName() {
return getToolDefinition().name();
}
@Override
@Deprecated // Call getToolDefinition().description() instead
default String getDescription() {
return getToolDefinition().description();
}
@Override
@Deprecated // Call getToolDefinition().inputTypeSchema() instead
default String getInputTypeSchema() {
return getToolDefinition().inputSchema();
}
}

View File

@@ -129,7 +129,7 @@ public final class MethodToolCallback implements ToolCallback {
});
}
// Based on the implementation in MethodInvokingFunctionCallback.
// Based on the implementation in MethodToolCallback.
private Object[] buildMethodArguments(Map<String, Object> toolInputArguments, @Nullable ToolContext toolContext) {
return Stream.of(this.toolMethod.getParameters()).map(parameter -> {
if (parameter.getType().isAssignableFrom(ToolContext.class)) {

View File

@@ -16,7 +16,6 @@
package org.springframework.ai.tool.resolution;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.lang.Nullable;
@@ -29,7 +28,7 @@ import org.springframework.lang.Nullable;
public interface ToolCallbackResolver {
/**
* Resolve the {@link FunctionCallback} for the given tool name.
* Resolve the {@link ToolCallback} for the given tool name.
*/
@Nullable
ToolCallback resolve(String toolName);

View File

@@ -112,7 +112,7 @@ public final class JsonParser {
/**
* Convert a Java Object to a typed Object. Based on the implementation in
* MethodInvokingFunctionCallback.
* MethodToolCallback.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public static Object toTypedObject(Object value, Class<?> type) {

View File

@@ -1,29 +0,0 @@
/*
* Copyright 2023-2024 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.model.function
import org.springframework.core.ParameterizedTypeReference
/**
* Extension for [FunctionCallback.FunctionInvokerBuilder.inputType] providing a `inputType<Foo>()`
* variant.
*
* @author Sebastien Deleuze
*/
inline fun <reified I, reified O> FunctionCallback.FunctionInvokingSpec<I, O>.inputType(): FunctionCallback.FunctionInvokingSpec<I, O> =
inputType(I::class.java)

View File

@@ -18,7 +18,8 @@ package org.springframework.ai.aot;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.aot.hint.RuntimeHints;
import static org.assertj.core.api.Assertions.assertThat;
@@ -32,11 +33,13 @@ class SpringAiCoreRuntimeHintsTest {
var runtimeHints = new RuntimeHints();
var springAiCore = new SpringAiCoreRuntimeHints();
springAiCore.registerHints(runtimeHints, null);
// Verify resource hints
assertThat(runtimeHints).matches(resource().forResource("embedding/embedding-model-dimensions.properties"));
assertThat(runtimeHints).matches(reflection().onMethod(FunctionCallback.class, "getDescription"));
assertThat(runtimeHints).matches(reflection().onMethod(FunctionCallback.class, "getInputTypeSchema"));
assertThat(runtimeHints).matches(reflection().onMethod(FunctionCallback.class, "getName"));
// Verify ToolCallback and ToolDefinition type registration
assertThat(runtimeHints).matches(reflection().onType(ToolCallback.class));
assertThat(runtimeHints).matches(reflection().onType(ToolDefinition.class));
}
}
}

View File

@@ -25,7 +25,6 @@ import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.model.function.FunctionCallingOptions;
import static org.assertj.core.api.Assertions.assertThat;
@@ -96,26 +95,9 @@ class DefaultToolExecutionEligibilityPredicateTests {
assertThat(result).isFalse();
}
@Test
void whenFunctionCallingOptionsAndToolExecutionEnabled() {
// Create a FunctionCallingOptions with proxy tool calls disabled (which means
// internal tool execution is enabled)
FunctionCallingOptions options = FunctionCallingOptions.builder().proxyToolCalls(false).build();
// Create a ChatResponse with tool calls
AssistantMessage.ToolCall toolCall = new AssistantMessage.ToolCall("id1", "function", "testTool", "{}");
AssistantMessage assistantMessage = new AssistantMessage("test", Map.of(), List.of(toolCall));
ChatResponse chatResponse = new ChatResponse(List.of(new Generation(assistantMessage)));
// Test the predicate
boolean result = this.predicate.test(options, chatResponse);
assertThat(result).isTrue();
}
@Test
void whenRegularChatOptionsAndHasToolCalls() {
// Create regular ChatOptions (not ToolCallingChatOptions or
// FunctionCallingOptions)
// Create regular ChatOptions (not ToolCallingChatOptions)
ChatOptions options = ChatOptions.builder().build();
// Create a ChatResponse with tool calls

View File

@@ -22,7 +22,6 @@ import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
@@ -56,13 +55,6 @@ class ToolCallingChatOptionsTests {
assertThat(ToolCallingChatOptions.isInternalToolExecutionEnabled(options)).isTrue();
}
@Test
void whenFunctionCallingOptionsAndExecutionEnabledTrue() {
FunctionCallingOptions options = FunctionCallingOptions.builder().build();
options.setProxyToolCalls(false);
assertThat(ToolCallingChatOptions.isInternalToolExecutionEnabled(options)).isTrue();
}
@Test
void whenMergeRuntimeAndDefaultToolNames() {
Set<String> runtimeToolNames = Set.of("toolA");
@@ -102,7 +94,7 @@ class ToolCallingChatOptionsTests {
List<ToolCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
defaultToolCallbacks);
assertThat(mergedToolCallbacks).hasSize(1);
assertThat(mergedToolCallbacks.get(0).getName()).isEqualTo("toolA");
assertThat(mergedToolCallbacks.get(0).getToolDefinition().name()).isEqualTo("toolA");
}
@Test
@@ -112,7 +104,7 @@ class ToolCallingChatOptionsTests {
List<ToolCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
defaultToolCallbacks);
assertThat(mergedToolCallbacks).hasSize(1);
assertThat(mergedToolCallbacks.get(0).getName()).isEqualTo("toolA");
assertThat(mergedToolCallbacks.get(0).getToolDefinition().name()).isEqualTo("toolA");
}
@Test
@@ -122,7 +114,7 @@ class ToolCallingChatOptionsTests {
List<ToolCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
defaultToolCallbacks);
assertThat(mergedToolCallbacks).hasSize(1);
assertThat(mergedToolCallbacks.get(0).getName()).isEqualTo("toolB");
assertThat(mergedToolCallbacks.get(0).getToolDefinition().name()).isEqualTo("toolB");
}
@Test

View File

@@ -1,36 +0,0 @@
/*
* 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.resolution
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Test
import org.springframework.ai.model.function.FunctionCallback
import org.springframework.ai.model.function.inputType
class FunctionCallbackExtensionsTests {
private val spec = mockk<FunctionCallback.FunctionInvokingSpec<WeatherRequest, WeatherResponse>>()
@Test
fun inputType() {
every { spec.inputType(any<Class<*>>()) } returns spec
spec.inputType<WeatherRequest, WeatherResponse>()
verify { spec.inputType(WeatherRequest::class.java) }
}
}

View File

@@ -31,8 +31,6 @@
<suppress files="BaseOllamaIT.java" checks="HideUtilityClassConstructor"/>
<suppress files="BaseOCIGenAITest.java" checks="HideUtilityClassConstructor"/>
<suppress files="OpenAiChatModelResponseFormatIT.java" checks="RegexpSinglelineJava"/>
<suppress files="MethodInvokingFunctionCallbackTests.java" checks="RegexpSinglelineJava"/>
<suppress files="DefaultFunctionCallbackBuilderTests.java" checks="RegexpSinglelineJava"/>
<suppress files="ClientIT.java" checks="RegexpSinglelineJava"/>
<suppress files="MistralAiApi.java" checks="RegexpSinglelineJava"/>
<suppress files="MariaDBSchemaValidator.java" checks="RegexpSinglelineJava"/>