Revamp function callback builder API

Introduces a simplified, type-safe builder pattern for function callbacks to
improve developer experience and code reliability. The new hierarchical API
separates concerns between direct function invocation and method reflection,
while providing better compile-time safety.

This change deprecates the older FunctionCallbackWrapper in favor of a more
intuitive FunctionCallback.Builder that better handles generic types via
ParameterizedTypeReference. It also adds automatic function description
generation as a fallback when none is provided, though explicit descriptions
are still recommended.

The update standardizes function callback handling across all AI model
implementations (OpenAI, Ollama, Minimax, etc.) and improves response
handling with configurable converters.

Core API Enhancements:

- New Builder Interface: Replaced FunctionCallbackWrapper.builder() with
   FunctionCallback.builder(), introducing a hierarchical approach that improves
   customization and type safety.
- Specialized Builders: Introduced FunctionInvokerBuilder for direct Function/BiFunction
   implementations and MethodInvokerBuilder for reflection-based invocations.
- Generic Type Support: Added ParameterizedTypeReference for better handling of generic parameters.
- Unified Method Definition: Merged method() and argumentTypes() into a single method() call
   for simplicity and type safety.
- Automatic Descriptions: Implemented auto-generation of function descriptions, with warnings
   to encourage explicit descriptions.
- Configurable Response Converters: Enhanced response handling with support for custom
   converters, reducing unnecessary JSON conversions.

Architecture Improvements:

- Established common Builder interface for shared properties
- Separated function object handling from constructor
- Added method-specific configuration (name, arg types, target)
- Added JSON schema generation support for ResolvableType
- Moved to standardized schema types across AI providers
- Set OPEN_API_SCHEMA as default for Vertex AI Gemini

Builder Pattern Standardization:

- Standardized builder method ordering across implementations
- Moved function() call after description() for consistency
- Improved function callback configuration with unified patterns
- Enhanced error handling and validation in DefaultFunctionCallbackBuilder

Deprecations:

- FunctionCallbackWrapper.Builder replaced by DefaultFunctionCallbackBuilder
- Removed CustomizedTypeReference in favor of ParameterizedTypeReference
- Deprecated older ChatClient API methods for function handling

Testing & Documentation:

- Updated all AI model implementations (OpenAI, Ollama, Minimax, Moonshot, ZhiPuAI)
- Added comprehensive integration tests for static/instance methods
- Added integration tests for auto-generated descriptions
- Updated documentation to reflect new builder pattern usage
- Added Kotlin extension for inputType() support

Co-authored-by: Sébastien Deleuze <sebastien.deleuze@broadcom.com>
This commit is contained in:
Christian Tzolov
2024-11-13 12:58:52 +01:00
committed by Mark Pollack
parent 72c84fe8b8
commit fb0d99dc37
78 changed files with 1704 additions and 918 deletions

View File

