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:
committed by
Ilayaperumal Gopinathan
parent
3288c55ca6
commit
c544d0c0a9
@@ -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);
|
||||
|
||||
}
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.springframework.ai.model.function;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user