Add support for BiFunction class type resolution in TypeResolverHelper

- Adds special handling for BiFunction class types
- Adds test cases to verify BiFunction class type resolution
- Removes deprecated AbstractFunctionCallSupport class
- Cleans up unused imports in FunctionCallback

Resolves #1576
This commit is contained in:
Christian Tzolov
2024-10-23 15:08:02 +02:00
committed by Ilayaperumal Gopinathan
parent 3288c55ca6
commit c544d0c0a9
4 changed files with 59 additions and 200 deletions

View File

@@ -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<Msg, Req, Resp> {
protected final static boolean IS_RUNTIME_CALL = true;
/**
* The function callback register is used to resolve the function callbacks by name.
*/
protected final Map<String, FunctionCallback> functionCallbackRegister = new ConcurrentHashMap<>();
/**
* The function callback 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<String, FunctionCallback> getFunctionCallbackRegister() {
return this.functionCallbackRegister;
}
protected Set<String> handleFunctionCallbackConfigurations(FunctionCallingOptions options, boolean isRuntimeCall) {
Set<String> 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<FunctionCallback> resolveFunctionCallbacks(Set<String> functionNames) {
List<FunctionCallback> 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<Msg> 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<Resp> callWithFunctionSupportStream(Req request) {
final Flux<Resp> response = this.doChatCompletionStream(request);
return this.handleFunctionCallOrReturnStream(request, response);
}
protected Flux<Resp> handleFunctionCallOrReturnStream(Req request, Flux<Resp> 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<Msg> 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<Msg> conversationHistory);
abstract protected List<Msg> doGetUserMessages(Req request);
abstract protected Msg doGetToolResponseMessage(Resp response);
abstract protected Resp doChatCompletion(Req request);
abstract protected Flux<Resp> doChatCompletionStream(Req request);
abstract protected boolean isToolFunctionCall(Resp response);
}

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.ai.model.function;
import java.util.Map;
import org.springframework.ai.chat.model.ToolContext;
/**

View File

@@ -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<BiFunction<?, ?, ?>>) functionalClass);
}
else {
functionType = FunctionTypeUtils.discoverFunctionTypeFromClass(functionalClass);
}
}
var argumentType = functionType instanceof ParameterizedType

View File

@@ -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<MockWeatherService.Request, ToolContext, MockWeatherService.Response> weatherFunctionWithContext() {
@@ -220,4 +259,14 @@ class FunctionCallbackWithPlainFunctionBeanIT {
}
public static class MyBiFunction
implements BiFunction<MockWeatherService.Request, ToolContext, MockWeatherService.Response> {
@Override
public MockWeatherService.Response apply(MockWeatherService.Request request, ToolContext context) {
return new MockWeatherService().apply(request);
}
}
}