@@ -48,7 +48,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
@@ -256,10 +256,11 @@ class AnthropicChatModelIT {
var promptOptions = AnthropicChatOptions.builder()
.withModel(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getName())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription(
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description(
"Get the weather in location. Return temperature in 36°F or 36°C format. Use multi-turn if needed.")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -283,10 +284,11 @@ class AnthropicChatModelIT {
var promptOptions = AnthropicChatOptions.builder()
.withModel(AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getName())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription(
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description(
"Get the weather in location. Return temperature in 36°F or 36°C format. Use multi-turn if needed.")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -42,6 +42,7 @@ import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
@@ -210,8 +211,30 @@ class AnthropicChatClientIT {
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius."))
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.functions(FunctionCallback.builder()
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.call()
.content();
// @formatter:on
logger.info("Response: {}", response);
assertThat(response).contains("30", "10", "15");
}
@Test
void functionCallWithGeneratedDescription() {
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.functions(FunctionCallback.builder()
.function("getCurrentWeatherInLocation", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.call()
.content();
// @formatter:on
@@ -226,7 +249,11 @@ class AnthropicChatClientIT {
// @formatter:off
String response = ChatClient.builder(this.chatModel)
.defaultFunction("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.defaultFunctions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.defaultUser(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius."))
.build()
.prompt()
@@ -245,7 +272,11 @@ class AnthropicChatClientIT {
// @formatter:off
Flux<String> response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.stream()
.content();
// @formatter:on

View File

@@ -29,11 +29,10 @@ import org.springframework.ai.anthropic.AnthropicTestConfiguration;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.model.function.MethodFunctionCallback;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@@ -41,9 +40,10 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@SpringBootTest(classes = AnthropicTestConfiguration.class, properties = "spring.ai.retry.on-http-codes=429")
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+")
@ActiveProfiles("logging-test")
class AnthropicChatClientMethodFunctionCallbackIT {
class AnthropicChatClientMethodInvokingFunctionCallbackIT {
private static final Logger logger = LoggerFactory.getLogger(AnthropicChatClientMethodFunctionCallbackIT.class);
private static final Logger logger = LoggerFactory
.getLogger(AnthropicChatClientMethodInvokingFunctionCallbackIT.class);
public static Map<String, Object> arguments = new ConcurrentHashMap<>();
@@ -53,15 +53,34 @@ class AnthropicChatClientMethodFunctionCallbackIT {
}
@Test
void methodGetWeatherStatic() {
void methodGetWeatherGeneratedDescription() {
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "getWeatherStatic", String.class, Unit.class);
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.functions(MethodFunctionCallback.builder()
.method(method)
.functions(FunctionCallback.builder()
.method("getWeatherInLocation", String.class, Unit.class)
.targetClass(TestFunctionClass.class)
.build())
.call()
.content();
// @formatter:on
logger.info("Response: {}", response);
assertThat(response).contains("30", "10", "15");
}
@Test
void methodGetWeatherStatic() {
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.method("getWeatherStatic", String.class, Unit.class)
.targetClass(TestFunctionClass.class)
.build())
.call()
.content();
@@ -77,15 +96,13 @@ class AnthropicChatClientMethodFunctionCallbackIT {
TestFunctionClass targetObject = new TestFunctionClass();
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "turnLight", String.class, boolean.class);
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("Turn light on in the living room.")
.functions(MethodFunctionCallback.builder()
.functionObject(targetObject)
.method(method)
.description("Can turn lights on or off by room name")
.functions(FunctionCallback.builder()
.description("Turn light on in the living room.")
.method("turnLight", String.class, boolean.class)
.targetObject(targetObject)
.build())
.call()
.content();
@@ -102,16 +119,13 @@ class AnthropicChatClientMethodFunctionCallbackIT {
TestFunctionClass targetObject = new TestFunctionClass();
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "getWeatherNonStatic", String.class,
Unit.class);
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.functions(MethodFunctionCallback.builder()
.functionObject(targetObject)
.method(method)
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.method("getWeatherNonStatic",String.class, Unit.class)
.targetObject(targetObject)
.build())
.call()
.content();
@@ -127,17 +141,14 @@ class AnthropicChatClientMethodFunctionCallbackIT {
TestFunctionClass targetObject = new TestFunctionClass();
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "getWeatherWithContext", String.class,
Unit.class, ToolContext.class);
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.functions(MethodFunctionCallback.builder()
.functionObject(targetObject)
.method(method)
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.build())
.method("getWeatherWithContext", String.class, Unit.class, ToolContext.class)
.targetObject(targetObject)
.build())
.toolContext(Map.of("tool", "value"))
.call()
.content();
@@ -154,17 +165,14 @@ class AnthropicChatClientMethodFunctionCallbackIT {
TestFunctionClass targetObject = new TestFunctionClass();
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "getWeatherNonStatic", String.class,
Unit.class);
// @formatter:off
assertThatThrownBy(() -> ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.functions(MethodFunctionCallback.builder()
.functionObject(targetObject)
.method(method)
.description("Get the weather in location")
.build())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.method("getWeatherNonStatic", String.class, Unit.class)
.targetObject(targetObject)
.build())
.toolContext(Map.of("tool", "value"))
.call()
.content())
@@ -178,15 +186,13 @@ class AnthropicChatClientMethodFunctionCallbackIT {
TestFunctionClass targetObject = new TestFunctionClass();
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "turnLivingRoomLightOn");
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("Turn light on in the living room.")
.functions(MethodFunctionCallback.builder()
.functionObject(targetObject)
.method(method)
.functions(FunctionCallback.builder()
.description("Can turn lights on in the Living Room")
.method("turnLivingRoomLightOn")
.targetObject(targetObject)
.build())
.call()
.content();
@@ -215,6 +221,10 @@ class AnthropicChatClientMethodFunctionCallbackIT {
arguments.put("method called", "argumentLessReturnVoid");
}
public static String getWeatherInLocation(String city, Unit unit) {
return getWeatherStatic(city, unit);
}
public static String getWeatherStatic(String city, Unit unit) {
logger.info("City: " + city + " Unit: " + unit);

View File

@@ -39,7 +39,7 @@ 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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
@@ -70,10 +70,10 @@ class AzureOpenAiChatModelFunctionCallIT {
var promptOptions = AzureOpenAiChatOptions.builder()
.withDeploymentName(this.selectedModel)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the current weather in a given location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -94,10 +94,10 @@ class AzureOpenAiChatModelFunctionCallIT {
var promptOptions = AzureOpenAiChatOptions.builder()
.withDeploymentName(this.selectedModel)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the current weather in a given location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -116,10 +116,10 @@ class AzureOpenAiChatModelFunctionCallIT {
var promptOptions = AzureOpenAiChatOptions.builder()
.withDeploymentName(this.selectedModel)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the current weather in a given location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -153,10 +153,10 @@ class AzureOpenAiChatModelFunctionCallIT {
var promptOptions = AzureOpenAiChatOptions.builder()
.withDeploymentName(this.selectedModel)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the current weather in a given location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -37,6 +37,7 @@ import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -212,7 +213,11 @@ class BedrockConverseChatClientIT {
// @formatter:off
String response = ChatClient.create(this.chatModel)
.prompt("What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.")
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.call()
.content();
// @formatter:on
@@ -228,7 +233,11 @@ class BedrockConverseChatClientIT {
// @formatter:off
String response = ChatClient.create(this.chatModel)
.prompt("What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.")
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.advisors(new SimpleLoggerAdvisor())
.call()
.content();
@@ -244,7 +253,11 @@ class BedrockConverseChatClientIT {
// @formatter:off
String response = ChatClient.builder(this.chatModel)
.defaultFunction("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.defaultFunctions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.defaultUser(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."))
.build()
.prompt()
@@ -263,7 +276,11 @@ class BedrockConverseChatClientIT {
// @formatter:off
Flux<String> response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.")
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.stream()
.content();
// @formatter:on
@@ -280,7 +297,11 @@ class BedrockConverseChatClientIT {
// @formatter:off
Flux<String> response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in Paris? Return the temperature in Celsius.")
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.stream()
.content();
// @formatter:on

View File

@@ -47,7 +47,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -254,10 +254,11 @@ class BedrockProxyChatModelIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = FunctionCallingOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription(
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description(
"Get the weather in location. Return temperature in 36°F or 36°C format. Use multi-turn if needed.")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -281,10 +282,11 @@ class BedrockProxyChatModelIT {
var promptOptions = FunctionCallingOptions.builder()
.withModel("anthropic.claude-3-5-sonnet-20240620-v1:0")
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription(
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description(
"Get the weather in location. Return temperature in 36°F or 36°C format. Use multi-turn if needed.")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -26,7 +26,7 @@ import software.amazon.awssdk.services.bedrockruntime.model.ConverseStreamOutput
import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
import org.springframework.ai.bedrock.converse.MockWeatherService;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptionsBuilder.PortableFunctionCallingOptions;
/**
@@ -52,9 +52,10 @@ public final class BedrockConverseChatModelMain2 {
"What's the weather like in Paris? Return the temperature in Celsius.",
PortableFunctionCallingOptions.builder()
.withModel(modelId)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build());
@@ -68,11 +69,6 @@ public final class BedrockConverseChatModelMain2 {
Flux<ConverseStreamOutput> responses = chatModel.converseStream(streamRequest);
List<ConverseStreamOutput> responseList = responses.collectList().block();
System.out.println(responseList);
// Flux<ChatResponse> responses2 = ConverseApiUtils.toChatResponse(responses);
// List<ChatResponse> responseList2 = responses2.collectList().block();
// System.out.println(responseList2);
}
}

View File

@@ -23,7 +23,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.minimax.api.MockWeatherService;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import static org.assertj.core.api.Assertions.assertThat;
@@ -67,10 +67,10 @@ public class ChatCompletionRequestTests {
var request = client.createRequest(new Prompt("Test message content",
MiniMaxChatOptions.builder()
.withModel("PROMPT_MODEL")
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build()),
false);
@@ -94,10 +94,10 @@ public class ChatCompletionRequestTests {
var client = new MiniMaxChatModel(new MiniMaxApi("TEST"),
MiniMaxChatOptions.builder()
.withModel("DEFAULT_MODEL")
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build());
@@ -126,9 +126,10 @@ public class ChatCompletionRequestTests {
// Override the default options function with one from the prompt
request = client.createRequest(new Prompt("Test message content",
MiniMaxChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Overridden function description")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Overridden function description")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build()),
false);

View File

@@ -34,6 +34,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.mistralai.api.MistralAiApi;
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
@@ -224,7 +225,11 @@ class MistralAiChatClientIT {
String response = ChatClient.create(this.chatModel).prompt()
.options(MistralAiChatOptions.builder().withModel(MistralAiApi.ChatModel.SMALL).withToolChoice(ToolChoice.AUTO).build())
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris? Use parallel function calling if required. Response should be in Celsius."))
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.call()
.content();
// @formatter:on
@@ -242,7 +247,11 @@ class MistralAiChatClientIT {
// @formatter:off
String response = ChatClient.builder(this.chatModel)
.defaultOptions(MistralAiChatOptions.builder().withModel(MistralAiApi.ChatModel.SMALL).build())
.defaultFunction("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.defaultFunctions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.defaultUser(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris? Use parallel function calling if required. Response should be in Celsius."))
.build()
.prompt().call().content();
@@ -262,7 +271,11 @@ class MistralAiChatClientIT {
Flux<String> response = ChatClient.create(this.chatModel).prompt()
.options(MistralAiChatOptions.builder().withModel(MistralAiApi.ChatModel.SMALL).build())
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use parallel function calling if required. Response should be in Celsius.")
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.stream()
.content();
// @formatter:on

View File

@@ -42,7 +42,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.mistralai.api.MistralAiApi;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
@@ -193,10 +193,10 @@ class MistralAiChatModelIT {
var promptOptions = MistralAiChatOptions.builder()
.withModel(MistralAiApi.ChatModel.SMALL.getValue())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -216,10 +216,10 @@ class MistralAiChatModelIT {
var promptOptions = MistralAiChatOptions.builder()
.withModel(MistralAiApi.ChatModel.SMALL.getValue())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -34,7 +34,7 @@ import org.springframework.ai.chat.model.ChatModel;
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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.moonshot.MoonshotTestConfiguration;
import org.springframework.ai.moonshot.api.MockWeatherService;
@@ -63,10 +63,10 @@ class MoonshotChatModelFunctionCallingIT {
var promptOptions = MoonshotChatOptions.builder()
.withModel(MoonshotApi.ChatModel.MOONSHOT_V1_8K.getValue())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -86,11 +86,9 @@ class MoonshotChatModelFunctionCallingIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = MoonshotChatOptions.builder()
// .withModel(OpenAiApi.ChatModel.GPT_4_TURBO_PREVIEW.getValue())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.build()))
.build();

View File

@@ -33,7 +33,7 @@ import org.springframework.ai.chat.model.ChatModel;
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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.ollama.api.tool.MockWeatherService;
@@ -63,11 +63,11 @@ class OllamaChatModelFunctionCallingIT extends BaseOllamaIT {
var promptOptions = OllamaOptions.builder()
.withModel(MODEL)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription(
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description(
"Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -88,11 +88,11 @@ class OllamaChatModelFunctionCallingIT extends BaseOllamaIT {
var promptOptions = OllamaOptions.builder()
.withModel(MODEL)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription(
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description(
"Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -21,7 +21,7 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.tool.MockWeatherService;
@@ -67,10 +67,10 @@ public class ChatCompletionRequestTests {
var request = client.createRequest(new Prompt("Test message content",
OpenAiChatOptions.builder()
.withModel("PROMPT_MODEL")
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build()),
false);
@@ -94,10 +94,10 @@ public class ChatCompletionRequestTests {
var client = new OpenAiChatModel(new OpenAiApi("TEST"),
OpenAiChatOptions.builder()
.withModel("DEFAULT_MODEL")
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build());
@@ -126,9 +126,10 @@ public class ChatCompletionRequestTests {
// Override the default options function with one from the prompt
request = client.createRequest(new Prompt("Test message content",
OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Overridden function description")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Overridden function description")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build()),
false);

View File

@@ -36,7 +36,7 @@ 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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
@@ -63,10 +63,10 @@ class OpenAiChatModelFunctionCallingIT {
void functionCallTest() {
functionCallTest(OpenAiChatOptions.builder()
.withModel(OpenAiApi.ChatModel.GPT_4_O.getValue())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build());
}
@@ -99,10 +99,10 @@ class OpenAiChatModelFunctionCallingIT {
functionCallTest(OpenAiChatOptions.builder()
.withModel(OpenAiApi.ChatModel.GPT_4_O.getValue())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(biFunction)
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", biFunction)
.inputType(MockWeatherService.Request.class)
.build()))
.withToolContext(Map.of("sessionId", "123"))
.build());
@@ -125,10 +125,11 @@ class OpenAiChatModelFunctionCallingIT {
void streamFunctionCallTest() {
streamFunctionCallTest(OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of((FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of((FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
// .responseConverter(response -> "" + response.temp() + response.unit())
.build())))
.build());
}
@@ -160,10 +161,10 @@ class OpenAiChatModelFunctionCallingIT {
};
OpenAiChatOptions promptOptions = OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of((FunctionCallbackWrapper.builder(biFunction)
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of((FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", biFunction)
.inputType(MockWeatherService.Request.class)
.build())))
.withToolContext(Map.of("sessionId", "123"))
.build();

View File

@@ -48,7 +48,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.OpenAiTestConfiguration;
import org.springframework.ai.openai.api.OpenAiApi;
@@ -328,10 +328,10 @@ public class OpenAiChatModelIT extends AbstractIT {
var promptOptions = OpenAiChatOptions.builder()
.withModel(OpenAiApi.ChatModel.GPT_4_O.getValue())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -353,10 +353,10 @@ public class OpenAiChatModelIT extends AbstractIT {
var promptOptions = OpenAiChatOptions.builder()
// .withModel(OpenAiApi.ChatModel.GPT_4_TURBO_PREVIEW.getValue())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -43,7 +43,7 @@ import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.ToolCallHelper;
import org.springframework.ai.model.function.FunctionCallingHelper;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
@@ -64,7 +64,7 @@ class OpenAiChatModelProxyToolCallsIT {
private static final String DEFAULT_MODEL = "gpt-4o-mini";
FunctionCallback functionDefinition = new ToolCallHelper.FunctionDefinition("getWeatherInLocation",
FunctionCallback functionDefinition = new FunctionCallingHelper.FunctionDefinition("getWeatherInLocation",
"Get the weather in location", """
{
"type": "object",
@@ -87,7 +87,7 @@ class OpenAiChatModelProxyToolCallsIT {
// Helper class that reuses some of the {@link AbstractToolCallSupport} functionality
// to help to implement the function call handling logic on the client side.
private ToolCallHelper toolCallHelper = new ToolCallHelper();
private FunctionCallingHelper functionCallingHelper = new FunctionCallingHelper();
@SuppressWarnings("unchecked")
private static Map<String, String> getFunctionArguments(String functionArguments) {
@@ -139,7 +139,7 @@ class OpenAiChatModelProxyToolCallsIT {
// Note that the tool call check could be platform specific because the finish
// reasons.
isToolCall = this.toolCallHelper.isToolCall(chatResponse,
isToolCall = this.functionCallingHelper.isToolCall(chatResponse,
Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(),
OpenAiApi.ChatCompletionFinishReason.STOP.name()));
@@ -176,7 +176,7 @@ class OpenAiChatModelProxyToolCallsIT {
ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of());
List<Message> toolCallConversation = this.toolCallHelper
List<Message> toolCallConversation = this.functionCallingHelper
.buildToolCallConversation(prompt.getInstructions(), assistantMessage, toolMessageResponse);
assertThat(toolCallConversation).isNotEmpty();
@@ -236,7 +236,7 @@ class OpenAiChatModelProxyToolCallsIT {
return chatResponses.flatMap(chatResponse -> {
boolean isToolCall = this.toolCallHelper.isToolCall(chatResponse, finishReasons);
boolean isToolCall = this.functionCallingHelper.isToolCall(chatResponse, finishReasons);
if (isToolCall) {
@@ -261,7 +261,7 @@ class OpenAiChatModelProxyToolCallsIT {
ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of());
List<Message> toolCallConversation = this.toolCallHelper
List<Message> toolCallConversation = this.functionCallingHelper
.buildToolCallConversation(prompt.getInstructions(), assistantMessage, toolMessageResponse);
assertThat(toolCallConversation).isNotEmpty();
@@ -285,7 +285,7 @@ class OpenAiChatModelProxyToolCallsIT {
var prompt = new Prompt(messages, promptOptions);
ChatResponse chatResponse = this.toolCallHelper.processCall(this.chatModel, prompt,
ChatResponse chatResponse = this.functionCallingHelper.processCall(this.chatModel, prompt,
Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(),
OpenAiApi.ChatCompletionFinishReason.STOP.name()),
toolCall -> {
@@ -319,7 +319,7 @@ class OpenAiChatModelProxyToolCallsIT {
var prompt = new Prompt(messages, promptOptions);
Flux<ChatResponse> responses = this.toolCallHelper.processStream(this.chatModel, prompt,
Flux<ChatResponse> responses = this.functionCallingHelper.processStream(this.chatModel, prompt,
Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(),
OpenAiApi.ChatCompletionFinishReason.STOP.name()),
toolCall -> {

View File

@@ -37,6 +37,7 @@ import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.OpenAiTestConfiguration;
import org.springframework.ai.openai.api.OpenAiApi;
@@ -244,10 +245,16 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void functionCallTest() {
FunctionCallback functionCallback = FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build();
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.functions(functionCallback)
.call()
.content();
// @formatter:on
@@ -262,7 +269,11 @@ class OpenAiChatClientIT extends AbstractIT {
// @formatter:off
String response = ChatClient.builder(this.chatModel)
.defaultFunction("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.defaultFunctions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.defaultUser(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.build()
.prompt().call().content();
@@ -279,7 +290,11 @@ class OpenAiChatClientIT extends AbstractIT {
// @formatter:off
Flux<String> response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris?")
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.stream()
.content();
// @formatter:on

View File

@@ -28,12 +28,11 @@ import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.model.function.MethodFunctionCallback;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiTestConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@@ -41,9 +40,10 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@SpringBootTest(classes = OpenAiTestConfiguration.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
@ActiveProfiles("logging-test")
class OpenAiChatClientMethodFunctionCallbackIT {
class OpenAiChatClientMethodInvokingFunctionCallbackIT {
private static final Logger logger = LoggerFactory.getLogger(OpenAiChatClientMethodFunctionCallbackIT.class);
private static final Logger logger = LoggerFactory
.getLogger(OpenAiChatClientMethodInvokingFunctionCallbackIT.class);
public static Map<String, Object> arguments = new ConcurrentHashMap<>();
@@ -57,14 +57,13 @@ class OpenAiChatClientMethodFunctionCallbackIT {
@Test
void methodGetWeatherStatic() {
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "getWeatherStatic", String.class, Unit.class);
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.functions(MethodFunctionCallback.builder()
.method(method)
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.method("getWeatherStatic",String.class, Unit.class)
.targetClass(TestFunctionClass.class)
.build())
.call()
.content();
@@ -80,15 +79,13 @@ class OpenAiChatClientMethodFunctionCallbackIT {
TestFunctionClass targetObject = new TestFunctionClass();
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "turnLight", String.class, boolean.class);
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("Turn light on in the living room.")
.functions(MethodFunctionCallback.builder()
.functionObject(targetObject)
.method(method)
.functions(FunctionCallback.builder()
.description("Can turn lights on or off by room name")
.method("turnLight", String.class, boolean.class)
.targetObject(targetObject)
.build())
.call()
.content();
@@ -105,16 +102,13 @@ class OpenAiChatClientMethodFunctionCallbackIT {
TestFunctionClass targetObject = new TestFunctionClass();
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "getWeatherNonStatic", String.class,
Unit.class);
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.functions(MethodFunctionCallback.builder()
.functionObject(targetObject)
.method(method)
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.method("getWeatherNonStatic",String.class, Unit.class)
.targetObject(targetObject)
.build())
.call()
.content();
@@ -130,16 +124,13 @@ class OpenAiChatClientMethodFunctionCallbackIT {
TestFunctionClass targetObject = new TestFunctionClass();
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "getWeatherWithContext", String.class,
Unit.class, ToolContext.class);
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.functions(MethodFunctionCallback.builder()
.functionObject(targetObject)
.method(method)
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.method("getWeatherWithContext", String.class, Unit.class, ToolContext.class)
.targetObject(targetObject)
.build())
.toolContext(Map.of("tool", "value"))
.call()
@@ -157,17 +148,14 @@ class OpenAiChatClientMethodFunctionCallbackIT {
TestFunctionClass targetObject = new TestFunctionClass();
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "getWeatherNonStatic", String.class,
Unit.class);
// @formatter:off
assertThatThrownBy(() -> ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.functions(MethodFunctionCallback.builder()
.functionObject(targetObject)
.method(method)
.description("Get the weather in location")
.build())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.method("getWeatherNonStatic", String.class, Unit.class)
.targetObject(targetObject)
.build())
.toolContext(Map.of("tool", "value"))
.call()
.content())
@@ -181,16 +169,14 @@ class OpenAiChatClientMethodFunctionCallbackIT {
TestFunctionClass targetObject = new TestFunctionClass();
var method = ReflectionUtils.findMethod(TestFunctionClass.class, "turnLivingRoomLightOn");
// @formatter:off
String response = ChatClient.create(this.chatModel).prompt()
.user("Turn light on in the living room.")
.functions(MethodFunctionCallback.builder()
.functionObject(targetObject)
.method(method)
.functions(FunctionCallback.builder()
.description("Can turn lights on in the Living Room")
.build())
.method("turnLivingRoomLightOn")
.targetObject(targetObject)
.build())
.call()
.content();
// @formatter:on

View File

@@ -31,6 +31,7 @@ import reactor.core.publisher.Flux;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiTestConfiguration;
import org.springframework.ai.openai.api.tool.MockWeatherService;
import org.springframework.ai.openai.api.tool.MockWeatherService.Request;
@@ -83,7 +84,11 @@ class OpenAiChatClientMultipleFunctionCallsIT extends AbstractIT {
// @formatter:off
response = chatClientBuilder.build().prompt()
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.call()
.content();
// @formatter:on
@@ -110,7 +115,11 @@ class OpenAiChatClientMultipleFunctionCallsIT extends AbstractIT {
// @formatter:off
String response = ChatClient.builder(this.chatModel)
.defaultFunction("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.defaultFunctions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.defaultUser(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.build()
.prompt().call().content();
@@ -149,7 +158,11 @@ class OpenAiChatClientMultipleFunctionCallsIT extends AbstractIT {
// @formatter:off
String response = ChatClient.builder(this.chatModel)
.defaultFunction("getCurrentWeather", "Get the weather in location", biFunction)
.defaultFunctions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", biFunction)
.inputType(MockWeatherService.Request.class)
.build())
.defaultUser(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.defaultToolContext(Map.of("sessionId", "123"))
.build()
@@ -189,7 +202,11 @@ class OpenAiChatClientMultipleFunctionCallsIT extends AbstractIT {
// @formatter:off
String response = ChatClient.builder(this.chatModel)
.defaultFunction("getCurrentWeather", "Get the weather in location", biFunction)
.defaultFunctions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", biFunction)
.inputType(MockWeatherService.Request.class)
.build())
.defaultUser(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.build()
.prompt()
@@ -208,7 +225,11 @@ class OpenAiChatClientMultipleFunctionCallsIT extends AbstractIT {
// @formatter:off
Flux<String> response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris?")
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.stream()
.content();
// @formatter:on

View File

@@ -39,7 +39,7 @@ import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.ToolCallHelper;
import org.springframework.ai.model.function.FunctionCallingHelper;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.OpenAiTestConfiguration;
@@ -64,7 +64,7 @@ class OpenAiChatClientProxyFunctionCallsIT extends AbstractIT {
@Value("classpath:/prompts/system-message.st")
private Resource systemTextResource;
FunctionCallback functionDefinition = new ToolCallHelper.FunctionDefinition("getWeatherInLocation",
FunctionCallback functionDefinition = new FunctionCallingHelper.FunctionDefinition("getWeatherInLocation",
"Get the weather in location", """
{
"type": "object",
@@ -87,7 +87,7 @@ class OpenAiChatClientProxyFunctionCallsIT extends AbstractIT {
// Helper class that reuses some of the {@link AbstractToolCallSupport} functionality
// to help to implement the function call handling logic on the client side.
private ToolCallHelper toolCallHelper = new ToolCallHelper();
private FunctionCallingHelper functionCallingHelper = new FunctionCallingHelper();
// Function which will be called by the AI model.
private String getWeatherInLocation(String location, String unit) {
@@ -130,7 +130,7 @@ class OpenAiChatClientProxyFunctionCallsIT extends AbstractIT {
// Note that the tool call check could be platform specific because the finish
// reasons.
isToolCall = this.toolCallHelper.isToolCall(chatResponse,
isToolCall = this.functionCallingHelper.isToolCall(chatResponse,
Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(),
OpenAiApi.ChatCompletionFinishReason.STOP.name()));
@@ -167,7 +167,7 @@ class OpenAiChatClientProxyFunctionCallsIT extends AbstractIT {
ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of());
messages = this.toolCallHelper.buildToolCallConversation(messages, assistantMessage,
messages = this.functionCallingHelper.buildToolCallConversation(messages, assistantMessage,
toolMessageResponse);
assertThat(messages).isNotEmpty();

View File

@@ -46,7 +46,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
@@ -249,10 +249,10 @@ class GroqWithOpenAiChatModelIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -272,10 +272,10 @@ class GroqWithOpenAiChatModelIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -46,7 +46,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
@@ -251,10 +251,10 @@ class MistralWithOpenAiChatModelIT {
var promptOptions = OpenAiChatOptions.builder()
.withModel(modelName)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -276,10 +276,10 @@ class MistralWithOpenAiChatModelIT {
var promptOptions = OpenAiChatOptions.builder()
.withModel(modelName)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -41,7 +41,7 @@ import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
@@ -246,10 +246,10 @@ class NvidiaWithOpenAiChatModelIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -269,10 +269,10 @@ class NvidiaWithOpenAiChatModelIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -49,7 +49,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
@@ -268,10 +268,10 @@ class OllamaWithOpenAiChatModelIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -292,10 +292,9 @@ class OllamaWithOpenAiChatModelIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.build()))
.build();

View File

@@ -32,7 +32,7 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel.GeminiRequest;
import org.springframework.ai.vertexai.gemini.function.MockWeatherService;
import org.springframework.util.MimeTypeUtils;
@@ -117,10 +117,10 @@ public class CreateGeminiRequestTests {
var request = client.createGeminiRequest(new Prompt("Test message content",
VertexAiGeminiChatOptions.builder()
.withModel("PROMPT_MODEL")
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build()),
null);
@@ -145,10 +145,10 @@ public class CreateGeminiRequestTests {
var client = new VertexAiGeminiChatModel(this.vertexAI,
VertexAiGeminiChatOptions.builder()
.withModel("DEFAULT_MODEL")
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build());
@@ -178,9 +178,10 @@ public class CreateGeminiRequestTests {
// Override the default options function with one from the prompt
request = client.createGeminiRequest(new Prompt("Test message content",
VertexAiGeminiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Overridden function description")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Overridden function description")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build()),
null);

View File

@@ -35,8 +35,8 @@ 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.function.FunctionCallbackContext.SchemaType;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
import org.springframework.beans.factory.annotation.Autowired;
@@ -83,11 +83,11 @@ public class VertexAiGeminiChatModelFunctionCallingIT {
""";
var promptOptions = VertexAiGeminiChatOptions.builder()
// .withModel(VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_FLASH)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("get_current_weather")
.withDescription("Get the current weather in a given location")
.withInputTypeSchema(openApiSchema)
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the current weather in a given location")
.inputTypeSchema(openApiSchema)
.function("get_current_weather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -108,16 +108,18 @@ public class VertexAiGeminiChatModelFunctionCallingIT {
var promptOptions = VertexAiGeminiChatOptions.builder()
.withModel(VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_FLASH)
.withFunctionCallbacks(List.of(
FunctionCallbackWrapper.builder(new MockWeatherService())
.withSchemaType(SchemaType.OPEN_API_SCHEMA)
.withName("get_current_weather")
.withDescription("Get the current weather in a given location.")
FunctionCallback.builder()
.schemaType(SchemaType.OPEN_API_SCHEMA)
.description("Get the current weather in a given location.")
.function("get_current_weather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build(),
FunctionCallbackWrapper.builder(new PaymentStatus())
.withSchemaType(SchemaType.OPEN_API_SCHEMA)
.withName("get_payment_status")
.withDescription(
FunctionCallback.builder()
.schemaType(SchemaType.OPEN_API_SCHEMA)
.description(
"Retrieves the payment status for transaction. For example what is the payment status for transaction 700?")
.function("get_payment_status", new PaymentStatus())
.inputType(PaymentInfoRequest.class)
.build()))
.build();
@@ -147,16 +149,18 @@ public class VertexAiGeminiChatModelFunctionCallingIT {
var promptOptions = VertexAiGeminiChatOptions.builder()
.withModel(VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_FLASH)
.withFunctionCallbacks(List.of(
FunctionCallbackWrapper.builder(new MockWeatherService())
.withSchemaType(SchemaType.OPEN_API_SCHEMA)
.withName("get_current_weather")
.withDescription("Get the current weather in a given location.")
FunctionCallback.builder()
.schemaType(SchemaType.OPEN_API_SCHEMA)
.description("Get the current weather in a given location.")
.function("get_current_weather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build(),
FunctionCallbackWrapper.builder(new PaymentStatus())
.withSchemaType(SchemaType.OPEN_API_SCHEMA)
.withName("get_payment_status")
.withDescription(
FunctionCallback.builder()
.schemaType(SchemaType.OPEN_API_SCHEMA)
.description(
"Retrieves the payment status for transaction. For example what is the payment status for transaction 700?")
.function("get_payment_status", new PaymentStatus())
.inputType(PaymentInfoRequest.class)
.build()))
.build();
@@ -185,10 +189,11 @@ public class VertexAiGeminiChatModelFunctionCallingIT {
var promptOptions = VertexAiGeminiChatOptions.builder()
.withModel(VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_FLASH)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withSchemaType(SchemaType.OPEN_API_SCHEMA)
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.schemaType(SchemaType.OPEN_API_SCHEMA)
.description("Get the current weather in a given location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -21,7 +21,7 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.zhipuai.api.MockWeatherService;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
@@ -67,10 +67,10 @@ public class ChatCompletionRequestTests {
var request = client.createRequest(new Prompt("Test message content",
ZhiPuAiChatOptions.builder()
.withModel("PROMPT_MODEL")
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build()),
false);
@@ -94,10 +94,10 @@ public class ChatCompletionRequestTests {
var client = new ZhiPuAiChatModel(new ZhiPuAiApi("TEST"),
ZhiPuAiChatOptions.builder()
.withModel("DEFAULT_MODEL")
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build());
@@ -126,9 +126,10 @@ public class ChatCompletionRequestTests {
// Override the default options function with one from the prompt
request = client.createRequest(new Prompt("Test message content",
ZhiPuAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Overridden function description")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Overridden function description")
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build()),
false);

View File

@@ -47,7 +47,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import org.springframework.ai.zhipuai.ZhiPuAiTestConfiguration;
import org.springframework.ai.zhipuai.api.MockWeatherService;
@@ -230,10 +230,10 @@ class ZhiPuAiChatModelIT {
var promptOptions = ZhiPuAiChatOptions.builder()
.withModel(ZhiPuAiApi.ChatModel.GLM_4.getValue())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -256,10 +256,10 @@ class ZhiPuAiChatModelIT {
var promptOptions = ZhiPuAiChatOptions.builder()
.withModel(ZhiPuAiApi.ChatModel.GLM_4.getValue())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -212,14 +212,26 @@ public interface ChatClient {
<T extends ChatOptions> ChatClientRequestSpec options(T options);
/**
* @deprecated use {@link #function(FunctionCallback)} instead.
*/
@Deprecated
<I, O> ChatClientRequestSpec function(String name, String description,
java.util.function.Function<I, O> function);
/**
* @deprecated use {@link #function(FunctionCallback)} instead.
*/
@Deprecated
<I, O> ChatClientRequestSpec function(String name, String description,
java.util.function.BiFunction<I, ToolContext, O> function);
<I, O> ChatClientRequestSpec functions(FunctionCallback... functionCallbacks);
/**
* @deprecated use {@link #function(FunctionCallback)} instead.
*/
@Deprecated
<I, O> ChatClientRequestSpec function(String name, String description, Class<I> inputType,
java.util.function.Function<I, O> function);
@@ -278,8 +290,16 @@ public interface ChatClient {
Builder defaultSystem(Consumer<PromptSystemSpec> systemSpecConsumer);
/**
* @deprecated use {@link #defaultFunction(FunctionCallback)} instead.
*/
@Deprecated
<I, O> Builder defaultFunction(String name, String description, java.util.function.Function<I, O> function);
/**
* @deprecated use {@link #defaultFunction(FunctionCallback)} instead.
*/
@Deprecated
<I, O> Builder defaultFunction(String name, String description,
java.util.function.BiFunction<I, ToolContext, O> function);

View File

@@ -836,19 +836,14 @@ public class DefaultChatClient implements ChatClient {
return this;
}
@Override
public <I, O> ChatClientRequestSpec function(String name, String description,
java.util.function.Function<I, O> function) {
return this.function(name, description, null, function);
}
public <I, O> ChatClientRequestSpec function(String name, String description,
java.util.function.BiFunction<I, ToolContext, O> biFunction) {
Assert.hasText(name, "name cannot be null or empty");
Assert.hasText(description, "description cannot be null or empty");
Assert.notNull(biFunction, "biFunction cannot be null");
Assert.notNull(function, "function cannot be null");
FunctionCallbackWrapper<I, O> fcw = FunctionCallbackWrapper.builder(biFunction)
var fcw = FunctionCallbackWrapper.builder(function)
.withDescription(description)
.withName(name)
.withResponseConverter(Object::toString)
@@ -857,6 +852,24 @@ public class DefaultChatClient implements ChatClient {
return this;
}
@Override
public <I, O> ChatClientRequestSpec function(String name, String description,
java.util.function.BiFunction<I, ToolContext, O> biFunction) {
Assert.hasText(name, "name cannot be null or empty");
Assert.hasText(description, "description cannot be null or empty");
Assert.notNull(biFunction, "biFunction cannot be null");
var fcw = FunctionCallbackWrapper.builder(biFunction)
.withDescription(description)
.withName(name)
.withResponseConverter(Object::toString)
.build();
this.functionCallbacks.add(fcw);
return this;
}
@Override
public <I, O> ChatClientRequestSpec function(String name, String description, @Nullable Class<I> inputType,
java.util.function.Function<I, O> function) {
@@ -864,11 +877,11 @@ public class DefaultChatClient implements ChatClient {
Assert.hasText(description, "description cannot be null or empty");
Assert.notNull(function, "function cannot be null");
var fcw = FunctionCallbackWrapper.builder(function)
.withDescription(description)
.withName(name)
.withInputType(inputType)
.withResponseConverter(Object::toString)
var fcw = FunctionCallback.builder()
.description(description)
.responseConverter(Object::toString)
.function(name, function)
.inputType(inputType)
.build();
this.functionCallbacks.add(fcw);
return this;

View File

@@ -63,7 +63,7 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
/**
* The target class type reference to which the output will be converted.
*/
private final TypeReference<T> typeRef;
private final Type type;
/** The object mapper used for deserialization and other JSON operations. */
private final ObjectMapper objectMapper;
@@ -94,7 +94,7 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
* @param typeRef The target class type reference.
*/
public BeanOutputConverter(ParameterizedTypeReference<T> typeRef) {
this(new CustomizedTypeReference<>(typeRef), null);
this(typeRef.getType(), null);
}
/**
@@ -105,19 +105,19 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
* @param objectMapper Custom object mapper for JSON operations. endings.
*/
public BeanOutputConverter(ParameterizedTypeReference<T> typeRef, ObjectMapper objectMapper) {
this(new CustomizedTypeReference<>(typeRef), objectMapper);
this(typeRef.getType(), objectMapper);
}
/**
* Constructor to initialize with the target class type reference, a custom object
* mapper, and a line endings normalizer to ensure consistent line endings on any
* platform.
* @param typeRef The target class type reference.
* @param type The target class type.
* @param objectMapper Custom object mapper for JSON operations. endings.
*/
private BeanOutputConverter(TypeReference<T> typeRef, ObjectMapper objectMapper) {
Objects.requireNonNull(typeRef, "Type reference cannot be null;");
this.typeRef = typeRef;
private BeanOutputConverter(Type type, ObjectMapper objectMapper) {
Objects.requireNonNull(type, "Type cannot be null;");
this.type = type;
this.objectMapper = objectMapper != null ? objectMapper : getObjectMapper();
generateSchema();
}
@@ -135,7 +135,7 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
.with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT);
SchemaGeneratorConfig config = configBuilder.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonNode = generator.generateSchema(this.typeRef.getType());
JsonNode jsonNode = generator.generateSchema(this.type);
ObjectWriter objectWriter = this.objectMapper.writer(new DefaultPrettyPrinter()
.withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator())));
try {
@@ -143,16 +143,17 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
}
catch (JsonProcessingException e) {
logger.error("Could not pretty print json schema for jsonNode: " + jsonNode);
throw new RuntimeException("Could not pretty print json schema for " + this.typeRef, e);
throw new RuntimeException("Could not pretty print json schema for " + this.type, e);
}
}
@Override
/**
* Parses the given text to transform it to the desired target type.
* @param text The LLM output in string format.
* @return The parsed output in the desired target type.
*/
@SuppressWarnings("unchecked")
@Override
public T convert(@NonNull String text) {
try {
// Remove leading and trailing whitespace
@@ -175,10 +176,10 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
// Trim again to remove any potential whitespace
text = text.trim();
}
return (T) this.objectMapper.readValue(text, this.typeRef);
return (T) this.objectMapper.readValue(text, this.objectMapper.constructType(this.type));
}
catch (JsonProcessingException e) {
logger.error("Could not parse the given text to the desired target type:" + text + " into " + this.typeRef);
logger.error("Could not parse the given text to the desired target type:" + text + " into " + this.type);
throw new RuntimeException(e);
}
}
@@ -220,19 +221,4 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
return this.jsonSchema;
}
private static class CustomizedTypeReference<T> extends TypeReference<T> {
private final Type type;
CustomizedTypeReference(ParameterizedTypeReference<T> typeRef) {
this.type = typeRef.getType();
}
@Override
public Type getType() {
return this.type;
}
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.ai.model;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -334,10 +335,12 @@ public abstract class ModelOptionsUtils {
/**
* Generates JSON Schema (version 2020_12) for the given class.
* @param clazz the class to generate JSON Schema for.
* @param clazz the class to generate JSON Schema from.
* @param toUpperCaseTypeValues if true, the type values are converted to upper case.
* @return the generated JSON Schema as a String.
* @deprecated use {@link #getJsonSchema(Type, boolean)} instead.
*/
@Deprecated
public static String getJsonSchema(Class<?> clazz, boolean toUpperCaseTypeValues) {
if (SCHEMA_GENERATOR_CACHE.get() == null) {
@@ -366,6 +369,40 @@ public abstract class ModelOptionsUtils {
return node.toPrettyString();
}
/**
* Generates JSON Schema (version 2020_12) for the given class.
* @param inputType the input {@link Type} to generate JSON Schema from.
* @param toUpperCaseTypeValues if true, the type values are converted to upper case.
* @return the generated JSON Schema as a String.
*/
public static String getJsonSchema(Type inputType, boolean toUpperCaseTypeValues) {
if (SCHEMA_GENERATOR_CACHE.get() == null) {
JacksonModule jacksonModule = new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED);
Swagger2Module swaggerModule = new Swagger2Module();
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12,
OptionPreset.PLAIN_JSON)
.with(Option.EXTRA_OPEN_API_FORMAT_VALUES)
.with(Option.PLAIN_DEFINITION_KEYS)
.with(swaggerModule)
.with(jacksonModule);
SchemaGeneratorConfig config = configBuilder.build();
SchemaGenerator generator = new SchemaGenerator(config);
SCHEMA_GENERATOR_CACHE.compareAndSet(null, generator);
}
ObjectNode node = SCHEMA_GENERATOR_CACHE.get().generateSchema(inputType);
if (toUpperCaseTypeValues) { // Required for OpenAPI 3.0 (at least Vertex AI
// version of it).
toUpperCaseTypeValues(node);
}
return node.toPrettyString();
}
public static void toUpperCaseTypeValues(ObjectNode node) {
if (node == null) {
return;

View File

@@ -16,6 +16,7 @@
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;
@@ -47,7 +48,7 @@ abstract class AbstractFunctionCallback<I, O> implements BiFunction<I, ToolConte
private final String description;
private final Class<I> inputType;
private final Type inputType;
private final String inputTypeSchema;
@@ -70,7 +71,7 @@ abstract class AbstractFunctionCallback<I, O> implements BiFunction<I, ToolConte
* @param objectMapper Used to convert the function's input and output types to and
* from JSON.
*/
protected AbstractFunctionCallback(String name, String description, String inputTypeSchema, Class<I> inputType,
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");
@@ -116,9 +117,9 @@ abstract class AbstractFunctionCallback<I, O> implements BiFunction<I, ToolConte
return this.andThen(this.responseConverter).apply(request, null);
}
private <T> T fromJson(String json, Class<T> targetClass) {
private <T> T fromJson(String json, Type targetType) {
try {
return this.objectMapper.readValue(json, targetClass);
return this.objectMapper.readValue(json, this.objectMapper.constructType(targetType));
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);

View File

@@ -0,0 +1,284 @@
/*
* 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 java.lang.reflect.Type;
import java.util.Arrays;
import java.util.function.BiFunction;
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.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.Builder;
import org.springframework.ai.model.function.FunctionCallback.FunctionInvokingSpec;
import org.springframework.ai.model.function.FunctionCallback.MethodInvokingSpec;
import org.springframework.ai.model.function.FunctionCallbackContext.SchemaType;
import org.springframework.ai.util.JacksonUtils;
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;
/**
* @author Christian Tzolov
* @since 1.0.0
*/
public class DefaultFunctionCallbackBuilder implements FunctionCallback.Builder {
private final static Logger logger = LoggerFactory.getLogger(DefaultFunctionCallbackBuilder.class);
/**
* The description of the function callback. Used to hint the LLM model about the
* tool's purpose and when to use it.
*/
private 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
*/
private 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.
*/
private 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.
*/
private String inputTypeSchema;
private 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 Builder description(String description) {
Assert.hasText(description, "Description must not be empty");
this.description = description;
return this;
}
@Override
public Builder schemaType(SchemaType schemaType) {
Assert.notNull(schemaType, "SchemaType must not be null");
this.schemaType = schemaType;
return this;
}
@Override
public Builder responseConverter(Function<Object, String> responseConverter) {
Assert.notNull(responseConverter, "ResponseConverter must not be null");
this.responseConverter = responseConverter;
return this;
}
@Override
public Builder inputTypeSchema(String inputTypeSchema) {
Assert.hasText(inputTypeSchema, "InputTypeSchema must not be empty");
this.inputTypeSchema = inputTypeSchema;
return this;
}
@Override
public Builder objectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper;
return this;
}
@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 MethodInvokingSpec method(String methodName, Class<?>... argumentTypes) {
return new DefaultMethodInvokingSpec(methodName, argumentTypes);
}
class DefaultFunctionInvokingSpec<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(objectMapper, "ObjectMapper must not be null");
Assert.hasText(this.name, "Name must not be empty");
Assert.notNull(responseConverter, "ResponseConverter must not be null");
Assert.notNull(this.inputType, "InputType must not be null");
if (inputTypeSchema == null) {
boolean upperCaseTypeValues = schemaType == SchemaType.OPEN_API_SCHEMA;
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.getDescription(), inputTypeSchema,
this.inputType, (Function<I, String>) responseConverter, objectMapper, finalBiFunction);
}
private String getDescription() {
if (StringUtils.hasText(description)) {
return description;
}
return generateDescription(this.name);
}
}
class DefaultMethodInvokingSpec implements FunctionCallback.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(targetClass, methodName, argumentTypes);
Assert.notNull(method,
"Method: '" + methodName + "' with arguments:" + Arrays.toString(argumentTypes) + " not found!");
return new MethodInvokingFunctionCallback(this.targetObject, method, this.getDescription(), objectMapper,
this.name, responseConverter);
}
private String getDescription() {
if (StringUtils.hasText(description)) {
return description;
}
return generateDescription(StringUtils.hasText(this.name) ? this.name : this.methodName);
}
}
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;
}
}

View File

@@ -16,7 +16,14 @@
package org.springframework.ai.model.function;
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.model.function.FunctionCallbackContext.SchemaType;
import org.springframework.core.ParameterizedTypeReference;
/**
* Represents a model function call handler. Implementations are registered with the
@@ -73,4 +80,105 @@ public interface FunctionCallback {
return call(functionInput);
}
/**
* Creates a new {@link FunctionCallback.Builder} instance used to build a default
* {@link FunctionCallback} instance. *
* @return Returns a new {@link FunctionCallback.Builder} instance.
*/
static FunctionCallback.Builder builder() {
return new DefaultFunctionCallbackBuilder();
}
interface Builder {
/**
* Function description. This description is used by the model do decide if the
* function should be called or not.
*/
Builder 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.
*/
Builder 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.
*/
Builder responseConverter(Function<Object, String> responseConverter);
/**
* You can provide the Input Type Schema directly. In this case it won't be
* generated from the inputType.
*/
Builder inputTypeSchema(String inputTypeSchema);
/**
* Custom object mapper for JSON operations.
*/
Builder objectMapper(ObjectMapper objectMapper);
<I, O> FunctionInvokingSpec<I, O> function(String name, Function<I, O> function);
<I, O> FunctionInvokingSpec<I, O> function(String name, BiFunction<I, ToolContext, O> biFunction);
MethodInvokingSpec method(String methodName, Class<?>... argumentTypes);
}
interface 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();
}
interface 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

@@ -105,36 +105,36 @@ public class FunctionCallbackContext implements ApplicationContextAware {
if (KotlinDetector.isKotlinPresent()) {
if (KotlinDelegate.isKotlinFunction(functionType.toClass())) {
return FunctionCallbackWrapper.builder(KotlinDelegate.wrapKotlinFunction(bean))
.withName(beanName)
.withSchemaType(this.schemaType)
.withDescription(functionDescription)
.withInputType(functionInputClass)
return FunctionCallback.builder()
.schemaType(this.schemaType)
.description(functionDescription)
.function(beanName, KotlinDelegate.wrapKotlinFunction(bean))
.inputType(functionInputClass)
.build();
}
else if (KotlinDelegate.isKotlinBiFunction(functionType.toClass())) {
return FunctionCallbackWrapper.builder(KotlinDelegate.wrapKotlinBiFunction(bean))
.withName(beanName)
.withSchemaType(this.schemaType)
.withDescription(functionDescription)
.withInputType(functionInputClass)
return FunctionCallback.builder()
.description(functionDescription)
.schemaType(this.schemaType)
.function(beanName, KotlinDelegate.wrapKotlinBiFunction(bean))
.inputType(functionInputClass)
.build();
}
}
if (bean instanceof Function<?, ?> function) {
return FunctionCallbackWrapper.builder(function)
.withName(beanName)
.withSchemaType(this.schemaType)
.withDescription(functionDescription)
.withInputType(functionInputClass)
return FunctionCallback.builder()
.schemaType(this.schemaType)
.description(functionDescription)
.function(beanName, function)
.inputType(functionInputClass)
.build();
}
else if (bean instanceof BiFunction<?, ?, ?>) {
return FunctionCallbackWrapper.builder((BiFunction<?, ToolContext, ?>) bean)
.withName(beanName)
.withSchemaType(this.schemaType)
.withDescription(functionDescription)
.withInputType(functionInputClass)
return FunctionCallback.builder()
.description(functionDescription)
.schemaType(this.schemaType)
.function(beanName, (BiFunction<?, ToolContext, ?>) bean)
.inputType(functionInputClass)
.build();
}
else {

View File

@@ -16,6 +16,7 @@
package org.springframework.ai.model.function;
import java.lang.reflect.Type;
import java.util.function.BiFunction;
import java.util.function.Function;
@@ -38,32 +39,43 @@ import org.springframework.util.Assert;
*
* @author Christian Tzolov
* @author Sebastien Deleuze
*
*/
public final class FunctionCallbackWrapper<I, O> extends AbstractFunctionCallback<I, O> {
private final BiFunction<I, ToolContext, O> biFunction;
private FunctionCallbackWrapper(String name, String description, String inputTypeSchema, Class<I> inputType,
FunctionCallbackWrapper(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;
}
public static <I, O> Builder<I, O> builder(BiFunction<I, ToolContext, O> biFunction) {
return new Builder<>(biFunction);
}
public static <I, O> Builder<I, O> builder(Function<I, O> function) {
return new Builder<>(function);
}
@Override
public O apply(I input, ToolContext context) {
return this.biFunction.apply(input, context);
}
/**
* @deprecated use {@link FunctionCallback#builder(BiFunction)} instead.
*/
@Deprecated
public static <I, O> Builder<I, O> builder(BiFunction<I, ToolContext, O> biFunction) {
return new Builder<>(biFunction);
}
/**
* @deprecated use {@link FunctionCallback#builder(Function)} instead.
*/
@Deprecated
public static <I, O> Builder<I, O> builder(Function<I, O> function) {
return new Builder<>(function);
}
/**
* @deprecated in favor of {@link DefaultFunctionCallbackBuilder}
*/
@Deprecated
public static class Builder<I, O> {
private final BiFunction<I, ToolContext, O> biFunction;
@@ -85,13 +97,13 @@ public final class FunctionCallbackWrapper<I, O> extends AbstractFunctionCallbac
private ObjectMapper objectMapper;
public Builder(BiFunction<I, ToolContext, O> biFunction) {
private Builder(BiFunction<I, ToolContext, O> biFunction) {
Assert.notNull(biFunction, "Function must not be null");
this.biFunction = biFunction;
this.function = null;
}
public Builder(Function<I, O> function) {
private Builder(Function<I, O> function) {
Assert.notNull(function, "Function must not be null");
this.biFunction = null;
this.function = function;

View File

@@ -42,13 +42,13 @@ import org.springframework.util.CollectionUtils;
* call handling logic on the client side. Used when the withProxyToolCalls(true) option
* is enabled.
*/
public class ToolCallHelper extends AbstractToolCallSupport {
public class FunctionCallingHelper extends AbstractToolCallSupport {
public ToolCallHelper() {
public FunctionCallingHelper() {
this(null, PortableFunctionCallingOptions.builder().build(), List.of());
}
public ToolCallHelper(FunctionCallbackContext functionCallbackContext,
public FunctionCallingHelper(FunctionCallbackContext functionCallbackContext,
FunctionCallingOptions functionCallingOptions, List<FunctionCallback> toolFunctionCallbacks) {
super(functionCallbackContext, functionCallingOptions, toolFunctionCallbacks);
}

View File

@@ -0,0 +1,52 @@
/*
* 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.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.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.
*
* @author Christian Tzolov
*/
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

@@ -20,6 +20,7 @@ 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;
@@ -51,9 +52,9 @@ import org.springframework.util.ReflectionUtils;
* @author Christian Tzolov
* @since 1.0.0
*/
public class MethodFunctionCallback implements FunctionCallback {
public class MethodInvokingFunctionCallback implements FunctionCallback {
private static final Logger logger = LoggerFactory.getLogger(MethodFunctionCallback.class);
private static final Logger logger = LoggerFactory.getLogger(MethodInvokingFunctionCallback.class);
/**
* Object instance that contains the method to be invoked. If the method is static
@@ -87,16 +88,30 @@ public class MethodFunctionCallback implements FunctionCallback {
*/
private boolean isToolContextMethod = false;
public MethodFunctionCallback(Object functionObject, Method method, String description, ObjectMapper mapper) {
/**
* 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!");
@@ -107,12 +122,12 @@ public class MethodFunctionCallback implements FunctionCallback {
this.inputSchema = this.generateJsonSchema(methodParameters);
logger.info("Generated JSON Schema: {}", this.inputSchema);
logger.debug("Generated JSON Schema: {}", this.inputSchema);
}
@Override
public String getName() {
return this.method.getName();
return org.springframework.util.StringUtils.hasText(this.name) ? this.name : this.method.getName();
}
@Override
@@ -165,10 +180,9 @@ public class MethodFunctionCallback implements FunctionCallback {
else if (returnType == Class.class || returnType.isRecord() || returnType == List.class
|| returnType == Map.class) {
return ModelOptionsUtils.toJsonString(response);
}
return "" + response;
return responseConverter.apply(response);
}
catch (Exception e) {
ReflectionUtils.handleReflectionException(e);
@@ -257,53 +271,4 @@ public class MethodFunctionCallback implements FunctionCallback {
}
}
/**
* Creates a new {@link Builder} for the {@link MethodFunctionCallback}.
* @return The builder.
*/
public static MethodFunctionCallback.Builder builder() {
return new Builder();
}
/**
* Builder for the {@link MethodFunctionCallback}.
*/
public static class Builder {
private Method method;
private String description;
private ObjectMapper mapper = ModelOptionsUtils.OBJECT_MAPPER;
private Object functionObject = null;
public MethodFunctionCallback.Builder functionObject(Object functionObject) {
this.functionObject = functionObject;
return this;
}
public MethodFunctionCallback.Builder method(Method method) {
Assert.notNull(method, "Method must not be null");
this.method = method;
return this;
}
public MethodFunctionCallback.Builder description(String description) {
Assert.hasText(description, "Description must not be empty");
this.description = description;
return this;
}
public MethodFunctionCallback.Builder mapper(ObjectMapper mapper) {
this.mapper = mapper;
return this;
}
public MethodFunctionCallback build() {
return new MethodFunctionCallback(this.functionObject, this.method, this.description, this.mapper);
}
}
}

View File

@@ -16,11 +16,14 @@
package org.springframework.ai.model.function
import org.springframework.core.ParameterizedTypeReference
/**
* Extension for [FunctionCallbackWrapper.Builder.withInputType] providing a `withInputType<Foo>()`
* Extension for [FunctionCallback.FunctionInvokerBuilder.inputType] providing a `inputType<Foo>()`
* variant.
*
* @author Sebastien Deleuze
*/
inline fun <reified T> FunctionCallbackWrapper.Builder<*, *>.withInputType() =
withInputType(T::class.java)
inline fun <reified I, reified O> FunctionCallback.FunctionInvokingSpec<I, O>.inputType(): FunctionCallback.FunctionInvokingSpec<I, O> =
inputType(I::class.java)

View File

@@ -27,7 +27,6 @@ import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallingOptions;
import static org.assertj.core.api.Assertions.assertThat;
@@ -80,9 +79,10 @@ public class ChatBuilderTests {
Set<String> functions = new HashSet<>();
String func = "func";
FunctionCallback cb = FunctionCallbackWrapper.<Integer, Integer>builder(i -> i)
.withName("cb")
.withDescription("cb")
FunctionCallback cb = FunctionCallback.builder()
.description("cb")
.function("cb", i -> i)
.inputType(Integer.class)
.build();
functions.add(func);

View File

@@ -40,6 +40,7 @@ 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.Media;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.function.FunctionCallingOptionsBuilder;
import org.springframework.ai.model.function.FunctionCallingOptionsBuilder.PortableFunctionCallingOptions;
@@ -217,7 +218,11 @@ public class ChatClientTest {
.param("param1", "value1")
.param("param2", "value2"))
.defaultFunctions("fun1", "fun2")
.defaultFunction("fun3", "fun3description", mockFunction)
.defaultFunctions(FunctionCallback.builder()
.description("fun3description")
.function("fun3", mockFunction)
.inputType(String.class)
.build())
.defaultUser(u -> u.text("Default user text {uparam1}, {uparam2}")
.param("uparam1", "value1")
.param("uparam2", "value2")
@@ -344,7 +349,11 @@ public class ChatClientTest {
.param("param1", "value1")
.param("param2", "value2"))
.defaultFunctions("fun1", "fun2")
.defaultFunction("fun3", "fun3description", mockFunction)
.defaultFunctions(FunctionCallback.builder()
.description("fun3description")
.function("fun3", mockFunction)
.inputType(String.class)
.build())
.defaultUser(u -> u.text("Default user text {uparam1}, {uparam2}")
.param("uparam1", "value1")
.param("uparam2", "value2")

View File

@@ -1350,31 +1350,39 @@ class DefaultChatClientTests {
assertThat(defaultSpec.getChatOptions()).isEqualTo(options);
}
// FunctionCallback.builder().description("description").function(null,input->"hello").inputType(String.class).build()
@Test
void whenFunctionNameIsNullThenThrow() {
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
assertThatThrownBy(() -> spec.function(null, "description", input -> "hello"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("name cannot be null or empty");
assertThatThrownBy(() -> spec.functions(FunctionCallback.builder()
.description("description")
.function(null, input -> "hello")
.inputType(String.class)
.build())).isInstanceOf(IllegalArgumentException.class).hasMessage("Name must not be empty");
}
@Test
void whenFunctionNameIsEmptyThenThrow() {
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
assertThatThrownBy(() -> spec.function("", "description", input -> "hello"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("name cannot be null or empty");
assertThatThrownBy(() -> spec.functions(FunctionCallback.builder()
.description("description")
.function("", input -> "hello")
.inputType(String.class)
.build())).isInstanceOf(IllegalArgumentException.class).hasMessage("Name must not be empty");
}
@Test
void whenFunctionDescriptionIsNullThenThrow() {
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
assertThatThrownBy(() -> spec.function("name", null, input -> "hello"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("description cannot be null or empty");
assertThatThrownBy(() -> spec.functions(FunctionCallback.builder()
.description(null)
.function("", input -> "hello")
.inputType(String.class)
.build())).isInstanceOf(IllegalArgumentException.class).hasMessage("Description must not be empty");
}
@Test
@@ -1399,7 +1407,7 @@ class DefaultChatClientTests {
void whenFunctionThenReturn() {
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
spec = spec.function("name", "description", input -> "hello");
spec = spec.function("name", "description", String.class, input -> "hello");
DefaultChatClient.DefaultChatClientRequestSpec defaultSpec = (DefaultChatClient.DefaultChatClientRequestSpec) spec;
assertThat(defaultSpec.getFunctionCallbacks()).anyMatch(callback -> callback.getName().equals("name"));
}

View File

@@ -0,0 +1,287 @@
/*
* 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 java.util.function.BiFunction;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.function.FunctionCallback.FunctionInvokingSpec;
import org.springframework.ai.model.function.FunctionCallback.MethodInvokingSpec;
import org.springframework.core.ParameterizedTypeReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link DefaultFunctionCallbackBuilder}.
*
* @author Christian Tzolov
*/
class DefaultFunctionCallbackBuilderTests {
// Common
@Test
void whenDescriptionIsNullThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().description(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Description must not be empty");
}
@Test
void whenDescriptionIsEmptyThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().description(""))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Description must not be empty");
}
@Test
void whenInputTypeSchemaIsNullThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().inputTypeSchema(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("InputTypeSchema must not be empty");
}
@Test
void whenInputTypeSchemaIsEmptyThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().inputTypeSchema(""))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("InputTypeSchema must not be empty");
}
@Test
void whenSchemaTypeIsNullThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().schemaType(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("SchemaType must not be null");
}
@Test
void whenResponseConverterIsNullThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().responseConverter(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("ResponseConverter must not be null");
}
// Function
@Test
void whenFunctionNameIsNullThenThrow2() {
assertThatThrownBy(() -> FunctionCallback.builder().function(null, (Function) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Name must not be empty");
}
@Test
void whenFunctionIsNullThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().function("functionName", (Function) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Function must not be null");
}
@Test
void whenFunctionThenReturn() {
FunctionInvokingSpec<?, ?> functionBuilder = FunctionCallback.builder()
.function("functionName", input -> "output");
assertThat(functionBuilder).isNotNull();
}
@Test
void whenFunctionWithNullInputTypeThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().function("functionName", input -> "output").build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("InputType must not be null");
}
@Test
void whenFunctionWithInputTypeThenReturn() {
FunctionCallback functionCallback = FunctionCallback.builder()
.description("description")
.function("functionName", input -> "output")
.inputType(String.class)
.build();
assertThat(functionCallback).isNotNull();
assertThat(functionCallback.getDescription()).isEqualTo("description");
assertThat(functionCallback.getName()).isEqualTo("functionName");
assertThat(functionCallback.getInputTypeSchema()).isNotEmpty();
}
@Test
void whenFunctionWithGeneratedDescriptionThenReturn() {
FunctionCallback functionCallback = FunctionCallback.builder()
.function("veryLongDescriptiveFunctionName", input -> "output")
.inputType(String.class)
.build();
assertThat(functionCallback.getDescription()).isEqualTo("very long descriptive function name");
assertThat(functionCallback.getName()).isEqualTo("veryLongDescriptiveFunctionName");
}
@Test
void whenFunctionWithGenericInputTypeThenReturn() {
FunctionCallback functionCallback = FunctionCallback.builder()
.function("functionName", input -> "output")
.inputType(new ParameterizedTypeReference<GenericsRequest<Request>>() {
})
.build();
assertThat(functionCallback.getName()).isEqualTo("functionName");
assertThat(functionCallback.getInputTypeSchema()).isEqualTo("""
{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"datum" : {
"type" : "object",
"properties" : {
"value" : {
"type" : "string"
}
}
}
}
}""");
}
// BiFunction
@Test
void whenBiFunctionNameIsNullThenThrow2() {
assertThatThrownBy(() -> FunctionCallback.builder().function(null, (BiFunction) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Name must not be empty");
}
@Test
void whenBiFunctionIsNullThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().function("functionName", (BiFunction) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("BiFunction must not be null");
}
@Test
void whenBiFunctionThenReturn() {
FunctionInvokingSpec<?, ?> functionBuilder = FunctionCallback.builder()
.function("functionName", (input, context) -> "output");
assertThat(functionBuilder).isNotNull();
}
// Method
@Test
void whenMethodNameIsNullThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().method(null)).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Method name must not be null");
}
@Test
void whenMethodArgumentTypesIsNullThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName", null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Argument types must not be null");
}
@Test
void whenMethodThenReturn() {
MethodInvokingSpec methodInvokeBuilder = FunctionCallback.builder().method("methodName");
assertThat(methodInvokeBuilder).isNotNull();
}
@Test
void whenMethodWithArgumentTypesThenReturn() {
MethodInvokingSpec methodInvokeBuilder = FunctionCallback.builder()
.method("methodName", String.class, Integer.class);
assertThat(methodInvokeBuilder).isNotNull();
}
@Test
void whenMethodWithMissingTargetObjectOrTargetClassThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName").build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Target class or object must not be null");
}
@Test
void whenMethodWithMissingTargetObjectThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder()
.method("methodName", String.class, Integer.class)
.targetClass(TestClass.class)
.build()).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Function object must be provided for non-static methods!");
}
@Test
void whenMethodNotExistingThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName").targetClass(TestClass.class).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Method: 'methodName' with arguments:[] not found!");
}
@Test
void whenMethodAndNameIsNullThenThrow() {
assertThatThrownBy(() -> FunctionCallback.builder()
.method("staticMethodName", String.class, Integer.class)
.targetClass(TestClass.class)
.name(null)
.build()).isInstanceOf(IllegalArgumentException.class).hasMessage("Name must not be empty");
}
@Test
void whenMethodAndTargetClassThenReturn() {
var functionCallback = FunctionCallback.builder()
.method("staticMethodName", String.class, Integer.class)
.targetClass(TestClass.class)
.build();
assertThat(functionCallback).isNotNull();
}
@Test
void whenMethodAndTargetObjectThenReturn() {
var functionCallback = FunctionCallback.builder()
.method("methodName", String.class, Integer.class)
.targetObject(new TestClass())
.build();
assertThat(functionCallback).isNotNull();
}
public static class TestClass {
public static String staticMethodName(String arg1, Integer arg2) {
return arg1 + arg2;
}
public String methodName(String arg1, Integer arg2) {
return arg1 + arg2;
}
}
public record Request(String value) {
}
public static class GenericsRequest<T> {
private T datum;
public T getDatum() {
return datum;
}
public void setDatum(T value) {
this.datum = value;
}
}
}

View File

@@ -16,8 +16,6 @@
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.concurrent.ConcurrentHashMap;
@@ -26,8 +24,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -59,16 +55,11 @@ public class MethodFunctionCallbackTests {
@Test
public void staticMethod() throws NoSuchMethodException, SecurityException {
Method method = ReflectionUtils.findMethod(TestClassWithFunctionMethods.class, "myStaticMethod", String.class,
Unit.class, int.class, MyRecord.class, List.class);
assertThat(method).isNotNull();
assertThat(Modifier.isStatic(method.getModifiers())).isTrue();
var functionCallback = MethodFunctionCallback.builder()
.method(method)
var functionCallback = FunctionCallback.builder()
.description("weather at location")
.mapper(new ObjectMapper())
.objectMapper(new ObjectMapper())
.method("myStaticMethod", String.class, Unit.class, int.class, MyRecord.class, List.class)
.targetClass(TestClassWithFunctionMethods.class)
.build();
String response = functionCallback.call(this.value);
@@ -86,16 +77,12 @@ public class MethodFunctionCallbackTests {
@Test
public void nonStaticMethod() throws NoSuchMethodException, SecurityException {
Method method = TestClassWithFunctionMethods.class.getMethod("myNonStaticMethod", String.class, Unit.class,
int.class, MyRecord.class, List.class);
var object = new TestClassWithFunctionMethods();
assertThat(Modifier.isStatic(method.getModifiers())).isFalse();
var functionCallback = MethodFunctionCallback.builder()
.functionObject(new TestClassWithFunctionMethods())
.method(method)
var functionCallback = FunctionCallback.builder()
.description("weather at location")
.mapper(new ObjectMapper())
.method("myNonStaticMethod", String.class, Unit.class, int.class, MyRecord.class, List.class)
.targetObject(object)
.build();
String response = functionCallback.call(this.value);
@@ -113,14 +100,11 @@ public class MethodFunctionCallbackTests {
@Test
public void noArgsNoReturnMethod() throws NoSuchMethodException, SecurityException {
Method method = TestClassWithFunctionMethods.class.getMethod("argumentLessReturnVoid");
assertThat(Modifier.isStatic(method.getModifiers())).isTrue();
var functionCallback = MethodFunctionCallback.builder()
.method(method)
var functionCallback = FunctionCallback.builder()
.description("weather at location")
.mapper(new ObjectMapper())
.objectMapper(new ObjectMapper())
.method("argumentLessReturnVoid")
.targetClass(TestClassWithFunctionMethods.class)
.build();
String response = functionCallback.call(this.value);

View File

@@ -21,14 +21,14 @@ import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Test
class FunctionCallbackWrapperExtensionsTests {
class FunctionCallbackExtensionsTests {
private val builder = mockk<FunctionCallbackWrapper.Builder<WeatherRequest, WeatherResponse>>()
private val spec = mockk<FunctionCallback.FunctionInvokingSpec<WeatherRequest, WeatherResponse>>()
@Test
fun withInputType() {
every { builder.withInputType(any<Class<*>>()) } returns builder
builder.withInputType<WeatherRequest>()
verify { builder.withInputType(WeatherRequest::class.java) }
fun inputType() {
every { spec.inputType(any<Class<*>>()) } returns spec
spec.inputType<WeatherRequest, WeatherResponse>()
verify { spec.inputType(WeatherRequest::class.java) }
}
}

View File

@@ -108,9 +108,10 @@ var options = FunctionCallingOptions.builder()
.withModel("anthropic.claude-3-5-sonnet-20240620-v1:0")
.withTemperature(0.6)
.withMaxTokens(300)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new WeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location. Return temperature in 36°F or 36°C format. Use multi-turn if needed.")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location. Return temperature in 36°F or 36°C format. Use multi-turn if needed.")
.function("getCurrentWeather", new WeatherService())
.inputType(WeatherService.Request.class)
.build()))
.build();

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-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java[FunctionCallbackWrapper.java] 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-core/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.
== 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 `FunctionCallbackWrapper` wrapper that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` 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/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithFunctionBeanIT.java.java[FunctionCallWithFunctionBeanIT.java] demonstrates this approach.
==== FunctionCallback Wrapper
==== FunctionCallback
Another way to register a function is to create a `FunctionCallbackWrapper` wrapper like this:
Another way to register a function is to create a `FunctionCallback` instance like this:
[source,java]
----
@@ -127,9 +127,10 @@ static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather") // (1) function name
.withDescription("Get the weather in location") // (2) function description
return FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function signature
.build();
}
...
@@ -137,11 +138,11 @@ static class Config {
----
It wraps the 3rd party `MockWeatherService` function and registers it as a `CurrentWeather` function with the `AnthropicChatModel`.
It also provides a description (2) and an optional response converter (3) to convert the response into a text as expected by the model.
It also provides a description (2) and input type (3) used to generate the JSON schema for the function call.
NOTE: By default, the response converter does a JSON serialization of the Response object.
NOTE: The `FunctionCallbackWrapper` internally resolves the function call signature based on the `MockWeatherService.Request` class.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Specifying functions in Chat Options
@@ -174,10 +175,11 @@ AnthropicChatModel chatModel = ...
UserMessage userMessage = new UserMessage("What's the weather like in Paris?");
var promptOptions = AnthropicChatOptions.builder()
.withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
"CurrentWeather", // name
"Get the weather in location", // function description
new MockWeatherService()))) // function code
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function signature
.build())) // function code
.build();
ChatResponse response = this.chatModel.call(new Prompt(List.of(this.userMessage), this.promptOptions));

View File

@@ -17,7 +17,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-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java[FunctionCallbackWrapper.java] 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-core/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.
== How it works
@@ -68,7 +68,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 `FunctionCallbackWrapper` wrapper that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` instance that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
@@ -113,7 +113,7 @@ The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring
==== FunctionCallback Wrapper
Another way to register a function is to create a `FunctionCallbackWrapper` wrapper like this:
Another way to register a function is to create a `FunctionCallback` instance like this:
[source,java]
----
@@ -123,9 +123,10 @@ static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather") // (1) function name
.withDescription("Get the current weather in a given location") // (2) function description
return FunctionCallback.builder()
.description("Get the current weather in a given location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name
.inputType(MockWeatherService.Request.class) // (3) function input type
.build();
}
...
@@ -136,7 +137,7 @@ It wraps the 3rd party `MockWeatherService` function and registers it as a `Curr
NOTE: The default response converter does a JSON serialization of the Response object.
NOTE: The `FunctionCallbackWrapper` internally resolves the function call signature based on the `MockWeatherService.Request` class and internally generates an JSON schema for the function call.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class and internally generates an JSON schema for the function call.
=== Specifying functions in Chat Options
@@ -179,10 +180,11 @@ AzureOpenAiChatModel chatModel = ...
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris? Use Multi-turn function calling.");
var promptOptions = AzureOpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather")
.withDescription("Get the weather in location")
.build()))
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the current weather in a given location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function input type
.build()))
.build();
ChatResponse response = this.chatModel.call(new Prompt(List.of(this.userMessage), this.promptOptions));

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-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java[FunctionCallbackWrapper.java] 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-core/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.
// 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 `FunctionCallbackWrapper` wrapper that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` instance that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
@@ -117,7 +117,7 @@ The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring
==== FunctionCallback Wrapper
Another way register a function is to create `FunctionCallbackWrapper` wrapper like this:
Another way register a function is to create `FunctionCallback` instance like this:
[source,java]
----
@@ -127,9 +127,10 @@ static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather") // (1) function name
.withDescription("Get the weather in location") // (2) function description
return FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function signature
.build();
}
...
@@ -137,11 +138,11 @@ static class Config {
----
It wraps the 3rd party, `MockWeatherService` function and registers it as a `CurrentWeather` function with the `MiniMaxChatModel`.
It also provides a description (2) and an optional response converter (3) to convert the response into a text as expected by the model.
It also provides a description (2) and the function signature (3) to let the model know what arguments the function expects.
NOTE: By default, the response converter does a JSON serialization of the Response object.
NOTE: The `FunctionCallbackWrapper` internally resolves the function call signature based on the `MockWeatherService.Request` class.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Specifying functions in Chat Options
@@ -170,7 +171,7 @@ Here is the current weather for the requested cities:
- Paris, France: 15.0°C
----
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/tool/FunctionCallbackWrapperIT.java[FunctionCallbackWrapperIT.java] test demo this approach.
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/tool/MiniMaxFunctionCallbackIT.java[MiniMaxFunctionCallbackIT.java] test demo this approach.
=== Register/Call Functions with Prompt Options
@@ -184,10 +185,11 @@ MiniMaxChatModel chatModel = ...
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
var promptOptions = MiniMaxChatOptions.builder()
.withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
"CurrentWeather", // name
"Get the weather in location", // function description
new MockWeatherService()))) // function code
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function signature
.build())) // function code
.build();
ChatResponse response = this.chatModel.call(new Prompt(List.of(this.userMessage), this.promptOptions));
@@ -198,29 +200,3 @@ NOTE: The in-prompt registered functions are enabled by default for the duration
This approach allows to dynamically chose different functions to be called based on the user input.
The https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackInPromptIT.java[FunctionCallbackInPromptIT.java] integration test provides a complete example of how to register a function with the `MiniMaxChatModel` and use it in a prompt request.
//
// === Register Functions with Default Options
//
// You can programmatically register functions with the `MiniMaxChatModel` using the `MiniMaxChatOptions#withFunctionCallbacks`:
//
// [source,java]
// ----
//
// MiniMaxApi miniMaxApi = new MiniMaxApi(apiKey);
//
// var defaultOptions = MiniMaxChatOptions.builder()
// .withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
// "CurrentWeather", // name
// "Get the weather in location", // function description
// new MockWeatherService()))) // function code
// .build();
//
// MiniMaxChatModel chatModel = new MiniMaxChatModel(miniMaxApi, defaultOptions);
//
// UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
//
// ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
// MiniMaxChatOptions.builder().withFunction("CurrentWeather").build())); // Enable the function
// ----
//
// NOTE: Functions are registered when MiniMaxChatModel is created, by you must enable in the Prompt the functions to be used in the request.

View File

@@ -16,7 +16,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-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java[FunctionCallbackWrapper.java] 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-core/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.
== How it works
@@ -68,7 +68,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 `FunctionCallbackWrapper` that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` 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 @@ Mistral AI is almost identical to OpenAI in this regard.
==== FunctionCallback Wrapper
Another way to register a function is to create a `FunctionCallbackWrapper` like this:
Another way to register a function is to create a `FunctionCallback` like this:
[source,java]
----
@@ -125,9 +125,10 @@ static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather") // (1) function name
.withDescription("Get the weather in location") // (2) function description
return FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function signature
.build();
}
@@ -135,11 +136,11 @@ static class Config {
----
It wraps the 3rd party `MockWeatherService` function and registers it as a `CurrentWeather` function with the `MistralAiChatModel`.
It also provides a description (2) and an optional response converter to convert the response into a text as expected by the model.
It also provides a description (2) and the function signature (3) to let the model know what arguments the function expects.
NOTE: By default, the response converter performs a JSON serialization of the Response object.
NOTE: The `FunctionCallbackWrapper` internally resolves the function call signature based on the `MockWeatherService.Request` class.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Specifying functions in Chat Options
@@ -172,10 +173,11 @@ MistralAiChatModel chatModel = ...
UserMessage userMessage = new UserMessage("What's the weather like in Paris?");
var promptOptions = MistralAiChatOptions.builder()
.withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
"CurrentWeather", // name
"Get the weather in location", // function description
new MockWeatherService()))) // function code
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function signature
.build())) // function code
.build();
ChatResponse response = this.chatModel.call(new Prompt(this.userMessage, this.promptOptions));

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-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java[FunctionCallbackWrapper.java] 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-core/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.
// 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 `FunctionCallbackWrapper` wrapper that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` instance that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
@@ -117,7 +117,7 @@ The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring
==== FunctionCallback Wrapper
Another way register a function is to create `FunctionCallbackWrapper` wrapper like this:
Another way register a function is to create `FunctionCallback` instance like this:
[source,java]
----
@@ -127,9 +127,10 @@ static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather") // (1) function name
.withDescription("Get the weather in location") // (2) function description
return FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function signature
.build();
}
...
@@ -137,11 +138,11 @@ static class Config {
----
It wraps the 3rd party, `MockWeatherService` function and registers it as a `CurrentWeather` function with the `MoonshotChatModel`.
It also provides a description (2) and an optional response converter (3) to convert the response into a text as expected by the model.
It also provides a description (2) and the function signature (3) to let the model know what arguments the function expects.
NOTE: By default, the response converter does a JSON serialization of the Response object.
NOTE: The `FunctionCallbackWrapper` internally resolves the function call signature based on the `MockWeatherService.Request` class.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Specifying functions in Chat Options
@@ -170,7 +171,7 @@ Here is the current weather for the requested cities:
- Paris, France: 15.0°C
----
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackWrapperIT.java[FunctionCallbackWrapperIT.java] test demo this approach.
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/MoonshotFunctionCallbackIT.java[MoonshotFunctionCallbackIT.java] test demo this approach.
=== Register/Call Functions with Prompt Options
@@ -184,10 +185,11 @@ MoonshotChatModel chatModel = ...
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
var promptOptions = MoonshotChatOptions.builder()
.withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
"CurrentWeather", // name
"Get the weather in location", // function description
new MockWeatherService()))) // function code
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name
.inputType(MockWeatherService.Request.class) // (3) function signature
.build())) // function code
.build();
ChatResponse response = this.chatModel.call(new Prompt(List.of(this.userMessage), this.promptOptions));
@@ -198,29 +200,3 @@ NOTE: The in-prompt registered functions are enabled by default for the duration
This approach allows to dynamically chose different functions to be called based on the user input.
The https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackInPromptIT.java[FunctionCallbackInPromptIT.java] integration test provides a complete example of how to register a function with the `MoonshotChatModel` and use it in a prompt request.
//
// === Register Functions with Default Options
//
// You can programmatically register functions with the `MoonshotChatModel` using the `MoonshotChatOptions#withFunctionCallbacks`:
//
// [source,java]
// ----
//
// MoonshotApi moonshotApi = new MoonshotApi(apiKey);
//
// var defaultOptions = MoonshotChatOptions.builder()
// .withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
// "CurrentWeather", // name
// "Get the weather in location", // function description
// new MockWeatherService()))) // function code
// .build();
//
// MoonshotChatModel chatModel = new MoonshotChatModel(moonshotApi, defaultOptions);
//
// UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
//
// ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
// MoonshotChatOptions.builder().withFunction("CurrentWeather").build())); // Enable the function
// ----
//
// NOTE: Functions are registered when MoonshotChatModel is created, by you must enable in the Prompt the functions to be used in the request.

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-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java[FunctionCallbackWrapper.java] 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-core/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.
== 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 `FunctionCallbackWrapper` that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` 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.
==== FunctionCallbackWrapper
==== FunctionCallback
Another way to register a function is to create a `FunctionCallbackWrapper` like this:
Another way to register a function is to create a `FunctionCallback` like this:
[source,java]
----
@@ -129,9 +129,10 @@ static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather") // (1) function name
.withDescription("Get the weather in location") // (2) function description
return FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name
.inputType(MockWeatherService.Request.class) // (3) function signature
.build();
}
@@ -139,11 +140,11 @@ static class Config {
----
It wraps the 3rd party `MockWeatherService` function and registers it as a `CurrentWeather` function with the `OllamaChatModel`.
It also provides a description (2) and an optional response converter to convert the response into a text as expected by the model.
It also provides a description (2) and the function signature (3) to let the model know what arguments the function expects.
NOTE: By default, the response converter performs a JSON serialization of the Response object.
NOTE: The `FunctionCallbackWrapper` internally resolves the function call signature based on the `MockWeatherService.Request` class.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Specifying functions in Chat Options
@@ -172,7 +173,7 @@ Here is the current weather for the requested cities:
- Paris, France: 15.0°C
----
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackWrapperIT.java[FunctionCallbackWrapperIT.java] test demo this approach.
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/OllamaFunctionCallbackIT.java[OllamaFunctionCallbackIT.java] test demo this approach.
=== Register/Call Functions with Prompt Options
@@ -185,10 +186,11 @@ OllamaChatModel chatModel = ...
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
var promptOptions = OllamaOptions.builder()
.withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
"CurrentWeather", // name
"Get the weather in location", // function description
new MockWeatherService()))) // function code
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function signature
.build())) // function code
.build();
ChatResponse response = this.chatModel.call(new Prompt(this.userMessage, this.promptOptions));

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-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java[FunctionCallbackWrapper.java] 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-core/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.
// Additionally, the Auto-Configuration provides a way to auto-register any Function<I, O> beans definition as function calling candidates in the `ChatModel`.
@@ -69,7 +69,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 `FunctionCallbackWrapper` that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` 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,7 +112,7 @@ The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring
==== FunctionCallback Wrapper
Another way to register a function is to create a `FunctionCallbackWrapper` like this:
Another way to register a function is to create a `FunctionCallback` like this:
[source,java]
----
@@ -122,9 +122,10 @@ static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather") // (1) function name
.withDescription("Get the weather in location") // (2) function description
return FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function input type
.build();
}
@@ -132,11 +133,11 @@ static class Config {
----
It wraps the 3rd party `MockWeatherService` function and registers it as a `CurrentWeather` function with the `OpenAiChatModel`.
It also provides a description (2) and an optional response converter to convert the response into a text as expected by the model.
It also provides a description (2) and an input type (3) used to generate the JSON schema for the function call.
NOTE: By default, the response converter performs a JSON serialization of the Response object.
NOTE: The `FunctionCallbackWrapper` internally resolves the function call signature based on the `MockWeatherService.Request` class.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Specifying functions in Chat Options
@@ -165,7 +166,7 @@ Here is the current weather for the requested cities:
- Paris, France: 15.0°C
----
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapperIT.java[FunctionCallbackWrapperIT.java] test demo this approach.
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/OpenAiFunctionCallbackIT.java[OpenAiFunctionCallbackIT.java] test demo this approach.
=== Register/Call Functions with Prompt Options
@@ -178,10 +179,11 @@ OpenAiChatModel chatModel = ...
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
var promptOptions = OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
"CurrentWeather", // name
"Get the weather in location", // function description
new MockWeatherService()))) // function code
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function input type
.build())) // function code
.build();
ChatResponse response = this.chatModel.call(new Prompt(this.userMessage, this.promptOptions));
@@ -192,32 +194,6 @@ NOTE: The in-prompt registered functions are enabled by default for the duration
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/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java[FunctionCallbackInPromptIT.java] integration test provides a complete example of how to register a function with the `OpenAiChatModel` and use it in a prompt request.
//
// === Register Functions with Default Options
//
// You can programmatically register functions with the `OpenAiChatModel` using the `OpenAiChatOptions#withFunctionCallbacks`:
//
// [source,java]
// ----
//
// OpenAiApi openaiApi = new OpenAiApi(apiKey);
//
// var defaultOptions = OpenAiChatOptions.builder()
// .withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
// "CurrentWeather", // name
// "Get the weather in location", // function description
// new MockWeatherService()))) // function code
// .build();
//
// OpenAiChatModel chatModel = new OpenAiChatModel(openaiApi, defaultOptions);
//
// UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
//
// ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
// OpenAiChatOptions.builder().withFunction("CurrentWeather").build())); // Enable the function
// ----
//
// NOTE: Functions are registered when OpenAiChatModel is created, by you must enable in the Prompt the functions to be used in the request.
=== Tool Context Support
@@ -254,9 +230,10 @@ BiFunction<MockWeatherService.Request, ToolContext, MockWeatherService.Response>
OpenAiChatOptions options = OpenAiChatOptions.builder()
.withModel(OpenAiApi.ChatModel.GPT_4_O.getValue())
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(this.weatherFunction)
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.function("getCurrentWeather", this.weatherFunction)
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build()))
.withToolContext(Map.of("sessionId", "123", "userId", "user456"))
.build();

View File

@@ -21,7 +21,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-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java[FunctionCallbackWrapper.java] 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-core/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.
// Additionally, the Auto-Configuration provides a way to auto-register any Function<I, O> beans definition as function calling candidates in the `ChatModel`.
@@ -74,7 +74,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 `FunctionCallbackWrapper` wrapper that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` instance that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
@@ -119,7 +119,7 @@ The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring
==== FunctionCallback Wrapper
Another way to register a function is to create a `FunctionCallbackWrapper` wrapper like this:
Another way to register a function is to create a `FunctionCallback` instance like this:
[source,java]
----
@@ -129,10 +129,11 @@ static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather") // (1) function name
.withDescription("Get the current weather in a given location") // (2) function description
.withSchemaType(SchemaType.OPEN_API_SCHEMA) // (3) schema type. Compulsory for Gemini function calling.
return FunctionCallback.builder()
.description("Get the current weather in a given location") // (2) function description
.schemaType(SchemaType.OPEN_API_SCHEMA) // (3) schema type. Compulsory for Gemini function calling.
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (4) input type
.build();
}
...
@@ -140,11 +141,11 @@ static class Config {
----
It wraps the 3rd party `MockWeatherService` function and registers it as a `CurrentWeather` function with the `VertexAiGeminiChatModel`.
It also provides a description (2) and sets the Schema type to Open API type (3).
It also provides a description (2), the Schema type to Open API type (3) and input type (4) used to generate the Open API schema for the function call.
NOTE: The default response converter does a JSON serialization of the Response object.
NOTE: The `FunctionCallbackWrapper` internally resolves the function call signature based on the `MockWeatherService.Request` class and internally generates an Open API schema for the function call.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class and internally generates an Open API schema for the function call.
=== Specifying functions in Chat Options
@@ -187,10 +188,11 @@ VertexAiGeminiChatModel chatModel = ...
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris? Use Multi-turn function calling.");
var promptOptions = VertexAiGeminiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather")
.withSchemaType(SchemaType.OPEN_API_SCHEMA) // IMPORTANT!!
.withDescription("Get the weather in location")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.schemaType(SchemaType.OPEN_API_SCHEMA) // IMPORTANT!!
.description("Get the weather in location")
.function("CurrentWeather", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

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-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java[FunctionCallbackWrapper.java] 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-core/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.
// 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 `FunctionCallbackWrapper` wrapper that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` instance that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is passed as a `ChatOption`.
@@ -117,7 +117,7 @@ The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring
==== FunctionCallback Wrapper
Another way register a function is to create `FunctionCallbackWrapper` wrapper like this:
Another way register a function is to create `FunctionCallback` instance like this:
[source,java]
----
@@ -127,9 +127,10 @@ static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather") // (1) function name
.withDescription("Get the weather in location") // (2) function description
return FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function signature
.build();
}
...
@@ -137,11 +138,11 @@ static class Config {
----
It wraps the 3rd party, `MockWeatherService` function and registers it as a `CurrentWeather` function with the `ZhiPuAiChatModel`.
It also provides a description (2) and an optional response converter (3) to convert the response into a text as expected by the model.
It also provides a description (2) and the input type (3) used to generate the JSON schema for the function call.
NOTE: By default, the response converter does a JSON serialization of the Response object.
NOTE: The `FunctionCallbackWrapper` internally resolves the function call signature based on the `MockWeatherService.Request` class.
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Specifying functions in Chat Options
@@ -170,7 +171,7 @@ Here is the current weather for the requested cities:
- Paris, France: 15.0°C
----
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWrapperIT.java[FunctionCallbackWrapperIT.java] test demo this approach.
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/ZhipuAiFunctionCallbackIT.java[ZhipuAiFunctionCallbackIT.java] test demo this approach.
=== Register/Call Functions with Prompt Options
@@ -184,10 +185,11 @@ ZhiPuAiChatModel chatModel = ...
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
var promptOptions = ZhiPuAiChatOptions.builder()
.withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
"CurrentWeather", // name
"Get the weather in location", // function description
new MockWeatherService()))) // function code
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) function signature
.build())) // function code
.build();
ChatResponse response = this.chatModel.call(new Prompt(List.of(this.userMessage), this.promptOptions));
@@ -198,29 +200,3 @@ NOTE: The in-prompt registered functions are enabled by default for the duration
This approach allows to dynamically chose different functions to be called based on the user input.
The https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackInPromptIT.java[FunctionCallbackInPromptIT.java] integration test provides a complete example of how to register a function with the `ZhiPuAiChatModel` and use it in a prompt request.
//
// === Register Functions with Default Options
//
// You can programmatically register functions with the `ZhiPuAiChatModel using the `ZhiPuAiChatOptions#withFunctionCallbacks`:
//
// [source,java]
// ----
//
// ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi(apiKey);
//
// var defaultOptions = ZhiPuAiChatOptions.builder()
// .withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
// "CurrentWeather", // name
// "Get the weather in location", // function description
// new MockWeatherService()))) // function code
// .build();
//
// ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(zhiPuAiApi, defaultOptions);
//
// UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
//
// ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
// ZhiPuAiChatOptions.builder().withFunction("CurrentWeather").build())); // Enable the function
// ----
//
// NOTE: Functions are registered when ZhiPuAiChatModel is created, by you must enable in the Prompt the functions to be used in the request.

View File

@@ -31,7 +31,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 `ChatClient`.
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/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java[FunctionCallbackWrapper.java] 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-core/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.
== How it works
@@ -101,7 +101,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 `FunctionCallbackWrapper` that adds the logic for it being invoked via the AI model.
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` that adds the logic for it being invoked via the AI model.
The name of the `@Bean` is used function name.
--
@@ -182,9 +182,9 @@ 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.
==== FunctionCallback Wrapper
==== FunctionCallback
Another way to register a function is to create a `FunctionCallbackWrapper` like this:
Another way to register a function is to create a `FunctionCallback` like this:
--
[tabs]
@@ -199,9 +199,10 @@ static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather") // (1) function name
.withDescription("Get the weather in location") // (2) function description
return FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) input type to build the JSON schema
.build();
}
}
@@ -218,11 +219,11 @@ class Config {
@Bean
fun weatherFunctionInfo(): FunctionCallback {
return FunctionCallbackWrapper.builder(MockWeatherService())
.withName("CurrentWeather") // (1) function name
.withDescription("Get the weather in location") // (2) function description
// (3) Required due to Kotlin SAM conversion beeing an opaque lambda
.withInputType<MockWeatherService.Request>()
return FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", MockWeatherService()) // (1) function name and instance
// (3) Required due to Kotlin SAM conversion being an opaque lambda
.inputType<MockWeatherService.Request>()
.build();
}
}
@@ -236,7 +237,7 @@ It also provides a description (2) and an optional response converter to convert
NOTE: By default, the response converter performs a JSON serialization of the Response object.
NOTE: The `FunctionCallbackWrapper` internally resolves the function call signature based on the `MockWeatherService.Request` class.
NOTE: The `FunctionCallback.Builder` internally resolves the function call signature based on the `MockWeatherService.Request` class.
=== Enable functions by bean name
@@ -274,10 +275,11 @@ In addition to the auto-configuration, you can register callback functions, dyna
ChatClient chatClient = ...
ChatResponse response = this.chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
.functions(new FunctionCallbackWrapper<>(
"CurrentWeather", // name
"Get the weather in location", // function description
new MockWeatherService()))
.functions(FunctionCallback.builder()
.description("Get the weather in location") // (2) function description
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
.inputType(MockWeatherService.Request.class) // (3) input type to build the JSON schema
.build())
.call()
.chatResponse();
----
@@ -288,7 +290,7 @@ This approach allows to choose dynamically different functions to be called base
The https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/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.
=== Register functions: MethodFunctionCallback
=== Register functions: Method Invoking FunctionCallback
The `MethodFunctionCallback` 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.
@@ -301,16 +303,16 @@ The `MethodFunctionCallback` implements the `FunctionCallback` interface and pro
- Any parameter/return types (primitives, objects, collections)
- Special handling for `ToolContext` parameters
The basic MethodFunctionCallback configuration looks like this:
You need the `FunctionCallback.Builder` to create `MethodFunctionCallback` like this:
[source,java]
----
// Create using builder pattern
MethodFunctionCallback callback = MethodFunctionCallback.builder()
.functionObject(targetObject) // Required for instance methods
.method(method) // Required: The method to invoke
FunctionCallback callback = FunctionCallback.builder()
.description("Method description") // Required: Helps AI understand the function
.mapper(objectMapper) // Optional: Custom ObjectMapper
.objectMapper(objectMapper) // Optional: Custom ObjectMapper
.method("MethodName", Class<?>...argumentTypes) // Required: The method to invoke and its argument types
.targetObject(targetObject) // Required only for instance methods
.build();
----
@@ -329,12 +331,10 @@ public class WeatherService {
}
// Usage
Method method = ReflectionUtils.findMethod(
WeatherService.class, "getWeather", String.class, TemperatureUnit.class);
MethodFunctionCallback callback = MethodFunctionCallback.builder()
.method(method)
FunctionCallback callback = FunctionCallback.builder()
.description("Get weather information for a city")
.method("getWeather", String.class, TemperatureUnit.class)
.targetClass(WeatherService.class)
.build();
----
Instance Method with ToolContext::
@@ -350,15 +350,13 @@ public class DeviceController {
// Usage
DeviceController controller = new DeviceController();
Method method = ReflectionUtils.findMethod(
DeviceController.class, "setDeviceState", String.class, boolean.class, ToolContext.class);
String response = ChatClient.create(chatModel).prompt()
.user("Turn on the living room lights")
.functions(MethodFunctionCallback.builder()
.functionObject(controller)
.method(method)
.functions(FunctionCallback.builder()
.description("Control device state")
.method("setDeviceState", String.class,boolean.class,ToolContext.class)
.targetObject(controller)
.build())
.toolContext(Map.of("location", "home"))
.call()
@@ -368,7 +366,7 @@ String response = ChatClient.create(chatModel).prompt()
======
The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMethodFunctionCallbackIT.java[OpenAiChatClientMethodFunctionCallbackIT]
integration test provides additional examples of how to use the MethodFunctionCallback.
integration test provides additional examples of how to use the FunctionCallback.Builder to create method invocation FunctionCallbacks.
=== Tool Context
@@ -404,9 +402,10 @@ BiFunction<MockWeatherService.Request, ToolContext, MockWeatherService.Response>
ChatResponse response = chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
.functions(FunctionCallbackWrapper.builder(this.weatherFunction)
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("getCurrentWeather", this.weatherFunction)
.inputType(MockWeatherService.Request.class)
.build())
.toolContext(Map.of("sessionId", "1234", "userId", "5678"))
.call()

View File

@@ -30,7 +30,7 @@ import org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration
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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -58,9 +58,10 @@ public class FunctionCallWithPromptFunctionIT {
"What's the weather like in San Francisco, in Paris and in Tokyo? Return the temperature in Celsius.");
var promptOptions = AnthropicChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the weather in location. Return temperature in 36°F or 36°C format.")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location. Return temperature in 36°F or 36°C format.")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

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.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
@@ -80,9 +79,10 @@ public class FunctionCallWithFunctionWrapperIT {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("WeatherInfo")
.withDescription("Get the current weather in a given location")
return FunctionCallback.builder()
.description("Get the current weather in a given location")
.function("WeatherInfo", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build();
}

View File

@@ -29,7 +29,7 @@ import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -61,9 +61,10 @@ public class FunctionCallWithPromptFunctionIT {
"What's the weather like in San Francisco, in Paris and in Tokyo? Use Multi-turn function calling.");
var promptOptions = AzureOpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the weather in location")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -29,7 +29,7 @@ import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -57,9 +57,10 @@ public class FunctionCallWithPromptFunctionIT {
"What's the weather like in San Francisco, in Paris and in Tokyo? Return the temperature in Celsius.");
var promptOptions = FunctionCallingOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the weather in location. Return temperature in 36°F or 36°C format.")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location. Return temperature in 36°F or 36°C format.")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -33,7 +33,7 @@ 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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -63,10 +63,10 @@ public class FunctionCallbackInPromptIT {
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
var promptOptions = MiniMaxChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -89,10 +89,10 @@ public class FunctionCallbackInPromptIT {
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
var promptOptions = MiniMaxChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -34,7 +34,6 @@ 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.function.FunctionCallbackWrapper;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -47,9 +46,9 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Geng Rong
*/
@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".*")
public class FunctionCallbackWrapperIT {
public class MiniMaxFunctionCallbackIT {
private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWrapperIT.class);
private final Logger logger = LoggerFactory.getLogger(MiniMaxFunctionCallbackIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY"))
@@ -111,10 +110,10 @@ public class FunctionCallbackWrapperIT {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("WeatherInfo")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
return FunctionCallback.builder()
.description("Get the weather in location")
.function("WeatherInfo", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build();
}

View File

@@ -18,7 +18,6 @@ package org.springframework.ai.autoconfigure.mistralai.tool;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.junit.jupiter.api.Test;
@@ -33,7 +32,7 @@ import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.mistralai.MistralAiChatModel;
import org.springframework.ai.mistralai.MistralAiChatOptions;
import org.springframework.ai.mistralai.api.MistralAiApi;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -65,14 +64,10 @@ public class PaymentStatusPromptIT {
UserMessage userMessage = new UserMessage("What's the status of my transaction with id T1001?");
var promptOptions = MistralAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new Function<Transaction, Status>() {
public Status apply(Transaction transaction) {
return new Status(DATA.get(transaction).status());
}
})
.withName("retrievePaymentStatus")
.withDescription("Get payment status of a transaction")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get payment status of a transaction")
.function("retrievePaymentStatus", transaction -> new Status(DATA.get(transaction).status()))
.inputType(Transaction.class)
.build()))
.build();

View File

@@ -37,7 +37,7 @@ import org.springframework.ai.mistralai.MistralAiChatModel;
import org.springframework.ai.mistralai.MistralAiChatOptions;
import org.springframework.ai.mistralai.api.MistralAiApi;
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.function.FunctionCallingOptionsBuilder.PortableFunctionCallingOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -73,9 +73,10 @@ public class WeatherServicePromptIT {
var promptOptions = MistralAiChatOptions.builder()
.withToolChoice(ToolChoice.AUTO)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MyWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the current weather in requested location")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the current weather in requested location")
.function("CurrentWeatherService", new MyWeatherService())
.inputType(MyWeatherService.Request.class)
.build()))
.build();
@@ -84,8 +85,6 @@ public class WeatherServicePromptIT {
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("15", "15.0");
// assertThat(response.getResult().getOutput().getContent()).contains("30.0",
// "10.0", "15.0");
});
}
@@ -100,9 +99,10 @@ public class WeatherServicePromptIT {
UserMessage userMessage = new UserMessage("What's the weather like in Paris? Use Celsius.");
PortableFunctionCallingOptions functionOptions = FunctionCallingOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MyWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the current weather in requested location")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the current weather in requested location")
.function("CurrentWeatherService", new MyWeatherService())
.inputType(MyWeatherService.Request.class)
.build()))
.build();

View File

@@ -32,7 +32,7 @@ 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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -64,10 +64,10 @@ public class FunctionCallbackInPromptIT {
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
var promptOptions = MoonshotChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -90,10 +90,10 @@ public class FunctionCallbackInPromptIT {
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
var promptOptions = MoonshotChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -34,7 +34,6 @@ 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.function.FunctionCallbackWrapper;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -49,9 +48,9 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Geng Rong
*/
@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*")
public class FunctionCallbackWrapperIT {
public class MoonshotFunctionCallbackIT {
private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWrapperIT.class);
private final Logger logger = LoggerFactory.getLogger(MoonshotFunctionCallbackIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
@@ -114,10 +113,10 @@ public class FunctionCallbackWrapperIT {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("WeatherInfo")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
return FunctionCallback.builder()
.description("Get the weather in location")
.function("WeatherInfo", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build();
}

View File

@@ -33,7 +33,7 @@ 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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -71,11 +71,11 @@ public class FunctionCallbackInPromptIT extends BaseOllamaIT {
"What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
var promptOptions = OllamaOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription(
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description(
"Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -98,11 +98,11 @@ public class FunctionCallbackInPromptIT extends BaseOllamaIT {
"What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
var promptOptions = OllamaOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription(
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description(
"Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -34,7 +34,6 @@ 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.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.function.FunctionCallingOptionsBuilder.PortableFunctionCallingOptions;
import org.springframework.ai.ollama.OllamaChatModel;
@@ -46,9 +45,9 @@ import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
public class FunctionCallbackWrapperIT extends BaseOllamaIT {
public class OllamaFunctionCallbackIT extends BaseOllamaIT {
private static final Logger logger = LoggerFactory.getLogger(FunctionCallbackWrapperIT.class);
private static final Logger logger = LoggerFactory.getLogger(OllamaFunctionCallbackIT.class);
private static final String MODEL_NAME = "qwen2.5:3b";
@@ -140,11 +139,11 @@ public class FunctionCallbackWrapperIT extends BaseOllamaIT {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("WeatherInfo")
.withDescription(
return FunctionCallback.builder()
.description(
"Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.function("WeatherInfo", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build();
}

View File

@@ -16,7 +16,6 @@
package org.springframework.ai.autoconfigure.openai.tool;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
@@ -26,6 +25,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -58,7 +58,11 @@ public class FunctionCallbackInPrompt2IT {
String content = ChatClient.builder(chatModel).build().prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris?")
.function("CurrentWeatherService", "Get the weather in location", new MockWeatherService())
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build())
.call().content();
// @formatter:on
@@ -78,13 +82,11 @@ public class FunctionCallbackInPrompt2IT {
// @formatter:off
String content = ChatClient.builder(chatModel).build().prompt()
.user("What's the weather like in Amsterdam?")
.function("CurrentWeatherService", "Get the weather in location",
new Function<MockWeatherService.Request, String>() {
@Override
public String apply(MockWeatherService.Request request) {
return "18 degrees Celsius";
}
})
.functions(FunctionCallback.builder()
.description("Get the weather in location")
.function("CurrentWeatherService", input -> "18 degrees Celsius")
.inputType(MockWeatherService.Request.class)
.build())
.call().content();
// @formatter:on
logger.info("Response: {}", content);

View File

@@ -31,7 +31,7 @@ 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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
@@ -62,10 +62,10 @@ public class FunctionCallbackInPromptIT {
"What's the weather like in San Francisco, Tokyo, and Paris?");
var promptOptions = OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();
@@ -91,10 +91,10 @@ public class FunctionCallbackInPromptIT {
"What's the weather like in San Francisco, Tokyo, and Paris?");
var promptOptions = OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -26,7 +26,6 @@ import org.slf4j.LoggerFactory;
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -37,9 +36,9 @@ import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
public class FunctionCallbackWrapper2IT {
public class OpenAiFunctionCallback2IT {
private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWrapperIT.class);
private final Logger logger = LoggerFactory.getLogger(OpenAiFunctionCallback2IT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"),
@@ -96,10 +95,10 @@ public class FunctionCallbackWrapper2IT {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("WeatherInfo")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
return FunctionCallback.builder()
.description("Get the weather in location")
.function("WeatherInfo", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build();
}

View File

@@ -32,7 +32,6 @@ 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.function.FunctionCallbackWrapper;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
@@ -44,9 +43,9 @@ import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
public class FunctionCallbackWrapperIT {
public class OpenAiFunctionCallbackIT {
private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWrapperIT.class);
private final Logger logger = LoggerFactory.getLogger(OpenAiFunctionCallbackIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"),
@@ -107,10 +106,10 @@ public class FunctionCallbackWrapperIT {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("WeatherInfo")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
return FunctionCallback.builder()
.description("Get the weather in location")
.function("WeatherInfo", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build();
}

View File

@@ -29,7 +29,6 @@ import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackContext.SchemaType;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -80,10 +79,11 @@ public class FunctionCallWithFunctionWrapperIT {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("WeatherInfo")
.withSchemaType(SchemaType.OPEN_API_SCHEMA)
.withDescription("Get the current weather in a given location")
return FunctionCallback.builder()
.description("Get the current weather in a given location")
.schemaType(SchemaType.OPEN_API_SCHEMA)
.function("WeatherInfo", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build();
}

View File

@@ -27,8 +27,8 @@ import org.springframework.ai.autoconfigure.vertexai.gemini.VertexAiGeminiAutoCo
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.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackContext.SchemaType;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -68,10 +68,11 @@ public class FunctionCallWithPromptFunctionIT {
""");
var promptOptions = VertexAiGeminiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withSchemaType(SchemaType.OPEN_API_SCHEMA) // IMPORTANT!!
.withDescription("Get the weather in location")
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.schemaType(SchemaType.OPEN_API_SCHEMA) // IMPORTANT!!
.description("Get the weather in location")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

View File

@@ -32,7 +32,7 @@ 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.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -64,10 +64,12 @@ public class FunctionCallbackInPromptIT {
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
var promptOptions = ZhiPuAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
// .responseConverter(response -> "" + response.temp() +
// response.unit())
.build()))
.build();
@@ -90,10 +92,10 @@ public class FunctionCallbackInPromptIT {
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
var promptOptions = ZhiPuAiChatOptions.builder()
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeatherService")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.description("Get the weather in location")
.function("CurrentWeatherService", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
.build()))
.build();

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.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -48,9 +47,9 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Geng Rong
*/
@EnabledIfEnvironmentVariable(named = "ZHIPU_AI_API_KEY", matches = ".*")
public class FunctionCallbackWrapperIT {
public class ZhipuAiFunctionCallbackIT {
private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWrapperIT.class);
private final Logger logger = LoggerFactory.getLogger(ZhipuAiFunctionCallbackIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY"))
@@ -112,10 +111,11 @@ public class FunctionCallbackWrapperIT {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("WeatherInfo")
.withDescription("Get the weather in location")
.withResponseConverter(response -> "" + response.temp() + response.unit())
return FunctionCallback.builder()
.description("Get the weather in location")
.function("WeatherInfo", new MockWeatherService())
.inputType(MockWeatherService.Request.class)
// .responseConverter(response -> "" + response.temp() + response.unit())
.build();
}