From 9692ebd7b467c745512126e33fef0ca018f3d0f8 Mon Sep 17 00:00:00 2001 From: Ilayaperumal Gopinathan Date: Fri, 25 Apr 2025 19:46:15 +0100 Subject: [PATCH] Refactor toolcalling support for ZhipuAI - Update ZhipuAI Chat Model to use ToolCalling Manager and ToolExecutionEligibilityPredicate - Update ZhipuAI ChatOptions to implement ToolCallingChatOptions - Update Autoconfiguration for ZhipuAI model to use ToolCallingAutoconfiguration - Update tests Signed-off-by: Ilayaperumal Gopinathan --- .../pom.xml | 6 + .../ZhiPuAiChatAutoConfiguration.java | 27 +- .../ZhiPuAiAutoConfigurationIT.java | 8 +- .../tool/FunctionCallbackInPromptIT.java | 7 +- ...nctionCallbackWithPlainFunctionBeanIT.java | 17 +- .../tool/ZhipuAiFunctionCallbackIT.java | 13 +- .../ai/zhipuai/ZhiPuAiChatModel.java | 230 +++++++++++++----- .../ai/zhipuai/ZhiPuAiChatOptions.java | 186 +++++++------- .../zhipuai/ChatCompletionRequestTests.java | 54 +--- .../ai/zhipuai/api/ZhiPuAiRetryTests.java | 8 +- .../ai/zhipuai/chat/ZhiPuAiChatModelIT.java | 18 +- .../chat/ZhiPuAiChatModelObservationIT.java | 4 +- 12 files changed, 334 insertions(+), 244 deletions(-) diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml index 786fcce7f..b1550cb4e 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml @@ -35,6 +35,12 @@ + + org.springframework.ai + spring-ai-autoconfigure-model-tool + ${project.parent.version} + + org.springframework.ai spring-ai-autoconfigure-retry diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java index 20df42b7c..a1ab37fbe 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java @@ -26,11 +26,16 @@ import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackResolver; +import org.springframework.ai.model.tool.DefaultToolExecutionEligibilityPredicate; +import org.springframework.ai.model.tool.ToolCallingManager; +import org.springframework.ai.model.tool.ToolExecutionEligibilityPredicate; +import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -50,28 +55,32 @@ import org.springframework.web.client.RestClient; * @author Geng Rong * @author Ilayaperumal Gopinathan */ -@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) +@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class, + ToolCallingAutoConfiguration.class }) @ConditionalOnClass(ZhiPuAiApi.class) @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ZHIPUAI, matchIfMissing = true) @EnableConfigurationProperties({ ZhiPuAiConnectionProperties.class, ZhiPuAiChatProperties.class }) +@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, + ToolCallingAutoConfiguration.class }) public class ZhiPuAiChatAutoConfiguration { @Bean @ConditionalOnMissingBean public ZhiPuAiChatModel zhiPuAiChatModel(ZhiPuAiConnectionProperties commonProperties, ZhiPuAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, - List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { + ObjectProvider observationConvention, ToolCallingManager toolCallingManager, + ObjectProvider toolExecutionEligibilityPredicate) { var zhiPuAiApi = zhiPuAiApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), chatProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); - var chatModel = new ZhiPuAiChatModel(zhiPuAiApi, chatProperties.getOptions(), functionCallbackResolver, - toolFunctionCallbacks, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + var chatModel = new ZhiPuAiChatModel(zhiPuAiApi, chatProperties.getOptions(), toolCallingManager, retryTemplate, + observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP), + toolExecutionEligibilityPredicate.getIfUnique(DefaultToolExecutionEligibilityPredicate::new)); observationConvention.ifAvailable(chatModel::setObservationConvention); @@ -90,12 +99,4 @@ public class ZhiPuAiChatAutoConfiguration { return new ZhiPuAiApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler); } - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java index 51d3e1998..badcbf8b6 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java @@ -27,6 +27,7 @@ import reactor.core.publisher.Flux; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.embedding.EmbeddingResponse; import org.springframework.ai.image.ImagePrompt; @@ -58,8 +59,8 @@ public class ZhiPuAiAutoConfigurationIT { void generate() { this.contextRunner.withConfiguration(AutoConfigurations.of(ZhiPuAiChatAutoConfiguration.class)).run(context -> { ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); - String response = chatModel.call("Hello"); - assertThat(response).isNotEmpty(); + ChatResponse response = chatModel.call(new Prompt("Hello", ChatOptions.builder().build())); + assertThat(response.getResult().getOutput().getText()).isNotEmpty(); logger.info("Response: " + response); }); } @@ -68,7 +69,8 @@ public class ZhiPuAiAutoConfigurationIT { void generateStreaming() { this.contextRunner.withConfiguration(AutoConfigurations.of(ZhiPuAiChatAutoConfiguration.class)).run(context -> { ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); + Flux responseFlux = chatModel + .stream(new Prompt(new UserMessage("Hello"), ChatOptions.builder().build())); String response = responseFlux.collectList() .block() .stream() diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java index 6b640953c..00367ebef 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java @@ -33,6 +33,7 @@ import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +import org.springframework.ai.tool.function.FunctionToolCallback; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -64,8 +65,7 @@ public class FunctionCallbackInPromptIT { "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); var promptOptions = ZhiPuAiChatOptions.builder() - .functionCallbacks(List.of(FunctionCallback.builder() - .function("CurrentWeatherService", new MockWeatherService()) + .toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService()) .description("Get the weather in location") .inputType(MockWeatherService.Request.class) // .responseConverter(response -> "" + response.temp() + @@ -92,8 +92,7 @@ public class FunctionCallbackInPromptIT { "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); var promptOptions = ZhiPuAiChatOptions.builder() - .functionCallbacks(List.of(FunctionCallback.builder() - .function("CurrentWeatherService", new MockWeatherService()) + .toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService()) .description("Get the weather in location") .inputType(MockWeatherService.Request.class) .build())) diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java index 0ab9a8758..03ff513be 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java @@ -32,6 +32,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.function.FunctionCallingOptions; +import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; @@ -69,8 +70,8 @@ class FunctionCallbackWithPlainFunctionBeanIT { UserMessage userMessage = new UserMessage( "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - ChatResponse response = chatModel.call( - new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().function("weatherFunction").build())); + ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), + ZhiPuAiChatOptions.builder().toolNames("weatherFunction").build())); logger.info("Response: {}", response); @@ -78,7 +79,7 @@ class FunctionCallbackWithPlainFunctionBeanIT { // Test weatherFunctionTwo response = chatModel.call(new Prompt(List.of(userMessage), - ZhiPuAiChatOptions.builder().function("weatherFunctionTwo").build())); + ZhiPuAiChatOptions.builder().toolNames("weatherFunctionTwo").build())); logger.info("Response: {}", response); @@ -97,8 +98,8 @@ class FunctionCallbackWithPlainFunctionBeanIT { UserMessage userMessage = new UserMessage( "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - FunctionCallingOptions functionOptions = FunctionCallingOptions.builder() - .function("weatherFunction") + ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder() + .toolNames("weatherFunction") .build(); ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions)); @@ -117,8 +118,8 @@ class FunctionCallbackWithPlainFunctionBeanIT { UserMessage userMessage = new UserMessage( "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - Flux response = chatModel.stream( - new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().function("weatherFunction").build())); + Flux response = chatModel.stream(new Prompt(List.of(userMessage), + ZhiPuAiChatOptions.builder().toolNames("weatherFunction").build())); String content = response.collectList() .block() @@ -136,7 +137,7 @@ class FunctionCallbackWithPlainFunctionBeanIT { // Test weatherFunctionTwo response = chatModel.stream(new Prompt(List.of(userMessage), - ZhiPuAiChatOptions.builder().function("weatherFunctionTwo").build())); + ZhiPuAiChatOptions.builder().toolNames("weatherFunctionTwo").build())); content = response.collectList() .block() diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java index edee1c1bd..b9a4176ea 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java @@ -33,6 +33,8 @@ import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -67,7 +69,7 @@ public class ZhipuAiFunctionCallbackIT { "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); ChatResponse response = chatModel - .call(new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().function("WeatherInfo").build())); + .call(new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().toolNames("WeatherInfo").build())); logger.info("Response: {}", response); @@ -85,8 +87,8 @@ public class ZhipuAiFunctionCallbackIT { UserMessage userMessage = new UserMessage( "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - Flux response = chatModel - .stream(new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().function("WeatherInfo").build())); + Flux response = chatModel.stream( + new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().toolNames("WeatherInfo").build())); String content = response.collectList() .block() @@ -109,10 +111,9 @@ public class ZhipuAiFunctionCallbackIT { static class Config { @Bean - public FunctionCallback weatherFunctionInfo() { + public ToolCallback weatherFunctionInfo() { - return FunctionCallback.builder() - .function("WeatherInfo", new MockWeatherService()) + return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService()) .description("Get the weather in location") .inputType(MockWeatherService.Request.class) // .responseConverter(response -> "" + response.temp() + response.unit()) diff --git a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java index 3ea4fe7a0..57a9527ca 100644 --- a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java +++ b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java @@ -18,10 +18,8 @@ package org.springframework.ai.zhipuai; import java.util.ArrayList; import java.util.Base64; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import io.micrometer.observation.Observation; @@ -41,7 +39,6 @@ import org.springframework.ai.chat.metadata.ChatGenerationMetadata; import org.springframework.ai.chat.metadata.ChatResponseMetadata; import org.springframework.ai.chat.metadata.DefaultUsage; import org.springframework.ai.chat.metadata.EmptyUsage; -import org.springframework.ai.chat.model.AbstractToolCallSupport; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; @@ -54,15 +51,17 @@ import org.springframework.ai.chat.observation.DefaultChatModelObservationConven import org.springframework.ai.chat.prompt.ChatOptions; 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.FunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallingOptions; +import org.springframework.ai.model.tool.DefaultToolExecutionEligibilityPredicate; +import org.springframework.ai.model.tool.ToolCallingChatOptions; +import org.springframework.ai.model.tool.ToolCallingManager; +import org.springframework.ai.model.tool.ToolExecutionEligibilityPredicate; +import org.springframework.ai.model.tool.ToolExecutionResult; import org.springframework.ai.retry.RetryUtils; +import org.springframework.ai.tool.definition.ToolDefinition; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletion; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletion.Choice; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionChunk; -import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionFinishReason; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionMessage; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionMessage.ChatCompletionFunction; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionMessage.MediaContent; @@ -81,13 +80,14 @@ import org.springframework.util.MimeType; * backed by {@link ZhiPuAiApi}. * * @author Geng Rong + * @author Alexandros Pappas + * @author Ilayaperumal Gopinathan * @see ChatModel * @see StreamingChatModel * @see ZhiPuAiApi - * @author Alexandros Pappas * @since 1.0.0 M1 */ -public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatModel, StreamingChatModel { +public class ZhiPuAiChatModel implements ChatModel { private static final Logger logger = LoggerFactory.getLogger(ZhiPuAiChatModel.class); @@ -113,11 +113,22 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod */ private final ObservationRegistry observationRegistry; + /** + * The Tool calling manager. + */ + private final ToolCallingManager toolCallingManager; + /** * Conventions to use for generating observations. */ private ChatModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION; + /** + * The tool execution eligibility predicate used to determine if a tool can be + * executed. + */ + private final ToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate; + /** * Creates an instance of the ZhiPuAiChatModel. * @param zhiPuAiApi The ZhiPuAiApi instance to be used for interacting with the @@ -135,7 +146,7 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod * @param options The ZhiPuAiChatOptions to configure the chat model. */ public ZhiPuAiChatModel(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options) { - this(zhiPuAiApi, options, null, RetryUtils.DEFAULT_RETRY_TEMPLATE); + this(zhiPuAiApi, options, RetryUtils.DEFAULT_RETRY_TEMPLATE); } /** @@ -143,12 +154,40 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod * @param zhiPuAiApi The ZhiPuAiApi instance to be used for interacting with the * ZhiPuAI Chat API. * @param options The ZhiPuAiChatOptions to configure the chat model. - * @param functionCallbackResolver The function callback resolver. * @param retryTemplate The retry template. */ - public ZhiPuAiChatModel(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options, - FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate) { - this(zhiPuAiApi, options, functionCallbackResolver, List.of(), retryTemplate, ObservationRegistry.NOOP); + public ZhiPuAiChatModel(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options, RetryTemplate retryTemplate) { + this(zhiPuAiApi, options, ToolCallingManager.builder().build(), retryTemplate, ObservationRegistry.NOOP, + new DefaultToolExecutionEligibilityPredicate()); + } + + /** + * Initializes an instance of the ZhiPuAiChatModel. + * @param zhiPuAiApi The ZhiPuAiApi instance to be used for interacting with the + * ZhiPuAI Chat API. + * @param options The ZhiPuAiChatOptions to configure the chat model. + * @param retryTemplate The retry template. + * @param observationRegistry The Observation Registry. + */ + public ZhiPuAiChatModel(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options, RetryTemplate retryTemplate, + ObservationRegistry observationRegistry) { + this(zhiPuAiApi, options, ToolCallingManager.builder().build(), retryTemplate, observationRegistry, + new DefaultToolExecutionEligibilityPredicate()); + } + + /** + * Initializes an instance of the ZhiPuAiChatModel. + * @param zhiPuAiApi The ZhiPuAiApi instance to be used for interacting with the + * ZhiPuAI Chat API. + * @param toolCallingManager The tool calling manager + * @param options The ZhiPuAiChatOptions to configure the chat model. + * @param retryTemplate The retry template. + * @param observationRegistry The Observation Registry. + */ + public ZhiPuAiChatModel(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options, ToolCallingManager toolCallingManager, + RetryTemplate retryTemplate, ObservationRegistry observationRegistry) { + this(zhiPuAiApi, options, toolCallingManager, retryTemplate, observationRegistry, + new DefaultToolExecutionEligibilityPredicate()); } /** @@ -156,25 +195,26 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod * @param zhiPuAiApi The ZhiPuAiApi instance to be used for interacting with the * ZhiPuAI Chat API. * @param options The ZhiPuAiChatOptions to configure the chat model. - * @param functionCallbackResolver The function callback resolver. - * @param toolFunctionCallbacks The tool function callbacks. + * @param toolCallingManager The tool calling manager * @param retryTemplate The retry template. * @param observationRegistry The ObservationRegistry used for instrumentation. + * @param toolExecutionEligibilityPredicate The Tool execution eligibility predicate. */ - public ZhiPuAiChatModel(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options, - FunctionCallbackResolver functionCallbackResolver, List toolFunctionCallbacks, - RetryTemplate retryTemplate, ObservationRegistry observationRegistry) { - super(functionCallbackResolver, options, toolFunctionCallbacks); + public ZhiPuAiChatModel(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options, ToolCallingManager toolCallingManager, + RetryTemplate retryTemplate, ObservationRegistry observationRegistry, + ToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate) { Assert.notNull(zhiPuAiApi, "ZhiPuAiApi must not be null"); Assert.notNull(options, "Options must not be null"); Assert.notNull(retryTemplate, "RetryTemplate must not be null"); - Assert.isTrue(CollectionUtils.isEmpty(options.getFunctionCallbacks()), - "The default function callbacks must be set via the toolFunctionCallbacks constructor parameter"); Assert.notNull(observationRegistry, "ObservationRegistry must not be null"); + Assert.notNull(toolCallingManager, "toolCallingManager cannot be null"); + Assert.notNull(toolExecutionEligibilityPredicate, "toolExecutionEligibilityPredicate cannot be null"); this.zhiPuAiApi = zhiPuAiApi; this.defaultOptions = options; this.retryTemplate = retryTemplate; this.observationRegistry = observationRegistry; + this.toolCallingManager = toolCallingManager; + this.toolExecutionEligibilityPredicate = toolExecutionEligibilityPredicate; } private static Generation buildGeneration(Choice choice, Map metadata) { @@ -194,12 +234,15 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod @Override public ChatResponse call(Prompt prompt) { - ChatCompletionRequest request = createRequest(prompt, false); + // Before moving any further, build the final request Prompt, + // merging runtime and default options. + Prompt requestPrompt = buildRequestPrompt(prompt); + ChatCompletionRequest request = createRequest(requestPrompt, false); ChatModelObservationContext observationContext = ChatModelObservationContext.builder() - .prompt(prompt) + .prompt(requestPrompt) .provider(ZhiPuApiConstants.PROVIDER_NAME) - .requestOptions(buildRequestOptions(request)) + .requestOptions(prompt.getOptions()) .build(); ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION @@ -236,14 +279,20 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod return chatResponse; }); - if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, - Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) { - var toolCallConversation = handleToolCalls(prompt, response); - // Recursively call the call method with the tool call message - // conversation that contains the call responses. - return this.call(new Prompt(toolCallConversation, prompt.getOptions())); + if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired(requestPrompt.getOptions(), response)) { + var toolExecutionResult = this.toolCallingManager.executeToolCalls(requestPrompt, response); + if (toolExecutionResult.returnDirect()) { + // Return tool execution result directly to the client. + return ChatResponse.builder() + .from(response) + .generations(ToolExecutionResult.buildGenerations(toolExecutionResult)) + .build(); + } + else { + // Send the tool execution result back to the model. + return this.call(new Prompt(toolExecutionResult.conversationHistory(), requestPrompt.getOptions())); + } } - return response; } @@ -255,7 +304,10 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod @Override public Flux stream(Prompt prompt) { return Flux.deferContextual(contextView -> { - ChatCompletionRequest request = createRequest(prompt, true); + // Before moving any further, build the final request Prompt, + // merging runtime and default options. + Prompt requestPrompt = buildRequestPrompt(prompt); + ChatCompletionRequest request = createRequest(requestPrompt, true); Flux completionChunks = this.retryTemplate .execute(ctx -> this.zhiPuAiApi.chatCompletionStream(request)); @@ -265,7 +317,7 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod ConcurrentHashMap roleMap = new ConcurrentHashMap<>(); final ChatModelObservationContext observationContext = ChatModelObservationContext.builder() - .prompt(prompt) + .prompt(requestPrompt) .provider(ZhiPuApiConstants.PROVIDER_NAME) .requestOptions(buildRequestOptions(request)) .build(); @@ -306,17 +358,24 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod // @formatter:off Flux flux = chatResponse.flatMap(response -> { - if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) { - // FIXME: bounded elastic needs to be used since tool calling - // is currently only synchronous - return Flux.defer(() -> { - var toolCallConversation = handleToolCalls(prompt, response); - // Recursively call the stream method with the tool call message - // conversation that contains the call responses. - return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); - }).subscribeOn(Schedulers.boundedElastic()); - } - return Flux.just(response); + if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired(requestPrompt.getOptions(), response)) { + return Flux.defer(() -> { + // FIXME: bounded elastic needs to be used since tool calling + // is currently only synchronous + var toolExecutionResult = this.toolCallingManager.executeToolCalls(requestPrompt, response); + if (toolExecutionResult.returnDirect()) { + // Return tool execution result directly to the client. + return Flux.just(ChatResponse.builder().from(response) + .generations(ToolExecutionResult.buildGenerations(toolExecutionResult)) + .build()); + } + else { + // Send the tool execution result back to the model. + return this.stream(new Prompt(toolExecutionResult.conversationHistory(), requestPrompt.getOptions())); + } + }).subscribeOn(Schedulers.boundedElastic()); + } + return Flux.just(response); }) .doOnError(observation::error) .doFinally(s -> observation.stop()) @@ -360,6 +419,57 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod "chat.completion", null); } + private List getFunctionTools(List toolDefinitions) { + return toolDefinitions.stream().map(toolDefinition -> { + var function = new ZhiPuAiApi.FunctionTool.Function(toolDefinition.description(), toolDefinition.name(), + toolDefinition.inputSchema()); + return new ZhiPuAiApi.FunctionTool(function); + }).toList(); + } + + Prompt buildRequestPrompt(Prompt prompt) { + // Process runtime options + ZhiPuAiChatOptions runtimeOptions = null; + if (prompt.getOptions() != null) { + if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) { + runtimeOptions = ModelOptionsUtils.copyToTarget(toolCallingChatOptions, ToolCallingChatOptions.class, + ZhiPuAiChatOptions.class); + } + else { + runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class, + ZhiPuAiChatOptions.class); + } + } + + // Define request options by merging runtime options and default options + ZhiPuAiChatOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions, + ZhiPuAiChatOptions.class); + + // Merge @JsonIgnore-annotated options explicitly since they are ignored by + // Jackson, used by ModelOptionsUtils. + if (runtimeOptions != null) { + requestOptions.setInternalToolExecutionEnabled( + ModelOptionsUtils.mergeOption(runtimeOptions.getInternalToolExecutionEnabled(), + this.defaultOptions.getInternalToolExecutionEnabled())); + requestOptions.setToolNames(ToolCallingChatOptions.mergeToolNames(runtimeOptions.getToolNames(), + this.defaultOptions.getToolNames())); + requestOptions.setToolCallbacks(ToolCallingChatOptions.mergeToolCallbacks(runtimeOptions.getToolCallbacks(), + this.defaultOptions.getToolCallbacks())); + requestOptions.setToolContext(ToolCallingChatOptions.mergeToolContext(runtimeOptions.getToolContext(), + this.defaultOptions.getToolContext())); + } + else { + requestOptions.setInternalToolExecutionEnabled(this.defaultOptions.getInternalToolExecutionEnabled()); + requestOptions.setToolNames(this.defaultOptions.getToolNames()); + requestOptions.setToolCallbacks(this.defaultOptions.getToolCallbacks()); + requestOptions.setToolContext(this.defaultOptions.getToolContext()); + } + + ToolCallingChatOptions.validateToolCallbacks(requestOptions.getToolCallbacks()); + + return new Prompt(prompt.getInstructions(), requestOptions); + } + /** * Accessible for testing. */ @@ -416,37 +526,31 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod ChatCompletionRequest request = new ChatCompletionRequest(chatCompletionMessages, stream); - Set enabledToolsToUse = new HashSet<>(); - if (prompt.getOptions() != null) { ZhiPuAiChatOptions updatedRuntimeOptions; - if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOptions) { - updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(functionCallingOptions, - FunctionCallingOptions.class, ZhiPuAiChatOptions.class); + if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) { + updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(toolCallingChatOptions, + ToolCallingChatOptions.class, ZhiPuAiChatOptions.class); } else { updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class, ZhiPuAiChatOptions.class); } - enabledToolsToUse.addAll(this.runtimeFunctionCallbackConfigurations(updatedRuntimeOptions)); - request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, ChatCompletionRequest.class); } - if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) { - enabledToolsToUse.addAll(this.defaultOptions.getFunctions()); - } - request = ModelOptionsUtils.merge(request, this.defaultOptions, ChatCompletionRequest.class); - if (!CollectionUtils.isEmpty(enabledToolsToUse)) { + ZhiPuAiChatOptions requestOptions = (ZhiPuAiChatOptions) prompt.getOptions(); + // Add the tool definitions to the request's tools parameter. + List toolDefinitions = this.toolCallingManager.resolveToolDefinitions(requestOptions); + if (!CollectionUtils.isEmpty(toolDefinitions)) { request = ModelOptionsUtils.merge( - ZhiPuAiChatOptions.builder().tools(this.getFunctionTools(enabledToolsToUse)).build(), request, + ZhiPuAiChatOptions.builder().tools(this.getFunctionTools(toolDefinitions)).build(), request, ChatCompletionRequest.class); } - return request; } @@ -476,14 +580,6 @@ public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatMod .build(); } - private List getFunctionTools(Set functionNames) { - return this.resolveFunctionCallbacks(functionNames).stream().map(functionCallback -> { - var function = new ZhiPuAiApi.FunctionTool.Function(functionCallback.getDescription(), - functionCallback.getName(), functionCallback.getInputTypeSchema()); - return new ZhiPuAiApi.FunctionTool(function); - }).toList(); - } - public void setObservationConvention(ChatModelObservationConvention observationConvention) { this.observationConvention = observationConvention; } diff --git a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatOptions.java b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatOptions.java index f57303b11..8b8d39744 100644 --- a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatOptions.java +++ b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatOptions.java @@ -17,6 +17,7 @@ package org.springframework.ai.zhipuai; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -29,9 +30,10 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.ai.chat.prompt.ChatOptions; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.model.function.FunctionCallingOptions; +import org.springframework.ai.model.tool.ToolCallingChatOptions; +import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -43,7 +45,7 @@ import org.springframework.util.Assert; * @since 1.0.0 M1 */ @JsonInclude(Include.NON_NULL) -public class ZhiPuAiChatOptions implements FunctionCallingOptions { +public class ZhiPuAiChatOptions implements ToolCallingChatOptions { // @formatter:off /** @@ -106,31 +108,25 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions { private @JsonProperty("do_sample") Boolean doSample; /** - * ZhiPuAI Tool Function Callbacks to register with the ChatModel. - * For Prompt Options the functionCallbacks are automatically enabled for the duration of the prompt execution. - * For Default Options the functionCallbacks are registered but disabled by default. Use the enableFunctions to set the functions - * from the registry to be used by the ChatModel chat completion requests. + * Collection of {@link ToolCallback}s to be used for tool calling in the chat completion requests. */ @JsonIgnore - private List functionCallbacks = new ArrayList<>(); + private List toolCallbacks = new ArrayList<>(); /** - * List of functions, identified by their names, to configure for function calling in - * the chat completion requests. - * Functions with those names must exist in the functionCallbacks registry. - * The {@link #functionCallbacks} from the PromptOptions are automatically enabled for the duration of the prompt execution. - * - * Note that function enabled with the default options are enabled for all chat completion requests. This could impact the token count and the billing. - * If the functions is set in a prompt options, then the enabled functions are only active for the duration of this prompt execution. + * Collection of tool names to be resolved at runtime and used for tool calling in the chat completion requests. */ @JsonIgnore - private Set functions = new HashSet<>(); + private Set toolNames = new HashSet<>(); + + /** + * Whether to enable the tool execution lifecycle internally in ChatModel. + */ + @JsonIgnore + private Boolean internalToolExecutionEnabled; @JsonIgnore - private Boolean proxyToolCalls; - - @JsonIgnore - private Map toolContext; + private Map toolContext = new HashMap<>(); // @formatter:on public static Builder builder() { @@ -149,9 +145,9 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions { .user(fromOptions.getUser()) .requestId(fromOptions.getRequestId()) .doSample(fromOptions.getDoSample()) - .functionCallbacks(fromOptions.getFunctionCallbacks()) - .functions(fromOptions.getFunctions()) - .proxyToolCalls(fromOptions.getProxyToolCalls()) + .toolCallbacks(fromOptions.getToolCallbacks()) + .toolNames(fromOptions.getToolNames()) + .internalToolExecutionEnabled(fromOptions.getInternalToolExecutionEnabled()) .toolContext(fromOptions.getToolContext()) .build(); } @@ -251,25 +247,6 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions { this.doSample = doSample; } - @Override - public List getFunctionCallbacks() { - return this.functionCallbacks; - } - - @Override - public void setFunctionCallbacks(List functionCallbacks) { - this.functionCallbacks = functionCallbacks; - } - - @Override - public Set getFunctions() { - return this.functions; - } - - public void setFunctions(Set functionNames) { - this.functions = functionNames; - } - @Override @JsonIgnore public Double getFrequencyPenalty() { @@ -289,12 +266,45 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions { } @Override - public Boolean getProxyToolCalls() { - return this.proxyToolCalls; + @JsonIgnore + public List getToolCallbacks() { + return this.toolCallbacks; } - public void setProxyToolCalls(Boolean proxyToolCalls) { - this.proxyToolCalls = proxyToolCalls; + @Override + @JsonIgnore + public void setToolCallbacks(List toolCallbacks) { + Assert.notNull(toolCallbacks, "toolCallbacks cannot be null"); + Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements"); + this.toolCallbacks = toolCallbacks; + } + + @Override + @JsonIgnore + public Set getToolNames() { + return this.toolNames; + } + + @Override + @JsonIgnore + public void setToolNames(Set toolNames) { + Assert.notNull(toolNames, "toolNames cannot be null"); + Assert.noNullElements(toolNames, "toolNames cannot contain null elements"); + toolNames.forEach(tool -> Assert.hasText(tool, "toolNames cannot contain empty elements")); + this.toolNames = toolNames; + } + + @Override + @Nullable + @JsonIgnore + public Boolean getInternalToolExecutionEnabled() { + return this.internalToolExecutionEnabled; + } + + @Override + @JsonIgnore + public void setInternalToolExecutionEnabled(@Nullable Boolean internalToolExecutionEnabled) { + this.internalToolExecutionEnabled = internalToolExecutionEnabled; } @Override @@ -319,7 +329,10 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions { result = prime * result + ((this.tools == null) ? 0 : this.tools.hashCode()); result = prime * result + ((this.toolChoice == null) ? 0 : this.toolChoice.hashCode()); result = prime * result + ((this.user == null) ? 0 : this.user.hashCode()); - result = prime * result + ((this.proxyToolCalls == null) ? 0 : this.proxyToolCalls.hashCode()); + result = prime * result + + ((this.internalToolExecutionEnabled == null) ? 0 : this.internalToolExecutionEnabled.hashCode()); + result = prime * result + ((this.toolCallbacks == null) ? 0 : this.toolCallbacks.hashCode()); + result = prime * result + ((this.toolNames == null) ? 0 : this.toolNames.hashCode()); result = prime * result + ((this.toolContext == null) ? 0 : this.toolContext.hashCode()); return result; } @@ -416,12 +429,12 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions { else if (!this.doSample.equals(other.doSample)) { return false; } - if (this.proxyToolCalls == null) { - if (other.proxyToolCalls != null) { + if (this.internalToolExecutionEnabled == null) { + if (other.internalToolExecutionEnabled != null) { return false; } } - else if (!this.proxyToolCalls.equals(other.proxyToolCalls)) { + else if (!this.internalToolExecutionEnabled.equals(other.internalToolExecutionEnabled)) { return false; } if (this.toolContext == null) { @@ -440,7 +453,7 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions { return fromOptions(this); } - public FunctionCallingOptions merge(ChatOptions options) { + public ToolCallingChatOptions merge(ChatOptions options) { ZhiPuAiChatOptions.Builder builder = ZhiPuAiChatOptions.builder(); // Merge chat-specific options @@ -450,44 +463,43 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions { .temperature(options.getTemperature() != null ? options.getTemperature() : this.getTemperature()) .topP(options.getTopP() != null ? options.getTopP() : this.getTopP()); - // Try to get function-specific properties if options is a FunctionCallingOptions - if (options instanceof FunctionCallingOptions functionOptions) { - builder.proxyToolCalls(functionOptions.getProxyToolCalls() != null ? functionOptions.getProxyToolCalls() - : this.proxyToolCalls); + // Try to get tool-specific properties if options is a ToolCallingChatOptions + if (options instanceof ToolCallingChatOptions toolCallingChatOptions) { + builder.internalToolExecutionEnabled(toolCallingChatOptions.getInternalToolExecutionEnabled() != null + ? (toolCallingChatOptions).getInternalToolExecutionEnabled() + : this.getInternalToolExecutionEnabled()); - Set functions = new HashSet<>(); - if (this.functions != null) { - functions.addAll(this.functions); + Set toolNames = new HashSet<>(); + if (this.toolNames != null) { + toolNames.addAll(this.toolNames); } - if (functionOptions.getFunctions() != null) { - functions.addAll(functionOptions.getFunctions()); + if (toolCallingChatOptions.getToolNames() != null) { + toolNames.addAll(toolCallingChatOptions.getToolNames()); } - builder.functions(functions); + builder.toolNames(toolNames); - List functionCallbacks = new ArrayList<>(); - if (this.functionCallbacks != null) { - functionCallbacks.addAll(this.functionCallbacks); + List toolCallbacks = new ArrayList<>(); + if (this.toolCallbacks != null) { + toolCallbacks.addAll(this.toolCallbacks); } - if (functionOptions.getFunctionCallbacks() != null) { - functionCallbacks.addAll(functionOptions.getFunctionCallbacks()); + if (toolCallingChatOptions.getToolCallbacks() != null) { + toolCallbacks.addAll(toolCallingChatOptions.getToolCallbacks()); } - builder.functionCallbacks(functionCallbacks); + builder.toolCallbacks(toolCallbacks); Map context = new HashMap<>(); if (this.toolContext != null) { context.putAll(this.toolContext); } - if (functionOptions.getToolContext() != null) { - context.putAll(functionOptions.getToolContext()); + if (toolCallingChatOptions.getToolContext() != null) { + context.putAll(toolCallingChatOptions.getToolContext()); } builder.toolContext(context); } else { - // If not a FunctionCallingOptions, preserve current function-specific - // properties - builder.proxyToolCalls(this.proxyToolCalls); - builder.functions(this.functions != null ? new HashSet<>(this.functions) : null); - builder.functionCallbacks(this.functionCallbacks != null ? new ArrayList<>(this.functionCallbacks) : null); + builder.internalToolExecutionEnabled(this.internalToolExecutionEnabled); + builder.toolNames(this.toolNames != null ? new HashSet<>(this.toolNames) : null); + builder.toolCallbacks(this.toolCallbacks != null ? new ArrayList<>(this.toolCallbacks) : null); builder.toolContext(this.toolContext != null ? new HashMap<>(this.toolContext) : null); } @@ -563,25 +575,31 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions { return this; } - public Builder functionCallbacks(List functionCallbacks) { - this.options.functionCallbacks = functionCallbacks; + public Builder toolCallbacks(List toolCallbacks) { + this.options.setToolCallbacks(toolCallbacks); return this; } - public Builder functions(Set functionNames) { - Assert.notNull(functionNames, "Function names must not be null"); - this.options.functions = functionNames; + public Builder toolCallbacks(ToolCallback... toolCallbacks) { + Assert.notNull(toolCallbacks, "toolCallbacks cannot be null"); + this.options.toolCallbacks.addAll(Arrays.asList(toolCallbacks)); return this; } - public Builder function(String functionName) { - Assert.hasText(functionName, "Function name must not be empty"); - this.options.functions.add(functionName); + public Builder toolNames(Set toolNames) { + Assert.notNull(toolNames, "toolNames cannot be null"); + this.options.setToolNames(toolNames); return this; } - public Builder proxyToolCalls(Boolean proxyToolCalls) { - this.options.proxyToolCalls = proxyToolCalls; + public Builder toolNames(String... toolNames) { + Assert.notNull(toolNames, "toolNames cannot be null"); + this.options.toolNames.addAll(Set.of(toolNames)); + return this; + } + + public Builder internalToolExecutionEnabled(@Nullable Boolean internalToolExecutionEnabled) { + this.options.setInternalToolExecutionEnabled(internalToolExecutionEnabled); return this; } 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 f7e6a70dd..da1f9e4ee 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 @@ -20,8 +20,10 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.function.FunctionCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; import org.springframework.ai.zhipuai.api.MockWeatherService; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; @@ -38,7 +40,9 @@ public class ChatCompletionRequestTests { var client = new ZhiPuAiChatModel(new ZhiPuAiApi("TEST"), ZhiPuAiChatOptions.builder().model("DEFAULT_MODEL").temperature(66.6).build()); - var request = client.createRequest(new Prompt("Test message content"), false); + var prompt = client.buildRequestPrompt(new Prompt("Test message content")); + + var request = client.createRequest(prompt, false); assertThat(request.messages()).hasSize(1); assertThat(request.stream()).isFalse(); @@ -67,17 +71,13 @@ public class ChatCompletionRequestTests { var request = client.createRequest(new Prompt("Test message content", ZhiPuAiChatOptions.builder() .model("PROMPT_MODEL") - .functionCallbacks(List.of(FunctionCallback.builder() - .function(TOOL_FUNCTION_NAME, new MockWeatherService()) + .toolCallbacks(List.of(FunctionToolCallback.builder(TOOL_FUNCTION_NAME, new MockWeatherService()) .description("Get the weather in location") .inputType(MockWeatherService.Request.class) .build())) .build()), false); - assertThat(client.getFunctionCallbackRegister()).hasSize(1); - assertThat(client.getFunctionCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME); - assertThat(request.messages()).hasSize(1); assertThat(request.stream()).isFalse(); assertThat(request.model()).isEqualTo("PROMPT_MODEL"); @@ -94,55 +94,19 @@ public class ChatCompletionRequestTests { var client = new ZhiPuAiChatModel(new ZhiPuAiApi("TEST"), ZhiPuAiChatOptions.builder() .model("DEFAULT_MODEL") - .functionCallbacks(List.of(FunctionCallback.builder() - .function(TOOL_FUNCTION_NAME, new MockWeatherService()) + .toolCallbacks(List.of(FunctionToolCallback.builder(TOOL_FUNCTION_NAME, new MockWeatherService()) .description("Get the weather in location") .inputType(MockWeatherService.Request.class) .build())) .build()); - var request = client.createRequest(new Prompt("Test message content"), false); + var prompt = client.buildRequestPrompt(new Prompt("Test message content")); - assertThat(client.getFunctionCallbackRegister()).hasSize(1); - assertThat(client.getFunctionCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME); - assertThat(client.getFunctionCallbackRegister().get(TOOL_FUNCTION_NAME).getDescription()) - .isEqualTo("Get the weather in location"); + var request = client.createRequest(prompt, false); assertThat(request.messages()).hasSize(1); assertThat(request.stream()).isFalse(); assertThat(request.model()).isEqualTo("DEFAULT_MODEL"); - - assertThat(request.tools()).as("Default Options callback functions are not automatically enabled!") - .isNullOrEmpty(); - - // Explicitly enable the function - request = client.createRequest( - new Prompt("Test message content", ZhiPuAiChatOptions.builder().function(TOOL_FUNCTION_NAME).build()), - false); - - assertThat(request.tools()).hasSize(1); - assertThat(request.tools().get(0).getFunction().getName()).as("Explicitly enabled function") - .isEqualTo(TOOL_FUNCTION_NAME); - - // Override the default options function with one from the prompt - request = client.createRequest(new Prompt("Test message content", - ZhiPuAiChatOptions.builder() - .functionCallbacks(List.of(FunctionCallback.builder() - .function(TOOL_FUNCTION_NAME, new MockWeatherService()) - .description("Overridden function description") - .inputType(MockWeatherService.Request.class) - .build())) - .build()), - false); - - assertThat(request.tools()).hasSize(1); - assertThat(request.tools().get(0).getFunction().getName()).as("Explicitly enabled function") - .isEqualTo(TOOL_FUNCTION_NAME); - - assertThat(client.getFunctionCallbackRegister()).hasSize(1); - assertThat(client.getFunctionCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME); - assertThat(client.getFunctionCallbackRegister().get(TOOL_FUNCTION_NAME).getDescription()) - .isEqualTo("Overridden function description"); } } diff --git a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/ZhiPuAiRetryTests.java b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/ZhiPuAiRetryTests.java index a2cdea7e5..3ef3225e6 100644 --- a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/ZhiPuAiRetryTests.java +++ b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/ZhiPuAiRetryTests.java @@ -26,6 +26,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Flux; +import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.document.MetadataMode; import org.springframework.ai.image.ImageMessage; @@ -88,7 +89,7 @@ public class ZhiPuAiRetryTests { this.retryListener = new TestRetryListener(); this.retryTemplate.registerListener(this.retryListener); - this.chatModel = new ZhiPuAiChatModel(this.zhiPuAiApi, ZhiPuAiChatOptions.builder().build(), null, + this.chatModel = new ZhiPuAiChatModel(this.zhiPuAiApi, ZhiPuAiChatOptions.builder().build(), this.retryTemplate); this.embeddingModel = new ZhiPuAiEmbeddingModel(this.zhiPuAiApi, MetadataMode.EMBED, ZhiPuAiEmbeddingOptions.builder().build(), this.retryTemplate); @@ -109,7 +110,7 @@ public class ZhiPuAiRetryTests { .willThrow(new TransientAiException("Transient Error 2")) .willReturn(ResponseEntity.of(Optional.of(expectedChatCompletion))); - var result = this.chatModel.call(new Prompt("text")); + var result = this.chatModel.call(new Prompt("text", ChatOptions.builder().build())); assertThat(result).isNotNull(); assertThat(result.getResult().getOutput().getText()).isSameAs("Response"); @@ -121,7 +122,8 @@ public class ZhiPuAiRetryTests { public void zhiPuAiChatNonTransientError() { given(this.zhiPuAiApi.chatCompletionEntity(isA(ChatCompletionRequest.class))) .willThrow(new RuntimeException("Non Transient Error")); - assertThrows(RuntimeException.class, () -> this.chatModel.call(new Prompt("text"))); + assertThrows(RuntimeException.class, + () -> this.chatModel.call(new Prompt("text", ChatOptions.builder().build()))); } @Test 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 ebbc69cd3..4baed31e5 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 @@ -40,6 +40,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.model.StreamingChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.ai.chat.prompt.SystemPromptTemplate; @@ -48,6 +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.function.FunctionCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiTestConfiguration; import org.springframework.ai.zhipuai.api.MockWeatherService; @@ -86,7 +88,7 @@ class ZhiPuAiChatModelIT { "Tell me about 3 famous pirates from the Golden Age of Piracy and what they did."); SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(this.systemResource); Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", "Bob", "voice", "pirate")); - Prompt prompt = new Prompt(List.of(userMessage, systemMessage)); + Prompt prompt = new Prompt(List.of(userMessage, systemMessage), ChatOptions.builder().build()); ChatResponse response = this.chatModel.call(prompt); assertThat(response.getResults()).hasSize(1); assertThat(response.getResults().get(0).getOutput().getText()).contains("Blackbeard"); @@ -128,7 +130,7 @@ class ZhiPuAiChatModelIT { """; PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("subject", "ice cream flavors", "format", format)); - Prompt prompt = new Prompt(promptTemplate.createMessage()); + Prompt prompt = new Prompt(promptTemplate.createMessage(), ChatOptions.builder().build()); Generation generation = this.chatModel.call(prompt).getResult(); List list = outputConverter.convert(generation.getOutput().getText()); @@ -147,7 +149,7 @@ class ZhiPuAiChatModelIT { """; PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("subject", "an array of numbers from 1 to 9 under they key name 'numbers'", "format", format)); - Prompt prompt = new Prompt(promptTemplate.createMessage()); + Prompt prompt = new Prompt(promptTemplate.createMessage(), ChatOptions.builder().build()); Generation generation = this.chatModel.call(prompt).getResult(); Map result = outputConverter.convert(generation.getOutput().getText()); @@ -166,7 +168,7 @@ class ZhiPuAiChatModelIT { {format} """; PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format)); - Prompt prompt = new Prompt(promptTemplate.createMessage()); + Prompt prompt = new Prompt(promptTemplate.createMessage(), ChatOptions.builder().build()); Generation generation = this.chatModel.call(prompt).getResult(); ActorsFilms actorsFilms = outputConverter.convert(generation.getOutput().getText()); @@ -183,7 +185,7 @@ class ZhiPuAiChatModelIT { {format} """; PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format)); - Prompt prompt = new Prompt(promptTemplate.createMessage()); + Prompt prompt = new Prompt(promptTemplate.createMessage(), ChatOptions.builder().build()); Generation generation = this.chatModel.call(prompt).getResult(); ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getText()); @@ -230,8 +232,7 @@ class ZhiPuAiChatModelIT { var promptOptions = ZhiPuAiChatOptions.builder() .model(ZhiPuAiApi.ChatModel.GLM_4.getValue()) - .functionCallbacks(List.of(FunctionCallback.builder() - .function("getCurrentWeather", new MockWeatherService()) + .toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService()) .description("Get the weather in location") .inputType(MockWeatherService.Request.class) .build())) @@ -256,8 +257,7 @@ class ZhiPuAiChatModelIT { var promptOptions = ZhiPuAiChatOptions.builder() .model(ZhiPuAiApi.ChatModel.GLM_4.getValue()) - .functionCallbacks(List.of(FunctionCallback.builder() - .function("getCurrentWeather", new MockWeatherService()) + .toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService()) .description("Get the weather in location") .inputType(MockWeatherService.Request.class) .build())) diff --git a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelObservationIT.java b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelObservationIT.java index 118cef3ba..0c65928b0 100644 --- a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelObservationIT.java +++ b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelObservationIT.java @@ -31,6 +31,7 @@ import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.observation.DefaultChatModelObservationConvention; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; +import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.observation.conventions.AiOperationType; import org.springframework.ai.observation.conventions.AiProvider; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; @@ -166,8 +167,7 @@ public class ZhiPuAiChatModelObservationIT { @Bean public ZhiPuAiChatModel zhiPuAiChatModel(ZhiPuAiApi zhiPuAiApi, TestObservationRegistry observationRegistry) { return new ZhiPuAiChatModel(zhiPuAiApi, ZhiPuAiChatOptions.builder().build(), - new DefaultFunctionCallbackResolver(), List.of(), RetryTemplate.defaultInstance(), - observationRegistry); + RetryTemplate.defaultInstance(), observationRegistry); } }