diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallSupport.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallSupport.java deleted file mode 100644 index 53f825c25..000000000 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallSupport.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2023 - 2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.ai.model.function; - -import org.springframework.util.CollectionUtils; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -/** - * @author Christian Tzolov - * @author Grogdunn - * @deprecated since 1.0.0-M1 in favor of - * {@link org.springframework.ai.chat.model.AbstractToolCallSupport} - */ -@Deprecated(since = "1.0.0-M2", forRemoval = true) -public abstract class AbstractFunctionCallSupport { - - protected final static boolean IS_RUNTIME_CALL = true; - - /** - * The function callback register is used to resolve the function callbacks by name. - */ - protected final Map functionCallbackRegister = new ConcurrentHashMap<>(); - - /** - * The function callback context is used to resolve the function callbacks by name - * from the Spring context. It is optional and usually used with Spring - * auto-configuration. - */ - protected final FunctionCallbackContext functionCallbackContext; - - protected AbstractFunctionCallSupport(FunctionCallbackContext functionCallbackContext) { - this.functionCallbackContext = functionCallbackContext; - } - - public Map getFunctionCallbackRegister() { - return this.functionCallbackRegister; - } - - protected Set handleFunctionCallbackConfigurations(FunctionCallingOptions options, boolean isRuntimeCall) { - - Set functionToCall = new HashSet<>(); - - if (options != null) { - if (!CollectionUtils.isEmpty(options.getFunctionCallbacks())) { - options.getFunctionCallbacks().stream().forEach(functionCallback -> { - - // Register the tool callback. - if (isRuntimeCall) { - this.functionCallbackRegister.put(functionCallback.getName(), functionCallback); - } - else { - this.functionCallbackRegister.putIfAbsent(functionCallback.getName(), functionCallback); - } - - // Automatically enable the function, usually from prompt callback. - if (isRuntimeCall) { - functionToCall.add(functionCallback.getName()); - } - }); - } - - // Add the explicitly enabled functions. - if (!CollectionUtils.isEmpty(options.getFunctions())) { - functionToCall.addAll(options.getFunctions()); - } - } - - return functionToCall; - } - - /** - * Resolve the function callbacks by name. Retrieve them from the registry or try to - * resolve them from the Application Context. - * @param functionNames Name of function callbacks to retrieve. - * @return list of resolved FunctionCallbacks. - */ - protected List resolveFunctionCallbacks(Set functionNames) { - - List retrievedFunctionCallbacks = new ArrayList<>(); - - for (String functionName : functionNames) { - if (!this.functionCallbackRegister.containsKey(functionName)) { - - if (this.functionCallbackContext != null) { - FunctionCallback functionCallback = this.functionCallbackContext.getFunctionCallback(functionName, - null); - if (functionCallback != null) { - this.functionCallbackRegister.put(functionName, functionCallback); - } - else { - throw new IllegalStateException( - "No function callback [" + functionName + "] fund in tht FunctionCallbackContext"); - } - } - else { - throw new IllegalStateException("No function callback found for name: " + functionName); - } - } - FunctionCallback functionCallback = this.functionCallbackRegister.get(functionName); - - retrievedFunctionCallbacks.add(functionCallback); - } - - return retrievedFunctionCallbacks; - } - - /// - protected Resp callWithFunctionSupport(Req request) { - Resp response = this.doChatCompletion(request); - return this.handleFunctionCallOrReturn(request, response); - } - - protected Resp handleFunctionCallOrReturn(Req request, Resp response) { - - if (!this.isToolFunctionCall(response)) { - return response; - } - - // The chat completion tool call requires the complete conversation - // history. Including the initial user message. - List conversationHistory = new ArrayList<>(this.doGetUserMessages(request)); - - Msg responseMessage = this.doGetToolResponseMessage(response); - - // Add the assistant response to the message conversation history. - conversationHistory.add(responseMessage); - - Req newRequest = this.doCreateToolResponseRequest(request, responseMessage, conversationHistory); - - return this.callWithFunctionSupport(newRequest); - } - - protected Flux callWithFunctionSupportStream(Req request) { - final Flux response = this.doChatCompletionStream(request); - return this.handleFunctionCallOrReturnStream(request, response); - } - - protected Flux handleFunctionCallOrReturnStream(Req request, Flux response) { - - return response.switchMap(resp -> { - if (!this.isToolFunctionCall(resp)) { - return Mono.just(resp); - } - - // The chat completion tool call requires the complete conversation - // history. Including the initial user message. - List conversationHistory = new ArrayList<>(this.doGetUserMessages(request)); - - Msg responseMessage = this.doGetToolResponseMessage(resp); - - // Add the assistant response to the message conversation history. - conversationHistory.add(responseMessage); - - Req newRequest = this.doCreateToolResponseRequest(request, responseMessage, conversationHistory); - - return this.callWithFunctionSupportStream(newRequest); - }); - - } - - abstract protected Req doCreateToolResponseRequest(Req previousRequest, Msg responseMessage, - List conversationHistory); - - abstract protected List doGetUserMessages(Req request); - - abstract protected Msg doGetToolResponseMessage(Resp response); - - abstract protected Resp doChatCompletion(Req request); - - abstract protected Flux doChatCompletionStream(Req request); - - abstract protected boolean isToolFunctionCall(Resp response); - -} 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 4cde55b38..dcad84140 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 @@ -15,8 +15,6 @@ */ package org.springframework.ai.model.function; -import java.util.Map; - import org.springframework.ai.chat.model.ToolContext; /** diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/TypeResolverHelper.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/TypeResolverHelper.java index 656765a7f..ae6176b78 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/TypeResolverHelper.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/TypeResolverHelper.java @@ -130,7 +130,14 @@ public abstract class TypeResolverHelper { // Resolves: https://github.com/spring-projects/spring-ai/issues/726 if (!(functionType instanceof ParameterizedType)) { - functionType = FunctionTypeUtils.discoverFunctionTypeFromClass(FunctionTypeUtils.getRawType(functionType)); + Class functionalClass = FunctionTypeUtils.getRawType(functionType); + // Resolves: https://github.com/spring-projects/spring-ai/issues/1576 + if (BiFunction.class.isAssignableFrom(functionalClass)) { + functionType = TypeResolver.reify(BiFunction.class, (Class>) functionalClass); + } + else { + functionType = FunctionTypeUtils.discoverFunctionTypeFromClass(functionalClass); + } } var argumentType = functionType instanceof ParameterizedType diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java index e477b257f..beb829278 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java @@ -59,7 +59,7 @@ class FunctionCallbackWithPlainFunctionBeanIT { .withUserConfiguration(Config.class); @Test - void functionCallTest2() { + void functionCallWithDirectBiFunction() { contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) .run(context -> { @@ -72,7 +72,7 @@ class FunctionCallbackWithPlainFunctionBeanIT { .toolContext(Map.of("sessionId", "123")) .call() .content(); - System.out.println(content); + logger.info(content); // Test weatherFunction UserMessage userMessage = new UserMessage( @@ -91,6 +91,39 @@ class FunctionCallbackWithPlainFunctionBeanIT { }); } + @Test + void functionCallWithBiFunctionClass() { + contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) + .run(context -> { + + OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); + + ChatClient chatClient = ChatClient.builder(chatModel).build(); + + String content = chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?") + .functions("weatherFunctionWithClassBiFunction") + .toolContext(Map.of("sessionId", "123")) + .call() + .content(); + logger.info(content); + + // Test weatherFunction + UserMessage userMessage = new UserMessage( + "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'"); + + ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), + OpenAiChatOptions.builder() + .withFunction("weatherFunctionWithClassBiFunction") + .withToolContext(Map.of("sessionId", "123")) + .build())); + + logger.info("Response: {}", response); + + assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15"); + + }); + } + @Test void functionCallTest() { contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) @@ -196,6 +229,12 @@ class FunctionCallbackWithPlainFunctionBeanIT { @Configuration static class Config { + @Bean + @Description("Get the weather in location") + public MyBiFunction weatherFunctionWithClassBiFunction() { + return new MyBiFunction(); + } + @Bean @Description("Get the weather in location") public BiFunction weatherFunctionWithContext() { @@ -220,4 +259,14 @@ class FunctionCallbackWithPlainFunctionBeanIT { } + public static class MyBiFunction + implements BiFunction { + + @Override + public MockWeatherService.Response apply(MockWeatherService.Request request, ToolContext context) { + return new MockWeatherService().apply(request); + } + + } + } \ No newline at end of file