From fb0d99dc3700117ad7100374e84d8f4e4bcbf3ed Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Wed, 13 Nov 2024 12:58:52 +0100 Subject: [PATCH] Revamp function callback builder API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../ai/anthropic/AnthropicChatModelIT.java | 16 +- .../client/AnthropicChatClientIT.java | 39 ++- ...ientMethodInvokingFunctionCallbackIT.java} | 90 +++--- .../AzureOpenAiChatModelFunctionCallIT.java | 34 +-- .../converse/BedrockConverseChatClientIT.java | 31 +- .../converse/BedrockProxyChatModelIT.java | 16 +- .../BedrockConverseChatModelMain2.java | 14 +- .../minimax/ChatCompletionRequestTests.java | 25 +- .../ai/mistralai/MistralAiChatClientIT.java | 19 +- .../ai/mistralai/MistralAiChatModelIT.java | 18 +- .../MoonshotChatModelFunctionCallingIT.java | 18 +- .../OllamaChatModelFunctionCallingIT.java | 18 +- .../ai/openai/ChatCompletionRequestTests.java | 25 +- .../OpenAiChatModelFunctionCallingIT.java | 35 +-- .../ai/openai/chat/OpenAiChatModelIT.java | 18 +- .../chat/OpenAiChatModelProxyToolCallsIT.java | 18 +- .../chat/client/OpenAiChatClientIT.java | 21 +- ...ientMethodInvokingFunctionCallbackIT.java} | 64 ++-- ...enAiChatClientMultipleFunctionCallsIT.java | 31 +- .../OpenAiChatClientProxyFunctionCallsIT.java | 10 +- .../chat/proxy/GroqWithOpenAiChatModelIT.java | 18 +- .../proxy/MistralWithOpenAiChatModelIT.java | 18 +- .../proxy/NvidiaWithOpenAiChatModelIT.java | 18 +- .../proxy/OllamaWithOpenAiChatModelIT.java | 17 +- .../gemini/CreateGeminiRequestTests.java | 25 +- ...texAiGeminiChatModelFunctionCallingIT.java | 57 ++-- .../zhipuai/ChatCompletionRequestTests.java | 25 +- .../ai/zhipuai/chat/ZhiPuAiChatModelIT.java | 18 +- .../ai/chat/client/ChatClient.java | 20 ++ .../ai/chat/client/DefaultChatClient.java | 39 ++- .../ai/converter/BeanOutputConverter.java | 40 +-- .../ai/model/ModelOptionsUtils.java | 39 ++- .../function/AbstractFunctionCallback.java | 9 +- .../DefaultFunctionCallbackBuilder.java | 284 +++++++++++++++++ .../ai/model/function/FunctionCallback.java | 108 +++++++ .../function/FunctionCallbackContext.java | 40 +-- .../function/FunctionCallbackWrapper.java | 36 ++- ...Helper.java => FunctionCallingHelper.java} | 6 +- .../FunctionInvokingFunctionCallback.java | 52 ++++ ...va => MethodInvokingFunctionCallback.java} | 77 ++--- ...sions.kt => FunctionCallbackExtensions.kt} | 9 +- .../ai/chat/ChatBuilderTests.java | 8 +- .../ai/chat/client/ChatClientTest.java | 13 +- .../chat/client/DefaultChatClientTests.java | 28 +- .../DefaultFunctionCallbackBuilderTests.java | 287 ++++++++++++++++++ .../function/MethodFunctionCallbackTests.java | 40 +-- ....kt => FunctionCallbackExtensionsTests.kt} | 12 +- .../ROOT/pages/api/bedrock-converse.adoc | 7 +- .../functions/anthropic-chat-functions.adoc | 28 +- .../azure-open-ai-chat-functions.adoc | 24 +- .../functions/minimax-chat-functions.adoc | 54 +--- .../functions/mistralai-chat-functions.adoc | 26 +- .../functions/moonshot-chat-functions.adoc | 54 +--- .../chat/functions/ollama-chat-functions.adoc | 30 +- .../chat/functions/openai-chat-functions.adoc | 61 ++-- .../vertexai-gemini-chat-functions.adoc | 28 +- .../functions/zhipuai-chat-functions.adoc | 54 +--- .../modules/ROOT/pages/api/functions.adoc | 73 +++-- .../FunctionCallWithPromptFunctionIT.java | 9 +- .../FunctionCallWithFunctionWrapperIT.java | 8 +- .../FunctionCallWithPromptFunctionIT.java | 9 +- .../FunctionCallWithPromptFunctionIT.java | 9 +- .../minimax/FunctionCallbackInPromptIT.java | 18 +- ...IT.java => MiniMaxFunctionCallbackIT.java} | 13 +- .../mistralai/tool/PaymentStatusPromptIT.java | 15 +- .../tool/WeatherServicePromptIT.java | 18 +- .../tool/FunctionCallbackInPromptIT.java | 18 +- ...T.java => MoonshotFunctionCallbackIT.java} | 13 +- .../tool/FunctionCallbackInPromptIT.java | 18 +- ...rIT.java => OllamaFunctionCallbackIT.java} | 13 +- .../tool/FunctionCallbackInPrompt2IT.java | 20 +- .../tool/FunctionCallbackInPromptIT.java | 18 +- ...IT.java => OpenAiFunctionCallback2IT.java} | 13 +- ...rIT.java => OpenAiFunctionCallbackIT.java} | 13 +- .../FunctionCallWithFunctionWrapperIT.java | 10 +- .../FunctionCallWithPromptFunctionIT.java | 11 +- .../tool/FunctionCallbackInPromptIT.java | 20 +- ...IT.java => ZhipuAiFunctionCallbackIT.java} | 14 +- 78 files changed, 1704 insertions(+), 918 deletions(-) rename models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/{AnthropicChatClientMethodFunctionCallbackIT.java => AnthropicChatClientMethodInvokingFunctionCallbackIT.java} (77%) rename models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/{OpenAiChatClientMethodFunctionCallbackIT.java => OpenAiChatClientMethodInvokingFunctionCallbackIT.java} (78%) create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/model/function/DefaultFunctionCallbackBuilder.java rename spring-ai-core/src/main/java/org/springframework/ai/model/function/{ToolCallHelper.java => FunctionCallingHelper.java} (97%) create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionInvokingFunctionCallback.java rename spring-ai-core/src/main/java/org/springframework/ai/model/function/{MethodFunctionCallback.java => MethodInvokingFunctionCallback.java} (83%) rename spring-ai-core/src/main/kotlin/org/springframework/ai/model/function/{FunctionCallbackWrapperExtensions.kt => FunctionCallbackExtensions.kt} (69%) create mode 100644 spring-ai-core/src/test/java/org/springframework/ai/model/function/DefaultFunctionCallbackBuilderTests.java rename spring-ai-core/src/test/kotlin/org/springframework/ai/model/function/{FunctionCallbackWrapperExtensionsTests.kt => FunctionCallbackExtensionsTests.kt} (69%) rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/{FunctionCallbackWrapperIT.java => MiniMaxFunctionCallbackIT.java} (90%) rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/{FunctionCallbackWrapperIT.java => MoonshotFunctionCallbackIT.java} (90%) rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/{FunctionCallbackWrapperIT.java => OllamaFunctionCallbackIT.java} (92%) rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/{FunctionCallbackWrapper2IT.java => OpenAiFunctionCallback2IT.java} (88%) rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/{FunctionCallbackWrapperIT.java => OpenAiFunctionCallbackIT.java} (90%) rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/{FunctionCallbackWrapperIT.java => ZhipuAiFunctionCallbackIT.java} (90%) diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java index 1e2e171c3..1df32f777 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java @@ -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(); diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientIT.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientIT.java index b4038d6ac..7a5ff4475 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientIT.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientIT.java @@ -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 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 diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientMethodFunctionCallbackIT.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientMethodInvokingFunctionCallbackIT.java similarity index 77% rename from models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientMethodFunctionCallbackIT.java rename to models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientMethodInvokingFunctionCallbackIT.java index cf8688cb8..e4c81eb1d 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientMethodFunctionCallbackIT.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientMethodInvokingFunctionCallbackIT.java @@ -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 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); diff --git a/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/function/AzureOpenAiChatModelFunctionCallIT.java b/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/function/AzureOpenAiChatModelFunctionCallIT.java index 4ac53bb5c..8be6779a1 100644 --- a/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/function/AzureOpenAiChatModelFunctionCallIT.java +++ b/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/function/AzureOpenAiChatModelFunctionCallIT.java @@ -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(); diff --git a/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockConverseChatClientIT.java b/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockConverseChatClientIT.java index 0bbc99be3..5e390b8f7 100644 --- a/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockConverseChatClientIT.java +++ b/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockConverseChatClientIT.java @@ -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 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 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 diff --git a/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModelIT.java b/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModelIT.java index f2f5c19f2..a46c06d79 100644 --- a/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModelIT.java +++ b/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModelIT.java @@ -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 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(); diff --git a/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/experiements/BedrockConverseChatModelMain2.java b/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/experiements/BedrockConverseChatModelMain2.java index e42b60d9a..d0cd9320d 100644 --- a/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/experiements/BedrockConverseChatModelMain2.java +++ b/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/experiements/BedrockConverseChatModelMain2.java @@ -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 responses = chatModel.converseStream(streamRequest); List responseList = responses.collectList().block(); System.out.println(responseList); - - // Flux responses2 = ConverseApiUtils.toChatResponse(responses); - // List responseList2 = responses2.collectList().block(); - // System.out.println(responseList2); - } } diff --git a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/ChatCompletionRequestTests.java b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/ChatCompletionRequestTests.java index dcb91c547..c89174872 100644 --- a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/ChatCompletionRequestTests.java +++ b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/ChatCompletionRequestTests.java @@ -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); diff --git a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java index 6298c7773..0315acbb5 100644 --- a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java +++ b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java @@ -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 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 diff --git a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatModelIT.java b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatModelIT.java index 594169985..a87592e11 100644 --- a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatModelIT.java +++ b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatModelIT.java @@ -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(); diff --git a/models/spring-ai-moonshot/src/test/java/org/springframework/ai/moonshot/chat/MoonshotChatModelFunctionCallingIT.java b/models/spring-ai-moonshot/src/test/java/org/springframework/ai/moonshot/chat/MoonshotChatModelFunctionCallingIT.java index d136686d1..a1964e87b 100644 --- a/models/spring-ai-moonshot/src/test/java/org/springframework/ai/moonshot/chat/MoonshotChatModelFunctionCallingIT.java +++ b/models/spring-ai-moonshot/src/test/java/org/springframework/ai/moonshot/chat/MoonshotChatModelFunctionCallingIT.java @@ -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 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(); diff --git a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatModelFunctionCallingIT.java b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatModelFunctionCallingIT.java index 72d1b1b62..3bad44cb1 100644 --- a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatModelFunctionCallingIT.java +++ b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatModelFunctionCallingIT.java @@ -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(); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/ChatCompletionRequestTests.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/ChatCompletionRequestTests.java index d948080cb..2956d4fb8 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/ChatCompletionRequestTests.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/ChatCompletionRequestTests.java @@ -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); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelFunctionCallingIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelFunctionCallingIT.java index 281f86e46..16ae7199f 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelFunctionCallingIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelFunctionCallingIT.java @@ -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(); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelIT.java index 14d927569..541ae7556 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelIT.java @@ -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(); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelProxyToolCallsIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelProxyToolCallsIT.java index b6e182559..acaca6836 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelProxyToolCallsIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelProxyToolCallsIT.java @@ -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 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 toolCallConversation = this.toolCallHelper + List 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 toolCallConversation = this.toolCallHelper + List 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 responses = this.toolCallHelper.processStream(this.chatModel, prompt, + Flux responses = this.functionCallingHelper.processStream(this.chatModel, prompt, Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(), OpenAiApi.ChatCompletionFinishReason.STOP.name()), toolCall -> { diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java index 3c8a724a0..7278bfe84 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java @@ -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 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 diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMethodFunctionCallbackIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMethodInvokingFunctionCallbackIT.java similarity index 78% rename from models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMethodFunctionCallbackIT.java rename to models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMethodInvokingFunctionCallbackIT.java index feed9add8..5fcd5ed8d 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMethodFunctionCallbackIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMethodInvokingFunctionCallbackIT.java @@ -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 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 diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMultipleFunctionCallsIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMultipleFunctionCallsIT.java index 6ba1682e1..eab372e71 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMultipleFunctionCallsIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMultipleFunctionCallsIT.java @@ -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 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 diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientProxyFunctionCallsIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientProxyFunctionCallsIT.java index 709398dee..b02892a51 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientProxyFunctionCallsIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientProxyFunctionCallsIT.java @@ -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(); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/GroqWithOpenAiChatModelIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/GroqWithOpenAiChatModelIT.java index a3d45486b..c3ee4f550 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/GroqWithOpenAiChatModelIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/GroqWithOpenAiChatModelIT.java @@ -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 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 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(); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/MistralWithOpenAiChatModelIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/MistralWithOpenAiChatModelIT.java index 80ec34b99..0c0ee7ad6 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/MistralWithOpenAiChatModelIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/MistralWithOpenAiChatModelIT.java @@ -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(); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/NvidiaWithOpenAiChatModelIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/NvidiaWithOpenAiChatModelIT.java index 4bbe60e68..010896df4 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/NvidiaWithOpenAiChatModelIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/NvidiaWithOpenAiChatModelIT.java @@ -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 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 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(); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/OllamaWithOpenAiChatModelIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/OllamaWithOpenAiChatModelIT.java index 81df8ac03..e766074f6 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/OllamaWithOpenAiChatModelIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/OllamaWithOpenAiChatModelIT.java @@ -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 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 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(); diff --git a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/CreateGeminiRequestTests.java b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/CreateGeminiRequestTests.java index d1244b97d..eb821caa3 100644 --- a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/CreateGeminiRequestTests.java +++ b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/CreateGeminiRequestTests.java @@ -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); diff --git a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/function/VertexAiGeminiChatModelFunctionCallingIT.java b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/function/VertexAiGeminiChatModelFunctionCallingIT.java index 06c0bbd38..e8b650c3a 100644 --- a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/function/VertexAiGeminiChatModelFunctionCallingIT.java +++ b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/function/VertexAiGeminiChatModelFunctionCallingIT.java @@ -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(); diff --git a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/ChatCompletionRequestTests.java b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/ChatCompletionRequestTests.java index f548ea011..c8a21d860 100644 --- a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/ChatCompletionRequestTests.java +++ b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/ChatCompletionRequestTests.java @@ -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); diff --git a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelIT.java b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelIT.java index 53e9cd367..17e273a1e 100644 --- a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelIT.java +++ b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelIT.java @@ -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(); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java index b6a5642a7..d1976b363 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java @@ -212,14 +212,26 @@ public interface ChatClient { ChatClientRequestSpec options(T options); + /** + * @deprecated use {@link #function(FunctionCallback)} instead. + */ + @Deprecated ChatClientRequestSpec function(String name, String description, java.util.function.Function function); + /** + * @deprecated use {@link #function(FunctionCallback)} instead. + */ + @Deprecated ChatClientRequestSpec function(String name, String description, java.util.function.BiFunction function); ChatClientRequestSpec functions(FunctionCallback... functionCallbacks); + /** + * @deprecated use {@link #function(FunctionCallback)} instead. + */ + @Deprecated ChatClientRequestSpec function(String name, String description, Class inputType, java.util.function.Function function); @@ -278,8 +290,16 @@ public interface ChatClient { Builder defaultSystem(Consumer systemSpecConsumer); + /** + * @deprecated use {@link #defaultFunction(FunctionCallback)} instead. + */ + @Deprecated Builder defaultFunction(String name, String description, java.util.function.Function function); + /** + * @deprecated use {@link #defaultFunction(FunctionCallback)} instead. + */ + @Deprecated Builder defaultFunction(String name, String description, java.util.function.BiFunction function); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java index 6f46f2749..4ab2f37d3 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java @@ -836,19 +836,14 @@ public class DefaultChatClient implements ChatClient { return this; } + @Override public ChatClientRequestSpec function(String name, String description, java.util.function.Function function) { - return this.function(name, description, null, function); - } - - public ChatClientRequestSpec function(String name, String description, - java.util.function.BiFunction 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 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 ChatClientRequestSpec function(String name, String description, + java.util.function.BiFunction 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 ChatClientRequestSpec function(String name, String description, @Nullable Class inputType, java.util.function.Function 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; diff --git a/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java index 0e718c1b9..84afa3f70 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java @@ -63,7 +63,7 @@ public class BeanOutputConverter implements StructuredOutputConverter { /** * The target class type reference to which the output will be converted. */ - private final TypeReference 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 implements StructuredOutputConverter { * @param typeRef The target class type reference. */ public BeanOutputConverter(ParameterizedTypeReference typeRef) { - this(new CustomizedTypeReference<>(typeRef), null); + this(typeRef.getType(), null); } /** @@ -105,19 +105,19 @@ public class BeanOutputConverter implements StructuredOutputConverter { * @param objectMapper Custom object mapper for JSON operations. endings. */ public BeanOutputConverter(ParameterizedTypeReference 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 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 implements StructuredOutputConverter { .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 implements StructuredOutputConverter { } 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 implements StructuredOutputConverter { // 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 implements StructuredOutputConverter { return this.jsonSchema; } - private static class CustomizedTypeReference extends TypeReference { - - private final Type type; - - CustomizedTypeReference(ParameterizedTypeReference typeRef) { - this.type = typeRef.getType(); - } - - @Override - public Type getType() { - return this.type; - } - - } - } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java b/spring-ai-core/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java index e049fb17f..e538f8ee3 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java @@ -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; diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallback.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallback.java index 8a2c84aca..c4ddfbb21 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallback.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallback.java @@ -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 implements BiFunction inputType; + private final Type inputType; private final String inputTypeSchema; @@ -70,7 +71,7 @@ abstract class AbstractFunctionCallback implements BiFunction inputType, + protected AbstractFunctionCallback(String name, String description, String inputTypeSchema, Type inputType, Function 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 implements BiFunction T fromJson(String json, Class targetClass) { + private 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); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/DefaultFunctionCallbackBuilder.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/DefaultFunctionCallbackBuilder.java new file mode 100644 index 000000000..9ce3a7945 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/DefaultFunctionCallbackBuilder.java @@ -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 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 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 FunctionInvokingSpec function(String name, Function function) { + return new DefaultFunctionInvokingSpec<>(name, function); + } + + @Override + public FunctionInvokingSpec function(String name, BiFunction biFunction) { + return new DefaultFunctionInvokingSpec<>(name, biFunction); + } + + @Override + public MethodInvokingSpec method(String methodName, Class... argumentTypes) { + return new DefaultMethodInvokingSpec(methodName, argumentTypes); + } + + class DefaultFunctionInvokingSpec implements FunctionInvokingSpec { + + private final String name; + + private Type inputType; + + private final BiFunction biFunction; + + private final Function function; + + private DefaultFunctionInvokingSpec(String name, BiFunction 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 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 inputType(Class inputType) { + Assert.notNull(inputType, "InputType must not be null"); + this.inputType = inputType; + return this; + } + + @Override + public FunctionInvokingSpec 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 finalBiFunction = (this.biFunction != null) ? this.biFunction + : (request, context) -> this.function.apply(request); + + return new FunctionInvokingFunctionCallback(this.name, this.getDescription(), inputTypeSchema, + this.inputType, (Function) 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; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java index accdae69a..f6a94f01b 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java @@ -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 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); + + FunctionInvokingSpec function(String name, Function function); + + FunctionInvokingSpec function(String name, BiFunction biFunction); + + MethodInvokingSpec method(String methodName, Class... argumentTypes); + + } + + interface FunctionInvokingSpec { + + /** + * Function input type. The input type is used to validate the function input + * arguments. + * @see #inputType(ParameterizedTypeReference) + */ + FunctionInvokingSpec inputType(Class inputType); + + /** + * Function input type retaining generic types. The input type is used to validate + * the function input arguments. + */ + FunctionInvokingSpec 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(); + + } + } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackContext.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackContext.java index 762d33969..d83123314 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackContext.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackContext.java @@ -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) bean) - .withName(beanName) - .withSchemaType(this.schemaType) - .withDescription(functionDescription) - .withInputType(functionInputClass) + return FunctionCallback.builder() + .description(functionDescription) + .schemaType(this.schemaType) + .function(beanName, (BiFunction) bean) + .inputType(functionInputClass) .build(); } else { diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java index fe9fa0a15..fc087b106 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java @@ -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 extends AbstractFunctionCallback { private final BiFunction biFunction; - private FunctionCallbackWrapper(String name, String description, String inputTypeSchema, Class inputType, + FunctionCallbackWrapper(String name, String description, String inputTypeSchema, Type inputType, Function responseConverter, ObjectMapper objectMapper, BiFunction function) { super(name, description, inputTypeSchema, inputType, responseConverter, objectMapper); Assert.notNull(function, "Function must not be null"); this.biFunction = function; } - public static Builder builder(BiFunction biFunction) { - return new Builder<>(biFunction); - } - - public static Builder builder(Function 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 Builder builder(BiFunction biFunction) { + return new Builder<>(biFunction); + } + + /** + * @deprecated use {@link FunctionCallback#builder(Function)} instead. + */ + @Deprecated + public static Builder builder(Function function) { + return new Builder<>(function); + } + + /** + * @deprecated in favor of {@link DefaultFunctionCallbackBuilder} + */ + @Deprecated public static class Builder { private final BiFunction biFunction; @@ -85,13 +97,13 @@ public final class FunctionCallbackWrapper extends AbstractFunctionCallbac private ObjectMapper objectMapper; - public Builder(BiFunction biFunction) { + private Builder(BiFunction biFunction) { Assert.notNull(biFunction, "Function must not be null"); this.biFunction = biFunction; this.function = null; } - public Builder(Function function) { + private Builder(Function function) { Assert.notNull(function, "Function must not be null"); this.biFunction = null; this.function = function; diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/ToolCallHelper.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingHelper.java similarity index 97% rename from spring-ai-core/src/main/java/org/springframework/ai/model/function/ToolCallHelper.java rename to spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingHelper.java index 4df569657..5fdb7203c 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/ToolCallHelper.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingHelper.java @@ -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 toolFunctionCallbacks) { super(functionCallbackContext, functionCallingOptions, toolFunctionCallbacks); } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionInvokingFunctionCallback.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionInvokingFunctionCallback.java new file mode 100644 index 000000000..256cf74e7 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionInvokingFunctionCallback.java @@ -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 extends AbstractFunctionCallback { + + private final BiFunction biFunction; + + FunctionInvokingFunctionCallback(String name, String description, String inputTypeSchema, Type inputType, + Function responseConverter, ObjectMapper objectMapper, BiFunction 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); + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/MethodFunctionCallback.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/MethodInvokingFunctionCallback.java similarity index 83% rename from spring-ai-core/src/main/java/org/springframework/ai/model/function/MethodFunctionCallback.java rename to spring-ai-core/src/main/java/org/springframework/ai/model/function/MethodInvokingFunctionCallback.java index 3d68148ab..35a253877 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/MethodFunctionCallback.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/MethodInvokingFunctionCallback.java @@ -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 responseConverter; + + MethodInvokingFunctionCallback(Object functionObject, Method method, String description, ObjectMapper mapper, + String name, Function 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); - } - - } - } diff --git a/spring-ai-core/src/main/kotlin/org/springframework/ai/model/function/FunctionCallbackWrapperExtensions.kt b/spring-ai-core/src/main/kotlin/org/springframework/ai/model/function/FunctionCallbackExtensions.kt similarity index 69% rename from spring-ai-core/src/main/kotlin/org/springframework/ai/model/function/FunctionCallbackWrapperExtensions.kt rename to spring-ai-core/src/main/kotlin/org/springframework/ai/model/function/FunctionCallbackExtensions.kt index d07dcbc2d..3201ea9a3 100644 --- a/spring-ai-core/src/main/kotlin/org/springframework/ai/model/function/FunctionCallbackWrapperExtensions.kt +++ b/spring-ai-core/src/main/kotlin/org/springframework/ai/model/function/FunctionCallbackExtensions.kt @@ -16,11 +16,14 @@ package org.springframework.ai.model.function +import org.springframework.core.ParameterizedTypeReference + /** - * Extension for [FunctionCallbackWrapper.Builder.withInputType] providing a `withInputType()` + * Extension for [FunctionCallback.FunctionInvokerBuilder.inputType] providing a `inputType()` * variant. * * @author Sebastien Deleuze */ -inline fun FunctionCallbackWrapper.Builder<*, *>.withInputType() = - withInputType(T::class.java) +inline fun FunctionCallback.FunctionInvokingSpec.inputType(): FunctionCallback.FunctionInvokingSpec = + inputType(I::class.java) + diff --git a/spring-ai-core/src/test/java/org/springframework/ai/chat/ChatBuilderTests.java b/spring-ai-core/src/test/java/org/springframework/ai/chat/ChatBuilderTests.java index 7ab6abb25..4789c5537 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/chat/ChatBuilderTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/chat/ChatBuilderTests.java @@ -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 functions = new HashSet<>(); String func = "func"; - FunctionCallback cb = FunctionCallbackWrapper.builder(i -> i) - .withName("cb") - .withDescription("cb") + FunctionCallback cb = FunctionCallback.builder() + .description("cb") + .function("cb", i -> i) + .inputType(Integer.class) .build(); functions.add(func); diff --git a/spring-ai-core/src/test/java/org/springframework/ai/chat/client/ChatClientTest.java b/spring-ai-core/src/test/java/org/springframework/ai/chat/client/ChatClientTest.java index 1f9d407c3..2fa31cacf 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/chat/client/ChatClientTest.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/chat/client/ChatClientTest.java @@ -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") diff --git a/spring-ai-core/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java b/spring-ai-core/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java index 269fa7812..58f49a115 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java @@ -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")); } diff --git a/spring-ai-core/src/test/java/org/springframework/ai/model/function/DefaultFunctionCallbackBuilderTests.java b/spring-ai-core/src/test/java/org/springframework/ai/model/function/DefaultFunctionCallbackBuilderTests.java new file mode 100644 index 000000000..8e254f0f2 --- /dev/null +++ b/spring-ai-core/src/test/java/org/springframework/ai/model/function/DefaultFunctionCallbackBuilderTests.java @@ -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>() { + }) + .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 { + + private T datum; + + public T getDatum() { + return datum; + } + + public void setDatum(T value) { + this.datum = value; + } + + } + +} diff --git a/spring-ai-core/src/test/java/org/springframework/ai/model/function/MethodFunctionCallbackTests.java b/spring-ai-core/src/test/java/org/springframework/ai/model/function/MethodFunctionCallbackTests.java index 59aede715..1a8fe4f15 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/model/function/MethodFunctionCallbackTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/model/function/MethodFunctionCallbackTests.java @@ -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); diff --git a/spring-ai-core/src/test/kotlin/org/springframework/ai/model/function/FunctionCallbackWrapperExtensionsTests.kt b/spring-ai-core/src/test/kotlin/org/springframework/ai/model/function/FunctionCallbackExtensionsTests.kt similarity index 69% rename from spring-ai-core/src/test/kotlin/org/springframework/ai/model/function/FunctionCallbackWrapperExtensionsTests.kt rename to spring-ai-core/src/test/kotlin/org/springframework/ai/model/function/FunctionCallbackExtensionsTests.kt index 531b3cc87..2ab01f2f6 100644 --- a/spring-ai-core/src/test/kotlin/org/springframework/ai/model/function/FunctionCallbackWrapperExtensionsTests.kt +++ b/spring-ai-core/src/test/kotlin/org/springframework/ai/model/function/FunctionCallbackExtensionsTests.kt @@ -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>() + private val spec = mockk>() @Test - fun withInputType() { - every { builder.withInputType(any>()) } returns builder - builder.withInputType() - verify { builder.withInputType(WeatherRequest::class.java) } + fun inputType() { + every { spec.inputType(any>()) } returns spec + spec.inputType() + verify { spec.inputType(WeatherRequest::class.java) } } } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock-converse.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock-converse.adoc index 0d051fe11..2c63aa9b2 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock-converse.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock-converse.adoc @@ -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(); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/anthropic-chat-functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/anthropic-chat-functions.adoc index 793e8940a..ef83072d8 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/anthropic-chat-functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/anthropic-chat-functions.adoc @@ -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)); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/azure-open-ai-chat-functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/azure-open-ai-chat-functions.adoc index 93810865c..9116fbc90 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/azure-open-ai-chat-functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/azure-open-ai-chat-functions.adoc @@ -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)); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/minimax-chat-functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/minimax-chat-functions.adoc index 3fbeef622..1f528f5ff 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/minimax-chat-functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/minimax-chat-functions.adoc @@ -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 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. diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/mistralai-chat-functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/mistralai-chat-functions.adoc index 0cf4e0589..8b1b1ffbb 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/mistralai-chat-functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/mistralai-chat-functions.adoc @@ -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)); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/moonshot-chat-functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/moonshot-chat-functions.adoc index fe04e62ea..f67cff371 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/moonshot-chat-functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/moonshot-chat-functions.adoc @@ -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 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. diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/ollama-chat-functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/ollama-chat-functions.adoc index 0e7ba4e90..f2e0aad9c 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/ollama-chat-functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/ollama-chat-functions.adoc @@ -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)); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/openai-chat-functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/openai-chat-functions.adoc index 1468380af..ed520bb71 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/openai-chat-functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/openai-chat-functions.adoc @@ -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 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 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(); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/vertexai-gemini-chat-functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/vertexai-gemini-chat-functions.adoc index 3fd4dd317..6259cd2f7 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/vertexai-gemini-chat-functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/vertexai-gemini-chat-functions.adoc @@ -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 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(); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/zhipuai-chat-functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/zhipuai-chat-functions.adoc index 30f425786..aa0d6b24e 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/zhipuai-chat-functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/zhipuai-chat-functions.adoc @@ -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 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. diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/functions.adoc index 07062e9ed..910771d26 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/functions.adoc @@ -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() + 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() .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 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() diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithPromptFunctionIT.java index aad0cbc24..aa2d11101 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithPromptFunctionIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithPromptFunctionIT.java @@ -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(); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionWrapperIT.java index 178a61d15..721359ef2 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionWrapperIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionWrapperIT.java @@ -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(); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithPromptFunctionIT.java index 7d5a7b913..908fbc59e 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithPromptFunctionIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithPromptFunctionIT.java @@ -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(); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/tool/FunctionCallWithPromptFunctionIT.java index a45fe0f49..d5bc70453 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/tool/FunctionCallWithPromptFunctionIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/tool/FunctionCallWithPromptFunctionIT.java @@ -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(); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackInPromptIT.java index a53c1fa59..e1ca67dba 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackInPromptIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackInPromptIT.java @@ -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(); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxFunctionCallbackIT.java similarity index 90% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWrapperIT.java rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxFunctionCallbackIT.java index 28d9eb6c6..d2fb03a69 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWrapperIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxFunctionCallbackIT.java @@ -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(); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/PaymentStatusPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/PaymentStatusPromptIT.java index 187d4da66..3bee692bc 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/PaymentStatusPromptIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/PaymentStatusPromptIT.java @@ -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() { - - 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(); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/WeatherServicePromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/WeatherServicePromptIT.java index 417a2b6a9..394a5f155 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/WeatherServicePromptIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/WeatherServicePromptIT.java @@ -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(); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackInPromptIT.java index 574057e3e..3b95bc222 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackInPromptIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackInPromptIT.java @@ -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(); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/MoonshotFunctionCallbackIT.java similarity index 90% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackWrapperIT.java rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/MoonshotFunctionCallbackIT.java index c97a2aee2..c5141515e 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackWrapperIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/MoonshotFunctionCallbackIT.java @@ -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(); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackInPromptIT.java index 047df41f7..66d9ef09d 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackInPromptIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackInPromptIT.java @@ -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(); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/OllamaFunctionCallbackIT.java similarity index 92% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackWrapperIT.java rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/OllamaFunctionCallbackIT.java index d500512c0..d8c4c0ae6 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackWrapperIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/OllamaFunctionCallbackIT.java @@ -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(); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java index b6e2c3024..940e2da7d 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java @@ -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() { - @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); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java index 53bf98c61..fc4d4a447 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java @@ -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(); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapper2IT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/OpenAiFunctionCallback2IT.java similarity index 88% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapper2IT.java rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/OpenAiFunctionCallback2IT.java index 062ce7f76..c471b154b 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapper2IT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/OpenAiFunctionCallback2IT.java @@ -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(); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/OpenAiFunctionCallbackIT.java similarity index 90% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapperIT.java rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/OpenAiFunctionCallbackIT.java index b438cd42a..468f8ff5e 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapperIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/OpenAiFunctionCallbackIT.java @@ -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(); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionWrapperIT.java index 7b1f2287c..8a3de8549 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionWrapperIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionWrapperIT.java @@ -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(); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithPromptFunctionIT.java index 3f8483746..4465df6a4 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithPromptFunctionIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithPromptFunctionIT.java @@ -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(); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackInPromptIT.java index e10d53103..7dbf49051 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackInPromptIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackInPromptIT.java @@ -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(); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/ZhipuAiFunctionCallbackIT.java similarity index 90% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWrapperIT.java rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/ZhipuAiFunctionCallbackIT.java index 15118dbab..73f9ea121 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWrapperIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/ZhipuAiFunctionCallbackIT.java @@ -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(); }