Remove FunctionCallback deprecations
- Remove the super type FunctionCallingOptions from ToolCallingChatOptions
- Move toolContext builder methods into ToolCallingChatOptions
- Remove Model chat options' function specific usages
- Replace them with tooling:
FunctionCallback -> ToolCallback
functions -> toolNames
- Remove proxyToolCalls use
- Remove deprecated methods
- Update ChatClient methods
- Replace FunctionCallback -> ToolCallback
- Remove deprecated methods
- Update DefaultChatClient
- functionNames -> toolNames
- functionCallbacks -> toolCallbacks
- Update AdviseRequest
- functionNames -> toolNames
- functionCallbacks -> toolCallbacks
- Remove FunctionCallingOptions and replace it with ToolCallingOptions
- Remove FunctionCallingHelper
- Update DefaultToolCallingChatOptions, ToolCallbackResolvers, ToolCallbackProvider to use Tool calling types
- Update documentation
- Remove VertexAiGeminiChatModelFunctionCallingIT and adjust MistralAiApiToolFunctionCallIT
Resolves #2528
Signed-off-by: Ilayaperumal Gopinathan <ilayaperumal.gopinathan@broadcom.com>
This commit is contained in:
committed by
Christian Tzolov
parent
3756e16526
commit
c0bc6231a5
@@ -66,14 +66,14 @@ class FunctionCallWithFunctionBeanIT {
|
||||
"What's the weather like in San Francisco, in Paris, France and in Tokyo, Japan? Return the temperature in Celsius.");
|
||||
|
||||
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
|
||||
AnthropicChatOptions.builder().function("weatherFunction").build()));
|
||||
AnthropicChatOptions.builder().toolNames("weatherFunction").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
|
||||
|
||||
response = chatModel.call(new Prompt(List.of(userMessage),
|
||||
AnthropicChatOptions.builder().function("weatherFunction3").build()));
|
||||
AnthropicChatOptions.builder().toolNames("weatherFunction3").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ public class FunctionCallWithPromptFunctionIT {
|
||||
"What's the weather like in San Francisco, in Paris and in Tokyo? Return the temperature in Celsius.");
|
||||
|
||||
var promptOptions = AnthropicChatOptions.builder()
|
||||
.functionCallbacks(
|
||||
.toolCallbacks(
|
||||
List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService())
|
||||
.description("Get the weather in location. Return temperature in 36°F or 36°C format.")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
|
||||
@@ -67,14 +67,14 @@ class FunctionCallWithFunctionBeanIT {
|
||||
"What's the weather like in San Francisco, Paris and in Tokyo? Use Multi-turn function calling.");
|
||||
|
||||
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
|
||||
AzureOpenAiChatOptions.builder().function("weatherFunction").build()));
|
||||
AzureOpenAiChatOptions.builder().toolNames("weatherFunction").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
|
||||
|
||||
response = chatModel.call(new Prompt(List.of(userMessage),
|
||||
AzureOpenAiChatOptions.builder().function("weatherFunction3").build()));
|
||||
AzureOpenAiChatOptions.builder().toolNames("weatherFunction3").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ public class FunctionCallWithFunctionWrapperIT {
|
||||
"What's the weather like in San Francisco, Paris and in Tokyo?");
|
||||
|
||||
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
|
||||
AzureOpenAiChatOptions.builder().function("WeatherInfo").build()));
|
||||
AzureOpenAiChatOptions.builder().toolNames("WeatherInfo").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ public class FunctionCallWithPromptFunctionIT {
|
||||
"What's the weather like in San Francisco, in Paris and in Tokyo? Use Multi-turn function calling.");
|
||||
|
||||
var promptOptions = AzureOpenAiChatOptions.builder()
|
||||
.functionCallbacks(
|
||||
.toolCallbacks(
|
||||
List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService())
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
|
||||
@@ -68,8 +68,8 @@ class PaymentStatusBeanIT {
|
||||
ChatResponse response = chatModel
|
||||
.call(new Prompt(List.of(new UserMessage("What's the status of my transaction with id T1001?")),
|
||||
MistralAiChatOptions.builder()
|
||||
.function("retrievePaymentStatus")
|
||||
.function("retrievePaymentDate")
|
||||
.toolNames("retrievePaymentStatus")
|
||||
.toolNames("retrievePaymentDate")
|
||||
.build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
@@ -75,8 +75,8 @@ class PaymentStatusBeanOpenAiIT {
|
||||
ChatResponse response = chatModel
|
||||
.call(new Prompt(List.of(new UserMessage("What's the status of my transaction with id T1001?")),
|
||||
OpenAiChatOptions.builder()
|
||||
.function("retrievePaymentStatus")
|
||||
.function("retrievePaymentDate")
|
||||
.toolNames("retrievePaymentStatus")
|
||||
.toolNames("retrievePaymentDate")
|
||||
.build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
@@ -64,7 +64,7 @@ public class PaymentStatusPromptIT {
|
||||
UserMessage userMessage = new UserMessage("What's the status of my transaction with id T1001?");
|
||||
|
||||
var promptOptions = MistralAiChatOptions.builder()
|
||||
.functionCallbacks(List.of(FunctionToolCallback
|
||||
.toolCallbacks(List.of(FunctionToolCallback
|
||||
.builder("retrievePaymentStatus",
|
||||
(Transaction transaction) -> new Status(DATA.get(transaction).status()))
|
||||
.description("Get payment status of a transaction")
|
||||
|
||||
@@ -73,11 +73,10 @@ public class WeatherServicePromptIT {
|
||||
|
||||
var promptOptions = MistralAiChatOptions.builder()
|
||||
.toolChoice(ToolChoice.AUTO)
|
||||
.functionCallbacks(
|
||||
List.of(FunctionToolCallback.builder("CurrentWeatherService", new MyWeatherService())
|
||||
.description("Get the current weather in requested location")
|
||||
.inputType(MyWeatherService.Request.class)
|
||||
.build()))
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeatherService", new MyWeatherService())
|
||||
.description("Get the current weather in requested location")
|
||||
.inputType(MyWeatherService.Request.class)
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
|
||||
|
||||
@@ -70,8 +70,7 @@ public class FunctionCallbackInPromptIT extends BaseOllamaIT {
|
||||
"What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
|
||||
|
||||
var promptOptions = OllamaOptions.builder()
|
||||
.functionCallbacks(List.of(FunctionToolCallback
|
||||
.builder("CurrentWeatherService", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService())
|
||||
.description(
|
||||
"Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
@@ -96,8 +95,7 @@ public class FunctionCallbackInPromptIT extends BaseOllamaIT {
|
||||
"What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
|
||||
|
||||
var promptOptions = OllamaOptions.builder()
|
||||
.functionCallbacks(List.of(FunctionToolCallback
|
||||
.builder("CurrentWeatherService", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService())
|
||||
.description(
|
||||
"Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
|
||||
@@ -75,7 +75,7 @@ public class OllamaFunctionCallbackIT extends BaseOllamaIT {
|
||||
"What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
|
||||
|
||||
ChatResponse response = chatModel
|
||||
.call(new Prompt(List.of(userMessage), OllamaOptions.builder().function("WeatherInfo").build()));
|
||||
.call(new Prompt(List.of(userMessage), OllamaOptions.builder().toolNames("WeatherInfo").build()));
|
||||
|
||||
logger.info("Response: " + response);
|
||||
|
||||
@@ -93,7 +93,7 @@ public class OllamaFunctionCallbackIT extends BaseOllamaIT {
|
||||
"What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
|
||||
|
||||
Flux<ChatResponse> response = chatModel
|
||||
.stream(new Prompt(List.of(userMessage), OllamaOptions.builder().function("WeatherInfo").build()));
|
||||
.stream(new Prompt(List.of(userMessage), OllamaOptions.builder().toolNames("WeatherInfo").build()));
|
||||
|
||||
String content = response.collectList()
|
||||
.block()
|
||||
|
||||
@@ -121,7 +121,7 @@ public class OllamaFunctionToolBeanIT extends BaseOllamaIT {
|
||||
"What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
|
||||
|
||||
Flux<ChatResponse> response = chatModel
|
||||
.stream(new Prompt(List.of(userMessage), OllamaOptions.builder().function("weatherInfo").build()));
|
||||
.stream(new Prompt(List.of(userMessage), OllamaOptions.builder().toolNames("weatherInfo").build()));
|
||||
|
||||
String content = response.collectList()
|
||||
.block()
|
||||
|
||||
@@ -68,7 +68,7 @@ class FunctionCallbackResolverKotlinIT : BaseOllamaIT() {
|
||||
"What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.")
|
||||
|
||||
val response = chatModel
|
||||
.call(Prompt(listOf(userMessage), OllamaOptions.builder().function("weatherInfo").build()))
|
||||
.call(Prompt(listOf(userMessage), OllamaOptions.builder().toolNames("weatherInfo").build()))
|
||||
|
||||
logger.info("Response: $response")
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ public class FunctionCallbackInPrompt2IT {
|
||||
|
||||
String content = ChatClient.builder(chatModel).build().prompt()
|
||||
.user("What's the weather like in San Francisco, Tokyo, and Paris?")
|
||||
.functions(FunctionToolCallback
|
||||
.tools(FunctionToolCallback
|
||||
.builder("CurrentWeatherService", new MockWeatherService())
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
@@ -88,7 +88,7 @@ public class FunctionCallbackInPrompt2IT {
|
||||
// @formatter:off
|
||||
String content = ChatClient.builder(chatModel).build().prompt()
|
||||
.user("Turn the light on in the kitchen and in the living room!")
|
||||
.functions(FunctionToolCallback
|
||||
.tools(FunctionToolCallback
|
||||
.builder("turnLight", (LightInfo lightInfo) -> {
|
||||
logger.info("Turning light to [" + lightInfo.isOn + "] in " + lightInfo.roomName());
|
||||
state.put(lightInfo.roomName(), lightInfo.isOn());
|
||||
@@ -114,7 +114,7 @@ public class FunctionCallbackInPrompt2IT {
|
||||
// @formatter:off
|
||||
String content = ChatClient.builder(chatModel).build().prompt()
|
||||
.user("What's the weather like in Amsterdam?")
|
||||
.functions(FunctionToolCallback
|
||||
.tools(FunctionToolCallback
|
||||
.builder("CurrentWeatherService", input -> "18 degrees Celsius")
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
@@ -138,7 +138,7 @@ public class FunctionCallbackInPrompt2IT {
|
||||
// @formatter:off
|
||||
String content = ChatClient.builder(chatModel).build().prompt()
|
||||
.user("What's the weather like in San Francisco, Tokyo, and Paris?")
|
||||
.functions(FunctionToolCallback
|
||||
.tools(FunctionToolCallback
|
||||
.builder("CurrentWeatherService", new MockWeatherService())
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
|
||||
@@ -62,7 +62,7 @@ public class FunctionCallbackInPromptIT {
|
||||
"What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.functionCallbacks(
|
||||
.toolCallbacks(
|
||||
List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService())
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
@@ -91,7 +91,7 @@ public class FunctionCallbackInPromptIT {
|
||||
"What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.functionCallbacks(
|
||||
.toolCallbacks(
|
||||
List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService())
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
|
||||
@@ -80,7 +80,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
UserMessage userMessage = new UserMessage("Turn the light on in the living room");
|
||||
|
||||
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder().function("turnLivingRoomLightOn").build()));
|
||||
OpenAiChatOptions.builder().toolNames("turnLivingRoomLightOn").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
assertThat(feedback).hasSize(1);
|
||||
@@ -98,7 +98,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
UserMessage userMessage = new UserMessage("Turn the light on in the living room");
|
||||
|
||||
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder().function("turnLivingRoomLightOnSupplier").build()));
|
||||
OpenAiChatOptions.builder().toolNames("turnLivingRoomLightOnSupplier").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
assertThat(feedback).hasSize(1);
|
||||
@@ -116,7 +116,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
UserMessage userMessage = new UserMessage("Turn the light on in the kitchen and in the living room");
|
||||
|
||||
ChatResponse response = chatModel
|
||||
.call(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("turnLight").build()));
|
||||
.call(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().toolNames("turnLight").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
assertThat(feedback).hasSize(2);
|
||||
@@ -135,7 +135,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
UserMessage userMessage = new UserMessage("Turn the light on in the kitchen and in the living room");
|
||||
|
||||
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder().function("turnLightConsumer").build()));
|
||||
OpenAiChatOptions.builder().toolNames("turnLightConsumer").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
assertThat(feedback).hasSize(2);
|
||||
@@ -174,7 +174,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
ChatClient chatClient = ChatClient.builder(chatModel).build();
|
||||
|
||||
String content = chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
|
||||
.functions("weatherFunctionWithContext")
|
||||
.tools("weatherFunctionWithContext")
|
||||
.toolContext(Map.of("sessionId", "123"))
|
||||
.call()
|
||||
.content();
|
||||
@@ -186,7 +186,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
|
||||
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder()
|
||||
.function("weatherFunctionWithContext")
|
||||
.toolNames("weatherFunctionWithContext")
|
||||
.toolContext(Map.of("sessionId", "123"))
|
||||
.build()));
|
||||
|
||||
@@ -206,7 +206,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
ChatClient chatClient = ChatClient.builder(chatModel).build();
|
||||
|
||||
String content = chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
|
||||
.functions("weatherFunctionWithClassBiFunction")
|
||||
.tools("weatherFunctionWithClassBiFunction")
|
||||
.toolContext(Map.of("sessionId", "123"))
|
||||
.call()
|
||||
.content();
|
||||
@@ -218,7 +218,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
|
||||
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder()
|
||||
.function("weatherFunctionWithClassBiFunction")
|
||||
.toolNames("weatherFunctionWithClassBiFunction")
|
||||
.toolContext(Map.of("sessionId", "123"))
|
||||
.build()));
|
||||
|
||||
@@ -240,7 +240,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
"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().function("weatherFunction").build()));
|
||||
new Prompt(List.of(userMessage), OpenAiChatOptions.builder().toolNames("weatherFunction").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
@@ -248,7 +248,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
|
||||
// Test weatherFunctionTwo
|
||||
response = chatModel.call(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder().function("weatherFunctionTwo").build()));
|
||||
OpenAiChatOptions.builder().toolNames("weatherFunctionTwo").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
@@ -289,7 +289,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
"What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'");
|
||||
|
||||
Flux<ChatResponse> response = chatModel.stream(
|
||||
new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("weatherFunction").build()));
|
||||
new Prompt(List.of(userMessage), OpenAiChatOptions.builder().toolNames("weatherFunction").build()));
|
||||
|
||||
String content = response.collectList()
|
||||
.block()
|
||||
@@ -305,7 +305,7 @@ class FunctionCallbackWithPlainFunctionBeanIT {
|
||||
|
||||
// Test weatherFunctionTwo
|
||||
response = chatModel.stream(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder().function("weatherFunctionTwo").build()));
|
||||
OpenAiChatOptions.builder().toolNames("weatherFunctionTwo").build()));
|
||||
|
||||
content = response.collectList()
|
||||
.block()
|
||||
|
||||
@@ -55,7 +55,7 @@ public class OpenAiFunctionCallback2IT {
|
||||
|
||||
// @formatter:off
|
||||
ChatClient chatClient = ChatClient.builder(chatModel)
|
||||
.defaultFunctions("WeatherInfo")
|
||||
.defaultTools("WeatherInfo")
|
||||
.defaultUser(u -> u.text("What's the weather like in {cities}?"))
|
||||
.build();
|
||||
|
||||
@@ -78,7 +78,7 @@ public class OpenAiFunctionCallback2IT {
|
||||
|
||||
// @formatter:off
|
||||
String content = ChatClient.builder(chatModel).build().prompt()
|
||||
.functions("WeatherInfo")
|
||||
.tools("WeatherInfo")
|
||||
.user("What's the weather like in San Francisco, Tokyo, and Paris?")
|
||||
.stream().content()
|
||||
.collectList().block().stream().collect(Collectors.joining());
|
||||
|
||||
@@ -63,7 +63,7 @@ public class OpenAiFunctionCallbackIT {
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
ChatResponse response = chatModel
|
||||
.call(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("WeatherInfo").build()));
|
||||
.call(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().toolNames("WeatherInfo").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
@@ -82,7 +82,7 @@ public class OpenAiFunctionCallbackIT {
|
||||
"What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'WeatherInfo'");
|
||||
|
||||
Flux<ChatResponse> response = chatModel
|
||||
.stream(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("WeatherInfo").build()));
|
||||
.stream(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().toolNames("WeatherInfo").build()));
|
||||
|
||||
String content = response.collectList()
|
||||
.block()
|
||||
|
||||
@@ -22,8 +22,8 @@ import java.util.List;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
|
||||
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
|
||||
@@ -52,9 +52,9 @@ public class ToolCallingAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationContext,
|
||||
List<FunctionCallback> functionCallbacks, List<ToolCallbackProvider> tcbProviders) {
|
||||
List<ToolCallback> toolCallbacks, List<ToolCallbackProvider> tcbProviders) {
|
||||
|
||||
List<FunctionCallback> allFunctionAndToolCallbacks = new ArrayList<>(functionCallbacks);
|
||||
List<ToolCallback> allFunctionAndToolCallbacks = new ArrayList<>(toolCallbacks);
|
||||
tcbProviders.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionAndToolCallbacks::addAll);
|
||||
|
||||
var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionAndToolCallbacks);
|
||||
|
||||
@@ -20,7 +20,6 @@ import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.DefaultToolCallingManager;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.tool.StaticToolCallbackProvider;
|
||||
@@ -135,18 +134,16 @@ class ToolCallingAutoConfigurationTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FunctionCallback functionCallbacks3() {
|
||||
return FunctionCallback.builder()
|
||||
.function("getCurrentWeather3", (Request request) -> "15.0°C")
|
||||
public ToolCallback functionCallbacks3() {
|
||||
return FunctionToolCallback.builder("getCurrentWeather3", (Request request) -> "15.0°C")
|
||||
.description("Gets the weather in location")
|
||||
.inputType(Request.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FunctionCallback functionCallbacks4() {
|
||||
return FunctionCallback.builder()
|
||||
.function("getCurrentWeather4", (Request request) -> "15.0°C")
|
||||
public ToolCallback functionCallbacks4() {
|
||||
return FunctionToolCallback.builder("getCurrentWeather4", (Request request) -> "15.0°C")
|
||||
.description("Gets the weather in location")
|
||||
.inputType(Request.class)
|
||||
.build();
|
||||
|
||||
@@ -32,7 +32,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -44,6 +43,7 @@ import org.springframework.util.Assert;
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
* @author Alexandros Pappas
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
@@ -64,7 +64,7 @@ public class AnthropicChatOptions implements ToolCallingChatOptions {
|
||||
* completion requests.
|
||||
*/
|
||||
@JsonIgnore
|
||||
private List<FunctionCallback> toolCallbacks = new ArrayList<>();
|
||||
private List<ToolCallback> toolCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Collection of tool names to be resolved at runtime and used for tool calling in the
|
||||
@@ -186,13 +186,13 @@ public class AnthropicChatOptions implements ToolCallingChatOptions {
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public List<FunctionCallback> getToolCallbacks() {
|
||||
public List<ToolCallback> getToolCallbacks() {
|
||||
return this.toolCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public void setToolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public void setToolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
this.toolCallbacks = toolCallbacks;
|
||||
@@ -226,34 +226,6 @@ public class AnthropicChatOptions implements ToolCallingChatOptions {
|
||||
this.internalToolExecutionEnabled = internalToolExecutionEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public List<FunctionCallback> getFunctionCallbacks() {
|
||||
return this.getToolCallbacks();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
this.setToolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public Set<String> getFunctions() {
|
||||
return this.getToolNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setFunctions(Set<String> functionNames) {
|
||||
this.setToolNames(functionNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public Double getFrequencyPenalty() {
|
||||
@@ -266,19 +238,6 @@ public class AnthropicChatOptions implements ToolCallingChatOptions {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public Boolean getProxyToolCalls() {
|
||||
return this.internalToolExecutionEnabled != null ? !this.internalToolExecutionEnabled : null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setProxyToolCalls(Boolean proxyToolCalls) {
|
||||
this.internalToolExecutionEnabled = proxyToolCalls != null ? !proxyToolCalls : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public Map<String, Object> getToolContext() {
|
||||
@@ -387,12 +346,12 @@ public class AnthropicChatOptions implements ToolCallingChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public Builder toolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
this.options.setToolCallbacks(toolCallbacks);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(FunctionCallback... toolCallbacks) {
|
||||
public Builder toolCallbacks(ToolCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
this.options.toolCallbacks.addAll(Arrays.asList(toolCallbacks));
|
||||
return this;
|
||||
@@ -415,29 +374,6 @@ public class AnthropicChatOptions implements ToolCallingChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
return toolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functions(Set<String> functionNames) {
|
||||
return toolNames(functionNames);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder function(String functionName) {
|
||||
return toolNames(functionName);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder proxyToolCalls(Boolean proxyToolCalls) {
|
||||
if (proxyToolCalls != null) {
|
||||
this.options.setInternalToolExecutionEnabled(!proxyToolCalls);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolContext(Map<String, Object> toolContext) {
|
||||
if (this.options.toolContext == null) {
|
||||
this.options.toolContext = toolContext;
|
||||
|
||||
@@ -424,7 +424,7 @@ class AnthropicChatModelIT {
|
||||
|
||||
var promptOptions = AnthropicChatOptions.builder()
|
||||
.model(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getName())
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.description(
|
||||
"Get the weather in location. Return temperature in 36°F or 36°C format. Use multi-turn if needed.")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
|
||||
@@ -32,7 +32,6 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -185,7 +184,7 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions {
|
||||
* completion requests.
|
||||
*/
|
||||
@JsonIgnore
|
||||
private List<FunctionCallback> toolCallbacks = new ArrayList<>();
|
||||
private List<ToolCallback> toolCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Collection of tool names to be resolved at runtime and used for tool calling in the
|
||||
@@ -202,13 +201,13 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions {
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public List<FunctionCallback> getToolCallbacks() {
|
||||
public List<ToolCallback> getToolCallbacks() {
|
||||
return this.toolCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public void setToolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public void setToolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
this.toolCallbacks = toolCallbacks;
|
||||
@@ -256,9 +255,9 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions {
|
||||
.temperature(fromOptions.getTemperature())
|
||||
.topP(fromOptions.getTopP())
|
||||
.user(fromOptions.getUser())
|
||||
.functionCallbacks(fromOptions.getFunctionCallbacks() != null
|
||||
? new ArrayList<>(fromOptions.getFunctionCallbacks()) : null)
|
||||
.functions(fromOptions.getFunctions() != null ? new HashSet<>(fromOptions.getFunctions()) : null)
|
||||
.toolCallbacks(
|
||||
fromOptions.getToolCallbacks() != null ? new ArrayList<>(fromOptions.getToolCallbacks()) : null)
|
||||
.toolNames(fromOptions.getToolNames() != null ? new HashSet<>(fromOptions.getToolNames()) : null)
|
||||
.responseFormat(fromOptions.getResponseFormat())
|
||||
.seed(fromOptions.getSeed())
|
||||
.logprobs(fromOptions.isLogprobs())
|
||||
@@ -380,27 +379,6 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions {
|
||||
this.topP = topP;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public List<FunctionCallback> getFunctionCallbacks() {
|
||||
return this.getToolCallbacks();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
this.setToolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public Set<String> getFunctions() {
|
||||
return this.getToolNames();
|
||||
}
|
||||
|
||||
public void setFunctions(Set<String> functions) {
|
||||
this.setToolNames(functions);
|
||||
}
|
||||
@@ -451,19 +429,6 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions {
|
||||
this.enhancements = enhancements;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public Boolean getProxyToolCalls() {
|
||||
return this.internalToolExecutionEnabled != null ? !this.internalToolExecutionEnabled : null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setProxyToolCalls(Boolean proxyToolCalls) {
|
||||
this.internalToolExecutionEnabled = proxyToolCalls != null ? !proxyToolCalls : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getToolContext() {
|
||||
return this.toolContext;
|
||||
@@ -583,34 +548,11 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
return toolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functions(Set<String> functionNames) {
|
||||
return toolNames(functionNames);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder function(String functionName) {
|
||||
return toolNames(functionName);
|
||||
}
|
||||
|
||||
public Builder responseFormat(AzureOpenAiResponseFormat responseFormat) {
|
||||
this.options.responseFormat = responseFormat;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder proxyToolCalls(Boolean proxyToolCalls) {
|
||||
if (proxyToolCalls != null) {
|
||||
this.options.setInternalToolExecutionEnabled(!proxyToolCalls);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder seed(Long seed) {
|
||||
this.options.seed = seed;
|
||||
return this;
|
||||
@@ -646,12 +588,12 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public Builder toolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
this.options.setToolCallbacks(toolCallbacks);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(FunctionCallback... toolCallbacks) {
|
||||
public Builder toolCallbacks(ToolCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
this.options.toolCallbacks.addAll(Arrays.asList(toolCallbacks));
|
||||
return this;
|
||||
|
||||
@@ -70,7 +70,7 @@ class AzureOpenAiChatModelFunctionCallIT {
|
||||
|
||||
var promptOptions = AzureOpenAiChatOptions.builder()
|
||||
.deploymentName(this.selectedModel)
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.description("Get the current weather in a given location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
@@ -98,7 +98,7 @@ class AzureOpenAiChatModelFunctionCallIT {
|
||||
|
||||
var promptOptions = AzureOpenAiChatOptions.builder()
|
||||
.deploymentName(this.selectedModel)
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.description("Get the current weather in a given location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
@@ -119,7 +119,7 @@ class AzureOpenAiChatModelFunctionCallIT {
|
||||
|
||||
var promptOptions = AzureOpenAiChatOptions.builder()
|
||||
.deploymentName(this.selectedModel)
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.description("Get the current weather in a given location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
@@ -156,7 +156,7 @@ class AzureOpenAiChatModelFunctionCallIT {
|
||||
|
||||
var promptOptions = AzureOpenAiChatOptions.builder()
|
||||
.deploymentName(this.selectedModel)
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.description("Get the current weather in a given location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
@@ -182,7 +182,7 @@ class AzureOpenAiChatModelFunctionCallIT {
|
||||
|
||||
var promptOptions = AzureOpenAiChatOptions.builder()
|
||||
.deploymentName(this.selectedModel)
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.description("Get the current weather in a given location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
|
||||
@@ -329,7 +329,7 @@ public final class ConverseApiUtils {
|
||||
attributes.remove("proxyToolCalls");
|
||||
attributes.remove("functions");
|
||||
attributes.remove("toolContext");
|
||||
attributes.remove("functionCallbacks");
|
||||
attributes.remove("toolCallbacks");
|
||||
|
||||
attributes.remove("toolCallbacks");
|
||||
attributes.remove("toolNames");
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.minimax.api.MiniMaxApi;
|
||||
import org.springframework.ai.minimax.api.MockWeatherService;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -64,16 +65,13 @@ public class ChatCompletionRequestTests {
|
||||
var client = new MiniMaxChatModel(new MiniMaxApi("TEST"),
|
||||
MiniMaxChatOptions.builder().model("DEFAULT_MODEL").build());
|
||||
|
||||
var request = client.createRequest(new Prompt("Test message content",
|
||||
MiniMaxChatOptions.builder()
|
||||
.model("PROMPT_MODEL")
|
||||
.functionCallbacks(List.of(FunctionCallback.builder()
|
||||
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
.build()),
|
||||
false);
|
||||
var request = client.createRequest(new Prompt("Test message content", MiniMaxChatOptions.builder()
|
||||
.model("PROMPT_MODEL")
|
||||
.functionCallbacks(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);
|
||||
|
||||
@@ -60,7 +60,6 @@ import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage.C
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage.ToolCall;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
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;
|
||||
@@ -381,10 +380,6 @@ public class MistralAiChatModel implements ChatModel {
|
||||
runtimeOptions = ModelOptionsUtils.copyToTarget(toolCallingChatOptions, ToolCallingChatOptions.class,
|
||||
MistralAiChatOptions.class);
|
||||
}
|
||||
else if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOptions) {
|
||||
runtimeOptions = ModelOptionsUtils.copyToTarget(functionCallingOptions, FunctionCallingOptions.class,
|
||||
MistralAiChatOptions.class);
|
||||
}
|
||||
else {
|
||||
runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,
|
||||
MistralAiChatOptions.class);
|
||||
|
||||
@@ -33,7 +33,6 @@ import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ResponseFormat;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.FunctionTool;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -120,7 +119,7 @@ public class MistralAiChatOptions implements ToolCallingChatOptions {
|
||||
* completion requests.
|
||||
*/
|
||||
@JsonIgnore
|
||||
private List<FunctionCallback> toolCallbacks = new ArrayList<>();
|
||||
private List<ToolCallback> toolCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Collection of tool names to be resolved at runtime and used for tool calling in the
|
||||
@@ -257,13 +256,13 @@ public class MistralAiChatOptions implements ToolCallingChatOptions {
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public List<FunctionCallback> getToolCallbacks() {
|
||||
public List<ToolCallback> getToolCallbacks() {
|
||||
return this.toolCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public void setToolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public void setToolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
this.toolCallbacks = toolCallbacks;
|
||||
@@ -297,34 +296,6 @@ public class MistralAiChatOptions implements ToolCallingChatOptions {
|
||||
this.internalToolExecutionEnabled = internalToolExecutionEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public List<FunctionCallback> getFunctionCallbacks() {
|
||||
return this.getToolCallbacks();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
this.setToolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public Set<String> getFunctions() {
|
||||
return this.getToolNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setFunctions(Set<String> functionNames) {
|
||||
this.setToolNames(functionNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public Double getFrequencyPenalty() {
|
||||
@@ -343,19 +314,6 @@ public class MistralAiChatOptions implements ToolCallingChatOptions {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public Boolean getProxyToolCalls() {
|
||||
return this.internalToolExecutionEnabled != null ? !this.internalToolExecutionEnabled : null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setProxyToolCalls(Boolean proxyToolCalls) {
|
||||
this.internalToolExecutionEnabled = proxyToolCalls != null ? !proxyToolCalls : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public Map<String, Object> getToolContext() {
|
||||
@@ -463,12 +421,12 @@ public class MistralAiChatOptions implements ToolCallingChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public Builder toolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
this.options.setToolCallbacks(toolCallbacks);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(FunctionCallback... toolCallbacks) {
|
||||
public Builder toolCallbacks(ToolCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
this.options.toolCallbacks.addAll(Arrays.asList(toolCallbacks));
|
||||
return this;
|
||||
@@ -491,29 +449,6 @@ public class MistralAiChatOptions implements ToolCallingChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
return toolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functions(Set<String> functionNames) {
|
||||
return toolNames(functionNames);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder function(String functionName) {
|
||||
return toolNames(functionName);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder proxyToolCalls(Boolean proxyToolCalls) {
|
||||
if (proxyToolCalls != null) {
|
||||
this.options.setInternalToolExecutionEnabled(!proxyToolCalls);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolContext(Map<String, Object> toolContext) {
|
||||
if (this.options.toolContext == null) {
|
||||
this.options.toolContext = toolContext;
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
@@ -96,7 +95,7 @@ public class MistralAiChatCompletionRequestTest {
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolCallbacks()).hasSize(2);
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolCallbacks()
|
||||
.stream()
|
||||
.map(FunctionCallback::getName)).containsExactlyInAnyOrder("tool3", "tool4");
|
||||
.map(toolCallback -> toolCallback.getToolDefinition().name())).containsExactlyInAnyOrder("tool3", "tool4");
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolNames()).containsExactlyInAnyOrder("tool3");
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolContext()).containsEntry("key1", "value1")
|
||||
.containsEntry("key2", "valueB");
|
||||
|
||||
@@ -204,7 +204,7 @@ class MistralAiChatModelIT {
|
||||
|
||||
var promptOptions = MistralAiChatOptions.builder()
|
||||
.model(MistralAiApi.ChatModel.SMALL.getValue())
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
@@ -229,7 +229,7 @@ class MistralAiChatModelIT {
|
||||
|
||||
var promptOptions = MistralAiChatOptions.builder()
|
||||
.model(MistralAiApi.ChatModel.SMALL.getValue())
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
@@ -315,7 +315,7 @@ class MistralAiChatModelIT {
|
||||
|
||||
var promptOptions = MistralAiChatOptions.builder()
|
||||
.model(MistralAiApi.ChatModel.SMALL.getValue())
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
|
||||
@@ -152,11 +152,11 @@ public class MistralAiApiToolFunctionCallIT {
|
||||
|
||||
assertThat(chatCompletion2.getBody().choices().get(0).message().role()).isEqualTo(Role.ASSISTANT);
|
||||
assertThat(chatCompletion2.getBody().choices().get(0).message().content()).contains("San Francisco")
|
||||
.containsAnyOf("30.0°C", "30°C");
|
||||
.containsAnyOf("30.0", "30");
|
||||
assertThat(chatCompletion2.getBody().choices().get(0).message().content()).contains("Tokyo")
|
||||
.containsAnyOf("10.0°C", "10°C");
|
||||
.containsAnyOf("10.0", "10");
|
||||
assertThat(chatCompletion2.getBody().choices().get(0).message().content()).contains("Paris")
|
||||
.containsAnyOf("15.0°C", "15°C");
|
||||
.containsAnyOf("15.0", "15");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.springframework.ai.embedding.EmbeddingOptions;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -323,17 +323,17 @@ public class OllamaOptions implements ToolCallingChatOptions, EmbeddingOptions {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* For Prompt Options the toolCallbacks are automatically enabled for the duration of the prompt execution.
|
||||
* For Default Options the toolCallbacks 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.
|
||||
*/
|
||||
@JsonIgnore
|
||||
private List<FunctionCallback> toolCallbacks = new ArrayList<>();
|
||||
private List<ToolCallback> 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.
|
||||
* Functions with those names must exist in the toolCallbacks registry.
|
||||
* The {@link #toolCallbacks} 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.
|
||||
@@ -706,13 +706,13 @@ public class OllamaOptions implements ToolCallingChatOptions, EmbeddingOptions {
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public List<FunctionCallback> getToolCallbacks() {
|
||||
public List<ToolCallback> getToolCallbacks() {
|
||||
return this.toolCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public void setToolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public void setToolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
this.toolCallbacks = toolCallbacks;
|
||||
@@ -746,53 +746,12 @@ public class OllamaOptions implements ToolCallingChatOptions, EmbeddingOptions {
|
||||
this.internalToolExecutionEnabled = internalToolExecutionEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public List<FunctionCallback> getFunctionCallbacks() {
|
||||
return this.getToolCallbacks();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
this.setToolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public Set<String> getFunctions() {
|
||||
return this.getToolNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setFunctions(Set<String> functions) {
|
||||
this.setToolNames(functions);
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public Integer getDimensions() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public Boolean getProxyToolCalls() {
|
||||
return this.internalToolExecutionEnabled != null ? !this.internalToolExecutionEnabled : null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setProxyToolCalls(Boolean proxyToolCalls) {
|
||||
this.internalToolExecutionEnabled = proxyToolCalls != null ? !proxyToolCalls : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public Map<String, Object> getToolContext() {
|
||||
@@ -1043,12 +1002,12 @@ public class OllamaOptions implements ToolCallingChatOptions, EmbeddingOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public Builder toolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
this.options.setToolCallbacks(toolCallbacks);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(FunctionCallback... toolCallbacks) {
|
||||
public Builder toolCallbacks(ToolCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
this.options.toolCallbacks.addAll(Arrays.asList(toolCallbacks));
|
||||
return this;
|
||||
@@ -1070,29 +1029,6 @@ public class OllamaOptions implements ToolCallingChatOptions, EmbeddingOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
return toolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functions(Set<String> functions) {
|
||||
return toolNames(functions);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder function(String functionName) {
|
||||
return toolNames(functionName);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder proxyToolCalls(Boolean proxyToolCalls) {
|
||||
if (proxyToolCalls != null) {
|
||||
this.options.setInternalToolExecutionEnabled(!proxyToolCalls);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolContext(Map<String, Object> toolContext) {
|
||||
if (this.options.toolContext == null) {
|
||||
this.options.toolContext = toolContext;
|
||||
|
||||
@@ -62,7 +62,7 @@ class OllamaChatModelFunctionCallingIT extends BaseOllamaIT {
|
||||
|
||||
var promptOptions = OllamaOptions.builder()
|
||||
.model(MODEL)
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.description(
|
||||
"Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
@@ -85,7 +85,7 @@ class OllamaChatModelFunctionCallingIT extends BaseOllamaIT {
|
||||
|
||||
var promptOptions = OllamaOptions.builder()
|
||||
.model(MODEL)
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.description(
|
||||
"Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
|
||||
@@ -22,7 +22,6 @@ 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.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.ollama.api.OllamaApi;
|
||||
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||
@@ -69,7 +68,7 @@ class OllamaChatRequestTests {
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolCallbacks()).hasSize(2);
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolCallbacks()
|
||||
.stream()
|
||||
.map(FunctionCallback::getName)).containsExactlyInAnyOrder("tool3", "tool4");
|
||||
.map(toolCallback -> toolCallback.getToolDefinition().name())).containsExactlyInAnyOrder("tool3", "tool4");
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolNames()).containsExactlyInAnyOrder("tool3");
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolContext()).containsEntry("key1", "value1")
|
||||
.containsEntry("key2", "valueB");
|
||||
|
||||
@@ -101,7 +101,6 @@ public class OllamaModelOptionsTests {
|
||||
.useMMap(true)
|
||||
.useMLock(false)
|
||||
.penalizeNewline(true)
|
||||
.proxyToolCalls(true)
|
||||
.build();
|
||||
|
||||
var optionsMap = options.toMap();
|
||||
@@ -128,9 +127,9 @@ public class OllamaModelOptionsTests {
|
||||
@Test
|
||||
public void testFunctionAndToolOptions() {
|
||||
var options = OllamaOptions.builder()
|
||||
.function("function1")
|
||||
.function("function2")
|
||||
.function("function3")
|
||||
.toolNames("function1")
|
||||
.toolNames("function2")
|
||||
.toolNames("function3")
|
||||
.toolContext(Map.of("key1", "value1", "key2", "value2"))
|
||||
.build();
|
||||
|
||||
@@ -140,7 +139,7 @@ public class OllamaModelOptionsTests {
|
||||
assertThat(optionsMap).doesNotContainKey("tool_context");
|
||||
|
||||
// But they are accessible through getters
|
||||
assertThat(options.getFunctions()).containsExactlyInAnyOrder("function1", "function2", "function3");
|
||||
assertThat(options.getToolNames()).containsExactlyInAnyOrder("function1", "function2", "function3");
|
||||
assertThat(options.getToolContext())
|
||||
.containsExactlyInAnyOrderEntriesOf(Map.of("key1", "value1", "key2", "value2"));
|
||||
}
|
||||
@@ -151,9 +150,9 @@ public class OllamaModelOptionsTests {
|
||||
functionSet.add("function1");
|
||||
functionSet.add("function2");
|
||||
|
||||
var options = OllamaOptions.builder().functions(functionSet).function("function3").build();
|
||||
var options = OllamaOptions.builder().toolNames(functionSet).toolNames("function3").build();
|
||||
|
||||
assertThat(options.getFunctions()).containsExactlyInAnyOrder("function1", "function2", "function3");
|
||||
assertThat(options.getToolNames()).containsExactlyInAnyOrder("function1", "function2", "function3");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -162,7 +161,7 @@ public class OllamaModelOptionsTests {
|
||||
.model("llama2")
|
||||
.temperature(0.7)
|
||||
.topK(40)
|
||||
.functions(Set.of("function1"))
|
||||
.toolNames(Set.of("function1"))
|
||||
.build();
|
||||
|
||||
var copiedOptions = OllamaOptions.fromOptions(originalOptions);
|
||||
@@ -171,30 +170,30 @@ public class OllamaModelOptionsTests {
|
||||
assertThat(copiedOptions.getModel()).isEqualTo("llama2");
|
||||
assertThat(copiedOptions.getTemperature()).isEqualTo(0.7);
|
||||
assertThat(copiedOptions.getTopK()).isEqualTo(40);
|
||||
assertThat(copiedOptions.getFunctions()).containsExactly("function1");
|
||||
assertThat(copiedOptions.getToolNames()).containsExactly("function1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFunctionOptionsNotInMap() {
|
||||
var options = OllamaOptions.builder().model("llama2").functions(Set.of("function1")).build();
|
||||
var options = OllamaOptions.builder().model("llama2").toolNames(Set.of("function1")).build();
|
||||
|
||||
var optionsMap = options.toMap();
|
||||
|
||||
// Verify function-related fields are not included in the map due to @JsonIgnore
|
||||
assertThat(optionsMap).containsEntry("model", "llama2");
|
||||
assertThat(optionsMap).doesNotContainKey("functions");
|
||||
assertThat(optionsMap).doesNotContainKey("functionCallbacks");
|
||||
assertThat(optionsMap).doesNotContainKey("toolCallbacks");
|
||||
assertThat(optionsMap).doesNotContainKey("proxyToolCalls");
|
||||
assertThat(optionsMap).doesNotContainKey("toolContext");
|
||||
|
||||
// But verify they are still accessible through getters
|
||||
assertThat(options.getFunctions()).containsExactly("function1");
|
||||
assertThat(options.getToolNames()).containsExactly("function1");
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void testDeprecatedMethods() {
|
||||
var options = OllamaOptions.builder().model("llama2").temperature(0.7).topK(40).function("function1").build();
|
||||
var options = OllamaOptions.builder().model("llama2").temperature(0.7).topK(40).toolNames("function1").build();
|
||||
|
||||
var optionsMap = options.toMap();
|
||||
assertThat(optionsMap).containsEntry("model", "llama2");
|
||||
@@ -202,7 +201,7 @@ public class OllamaModelOptionsTests {
|
||||
assertThat(optionsMap).containsEntry("top_k", 40);
|
||||
|
||||
// Function is not in map but accessible via getter
|
||||
assertThat(options.getFunctions()).containsExactly("function1");
|
||||
assertThat(options.getToolNames()).containsExactly("function1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.content.Media;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
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;
|
||||
@@ -493,10 +492,6 @@ public class OpenAiChatModel implements ChatModel {
|
||||
runtimeOptions = ModelOptionsUtils.copyToTarget(toolCallingChatOptions, ToolCallingChatOptions.class,
|
||||
OpenAiChatOptions.class);
|
||||
}
|
||||
else if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOptions) {
|
||||
runtimeOptions = ModelOptionsUtils.copyToTarget(functionCallingOptions, FunctionCallingOptions.class,
|
||||
OpenAiChatOptions.class);
|
||||
}
|
||||
else {
|
||||
runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,
|
||||
OpenAiChatOptions.class);
|
||||
|
||||
@@ -31,7 +31,6 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.AudioParameters;
|
||||
@@ -198,7 +197,7 @@ public class OpenAiChatOptions implements ToolCallingChatOptions {
|
||||
* Collection of {@link ToolCallback}s to be used for tool calling in the chat completion requests.
|
||||
*/
|
||||
@JsonIgnore
|
||||
private List<FunctionCallback> toolCallbacks = new ArrayList<>();
|
||||
private List<ToolCallback> toolCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Collection of tool names to be resolved at runtime and used for tool calling in the chat completion requests.
|
||||
@@ -440,23 +439,6 @@ public class OpenAiChatOptions implements ToolCallingChatOptions {
|
||||
this.toolChoice = toolChoice;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public Boolean getProxyToolCalls() {
|
||||
return this.getToolExecutionEnabled() != null ? !this.internalToolExecutionEnabled : null;
|
||||
}
|
||||
|
||||
private Boolean getToolExecutionEnabled() {
|
||||
return this.internalToolExecutionEnabled;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setProxyToolCalls(Boolean proxyToolCalls) {
|
||||
this.internalToolExecutionEnabled = proxyToolCalls != null ? !proxyToolCalls : null;
|
||||
}
|
||||
|
||||
public String getUser() {
|
||||
return this.user;
|
||||
}
|
||||
@@ -475,13 +457,13 @@ public class OpenAiChatOptions implements ToolCallingChatOptions {
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public List<FunctionCallback> getToolCallbacks() {
|
||||
public List<ToolCallback> getToolCallbacks() {
|
||||
return this.toolCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public void setToolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public void setToolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
this.toolCallbacks = toolCallbacks;
|
||||
@@ -515,34 +497,6 @@ public class OpenAiChatOptions implements ToolCallingChatOptions {
|
||||
this.internalToolExecutionEnabled = internalToolExecutionEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public List<FunctionCallback> getFunctionCallbacks() {
|
||||
return this.getToolCallbacks();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
this.setToolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public Set<String> getFunctions() {
|
||||
return this.getToolNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setFunctions(Set<String> functionNames) {
|
||||
this.setToolNames(functionNames);
|
||||
}
|
||||
|
||||
public Map<String, String> getHttpHeaders() {
|
||||
return this.httpHeaders;
|
||||
}
|
||||
@@ -767,12 +721,12 @@ public class OpenAiChatOptions implements ToolCallingChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public Builder toolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
this.options.setToolCallbacks(toolCallbacks);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(FunctionCallback... toolCallbacks) {
|
||||
public Builder toolCallbacks(ToolCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
this.options.toolCallbacks.addAll(Arrays.asList(toolCallbacks));
|
||||
return this;
|
||||
@@ -795,29 +749,6 @@ public class OpenAiChatOptions implements ToolCallingChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
return toolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functions(Set<String> functionNames) {
|
||||
return toolNames(functionNames);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder function(String functionName) {
|
||||
return toolNames(functionName);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder proxyToolCalls(Boolean proxyToolCalls) {
|
||||
if (proxyToolCalls != null) {
|
||||
this.options.setInternalToolExecutionEnabled(!proxyToolCalls);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder httpHeaders(Map<String, String> httpHeaders) {
|
||||
this.options.httpHeaders = httpHeaders;
|
||||
return this;
|
||||
|
||||
@@ -23,12 +23,12 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.SimpleApiKey;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.tool.MockWeatherService;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -66,7 +66,7 @@ class ChatCompletionRequestTests {
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolCallbacks()).hasSize(2);
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolCallbacks()
|
||||
.stream()
|
||||
.map(FunctionCallback::getName)).containsExactlyInAnyOrder("tool3", "tool4");
|
||||
.map(toolCallback -> toolCallback.getToolDefinition().name())).containsExactlyInAnyOrder("tool3", "tool4");
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolNames()).containsExactlyInAnyOrder("tool3");
|
||||
assertThat(((ToolCallingChatOptions) prompt.getOptions()).getToolContext()).containsEntry("key1", "value1")
|
||||
.containsEntry("key2", "valueB");
|
||||
@@ -111,8 +111,7 @@ class ChatCompletionRequestTests {
|
||||
var prompt = client.buildRequestPrompt(new Prompt("Test message content",
|
||||
OpenAiChatOptions.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()))
|
||||
@@ -136,8 +135,7 @@ class ChatCompletionRequestTests {
|
||||
.openAiApi(OpenAiApi.builder().apiKey("TEST").build())
|
||||
.defaultOptions(OpenAiChatOptions.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()))
|
||||
@@ -156,7 +154,7 @@ class ChatCompletionRequestTests {
|
||||
|
||||
// Reference the default options tool by name at runtime
|
||||
prompt = client.buildRequestPrompt(
|
||||
new Prompt("Test message content", OpenAiChatOptions.builder().function(TOOL_FUNCTION_NAME).build()));
|
||||
new Prompt("Test message content", OpenAiChatOptions.builder().toolNames(TOOL_FUNCTION_NAME).build()));
|
||||
request = client.createRequest(prompt, false);
|
||||
|
||||
assertThat(request.tools()).hasSize(1);
|
||||
|
||||
@@ -79,7 +79,7 @@ class OpenAiChatOptionsTests {
|
||||
.store(false)
|
||||
.metadata(metadata)
|
||||
.reasoningEffort("medium")
|
||||
.proxyToolCalls(false)
|
||||
.internalToolExecutionEnabled(false)
|
||||
.httpHeaders(Map.of("header1", "value1"))
|
||||
.toolContext(toolContext)
|
||||
.build();
|
||||
@@ -88,8 +88,8 @@ class OpenAiChatOptionsTests {
|
||||
.extracting("model", "frequencyPenalty", "logitBias", "logprobs", "topLogprobs", "maxTokens",
|
||||
"maxCompletionTokens", "n", "outputModalities", "outputAudio", "presencePenalty", "responseFormat",
|
||||
"streamOptions", "seed", "stop", "temperature", "topP", "tools", "toolChoice", "user",
|
||||
"parallelToolCalls", "store", "metadata", "reasoningEffort", "proxyToolCalls", "httpHeaders",
|
||||
"toolContext")
|
||||
"parallelToolCalls", "store", "metadata", "reasoningEffort", "internalToolExecutionEnabled",
|
||||
"httpHeaders", "toolContext")
|
||||
.containsExactly("test-model", 0.5, logitBias, true, 5, 100, 50, 2, outputModalities, outputAudio, 0.8,
|
||||
responseFormat, streamOptions, 12345, stopSequences, 0.7, 0.9, tools, toolChoice, "test-user", true,
|
||||
false, metadata, "medium", false, Map.of("header1", "value1"), toolContext);
|
||||
@@ -138,7 +138,7 @@ class OpenAiChatOptionsTests {
|
||||
.store(true)
|
||||
.metadata(metadata)
|
||||
.reasoningEffort("low")
|
||||
.proxyToolCalls(true)
|
||||
.internalToolExecutionEnabled(true)
|
||||
.httpHeaders(Map.of("header1", "value1"))
|
||||
.build();
|
||||
|
||||
@@ -186,7 +186,7 @@ class OpenAiChatOptionsTests {
|
||||
options.setStore(false);
|
||||
options.setMetadata(metadata);
|
||||
options.setReasoningEffort("high");
|
||||
options.setProxyToolCalls(false);
|
||||
options.setInternalToolExecutionEnabled(false);
|
||||
options.setHttpHeaders(Map.of("header2", "value2"));
|
||||
|
||||
assertThat(options.getModel()).isEqualTo("test-model");
|
||||
@@ -213,7 +213,7 @@ class OpenAiChatOptionsTests {
|
||||
assertThat(options.getStore()).isFalse();
|
||||
assertThat(options.getMetadata()).isEqualTo(metadata);
|
||||
assertThat(options.getReasoningEffort()).isEqualTo("high");
|
||||
assertThat(options.getProxyToolCalls()).isFalse();
|
||||
assertThat(options.getInternalToolExecutionEnabled()).isFalse();
|
||||
assertThat(options.getHttpHeaders()).isEqualTo(Map.of("header2", "value2"));
|
||||
assertThat(options.getStreamUsage()).isTrue();
|
||||
options.setStreamUsage(false);
|
||||
@@ -251,9 +251,8 @@ class OpenAiChatOptionsTests {
|
||||
assertThat(options.getStore()).isNull();
|
||||
assertThat(options.getMetadata()).isNull();
|
||||
assertThat(options.getReasoningEffort()).isNull();
|
||||
assertThat(options.getFunctionCallbacks()).isNotNull().isEmpty();
|
||||
assertThat(options.getFunctions()).isNotNull().isEmpty();
|
||||
assertThat(options.getProxyToolCalls()).isNull();
|
||||
assertThat(options.getToolCallbacks()).isNotNull().isEmpty();
|
||||
assertThat(options.getInternalToolExecutionEnabled()).isNull();
|
||||
assertThat(options.getHttpHeaders()).isNotNull().isEmpty();
|
||||
assertThat(options.getToolContext()).isEqualTo(new HashMap<>());
|
||||
assertThat(options.getStreamUsage()).isFalse();
|
||||
|
||||
@@ -38,13 +38,13 @@ import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.model.Generation;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.tool.MockWeatherService;
|
||||
import org.springframework.ai.openai.api.tool.MockWeatherService.Request;
|
||||
import org.springframework.ai.openai.api.tool.MockWeatherService.Response;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
@@ -69,8 +69,7 @@ class OpenAiChatModelFunctionCallingIT {
|
||||
// @formatter:off
|
||||
String response = ChatClient.create(this.chatModel).prompt()
|
||||
.user("Turn the light on in the living room")
|
||||
.functions(FunctionCallback.builder()
|
||||
.function("turnsLightOnInTheLivingRoom", () -> state.put("Light", "ON"))
|
||||
.tools(FunctionToolCallback.builder("turnsLightOnInTheLivingRoom", () -> state.put("Light", "ON"))
|
||||
.build())
|
||||
.call()
|
||||
.content();
|
||||
@@ -84,8 +83,7 @@ class OpenAiChatModelFunctionCallingIT {
|
||||
void functionCallTest() {
|
||||
functionCallTest(OpenAiChatOptions.builder()
|
||||
.model(OpenAiApi.ChatModel.GPT_4_O.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()))
|
||||
@@ -120,8 +118,7 @@ class OpenAiChatModelFunctionCallingIT {
|
||||
|
||||
functionCallTest(OpenAiChatOptions.builder()
|
||||
.model(OpenAiApi.ChatModel.GPT_4_O.getValue())
|
||||
.functionCallbacks(List.of(FunctionCallback.builder()
|
||||
.function("getCurrentWeather", biFunction)
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", biFunction)
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
@@ -146,8 +143,7 @@ class OpenAiChatModelFunctionCallingIT {
|
||||
void streamFunctionCallTest() {
|
||||
|
||||
streamFunctionCallTest(OpenAiChatOptions.builder()
|
||||
.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)
|
||||
// .responseConverter(response -> "" + response.temp() + response.unit())
|
||||
@@ -182,8 +178,7 @@ class OpenAiChatModelFunctionCallingIT {
|
||||
};
|
||||
|
||||
OpenAiChatOptions promptOptions = OpenAiChatOptions.builder()
|
||||
.functionCallbacks(List.of((FunctionCallback.builder()
|
||||
.function("getCurrentWeather", biFunction)
|
||||
.toolCallbacks(List.of((FunctionToolCallback.builder("getCurrentWeather", biFunction)
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build())))
|
||||
|
||||
@@ -53,7 +53,6 @@ import org.springframework.ai.content.Media;
|
||||
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.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.OpenAiTestConfiguration;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
@@ -342,8 +341,7 @@ public class OpenAiChatModelIT extends AbstractIT {
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.model(OpenAiApi.ChatModel.GPT_4_O.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()))
|
||||
@@ -389,8 +387,7 @@ public class OpenAiChatModelIT extends AbstractIT {
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
// .withModel(OpenAiApi.ChatModel.GPT_4_TURBO_PREVIEW.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()))
|
||||
@@ -422,8 +419,7 @@ public class OpenAiChatModelIT extends AbstractIT {
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
// .withModel(OpenAiApi.ChatModel.GPT_4_TURBO_PREVIEW.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()))
|
||||
@@ -451,8 +447,7 @@ public class OpenAiChatModelIT extends AbstractIT {
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
// .withModel(OpenAiApi.ChatModel.GPT_4_TURBO_PREVIEW.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()))
|
||||
|
||||
@@ -36,13 +36,13 @@ import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
|
||||
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
|
||||
import org.springframework.ai.retry.RetryUtils;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
|
||||
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
|
||||
@@ -171,7 +171,7 @@ public class OpenAiPaymentTransactionIT {
|
||||
logger.info("System params: " + request.systemParams());
|
||||
logger.info("User text: \n" + request.userText());
|
||||
logger.info("User params:" + request.userParams());
|
||||
logger.info("Function names: " + request.functionNames());
|
||||
logger.info("Function names: " + request.toolNames());
|
||||
|
||||
logger.info("Options: " + request.chatOptions().toString());
|
||||
|
||||
@@ -245,9 +245,9 @@ public class OpenAiPaymentTransactionIT {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationContext,
|
||||
List<FunctionCallback> functionCallbacks, List<ToolCallbackProvider> tcbProviders) {
|
||||
List<ToolCallback> toolCallback, List<ToolCallbackProvider> tcbProviders) {
|
||||
|
||||
List<FunctionCallback> allFunctionAndToolCallbacks = new ArrayList<>(functionCallbacks);
|
||||
List<ToolCallback> allFunctionAndToolCallbacks = new ArrayList<>(toolCallback);
|
||||
tcbProviders.stream()
|
||||
.map(pr -> List.of(pr.getToolCallbacks()))
|
||||
.forEach(allFunctionAndToolCallbacks::addAll);
|
||||
|
||||
@@ -1,185 +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.openai.chat.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.ToolResponseMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.model.Generation;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallingHelper;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.OpenAiTestConfiguration;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.testutils.AbstractIT;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest(classes = OpenAiTestConfiguration.class)
|
||||
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
|
||||
@ActiveProfiles("logging-test")
|
||||
class OpenAiChatClientProxyFunctionCallsIT extends AbstractIT {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OpenAiChatClientMultipleFunctionCallsIT.class);
|
||||
|
||||
@Value("classpath:/prompts/system-message.st")
|
||||
private Resource systemTextResource;
|
||||
|
||||
FunctionCallback functionDefinition = new FunctionCallingHelper.FunctionDefinition("getWeatherInLocation",
|
||||
"Get the weather in location", """
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state e.g. San Francisco, CA"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": ["C", "F"]
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
}
|
||||
""");
|
||||
|
||||
@Autowired
|
||||
private OpenAiChatModel chatModel;
|
||||
|
||||
// Helper class that reuses some of the {@link AbstractToolCallSupport} functionality
|
||||
// to help to implement the function call handling logic on the client side.
|
||||
private FunctionCallingHelper functionCallingHelper = new FunctionCallingHelper();
|
||||
|
||||
// Function which will be called by the AI model.
|
||||
private String getWeatherInLocation(String location, String unit) {
|
||||
|
||||
double temperature = 0;
|
||||
|
||||
if (location.contains("Paris")) {
|
||||
temperature = 15;
|
||||
}
|
||||
else if (location.contains("Tokyo")) {
|
||||
temperature = 10;
|
||||
}
|
||||
else if (location.contains("San Francisco")) {
|
||||
temperature = 30;
|
||||
}
|
||||
|
||||
return String.format("The weather in %s is %s%s", location, temperature, unit);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toolProxyFunctionCall() throws JsonMappingException, JsonProcessingException {
|
||||
|
||||
List<Message> messages = List
|
||||
.of(new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?"));
|
||||
|
||||
boolean isToolCall = false;
|
||||
|
||||
ChatResponse chatResponse = null;
|
||||
|
||||
var chatClient = ChatClient.builder(this.chatModel).build();
|
||||
|
||||
do {
|
||||
|
||||
chatResponse = chatClient.prompt()
|
||||
.messages(messages)
|
||||
.tools(this.functionDefinition)
|
||||
.options(OpenAiChatOptions.builder().proxyToolCalls(true).build())
|
||||
.call()
|
||||
.chatResponse();
|
||||
|
||||
// Note that the tool call check could be platform specific because the finish
|
||||
// reasons.
|
||||
isToolCall = this.functionCallingHelper.isToolCall(chatResponse,
|
||||
Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(),
|
||||
OpenAiApi.ChatCompletionFinishReason.STOP.name()));
|
||||
|
||||
if (isToolCall) {
|
||||
|
||||
Optional<Generation> toolCallGeneration = chatResponse.getResults()
|
||||
.stream()
|
||||
.filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
|
||||
.findFirst();
|
||||
|
||||
assertThat(toolCallGeneration).isNotEmpty();
|
||||
|
||||
AssistantMessage assistantMessage = toolCallGeneration.get().getOutput();
|
||||
|
||||
List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();
|
||||
|
||||
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
|
||||
|
||||
var functionName = toolCall.name();
|
||||
|
||||
assertThat(functionName).isEqualTo("getWeatherInLocation");
|
||||
|
||||
String functionArguments = toolCall.arguments();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> argumentsMap = new ObjectMapper().readValue(functionArguments, Map.class);
|
||||
|
||||
String functionResponse = getWeatherInLocation(argumentsMap.get("location").toString(),
|
||||
argumentsMap.get("unit").toString());
|
||||
|
||||
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), functionName,
|
||||
ModelOptionsUtils.toJsonString(functionResponse)));
|
||||
}
|
||||
|
||||
ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of());
|
||||
|
||||
messages = this.functionCallingHelper.buildToolCallConversation(messages, assistantMessage,
|
||||
toolMessageResponse);
|
||||
|
||||
assertThat(messages).isNotEmpty();
|
||||
|
||||
// prompt = new Prompt(toolCallConversation, prompt.getOptions());
|
||||
}
|
||||
}
|
||||
while (isToolCall);
|
||||
|
||||
logger.info("Response: {}", chatResponse);
|
||||
|
||||
assertThat(chatResponse.getResult().getOutput().getText()).contains("30", "10", "15");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,12 +43,12 @@ import org.springframework.ai.chat.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.converter.ListOutputConverter;
|
||||
import org.springframework.ai.converter.MapOutputConverter;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.tool.MockWeatherService;
|
||||
import org.springframework.ai.openai.chat.ActorsFilms;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
@@ -256,8 +256,7 @@ class DeepSeekWithOpenAiChatModelIT {
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.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()))
|
||||
@@ -280,8 +279,7 @@ class DeepSeekWithOpenAiChatModelIT {
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.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()))
|
||||
|
||||
@@ -46,12 +46,12 @@ import org.springframework.ai.chat.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.converter.ListOutputConverter;
|
||||
import org.springframework.ai.converter.MapOutputConverter;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.tool.MockWeatherService;
|
||||
import org.springframework.ai.openai.chat.ActorsFilms;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
@@ -268,8 +268,7 @@ class DockerModelRunnerWithOpenAiChatModelIT {
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.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()))
|
||||
@@ -292,8 +291,7 @@ class DockerModelRunnerWithOpenAiChatModelIT {
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.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()))
|
||||
|
||||
@@ -46,12 +46,12 @@ import org.springframework.ai.content.Media;
|
||||
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.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.tool.MockWeatherService;
|
||||
import org.springframework.ai.openai.chat.ActorsFilms;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
@@ -249,8 +249,7 @@ class GroqWithOpenAiChatModelIT {
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.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()))
|
||||
@@ -272,8 +271,7 @@ class GroqWithOpenAiChatModelIT {
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.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()))
|
||||
|
||||
@@ -47,7 +47,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.converter.ListOutputConverter;
|
||||
import org.springframework.ai.converter.MapOutputConverter;
|
||||
import org.springframework.ai.model.SimpleApiKey;
|
||||
import org.springframework.ai.model.tool.LegacyToolCallingManager;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
@@ -398,7 +398,7 @@ class MistralWithOpenAiChatModelIT {
|
||||
public OpenAiChatModel openAiClient(OpenAiApi openAiApi) {
|
||||
return OpenAiChatModel.builder()
|
||||
.openAiApi(openAiApi)
|
||||
.toolCallingManager(LegacyToolCallingManager.builder().build())
|
||||
.toolCallingManager(ToolCallingManager.builder().build())
|
||||
.defaultOptions(OpenAiChatOptions.builder().model(MISTRAL_DEFAULT_MODEL).build())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -41,12 +41,12 @@ import org.springframework.ai.chat.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.converter.ListOutputConverter;
|
||||
import org.springframework.ai.converter.MapOutputConverter;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.tool.MockWeatherService;
|
||||
import org.springframework.ai.openai.chat.ActorsFilms;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
@@ -246,8 +246,7 @@ class NvidiaWithOpenAiChatModelIT {
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.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()))
|
||||
@@ -269,8 +268,7 @@ class NvidiaWithOpenAiChatModelIT {
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.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()))
|
||||
|
||||
@@ -50,8 +50,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.converter.ListOutputConverter;
|
||||
import org.springframework.ai.converter.MapOutputConverter;
|
||||
import org.springframework.ai.model.NoopApiKey;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.LegacyToolCallingManager;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
@@ -302,8 +301,7 @@ class OllamaWithOpenAiChatModelIT {
|
||||
// Note for Ollama you must set the tool choice to explicitly. Unlike OpenAI
|
||||
// (which defaults to "auto") Ollama defaults to "nono"
|
||||
.toolChoice("auto")
|
||||
.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()))
|
||||
@@ -421,7 +419,7 @@ class OllamaWithOpenAiChatModelIT {
|
||||
public OpenAiChatModel openAiClient(OpenAiApi openAiApi) {
|
||||
return OpenAiChatModel.builder()
|
||||
.openAiApi(openAiApi)
|
||||
.toolCallingManager(LegacyToolCallingManager.builder().build())
|
||||
.toolCallingManager(ToolCallingManager.builder().build())
|
||||
.defaultOptions(OpenAiChatOptions.builder().model(DEFAULT_OLLAMA_MODEL).build())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -41,12 +41,12 @@ import org.springframework.ai.chat.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.converter.ListOutputConverter;
|
||||
import org.springframework.ai.converter.MapOutputConverter;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.tool.MockWeatherService;
|
||||
import org.springframework.ai.openai.chat.ActorsFilms;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
@@ -255,8 +255,7 @@ class PerplexityWithOpenAiChatModelIT {
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.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()))
|
||||
@@ -277,8 +276,7 @@ class PerplexityWithOpenAiChatModelIT {
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.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()))
|
||||
|
||||
@@ -76,10 +76,7 @@ import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.content.Media;
|
||||
import org.springframework.ai.model.ChatModelDescription;
|
||||
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.tool.DefaultToolExecutionEligibilityPredicate;
|
||||
import org.springframework.ai.model.tool.LegacyToolCallingManager;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.model.tool.ToolExecutionEligibilityPredicate;
|
||||
@@ -183,70 +180,6 @@ public class VertexAiGeminiChatModel implements ChatModel, DisposableBean {
|
||||
*/
|
||||
private ChatModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
public VertexAiGeminiChatModel(VertexAI vertexAI) {
|
||||
this(vertexAI, VertexAiGeminiChatOptions.builder().model(ChatModel.GEMINI_1_5_PRO).temperature(0.8).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options) {
|
||||
this(vertexAI, options, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options,
|
||||
FunctionCallbackResolver functionCallbackResolver) {
|
||||
this(vertexAI, options, functionCallbackResolver, List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options,
|
||||
FunctionCallbackResolver functionCallbackResolver, List<FunctionCallback> toolFunctionCallbacks) {
|
||||
this(vertexAI, options, functionCallbackResolver, toolFunctionCallbacks, RetryUtils.DEFAULT_RETRY_TEMPLATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options,
|
||||
FunctionCallbackResolver functionCallbackResolver, List<FunctionCallback> toolFunctionCallbacks,
|
||||
RetryTemplate retryTemplate) {
|
||||
this(vertexAI, options, functionCallbackResolver, toolFunctionCallbacks, retryTemplate,
|
||||
ObservationRegistry.NOOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options,
|
||||
FunctionCallbackResolver functionCallbackResolver, List<FunctionCallback> toolFunctionCallbacks,
|
||||
RetryTemplate retryTemplate, ObservationRegistry observationRegistry) {
|
||||
|
||||
this(vertexAI, options,
|
||||
LegacyToolCallingManager.builder()
|
||||
.functionCallbackResolver(functionCallbackResolver)
|
||||
.functionCallbacks(toolFunctionCallbacks)
|
||||
.build(),
|
||||
retryTemplate, observationRegistry);
|
||||
logger.warn("This constructor is deprecated and will be removed in the next milestone. "
|
||||
+ "Please use the new constructor accepting ToolCallingManager instead.");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of VertexAiGeminiChatModel.
|
||||
* @param vertexAI the Vertex AI instance to use
|
||||
@@ -850,10 +783,6 @@ public class VertexAiGeminiChatModel implements ChatModel, DisposableBean {
|
||||
|
||||
private ToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate = new DefaultToolExecutionEligibilityPredicate();
|
||||
|
||||
private FunctionCallbackResolver functionCallbackResolver;
|
||||
|
||||
private List<FunctionCallback> toolFunctionCallbacks;
|
||||
|
||||
private RetryTemplate retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE;
|
||||
|
||||
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
|
||||
@@ -882,18 +811,6 @@ public class VertexAiGeminiChatModel implements ChatModel, DisposableBean {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functionCallbackResolver(FunctionCallbackResolver functionCallbackResolver) {
|
||||
this.functionCallbackResolver = functionCallbackResolver;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder toolFunctionCallbacks(List<FunctionCallback> toolFunctionCallbacks) {
|
||||
this.toolFunctionCallbacks = toolFunctionCallbacks;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder retryTemplate(RetryTemplate retryTemplate) {
|
||||
this.retryTemplate = retryTemplate;
|
||||
return this;
|
||||
@@ -906,25 +823,9 @@ public class VertexAiGeminiChatModel implements ChatModel, DisposableBean {
|
||||
|
||||
public VertexAiGeminiChatModel build() {
|
||||
if (this.toolCallingManager != null) {
|
||||
Assert.isNull(this.functionCallbackResolver,
|
||||
"functionCallbackResolver cannot be set when toolCallingManager is set");
|
||||
Assert.isNull(this.toolFunctionCallbacks,
|
||||
"toolFunctionCallbacks cannot be set when toolCallingManager is set");
|
||||
|
||||
return new VertexAiGeminiChatModel(this.vertexAI, this.defaultOptions, this.toolCallingManager,
|
||||
this.retryTemplate, this.observationRegistry, this.toolExecutionEligibilityPredicate);
|
||||
}
|
||||
|
||||
if (this.functionCallbackResolver != null) {
|
||||
Assert.isNull(this.toolCallingManager,
|
||||
"toolCallingManager cannot be set when functionCallbackResolver is set");
|
||||
List<FunctionCallback> toolCallbacks = this.toolFunctionCallbacks != null ? this.toolFunctionCallbacks
|
||||
: List.of();
|
||||
|
||||
return new VertexAiGeminiChatModel(this.vertexAI, this.defaultOptions, this.functionCallbackResolver,
|
||||
toolCallbacks, this.retryTemplate, this.observationRegistry);
|
||||
}
|
||||
|
||||
return new VertexAiGeminiChatModel(this.vertexAI, this.defaultOptions, DEFAULT_TOOL_CALLING_MANAGER,
|
||||
this.retryTemplate, this.observationRegistry, this.toolExecutionEligibilityPredicate);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel.ChatModel;
|
||||
@@ -101,7 +100,7 @@ public class VertexAiGeminiChatOptions implements ToolCallingChatOptions {
|
||||
* completion requests.
|
||||
*/
|
||||
@JsonIgnore
|
||||
private List<FunctionCallback> toolCallbacks = new ArrayList<>();
|
||||
private List<ToolCallback> toolCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Collection of tool names to be resolved at runtime and used for tool calling in the
|
||||
@@ -234,44 +233,17 @@ public class VertexAiGeminiChatOptions implements ToolCallingChatOptions {
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
@Deprecated
|
||||
public List<FunctionCallback> getFunctionCallbacks() {
|
||||
return this.getToolCallbacks();
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
@Deprecated
|
||||
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
this.setToolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FunctionCallback> getToolCallbacks() {
|
||||
public List<ToolCallback> getToolCallbacks() {
|
||||
return this.toolCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setToolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public void setToolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
this.toolCallbacks = toolCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
@Deprecated
|
||||
public Set<String> getFunctions() {
|
||||
return this.getToolNames();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Deprecated
|
||||
public void setFunctions(Set<String> functions) {
|
||||
this.setToolNames(functions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getToolNames() {
|
||||
return this.toolNames;
|
||||
@@ -325,19 +297,6 @@ public class VertexAiGeminiChatOptions implements ToolCallingChatOptions {
|
||||
this.safetySettings = safetySettings;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public Boolean getProxyToolCalls() {
|
||||
return this.internalToolExecutionEnabled != null ? !this.internalToolExecutionEnabled : null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public void setProxyToolCalls(Boolean proxyToolCalls) {
|
||||
this.internalToolExecutionEnabled = proxyToolCalls != null ? !proxyToolCalls : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getToolContext() {
|
||||
return this.toolContext;
|
||||
@@ -447,17 +406,12 @@ public class VertexAiGeminiChatOptions implements ToolCallingChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
return toolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public Builder toolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
this.options.toolCallbacks = toolCallbacks;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder toolCallbacks(FunctionCallback... toolCallbacks) {
|
||||
public Builder toolCallbacks(ToolCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
this.options.toolCallbacks.addAll(Arrays.asList(toolCallbacks));
|
||||
return this;
|
||||
|
||||
@@ -32,7 +32,6 @@ import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.content.Media;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
@@ -165,8 +164,7 @@ public class CreateGeminiRequestTests {
|
||||
.toolCallingManager(toolCallingManager)
|
||||
.defaultOptions(VertexAiGeminiChatOptions.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()))
|
||||
@@ -205,8 +203,7 @@ public class CreateGeminiRequestTests {
|
||||
// Override the default options function with one from the prompt
|
||||
requestPrompt = client.buildRequestPrompt(new Prompt("Test message content",
|
||||
VertexAiGeminiChatOptions.builder()
|
||||
.functionCallbacks(List.of(FunctionCallback.builder()
|
||||
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder(TOOL_FUNCTION_NAME, new MockWeatherService())
|
||||
.description("Overridden function description")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
|
||||
@@ -17,14 +17,12 @@
|
||||
package org.springframework.ai.vertexai.gemini;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.cloud.vertexai.VertexAI;
|
||||
import com.google.cloud.vertexai.api.GenerateContentResponse;
|
||||
import com.google.cloud.vertexai.generativeai.GenerativeModel;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallbackResolver;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
|
||||
/**
|
||||
@@ -35,9 +33,8 @@ public class TestVertexAiGeminiChatModel extends VertexAiGeminiChatModel {
|
||||
private GenerativeModel mockGenerativeModel;
|
||||
|
||||
public TestVertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options,
|
||||
FunctionCallbackResolver functionCallbackResolver, List<FunctionCallback> toolFunctionCallbacks,
|
||||
RetryTemplate retryTemplate) {
|
||||
super(vertexAI, options, functionCallbackResolver, toolFunctionCallbacks, retryTemplate);
|
||||
super(vertexAI, options, ToolCallingManager.builder().build(), retryTemplate, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.springframework.ai.vertexai.gemini;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.cloud.vertexai.VertexAI;
|
||||
@@ -77,7 +76,7 @@ public class VertexAiGeminiRetryTests {
|
||||
.topP(1.0)
|
||||
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue())
|
||||
.build(),
|
||||
null, Collections.emptyList(), this.retryTemplate);
|
||||
this.retryTemplate);
|
||||
|
||||
this.chatModel.setMockGenerativeModel(this.mockGenerativeModel);
|
||||
}
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2025 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.vertexai.gemini.tool;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.cloud.vertexai.Transport;
|
||||
import com.google.cloud.vertexai.VertexAI;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.model.Generation;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.ai.util.json.schema.JsonSchemaGenerator;
|
||||
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
|
||||
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
|
||||
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
|
||||
@Deprecated
|
||||
public class VertexAiGeminiChatModelFunctionCallingIT {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VertexAiGeminiChatModelFunctionCallingIT.class);
|
||||
|
||||
@Autowired
|
||||
private VertexAiGeminiChatModel chatModel;
|
||||
|
||||
@Test
|
||||
public void functionCallExplicitOpenApiSchema() {
|
||||
|
||||
UserMessage userMessage = new UserMessage(
|
||||
"What's the weather like in San Francisco, Paris and in Tokyo? Return the temperature in Celsius.");
|
||||
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
String openApiSchema = """
|
||||
{
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "STRING",
|
||||
"description": "The city and state e.g. San Francisco, CA"
|
||||
},
|
||||
"unit" : {
|
||||
"type" : "STRING",
|
||||
"enum" : [ "C", "F" ],
|
||||
"description" : "Temperature unit"
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
}
|
||||
""";
|
||||
|
||||
var promptOptions = VertexAiGeminiChatOptions.builder()
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("get_current_weather", new MockWeatherService())
|
||||
.description("Get the current weather in a given location")
|
||||
.inputSchema(openApiSchema)
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
ChatResponse response = this.chatModel.call(new Prompt(messages, promptOptions));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void functionCallTestInferredOpenApiSchema() {
|
||||
|
||||
UserMessage userMessage = new UserMessage(
|
||||
"What's the weather like in San Francisco, Paris and in Tokyo? Return the temperature in Celsius.");
|
||||
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = VertexAiGeminiChatOptions.builder()
|
||||
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
|
||||
.functionCallbacks(List.of(
|
||||
FunctionToolCallback.builder("get_current_weather", new MockWeatherService())
|
||||
.inputSchema(JsonSchemaGenerator.generateForType(MockWeatherService.Request.class,
|
||||
JsonSchemaGenerator.SchemaOption.UPPER_CASE_TYPE_VALUES))
|
||||
.description("Get the current weather in a given location.")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build(),
|
||||
FunctionToolCallback.builder("get_payment_status", new PaymentStatus())
|
||||
.inputSchema(JsonSchemaGenerator.generateForType(PaymentInfoRequest.class,
|
||||
JsonSchemaGenerator.SchemaOption.UPPER_CASE_TYPE_VALUES))
|
||||
.description(
|
||||
"Retrieves the payment status for transaction. For example what is the payment status for transaction 700?")
|
||||
.inputType(PaymentInfoRequest.class)
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
ChatResponse response = this.chatModel.call(new Prompt(messages, promptOptions));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
|
||||
|
||||
ChatResponse response2 = this.chatModel
|
||||
.call(new Prompt("What is the payment status for transaction 696?", promptOptions));
|
||||
|
||||
logger.info("Response: {}", response2);
|
||||
|
||||
assertThat(response2.getResult().getOutput().getText()).containsIgnoringCase("transaction 696 is PAYED");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void functionCallTestInferredOpenApiSchema2() {
|
||||
|
||||
UserMessage userMessage = new UserMessage(
|
||||
"What's the weather like in San Francisco, Paris and in Tokyo? Return the temperature in Celsius.");
|
||||
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = VertexAiGeminiChatOptions.builder()
|
||||
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
|
||||
.functionCallbacks(List.of(
|
||||
FunctionToolCallback.builder("get_current_weather", new MockWeatherService())
|
||||
.description("Get the current weather in a given location.")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build(),
|
||||
FunctionToolCallback.builder("get_payment_status", new PaymentStatus())
|
||||
.description(
|
||||
"Retrieves the payment status for transaction. For example what is the payment status for transaction 700?")
|
||||
.inputType(PaymentInfoRequest.class)
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
ChatResponse response = this.chatModel.call(new Prompt(messages, promptOptions));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
|
||||
|
||||
ChatResponse response2 = this.chatModel
|
||||
.call(new Prompt("What is the payment status for transaction 696?", promptOptions));
|
||||
|
||||
logger.info("Response: {}", response2);
|
||||
|
||||
assertThat(response2.getResult().getOutput().getText()).containsIgnoringCase("transaction 696 is PAYED");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void functionCallTestInferredOpenApiSchemaStream() {
|
||||
|
||||
UserMessage userMessage = new UserMessage(
|
||||
"What's the weather like in San Francisco, Paris and in Tokyo? Return the temperature in Celsius.");
|
||||
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = VertexAiGeminiChatOptions.builder()
|
||||
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
|
||||
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
|
||||
.inputSchema(JsonSchemaGenerator.generateForType(MockWeatherService.Request.class,
|
||||
JsonSchemaGenerator.SchemaOption.UPPER_CASE_TYPE_VALUES))
|
||||
.description("Get the current weather in a given location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
Flux<ChatResponse> response = this.chatModel.stream(new Prompt(messages, promptOptions));
|
||||
|
||||
String responseString = response.collectList()
|
||||
.block()
|
||||
.stream()
|
||||
.map(ChatResponse::getResults)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getOutput)
|
||||
.map(AssistantMessage::getText)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
logger.info("Response: {}", responseString);
|
||||
|
||||
assertThat(responseString).contains("30", "10", "15");
|
||||
|
||||
}
|
||||
|
||||
public record PaymentInfoRequest(String id) {
|
||||
|
||||
}
|
||||
|
||||
public record TransactionStatus(String status) {
|
||||
|
||||
}
|
||||
|
||||
public static class PaymentStatus implements Function<PaymentInfoRequest, TransactionStatus> {
|
||||
|
||||
@Override
|
||||
public TransactionStatus apply(PaymentInfoRequest paymentInfoRequest) {
|
||||
return new TransactionStatus("Transaction " + paymentInfoRequest.id() + " is PAYED");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
public static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
public VertexAI vertexAiApi() {
|
||||
String projectId = System.getenv("VERTEX_AI_GEMINI_PROJECT_ID");
|
||||
String location = System.getenv("VERTEX_AI_GEMINI_LOCATION");
|
||||
return new VertexAI.Builder().setLocation(location)
|
||||
.setProjectId(projectId)
|
||||
.setTransport(Transport.REST)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public VertexAiGeminiChatModel vertexAiEmbedding(VertexAI vertexAi) {
|
||||
return new VertexAiGeminiChatModel(vertexAi,
|
||||
VertexAiGeminiChatOptions.builder()
|
||||
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
|
||||
.temperature(0.9)
|
||||
.build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -36,8 +36,8 @@ import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
|
||||
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
|
||||
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
|
||||
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
|
||||
import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
|
||||
@@ -146,7 +146,7 @@ public class VertexAiGeminiPaymentTransactionIT {
|
||||
logger.info("System params: " + request.systemParams());
|
||||
logger.info("User text: \n" + request.userText());
|
||||
logger.info("User params:" + request.userParams());
|
||||
logger.info("Function names: " + request.functionNames());
|
||||
logger.info("Function names: " + request.toolNames());
|
||||
|
||||
logger.info("Options: " + request.chatOptions().toString());
|
||||
|
||||
@@ -225,7 +225,7 @@ public class VertexAiGeminiPaymentTransactionIT {
|
||||
|
||||
@Bean
|
||||
ToolCallingManager toolCallingManager(GenericApplicationContext applicationContext,
|
||||
List<FunctionCallback> toolCallbacks, ObjectProvider<ObservationRegistry> observationRegistry) {
|
||||
List<ToolCallback> toolCallbacks, ObjectProvider<ObservationRegistry> observationRegistry) {
|
||||
|
||||
var staticToolCallbackResolver = new StaticToolCallbackResolver(toolCallbacks);
|
||||
var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
|
||||
|
||||
@@ -36,8 +36,8 @@ import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
|
||||
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
|
||||
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.ToolCallbacks;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
@@ -144,7 +144,7 @@ public class VertexAiGeminiPaymentTransactionMethodIT {
|
||||
logger.info("System params: " + request.systemParams());
|
||||
logger.info("User text: \n" + request.userText());
|
||||
logger.info("User params:" + request.userParams());
|
||||
logger.info("Function names: " + request.functionNames());
|
||||
logger.info("Function names: " + request.toolNames());
|
||||
|
||||
logger.info("Options: " + request.chatOptions().toString());
|
||||
|
||||
@@ -220,10 +220,10 @@ public class VertexAiGeminiPaymentTransactionMethodIT {
|
||||
|
||||
@Bean
|
||||
ToolCallingManager toolCallingManager(GenericApplicationContext applicationContext,
|
||||
List<ToolCallbackProvider> tcps, List<FunctionCallback> functionCallbacks,
|
||||
List<ToolCallbackProvider> tcps, List<ToolCallback> toolCallbacks,
|
||||
ObjectProvider<ObservationRegistry> observationRegistry) {
|
||||
|
||||
List<FunctionCallback> allFunctionCallbacks = new ArrayList(functionCallbacks);
|
||||
List<ToolCallback> allFunctionCallbacks = new ArrayList(toolCallbacks);
|
||||
tcps.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionCallbacks::addAll);
|
||||
|
||||
var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionCallbacks);
|
||||
|
||||
@@ -35,8 +35,8 @@ import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
|
||||
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
|
||||
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
|
||||
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
|
||||
@@ -145,7 +145,7 @@ public class VertexAiGeminiPaymentTransactionToolsIT {
|
||||
logger.info("System params: " + request.systemParams());
|
||||
logger.info("User text: \n" + request.userText());
|
||||
logger.info("User params:" + request.userParams());
|
||||
logger.info("Function names: " + request.functionNames());
|
||||
logger.info("Function names: " + request.toolNames());
|
||||
|
||||
logger.info("Options: " + request.chatOptions().toString());
|
||||
|
||||
@@ -216,7 +216,7 @@ public class VertexAiGeminiPaymentTransactionToolsIT {
|
||||
|
||||
@Bean
|
||||
ToolCallingManager toolCallingManager(GenericApplicationContext applicationContext,
|
||||
List<FunctionCallback> toolCallbacks, ObjectProvider<ObservationRegistry> observationRegistry) {
|
||||
List<ToolCallback> toolCallbacks, ObjectProvider<ObservationRegistry> observationRegistry) {
|
||||
|
||||
var staticToolCallbackResolver = new StaticToolCallbackResolver(toolCallbacks);
|
||||
var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.content.Media;
|
||||
import org.springframework.ai.converter.StructuredOutputConverter;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
@@ -222,7 +221,7 @@ public interface ChatClient {
|
||||
|
||||
ChatClientRequestSpec tools(String... toolNames);
|
||||
|
||||
ChatClientRequestSpec tools(FunctionCallback... toolCallbacks);
|
||||
ChatClientRequestSpec tools(ToolCallback... toolCallbacks);
|
||||
|
||||
ChatClientRequestSpec tools(List<ToolCallback> toolCallbacks);
|
||||
|
||||
@@ -230,12 +229,6 @@ public interface ChatClient {
|
||||
|
||||
ChatClientRequestSpec tools(ToolCallbackProvider... toolCallbackProviders);
|
||||
|
||||
@Deprecated
|
||||
<I, O> ChatClientRequestSpec functions(FunctionCallback... functionCallbacks);
|
||||
|
||||
@Deprecated
|
||||
ChatClientRequestSpec functions(String... functionBeanNames);
|
||||
|
||||
ChatClientRequestSpec toolContext(Map<String, Object> toolContext);
|
||||
|
||||
ChatClientRequestSpec system(String text);
|
||||
@@ -291,7 +284,7 @@ public interface ChatClient {
|
||||
|
||||
Builder defaultTools(String... toolNames);
|
||||
|
||||
Builder defaultTools(FunctionCallback... toolCallbacks);
|
||||
Builder defaultTools(ToolCallback... toolCallbacks);
|
||||
|
||||
Builder defaultTools(List<ToolCallback> toolCallbacks);
|
||||
|
||||
@@ -299,18 +292,6 @@ public interface ChatClient {
|
||||
|
||||
Builder defaultTools(ToolCallbackProvider... toolCallbackProviders);
|
||||
|
||||
/**
|
||||
* @deprecated in favor of {@link #defaultTools(String...)}
|
||||
*/
|
||||
@Deprecated
|
||||
Builder defaultFunctions(String... functionNames);
|
||||
|
||||
/**
|
||||
* @deprecated in favor of {@link #defaultTools(Object...)}
|
||||
*/
|
||||
@Deprecated
|
||||
Builder defaultFunctions(FunctionCallback... functionCallbacks);
|
||||
|
||||
Builder defaultToolContext(Map<String, Object> toolContext);
|
||||
|
||||
Builder clone();
|
||||
|
||||
@@ -32,19 +32,14 @@ import java.util.function.Consumer;
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.chat.client.advisor.ChatModelCallAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.ChatModelStreamAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain;
|
||||
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
|
||||
import org.springframework.ai.chat.client.advisor.api.Advisor;
|
||||
import org.springframework.ai.chat.client.advisor.api.BaseAdvisorChain;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.ToolCallbacks;
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain;
|
||||
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
|
||||
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
|
||||
import org.springframework.ai.chat.client.observation.ChatClientObservationDocumentation;
|
||||
@@ -62,9 +57,12 @@ import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.content.Media;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.converter.StructuredOutputConverter;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.ToolCallbacks;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -135,7 +133,7 @@ public class DefaultChatClient implements ChatClient {
|
||||
|
||||
return new DefaultChatClientRequestSpec(advisedRequest.chatModel(), advisedRequest.userText(),
|
||||
advisedRequest.userParams(), advisedRequest.systemText(), advisedRequest.systemParams(),
|
||||
advisedRequest.functionCallbacks(), advisedRequest.messages(), advisedRequest.functionNames(),
|
||||
advisedRequest.toolCallbacks(), advisedRequest.messages(), advisedRequest.toolNames(),
|
||||
advisedRequest.media(), advisedRequest.chatOptions(), advisedRequest.advisors(),
|
||||
advisedRequest.advisorParams(), observationRegistry, customObservationConvention,
|
||||
advisedRequest.toolContext());
|
||||
@@ -650,7 +648,7 @@ public class DefaultChatClient implements ChatClient {
|
||||
|
||||
private final List<String> toolNames = new ArrayList<>();
|
||||
|
||||
private final List<FunctionCallback> toolCallbacks = new ArrayList<>();
|
||||
private final List<ToolCallback> toolCallbacks = new ArrayList<>();
|
||||
|
||||
private final List<Message> messages = new ArrayList<>();
|
||||
|
||||
@@ -684,7 +682,7 @@ public class DefaultChatClient implements ChatClient {
|
||||
|
||||
public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userText,
|
||||
Map<String, Object> userParams, @Nullable String systemText, Map<String, Object> systemParams,
|
||||
List<FunctionCallback> toolCallbacks, List<Message> messages, List<String> toolNames, List<Media> media,
|
||||
List<ToolCallback> toolCallbacks, List<Message> messages, List<String> toolNames, List<Media> media,
|
||||
@Nullable ChatOptions chatOptions, List<Advisor> advisors, Map<String, Object> advisorParams,
|
||||
ObservationRegistry observationRegistry,
|
||||
@Nullable ChatClientObservationConvention observationConvention, Map<String, Object> toolContext) {
|
||||
@@ -777,11 +775,11 @@ public class DefaultChatClient implements ChatClient {
|
||||
return this.media;
|
||||
}
|
||||
|
||||
public List<String> getFunctionNames() {
|
||||
public List<String> getToolNames() {
|
||||
return this.toolNames;
|
||||
}
|
||||
|
||||
public List<FunctionCallback> getFunctionCallbacks() {
|
||||
public List<ToolCallback> getToolCallbacks() {
|
||||
return this.toolCallbacks;
|
||||
}
|
||||
|
||||
@@ -873,7 +871,7 @@ public class DefaultChatClient implements ChatClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatClientRequestSpec tools(FunctionCallback... toolCallbacks) {
|
||||
public ChatClientRequestSpec tools(ToolCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
this.toolCallbacks.addAll(List.of(toolCallbacks));
|
||||
@@ -906,19 +904,6 @@ public class DefaultChatClient implements ChatClient {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated // Use tools()
|
||||
public ChatClientRequestSpec functions(String... functionBeanNames) {
|
||||
return tools(functionBeanNames);
|
||||
}
|
||||
|
||||
@Deprecated // Use tools()
|
||||
public ChatClientRequestSpec functions(FunctionCallback... functionCallbacks) {
|
||||
Assert.notNull(functionCallbacks, "functionCallbacks cannot be null");
|
||||
Assert.noNullElements(functionCallbacks, "functionCallbacks cannot contain null elements");
|
||||
this.toolCallbacks.addAll(Arrays.asList(functionCallbacks));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatClientRequestSpec toolContext(Map<String, Object> toolContext) {
|
||||
Assert.notNull(toolContext, "toolContext cannot be null");
|
||||
Assert.noNullElements(toolContext.keySet(), "toolContext keys cannot contain null elements");
|
||||
|
||||
@@ -32,11 +32,10 @@ import org.springframework.ai.chat.client.advisor.api.Advisor;
|
||||
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -157,7 +156,7 @@ public class DefaultChatClientBuilder implements Builder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder defaultTools(FunctionCallback... toolCallbacks) {
|
||||
public Builder defaultTools(ToolCallback... toolCallbacks) {
|
||||
this.defaultRequest.tools(toolCallbacks);
|
||||
return this;
|
||||
}
|
||||
@@ -182,28 +181,7 @@ public class DefaultChatClientBuilder implements Builder {
|
||||
|
||||
@Deprecated // Use defaultTools()
|
||||
public <I, O> Builder defaultFunction(String name, String description, java.util.function.Function<I, O> function) {
|
||||
this.defaultRequest
|
||||
.functions(FunctionCallback.builder().function(name, function).description(description).build());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated // Use defaultTools()
|
||||
public <I, O> Builder defaultFunction(String name, String description,
|
||||
java.util.function.BiFunction<I, ToolContext, O> biFunction) {
|
||||
this.defaultRequest
|
||||
.functions(FunctionCallback.builder().function(name, biFunction).description(description).build());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated // Use defaultTools()
|
||||
public Builder defaultFunctions(String... functionNames) {
|
||||
this.defaultRequest.functions(functionNames);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated // Use defaultTools()
|
||||
public Builder defaultFunctions(FunctionCallback... functionCallbacks) {
|
||||
this.defaultRequest.functions(functionCallbacks);
|
||||
this.defaultRequest.tools(FunctionToolCallback.builder(name, function).description(description).build());
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -216,9 +194,9 @@ public class DefaultChatClientBuilder implements Builder {
|
||||
this.defaultRequest.messages(messages);
|
||||
}
|
||||
|
||||
void addToolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
void addToolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
this.defaultRequest.tools(toolCallbacks.toArray(FunctionCallback[]::new));
|
||||
this.defaultRequest.tools(toolCallbacks.toArray(ToolCallback[]::new));
|
||||
}
|
||||
|
||||
void addToolContext(Map<String, Object> toolContext) {
|
||||
|
||||
@@ -23,8 +23,8 @@ import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClientAttributes;
|
||||
import org.springframework.ai.chat.client.ChatClientRequest;
|
||||
@@ -36,9 +36,8 @@ 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.content.Media;
|
||||
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.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -53,8 +52,8 @@ import org.springframework.util.StringUtils;
|
||||
* @param systemText the text provided by the system
|
||||
* @param chatOptions the options for the chat
|
||||
* @param media the list of media items
|
||||
* @param functionNames the list of function names
|
||||
* @param functionCallbacks the list of function callbacks
|
||||
* @param toolNames the list of function names
|
||||
* @param toolCallbacks the list of function callbacks
|
||||
* @param messages the list of messages
|
||||
* @param userParams the map of user parameters
|
||||
* @param systemParams the map of system parameters
|
||||
@@ -68,7 +67,6 @@ import org.springframework.util.StringUtils;
|
||||
* @deprecated Use {@link ChatClientRequest} instead.
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Deprecated
|
||||
public record AdvisedRequest(
|
||||
// @formatter:off
|
||||
ChatModel chatModel,
|
||||
@@ -78,8 +76,8 @@ public record AdvisedRequest(
|
||||
@Nullable
|
||||
ChatOptions chatOptions,
|
||||
List<Media> media,
|
||||
List<String> functionNames,
|
||||
List<FunctionCallback> functionCallbacks,
|
||||
List<String> toolNames,
|
||||
List<ToolCallback> toolCallbacks,
|
||||
List<Message> messages,
|
||||
Map<String, Object> userParams,
|
||||
Map<String, Object> systemParams,
|
||||
@@ -97,10 +95,10 @@ public record AdvisedRequest(
|
||||
"userText cannot be null or empty unless messages are provided and contain Tool Response message.");
|
||||
Assert.notNull(media, "media cannot be null");
|
||||
Assert.noNullElements(media, "media cannot contain null elements");
|
||||
Assert.notNull(functionNames, "functionNames cannot be null");
|
||||
Assert.noNullElements(functionNames, "functionNames cannot contain null elements");
|
||||
Assert.notNull(functionCallbacks, "functionCallbacks cannot be null");
|
||||
Assert.noNullElements(functionCallbacks, "functionCallbacks cannot contain null elements");
|
||||
Assert.notNull(toolNames, "toolNames cannot be null");
|
||||
Assert.noNullElements(toolNames, "toolNames cannot contain null elements");
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
Assert.notNull(messages, "messages cannot be null");
|
||||
Assert.noNullElements(messages, "messages cannot contain null elements");
|
||||
Assert.notNull(userParams, "userParams cannot be null");
|
||||
@@ -135,8 +133,8 @@ public record AdvisedRequest(
|
||||
builder.systemText = from.systemText;
|
||||
builder.chatOptions = from.chatOptions;
|
||||
builder.media = from.media;
|
||||
builder.functionNames = from.functionNames;
|
||||
builder.functionCallbacks = from.functionCallbacks;
|
||||
builder.toolNames = from.toolNames;
|
||||
builder.toolCallbacks = from.toolCallbacks;
|
||||
builder.messages = from.messages;
|
||||
builder.userParams = from.userParams;
|
||||
builder.systemParams = from.systemParams;
|
||||
@@ -179,8 +177,8 @@ public record AdvisedRequest(
|
||||
|
||||
builder.chatOptions = Objects.requireNonNullElse(from.prompt().getOptions(), ChatOptions.builder().build());
|
||||
if (from.prompt().getOptions() instanceof ToolCallingChatOptions options) {
|
||||
builder.functionNames = options.getToolNames().stream().toList();
|
||||
builder.functionCallbacks = options.getToolCallbacks();
|
||||
builder.toolNames = options.getToolNames().stream().toList();
|
||||
builder.toolCallbacks = options.getToolCallbacks();
|
||||
builder.toolContext = options.getToolContext();
|
||||
}
|
||||
|
||||
@@ -231,15 +229,15 @@ public record AdvisedRequest(
|
||||
messages.add(new UserMessage(processedUserText, this.media()));
|
||||
}
|
||||
|
||||
if (this.chatOptions() instanceof FunctionCallingOptions functionCallingOptions) {
|
||||
if (!this.functionNames().isEmpty()) {
|
||||
functionCallingOptions.setFunctions(new HashSet<>(this.functionNames()));
|
||||
if (this.chatOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) {
|
||||
if (!this.toolNames().isEmpty()) {
|
||||
toolCallingChatOptions.setToolNames(new HashSet<>(this.toolNames()));
|
||||
}
|
||||
if (!this.functionCallbacks().isEmpty()) {
|
||||
functionCallingOptions.setFunctionCallbacks(this.functionCallbacks());
|
||||
if (!this.toolCallbacks().isEmpty()) {
|
||||
toolCallingChatOptions.setToolCallbacks(this.toolCallbacks());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(this.toolContext())) {
|
||||
functionCallingOptions.setToolContext(this.toolContext());
|
||||
toolCallingChatOptions.setToolContext(this.toolContext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,9 +259,9 @@ public record AdvisedRequest(
|
||||
|
||||
private List<Media> media = List.of();
|
||||
|
||||
private List<String> functionNames = List.of();
|
||||
private List<String> toolNames = List.of();
|
||||
|
||||
private List<FunctionCallback> functionCallbacks = List.of();
|
||||
private List<ToolCallback> toolCallbacks = List.of();
|
||||
|
||||
private List<Message> messages = List.of();
|
||||
|
||||
@@ -333,22 +331,22 @@ public record AdvisedRequest(
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the function names.
|
||||
* @param functionNames the function names
|
||||
* Set the tool names.
|
||||
* @param toolNames the function names
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder functionNames(List<String> functionNames) {
|
||||
this.functionNames = functionNames;
|
||||
public Builder toolNames(List<String> toolNames) {
|
||||
this.toolNames = toolNames;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the function callbacks.
|
||||
* @param functionCallbacks the function callbacks
|
||||
* Set the tool callbacks.
|
||||
* @param toolCallbacks the tool callbacks
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
this.functionCallbacks = functionCallbacks;
|
||||
public Builder functionCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
this.toolCallbacks = toolCallbacks;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -430,7 +428,7 @@ public record AdvisedRequest(
|
||||
*/
|
||||
public AdvisedRequest build() {
|
||||
return new AdvisedRequest(this.chatModel, this.userText, this.systemText, this.chatOptions, this.media,
|
||||
this.functionNames, this.functionCallbacks, this.messages, this.userParams, this.systemParams,
|
||||
this.toolNames, this.toolCallbacks, this.messages, this.userParams, this.systemParams,
|
||||
this.advisors, this.advisorParams, this.adviseContext, this.toolContext);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.ai.chat.client.observation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import io.micrometer.common.KeyValue;
|
||||
import io.micrometer.common.KeyValues;
|
||||
|
||||
@@ -23,16 +26,12 @@ import org.springframework.ai.chat.client.ChatClientAttributes;
|
||||
import org.springframework.ai.chat.client.advisor.api.Advisor;
|
||||
import org.springframework.ai.chat.client.observation.ChatClientObservationDocumentation.LowCardinalityKeyNames;
|
||||
import org.springframework.ai.chat.observation.ChatModelObservationDocumentation;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.observation.conventions.SpringAiKind;
|
||||
import org.springframework.ai.observation.tracing.TracingHelper;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Default conventions to populate observations for chat client workflows.
|
||||
*
|
||||
@@ -154,7 +153,10 @@ public class DefaultChatClientObservationConvention implements ChatClientObserva
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
var toolCallbackNames = toolCallbacks.stream().map(FunctionCallback::getName).sorted().toList();
|
||||
var toolCallbackNames = toolCallbacks.stream()
|
||||
.map(toolCallback -> toolCallback.getToolDefinition().name())
|
||||
.sorted()
|
||||
.toList();
|
||||
return keyValues
|
||||
.and(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_TOOL_FUNCTION_CALLBACKS
|
||||
.asString(), TracingHelper.concatenateStrings(toolCallbackNames));
|
||||
|
||||
@@ -40,9 +40,9 @@ 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.content.Media;
|
||||
import org.springframework.ai.model.function.DefaultFunctionCallingOptions;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallingOptions;
|
||||
import org.springframework.ai.model.tool.DefaultToolCallingChatOptions;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
@@ -198,7 +198,7 @@ public class ChatClientTest {
|
||||
@Test
|
||||
void mutateDefaults() {
|
||||
|
||||
FunctionCallingOptions options = new DefaultFunctionCallingOptions();
|
||||
ToolCallingChatOptions options = new DefaultToolCallingChatOptions();
|
||||
given(this.chatModel.getDefaultOptions()).willReturn(options);
|
||||
|
||||
given(this.chatModel.call(this.promptCaptor.capture()))
|
||||
@@ -216,9 +216,8 @@ public class ChatClientTest {
|
||||
.defaultSystem(s -> s.text("Default system text {param1}, {param2}")
|
||||
.param("param1", "value1")
|
||||
.param("param2", "value2"))
|
||||
.defaultFunctions("fun1", "fun2")
|
||||
.defaultFunctions(FunctionCallback.builder()
|
||||
.function("fun3", mockFunction)
|
||||
.defaultTools("fun1", "fun2")
|
||||
.defaultTools(FunctionToolCallback.builder("fun3", mockFunction)
|
||||
.description("fun3description")
|
||||
.inputType(String.class)
|
||||
.build())
|
||||
@@ -246,10 +245,10 @@ public class ChatClientTest {
|
||||
assertThat(userMessage.getMedia()).hasSize(1);
|
||||
assertThat(userMessage.getMedia().iterator().next().getMimeType()).isEqualTo(MimeTypeUtils.IMAGE_JPEG);
|
||||
|
||||
var fco = (FunctionCallingOptions) prompt.getOptions();
|
||||
var fco = (ToolCallingChatOptions) prompt.getOptions();
|
||||
|
||||
assertThat(fco.getFunctions()).containsExactly("fun1", "fun2");
|
||||
assertThat(fco.getFunctionCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
assertThat(fco.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2");
|
||||
assertThat(fco.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
|
||||
// Streaming
|
||||
content = join(chatClient.prompt().stream().content());
|
||||
@@ -268,16 +267,16 @@ public class ChatClientTest {
|
||||
assertThat(userMessage.getMedia()).hasSize(1);
|
||||
assertThat(userMessage.getMedia().iterator().next().getMimeType()).isEqualTo(MimeTypeUtils.IMAGE_JPEG);
|
||||
|
||||
fco = (FunctionCallingOptions) prompt.getOptions();
|
||||
fco = (ToolCallingChatOptions) prompt.getOptions();
|
||||
|
||||
assertThat(fco.getFunctions()).containsExactly("fun1", "fun2");
|
||||
assertThat(fco.getFunctionCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
assertThat(fco.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2");
|
||||
assertThat(fco.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
|
||||
// mutate builder
|
||||
// @formatter:off
|
||||
chatClient = chatClient.mutate()
|
||||
.defaultSystem("Mutated default system text {param1}, {param2}")
|
||||
.defaultFunctions("fun4")
|
||||
.defaultTools("fun4")
|
||||
.defaultUser("Mutated default user text {uparam1}, {uparam2}")
|
||||
.build();
|
||||
// @formatter:on
|
||||
@@ -298,10 +297,10 @@ public class ChatClientTest {
|
||||
assertThat(userMessage.getMedia()).hasSize(1);
|
||||
assertThat(userMessage.getMedia().iterator().next().getMimeType()).isEqualTo(MimeTypeUtils.IMAGE_JPEG);
|
||||
|
||||
fco = (FunctionCallingOptions) prompt.getOptions();
|
||||
fco = (ToolCallingChatOptions) prompt.getOptions();
|
||||
|
||||
assertThat(fco.getFunctions()).containsExactly("fun1", "fun2", "fun4");
|
||||
assertThat(fco.getFunctionCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
assertThat(fco.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2", "fun4");
|
||||
assertThat(fco.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
|
||||
// Streaming
|
||||
content = join(chatClient.prompt().stream().content());
|
||||
@@ -320,17 +319,17 @@ public class ChatClientTest {
|
||||
assertThat(userMessage.getMedia()).hasSize(1);
|
||||
assertThat(userMessage.getMedia().iterator().next().getMimeType()).isEqualTo(MimeTypeUtils.IMAGE_JPEG);
|
||||
|
||||
fco = (FunctionCallingOptions) prompt.getOptions();
|
||||
fco = (ToolCallingChatOptions) prompt.getOptions();
|
||||
|
||||
assertThat(fco.getFunctions()).containsExactly("fun1", "fun2", "fun4");
|
||||
assertThat(fco.getFunctionCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
assertThat(fco.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2", "fun4");
|
||||
assertThat(fco.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void mutatePrompt() {
|
||||
|
||||
FunctionCallingOptions options = new DefaultFunctionCallingOptions();
|
||||
ToolCallingChatOptions options = new DefaultToolCallingChatOptions();
|
||||
given(this.chatModel.getDefaultOptions()).willReturn(options);
|
||||
|
||||
given(this.chatModel.call(this.promptCaptor.capture()))
|
||||
@@ -347,9 +346,8 @@ public class ChatClientTest {
|
||||
.defaultSystem(s -> s.text("Default system text {param1}, {param2}")
|
||||
.param("param1", "value1")
|
||||
.param("param2", "value2"))
|
||||
.defaultFunctions("fun1", "fun2")
|
||||
.defaultFunctions(FunctionCallback.builder()
|
||||
.function("fun3", mockFunction)
|
||||
.defaultTools("fun1", "fun2")
|
||||
.defaultTools(FunctionToolCallback.builder("fun3", mockFunction)
|
||||
.description("fun3description")
|
||||
.inputType(String.class)
|
||||
.build())
|
||||
@@ -365,7 +363,7 @@ public class ChatClientTest {
|
||||
.system("New default system text {param1}, {param2}")
|
||||
.user(u -> u.param("uparam1", "userValue1")
|
||||
.param("uparam2", "userValue2"))
|
||||
.functions("fun5")
|
||||
.tools("fun5")
|
||||
.mutate().build() // mutate and build new prompt
|
||||
.prompt().call().content();
|
||||
// @formatter:on
|
||||
@@ -384,10 +382,10 @@ public class ChatClientTest {
|
||||
assertThat(userMessage.getMedia()).hasSize(1);
|
||||
assertThat(userMessage.getMedia().iterator().next().getMimeType()).isEqualTo(MimeTypeUtils.IMAGE_JPEG);
|
||||
|
||||
var fco = (FunctionCallingOptions) prompt.getOptions();
|
||||
var tco = (ToolCallingChatOptions) prompt.getOptions();
|
||||
|
||||
assertThat(fco.getFunctions()).containsExactly("fun1", "fun2", "fun5");
|
||||
assertThat(fco.getFunctionCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
assertThat(tco.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2", "fun5");
|
||||
assertThat(tco.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
|
||||
// Streaming
|
||||
// @formatter:off
|
||||
@@ -396,7 +394,7 @@ public class ChatClientTest {
|
||||
.system("New default system text {param1}, {param2}")
|
||||
.user(u -> u.param("uparam1", "userValue1")
|
||||
.param("uparam2", "userValue2"))
|
||||
.functions("fun5")
|
||||
.tools("fun5")
|
||||
.mutate().build() // mutate and build new prompt
|
||||
.prompt().stream().content());
|
||||
// @formatter:on
|
||||
@@ -415,10 +413,10 @@ public class ChatClientTest {
|
||||
assertThat(userMessage.getMedia()).hasSize(1);
|
||||
assertThat(userMessage.getMedia().iterator().next().getMimeType()).isEqualTo(MimeTypeUtils.IMAGE_JPEG);
|
||||
|
||||
fco = (FunctionCallingOptions) prompt.getOptions();
|
||||
var tcoptions = (ToolCallingChatOptions) prompt.getOptions();
|
||||
|
||||
assertThat(fco.getFunctions()).containsExactly("fun1", "fun2", "fun5");
|
||||
assertThat(fco.getFunctionCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
assertThat(tcoptions.getToolNames()).containsExactlyInAnyOrder("fun1", "fun2", "fun5");
|
||||
assertThat(tcoptions.getToolCallbacks().iterator().next().getName()).isEqualTo("fun3");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -517,7 +515,7 @@ public class ChatClientTest {
|
||||
given(this.chatModel.call(this.promptCaptor.capture()))
|
||||
.willReturn(new ChatResponse(List.of(new Generation(new AssistantMessage("response")))));
|
||||
|
||||
var options = FunctionCallingOptions.builder().build();
|
||||
var options = ToolCallingChatOptions.builder().build();
|
||||
given(this.chatModel.getDefaultOptions()).willReturn(options);
|
||||
|
||||
var url = new URL("https://docs.spring.io/spring-ai/reference/_images/multimodal.test.png");
|
||||
@@ -525,7 +523,7 @@ public class ChatClientTest {
|
||||
// @formatter:off
|
||||
ChatClient client = ChatClient.builder(this.chatModel)
|
||||
.defaultSystem("System text")
|
||||
.defaultFunctions("function1")
|
||||
.defaultTools("function1")
|
||||
.build();
|
||||
|
||||
String response = client.prompt()
|
||||
@@ -549,10 +547,10 @@ public class ChatClientTest {
|
||||
assertThat(userMessage.getMedia().iterator().next().getData())
|
||||
.isEqualTo("https://docs.spring.io/spring-ai/reference/_images/multimodal.test.png");
|
||||
|
||||
FunctionCallingOptions runtieOptions = (FunctionCallingOptions) this.promptCaptor.getValue().getOptions();
|
||||
ToolCallingChatOptions runtieOptions = (ToolCallingChatOptions) this.promptCaptor.getValue().getOptions();
|
||||
|
||||
assertThat(runtieOptions.getFunctions()).containsExactly("function1");
|
||||
assertThat(options.getFunctions()).isEmpty();
|
||||
assertThat(runtieOptions.getToolNames()).containsExactly("function1");
|
||||
assertThat(options.getToolNames()).isEmpty();
|
||||
}
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -27,15 +27,15 @@ import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.ai.chat.client.advisor.api.BaseAdvisorChain;
|
||||
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.api.Advisor;
|
||||
import org.springframework.ai.chat.client.advisor.api.BaseAdvisorChain;
|
||||
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
@@ -48,7 +48,8 @@ import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.content.Media;
|
||||
import org.springframework.ai.converter.ListOutputConverter;
|
||||
import org.springframework.ai.converter.StructuredOutputConverter;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
@@ -1467,7 +1468,7 @@ class DefaultChatClientTests {
|
||||
String toolName = "myTool";
|
||||
spec = spec.tools(toolName);
|
||||
DefaultChatClient.DefaultChatClientRequestSpec defaultSpec = (DefaultChatClient.DefaultChatClientRequestSpec) spec;
|
||||
assertThat(defaultSpec.getFunctionNames()).contains(toolName);
|
||||
assertThat(defaultSpec.getToolNames()).contains(toolName);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1486,7 +1487,7 @@ class DefaultChatClientTests {
|
||||
ToolCallback toolCallback = mock(ToolCallback.class);
|
||||
spec = spec.tools(toolCallback);
|
||||
DefaultChatClient.DefaultChatClientRequestSpec defaultSpec = (DefaultChatClient.DefaultChatClientRequestSpec) spec;
|
||||
assertThat(defaultSpec.getFunctionCallbacks()).contains(toolCallback);
|
||||
assertThat(defaultSpec.getToolCallbacks()).contains(toolCallback);
|
||||
}
|
||||
|
||||
// FunctionCallback.builder().description("description").function(null,input->"hello").inputType(String.class).build()
|
||||
@@ -1495,108 +1496,106 @@ class DefaultChatClientTests {
|
||||
void whenFunctionNameIsNullThenThrow() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
assertThatThrownBy(() -> spec.functions(FunctionCallback.builder()
|
||||
.function(null, input -> "hello")
|
||||
assertThatThrownBy(() -> spec.tools(FunctionToolCallback.builder(null, input -> "hello")
|
||||
.description("description")
|
||||
.inputType(String.class)
|
||||
.build())).isInstanceOf(IllegalArgumentException.class).hasMessage("Name must not be empty");
|
||||
.build())).isInstanceOf(IllegalArgumentException.class).hasMessage("name cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionNameIsEmptyThenThrow() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
assertThatThrownBy(() -> spec.functions(FunctionCallback.builder()
|
||||
.function("", input -> "hello")
|
||||
assertThatThrownBy(() -> spec.tools(FunctionToolCallback.builder("", input -> "hello")
|
||||
.description("description")
|
||||
.inputType(String.class)
|
||||
.build())).isInstanceOf(IllegalArgumentException.class).hasMessage("Name must not be empty");
|
||||
.build())).isInstanceOf(IllegalArgumentException.class).hasMessage("name cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("This fails now as the FunctionToolCallback description is allowed to be empty")
|
||||
void whenFunctionDescriptionIsNullThenThrow() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
assertThatThrownBy(() -> spec.functions(FunctionCallback.builder()
|
||||
.function("name", input -> "hello")
|
||||
assertThatThrownBy(() -> spec.tools(FunctionToolCallback.builder("name", input -> "hello")
|
||||
.description(null)
|
||||
.inputType(String.class)
|
||||
.build())).isInstanceOf(IllegalArgumentException.class).hasMessage("Description must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("This fails now as the FunctionToolCallback description is allowed to be empty")
|
||||
void whenFunctionDescriptionIsEmptyThenThrow() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
assertThatThrownBy(() -> spec.functions(FunctionCallback.builder()
|
||||
.function("name", input -> "hello")
|
||||
.description("")
|
||||
.inputType(String.class)
|
||||
.build())).isInstanceOf(IllegalArgumentException.class).hasMessage("Description must not be empty");
|
||||
assertThatThrownBy(() -> spec.tools(
|
||||
FunctionToolCallback.builder("name", input -> "hello").description("").inputType(String.class).build()))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Description must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionThenReturn() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
spec = spec.functions(FunctionCallback.builder()
|
||||
.function("name", input -> "hello")
|
||||
spec = spec.tools(FunctionToolCallback.builder("name", input -> "hello")
|
||||
.inputType(String.class)
|
||||
.description("description")
|
||||
.build());
|
||||
DefaultChatClient.DefaultChatClientRequestSpec defaultSpec = (DefaultChatClient.DefaultChatClientRequestSpec) spec;
|
||||
assertThat(defaultSpec.getFunctionCallbacks()).anyMatch(callback -> callback.getName().equals("name"));
|
||||
assertThat(defaultSpec.getToolCallbacks()).anyMatch(callback -> callback.getName().equals("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionAndInputTypeThenReturn() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
spec = spec.functions(FunctionCallback.builder()
|
||||
.function("name", input -> "hello")
|
||||
spec = spec.tools(FunctionToolCallback.builder("name", input -> "hello")
|
||||
.inputType(String.class)
|
||||
.description("description")
|
||||
.build());
|
||||
DefaultChatClient.DefaultChatClientRequestSpec defaultSpec = (DefaultChatClient.DefaultChatClientRequestSpec) spec;
|
||||
assertThat(defaultSpec.getFunctionCallbacks()).anyMatch(callback -> callback.getName().equals("name"));
|
||||
assertThat(defaultSpec.getToolCallbacks()).anyMatch(callback -> callback.getName().equals("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenBiFunctionNameIsNullThenThrow() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
assertThatThrownBy(() -> spec.functions(
|
||||
FunctionCallback.builder().function(null, (input, ctx) -> "hello").description("description").build()))
|
||||
assertThatThrownBy(() -> spec
|
||||
.tools(FunctionToolCallback.builder(null, (input, ctx) -> "hello").description("description").build()))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Name must not be empty");
|
||||
.hasMessage("name cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenBiFunctionNameIsEmptyThenThrow() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
assertThatThrownBy(() -> spec.functions(
|
||||
FunctionCallback.builder().function("", (input, ctx) -> "hello").description("description").build()))
|
||||
assertThatThrownBy(() -> spec
|
||||
.tools(FunctionToolCallback.builder("", (input, ctx) -> "hello").description("description").build()))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Name must not be empty");
|
||||
.hasMessage("name cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("This fails now as the FunctionToolCallback description is allowed to be empty")
|
||||
void whenBiFunctionDescriptionIsNullThenThrow() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
assertThatThrownBy(() -> spec
|
||||
.functions(FunctionCallback.builder().function("name", (input, ctx) -> "hello").description(null).build()))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Description must not be empty");
|
||||
assertThatThrownBy(() -> spec.tools(FunctionToolCallback.builder("name", (input, ctx) -> "hello")
|
||||
.inputType(String.class)
|
||||
.description(null)
|
||||
.build())).isInstanceOf(IllegalArgumentException.class).hasMessage("Description must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("This fails now as the FunctionToolCallback description is allowed to be empty")
|
||||
void whenBiFunctionDescriptionIsEmptyThenThrow() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
assertThatThrownBy(() -> spec
|
||||
.functions(FunctionCallback.builder().function("name", (input, ctx) -> "hello").description("").build()))
|
||||
assertThatThrownBy(
|
||||
() -> spec.tools(FunctionToolCallback.builder("name", (input, ctx) -> "hello").description("").build()))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Description must not be empty");
|
||||
}
|
||||
@@ -1605,20 +1604,19 @@ class DefaultChatClientTests {
|
||||
void whenBiFunctionThenReturn() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
spec = spec.functions(FunctionCallback.builder()
|
||||
.function("name", (input, ctx) -> "hello")
|
||||
spec = spec.tools(FunctionToolCallback.builder("name", (input, ctx) -> "hello")
|
||||
.description("description")
|
||||
.inputType(String.class)
|
||||
.build());
|
||||
DefaultChatClient.DefaultChatClientRequestSpec defaultSpec = (DefaultChatClient.DefaultChatClientRequestSpec) spec;
|
||||
assertThat(defaultSpec.getFunctionCallbacks()).anyMatch(callback -> callback.getName().equals("name"));
|
||||
assertThat(defaultSpec.getToolCallbacks()).anyMatch(callback -> callback.getName().equals("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionBeanNamesElementIsNullThenThrow() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
assertThatThrownBy(() -> spec.functions("myFunction", null)).isInstanceOf(IllegalArgumentException.class)
|
||||
assertThatThrownBy(() -> spec.tools("myFunction", null)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("toolNames cannot contain null elements");
|
||||
}
|
||||
|
||||
@@ -1627,28 +1625,28 @@ class DefaultChatClientTests {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
String functionBeanName = "myFunction";
|
||||
spec = spec.functions(functionBeanName);
|
||||
spec = spec.tools(functionBeanName);
|
||||
DefaultChatClient.DefaultChatClientRequestSpec defaultSpec = (DefaultChatClient.DefaultChatClientRequestSpec) spec;
|
||||
assertThat(defaultSpec.getFunctionNames()).contains(functionBeanName);
|
||||
assertThat(defaultSpec.getToolNames()).contains(functionBeanName);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionCallbacksElementIsNullThenThrow() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
assertThatThrownBy(() -> spec.functions(mock(FunctionCallback.class), null))
|
||||
assertThatThrownBy(() -> spec.tools(mock(FunctionToolCallback.class), null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("functionCallbacks cannot contain null elements");
|
||||
.hasMessage("toolCallbacks cannot contain null elements");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionCallbacksThenReturn() {
|
||||
ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();
|
||||
ChatClient.ChatClientRequestSpec spec = chatClient.prompt();
|
||||
FunctionCallback functionCallback = mock(FunctionCallback.class);
|
||||
spec = spec.functions(functionCallback);
|
||||
FunctionToolCallback functionToolCallback = mock(FunctionToolCallback.class);
|
||||
spec = spec.tools(functionToolCallback);
|
||||
DefaultChatClient.DefaultChatClientRequestSpec defaultSpec = (DefaultChatClient.DefaultChatClientRequestSpec) spec;
|
||||
assertThat(defaultSpec.getFunctionCallbacks()).contains(functionCallback);
|
||||
assertThat(defaultSpec.getToolCallbacks()).contains(functionToolCallback);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -89,7 +89,7 @@ class AdvisedRequestTests {
|
||||
assertThatThrownBy(() -> new AdvisedRequest(mock(ChatModel.class), "user", null, null, List.of(), null,
|
||||
List.of(), List.of(), Map.of(), Map.of(), List.of(), Map.of(), Map.of(), Map.of()))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("functionNames cannot be null");
|
||||
.hasMessage("toolNames cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -97,7 +97,7 @@ class AdvisedRequestTests {
|
||||
assertThatThrownBy(() -> new AdvisedRequest(mock(ChatModel.class), "user", null, null, List.of(), List.of(),
|
||||
null, List.of(), Map.of(), Map.of(), List.of(), Map.of(), Map.of(), Map.of()))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("functionCallbacks cannot be null");
|
||||
.hasMessage("toolCallbacks cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -180,7 +180,7 @@ class AdvisedRequestTests {
|
||||
.userText(userMessage.getText())
|
||||
.userParams(userParams)
|
||||
.media(userMessage.getMedia())
|
||||
.functionNames(toolNames)
|
||||
.toolNames(toolNames)
|
||||
.functionCallbacks(List.of(toolCallback))
|
||||
.toolContext(toolContext)
|
||||
.advisors(advisors)
|
||||
|
||||
@@ -36,10 +36,11 @@ import org.springframework.ai.chat.client.observation.ChatClientObservationDocum
|
||||
import org.springframework.ai.chat.client.observation.ChatClientObservationDocumentation.LowCardinalityKeyNames;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.observation.conventions.AiProvider;
|
||||
import org.springframework.ai.observation.conventions.SpringAiKind;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -80,24 +81,12 @@ class DefaultChatClientObservationConventionTests {
|
||||
};
|
||||
}
|
||||
|
||||
static FunctionCallback dummyFunction(String name) {
|
||||
return new FunctionCallback() {
|
||||
static ToolCallback dummyFunction(String name) {
|
||||
return new ToolCallback() {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getDescription'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInputTypeSchema() {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getInputTypeSchema'");
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return ToolDefinition.builder().name(name).inputSchema("{}").build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
** xref:api/chat/prompt-engineering-patterns.adoc[]
|
||||
* xref:api/testing.adoc[AI Model Evaluation]
|
||||
* xref:api/functions.adoc[Function Calling (Deprecated)]
|
||||
** xref:api/function-callback.adoc[FunctionCallback API (Deprecated)]
|
||||
|
||||
|
||||
* Service Connections
|
||||
** xref:api/docker-compose.adoc[Docker Compose]
|
||||
|
||||
@@ -127,11 +127,10 @@ Another way to register a function is to create a `FunctionCallback` instance li
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public FunctionCallback weatherFunctionInfo() {
|
||||
public FunctionToolCallback weatherFunctionInfo() {
|
||||
|
||||
return FunctionCallback.builder()
|
||||
return FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.description("Get the weather in location") // (2) function description
|
||||
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.inputType(MockWeatherService.Request.class) // (3) function signature
|
||||
.build();
|
||||
}
|
||||
@@ -144,7 +143,7 @@ It also provides a description (2) and input type (3) used to generate the JSON
|
||||
|
||||
NOTE: By default, the response converter does a JSON serialization of the Response object.
|
||||
|
||||
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
|
||||
NOTE: The `FunctionToolCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
|
||||
|
||||
=== Specifying functions in Chat Options
|
||||
|
||||
@@ -157,7 +156,7 @@ AnthropicChatModel chatModel = ...
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in Paris?");
|
||||
|
||||
ChatResponse response = this.chatModel.call(new Prompt(List.of(this.userMessage),
|
||||
AnthropicChatOptions.builder().function("CurrentWeather").build())); // (1) Enable the function
|
||||
AnthropicChatOptions.builder().toolNames("CurrentWeather").build())); // (1) Enable the function
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
----
|
||||
@@ -177,8 +176,7 @@ AnthropicChatModel chatModel = ...
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in Paris?");
|
||||
|
||||
var promptOptions = AnthropicChatOptions.builder()
|
||||
.functionCallbacks(List.of(FunctionCallback.builder()
|
||||
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.description("Get the weather in location") // (2) function description
|
||||
.inputType(MockWeatherService.Request.class) // (3) function signature
|
||||
.build())) // function code
|
||||
|
||||
@@ -123,10 +123,9 @@ Another way to register a function is to create a `FunctionCallback` instance li
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public FunctionCallback weatherFunctionInfo() {
|
||||
public FunctionToolCallback weatherFunctionInfo() {
|
||||
|
||||
return FunctionCallback.builder()
|
||||
.function("CurrentWeather", new MockWeatherService()) // (1) function name
|
||||
return FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name
|
||||
.description("Get the current weather in a given location") // (2) function description
|
||||
.inputType(MockWeatherService.Request.class) // (3) function input type
|
||||
.build();
|
||||
@@ -139,7 +138,7 @@ It wraps the 3rd party `MockWeatherService` function and registers it as a `Curr
|
||||
|
||||
NOTE: The default response converter does a JSON serialization of the Response object.
|
||||
|
||||
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class and internally generates an JSON schema for the function call.
|
||||
NOTE: The `FunctionToolCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class and internally generates an JSON schema for the function call.
|
||||
|
||||
=== Specifying functions in Chat Options
|
||||
|
||||
@@ -152,7 +151,7 @@ AzureOpenAiChatModel chatModel = ...
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
ChatResponse response = this.chatModel.call(new Prompt(List.of(this.userMessage),
|
||||
AzureOpenAiChatOptions.builder().function("CurrentWeather").build())); // (1) Enable the function
|
||||
AzureOpenAiChatOptions.builder().tools("CurrentWeather").build())); // (1) Enable the function
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
----
|
||||
@@ -182,8 +181,7 @@ AzureOpenAiChatModel chatModel = ...
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris? Use Multi-turn function calling.");
|
||||
|
||||
var promptOptions = AzureOpenAiChatOptions.builder()
|
||||
.functionCallbacks(List.of(FunctionCallback.builder()
|
||||
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.description("Get the current weather in a given location") // (2) function description
|
||||
.inputType(MockWeatherService.Request.class) // (3) function input type
|
||||
.build()))
|
||||
|
||||
@@ -117,7 +117,7 @@ Mistral AI is almost identical to OpenAI in this regard.
|
||||
|
||||
==== FunctionCallback Wrapper
|
||||
|
||||
Another way to register a function is to create a `FunctionCallback` like this:
|
||||
Another way to register a function is to create a `FunctionToolCallback` like this:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -125,10 +125,9 @@ Another way to register a function is to create a `FunctionCallback` like this:
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public FunctionCallback weatherFunctionInfo() {
|
||||
public FunctionToolCallback weatherFunctionInfo() {
|
||||
|
||||
return FunctionCallback.builder()
|
||||
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
return FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.description("Get the weather in location") // (2) function description
|
||||
.inputType(MockWeatherService.Request.class) // (3) function signature
|
||||
.build();
|
||||
@@ -155,7 +154,7 @@ MistralAiChatModel chatModel = ...
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in Paris?");
|
||||
|
||||
ChatResponse response = this.chatModel.call(new Prompt(this.userMessage,
|
||||
MistralAiChatOptions.builder().function("CurrentWeather").build())); // Enable the function
|
||||
MistralAiChatOptions.builder().tools("CurrentWeather").build())); // Enable the function
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
----
|
||||
@@ -175,8 +174,7 @@ MistralAiChatModel chatModel = ...
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in Paris?");
|
||||
|
||||
var promptOptions = MistralAiChatOptions.builder()
|
||||
.functionCallbacks(List.of(FunctionCallback.builder()
|
||||
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.description("Get the weather in location") // (2) function description
|
||||
.inputType(MockWeatherService.Request.class) // (3) function signature
|
||||
.build())) // function code
|
||||
|
||||
@@ -127,10 +127,9 @@ Another way to register a function is to create a `FunctionCallback` like this:
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public FunctionCallback weatherFunctionInfo() {
|
||||
public FunctionToolCallback weatherFunctionInfo() {
|
||||
|
||||
return FunctionCallback.builder()
|
||||
.function("CurrentWeather", new MockWeatherService()) // (1) function name
|
||||
return FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name
|
||||
.description("Get the weather in location") // (2) function description
|
||||
.inputType(MockWeatherService.Request.class) // (3) function signature
|
||||
.build();
|
||||
@@ -144,7 +143,7 @@ It also provides a description (2) and the function signature (3) to let the mod
|
||||
|
||||
NOTE: By default, the response converter performs a JSON serialization of the Response object.
|
||||
|
||||
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
|
||||
NOTE: The `FunctionToolCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
|
||||
|
||||
=== Specifying functions in Chat Options
|
||||
|
||||
@@ -157,7 +156,7 @@ OllamaChatModel chatModel = ...
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
ChatResponse response = this.chatModel.call(new Prompt(this.userMessage,
|
||||
OllamaOptions.builder().function("CurrentWeather").build())); // Enable the function
|
||||
OllamaOptions.builder().tools("CurrentWeather").build())); // Enable the function
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
----
|
||||
@@ -186,8 +185,7 @@ OllamaChatModel chatModel = ...
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
var promptOptions = OllamaOptions.builder()
|
||||
.functionCallbacks(List.of(FunctionCallback.builder()
|
||||
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.description("Get the weather in location") // (2) function description
|
||||
.inputType(MockWeatherService.Request.class) // (3) function signature
|
||||
.build())) // function code
|
||||
|
||||
@@ -122,10 +122,9 @@ Another way to register a function is to create a `FunctionCallback` like this:
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public FunctionCallback weatherFunctionInfo() {
|
||||
public FunctionToolCallback weatherFunctionInfo() {
|
||||
|
||||
return FunctionCallback.builder()
|
||||
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
return FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.description("Get the weather in location") // (2) function description
|
||||
.inputType(MockWeatherService.Request.class) // (3) function input type
|
||||
.build();
|
||||
@@ -139,7 +138,7 @@ It also provides a description (2) and an input type (3) used to generate the JS
|
||||
|
||||
NOTE: By default, the response converter performs a JSON serialization of the Response object.
|
||||
|
||||
NOTE: The `FunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
|
||||
NOTE: The `FunctionToolCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
|
||||
|
||||
=== Specifying functions in Chat Options
|
||||
|
||||
@@ -152,7 +151,7 @@ OpenAiChatModel chatModel = ...
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
ChatResponse response = this.chatModel.call(new Prompt(this.userMessage,
|
||||
OpenAiChatOptions.builder().function("CurrentWeather").build())); // Enable the function
|
||||
OpenAiChatOptions.builder().tools("CurrentWeather").build())); // Enable the function
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
----
|
||||
@@ -181,8 +180,7 @@ OpenAiChatModel chatModel = ...
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.functionCallbacks(List.of(FunctionCallback.builder()
|
||||
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.description("Get the weather in location") // (2) function description
|
||||
.inputType(MockWeatherService.Request.class) // (3) function input type
|
||||
.build())) // function code
|
||||
@@ -232,8 +230,7 @@ BiFunction<MockWeatherService.Request, ToolContext, MockWeatherService.Response>
|
||||
|
||||
OpenAiChatOptions options = OpenAiChatOptions.builder()
|
||||
.model(OpenAiApi.ChatModel.GPT_4_O.getValue())
|
||||
.functionCallbacks(List.of(FunctionCallback.builder()
|
||||
.function("getCurrentWeather", this.weatherFunction)
|
||||
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", this.weatherFunction)
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build()))
|
||||
|
||||
@@ -34,7 +34,7 @@ As a developer, you need to implement a function that takes the function call ar
|
||||
Spring AI makes this as easy as defining a `@Bean` definition that returns a `java.util.Function` and supplying the bean name as an option when invoking the `ChatClient` or registering the function dynamically in your prompt request.
|
||||
|
||||
Under the hood, Spring wraps your POJO (the function) with the appropriate adapter code that enables interaction with the AI Model, saving you from writing tedious boilerplate code.
|
||||
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
|
||||
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/tool/function/FunctionToolCallback.java[FunctionToolCallback.java] interface and the companion Builder utility class to simplify the implementation and registration of Java callback functions.
|
||||
|
||||
== How it works
|
||||
|
||||
@@ -110,7 +110,7 @@ We start by describing the most POJO-friendly options.
|
||||
|
||||
In this approach, you define a `@Bean` in your application context as you would any other Spring managed object.
|
||||
|
||||
Internally, Spring AI `ChatModel` will create an instance of a `FunctionCallback` that adds the logic for it being invoked via the AI model.
|
||||
Internally, Spring AI `ChatModel` will create an instance of a `FunctionToolCallback` that adds the logic for it being invoked via the AI model.
|
||||
The name of the `@Bean` is used function name.
|
||||
|
||||
--
|
||||
@@ -191,9 +191,9 @@ data class Request(val location: String, val unit: Unit)
|
||||
|
||||
It is a best practice to annotate the request object with information such that the generated JSON schema of that function is as descriptive as possible to help the AI model pick the correct function to invoke.
|
||||
|
||||
==== FunctionCallback
|
||||
==== FunctionToolCallback
|
||||
|
||||
Another way to register a function is to create a `FunctionCallback` like this:
|
||||
Another way to register a function is to create a `FunctionToolCallback` like this:
|
||||
|
||||
--
|
||||
[tabs]
|
||||
@@ -206,10 +206,9 @@ Java::
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public FunctionCallback weatherFunctionInfo() {
|
||||
public FunctionToolCallback weatherFunctionInfo() {
|
||||
|
||||
return FunctionCallback.builder()
|
||||
.function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
return FunctionToolCallback.builder("CurrentWeather", new MockWeatherService()) // (1) function name and instance
|
||||
.description("Get the weather in location") // (2) function description
|
||||
.inputType(MockWeatherService.Request.class) // (3) input type to build the JSON schema
|
||||
.build();
|
||||
@@ -226,10 +225,9 @@ import org.springframework.ai.model.function.withInputType
|
||||
class Config {
|
||||
|
||||
@Bean
|
||||
fun weatherFunctionInfo(): FunctionCallback {
|
||||
fun weatherFunctionInfo(): FunctionToolCallback {
|
||||
|
||||
return FunctionCallback.builder()
|
||||
.function("CurrentWeather", MockWeatherService()) // (1) function name and instance
|
||||
return FunctionToolCallback.builder("CurrentWeather", MockWeatherService()) // (1) function name and instance
|
||||
.description("Get the weather in location") // (2) function description
|
||||
// (3) Required due to Kotlin SAM conversion being an opaque lambda
|
||||
.inputType<MockWeatherService.Request>()
|
||||
@@ -246,7 +244,7 @@ It also provides a description (2) and an optional response converter to convert
|
||||
|
||||
NOTE: By default, the response converter performs a JSON serialization of the Response object.
|
||||
|
||||
NOTE: The `FunctionCallback.Builder` internally resolves the function call signature based on the `MockWeatherService.Request` class.
|
||||
NOTE: The `FunctionToolCallback.Builder` internally resolves the function call signature based on the `MockWeatherService.Request` class.
|
||||
|
||||
=== Enable functions by bean name
|
||||
|
||||
@@ -257,7 +255,7 @@ To let the model know and call your `CurrentWeather` function you need to enable
|
||||
ChatClient chatClient = ...
|
||||
|
||||
ChatResponse response = this.chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
|
||||
.functions("CurrentWeather") // Enable the function
|
||||
.tools("CurrentWeather") // Enable the function
|
||||
.call().
|
||||
chatResponse();
|
||||
|
||||
@@ -289,8 +287,7 @@ The client-side registration enables you to register functions by default.
|
||||
ChatClient chatClient = ...
|
||||
|
||||
ChatResponse response = this.chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
|
||||
.functions(FunctionCallback.builder()
|
||||
.function("currentWeather", (Request request) -> new Response(30.0, Unit.C)) // (1) function name and instance
|
||||
.tools(FunctionToolCallback.builder("currentWeather", (Request request) -> new Response(30.0, Unit.C)) // (1) function name and instance
|
||||
.description("Get the weather in location") // (2) function description
|
||||
.inputType(MockWeatherService.Request.class) // (3) input type to build the JSON schema
|
||||
.build())
|
||||
@@ -317,7 +314,7 @@ The `MethodInvokingFunctionCallback` implements the `FunctionCallback` interface
|
||||
- Any parameter/return types (primitives, objects, collections)
|
||||
- Special handling for `ToolContext` parameters
|
||||
|
||||
You need the `FunctionCallback.Builder` to create `MethodInvokingFunctionCallback` like this:
|
||||
You need the `MethodToolCallback.Builder` to create `MethodInvokingFunctionCallback` like this:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -344,11 +341,14 @@ public class WeatherService {
|
||||
}
|
||||
|
||||
// Usage
|
||||
FunctionCallback callback = FunctionCallback.builder()
|
||||
.method("getWeather", String.class, TemperatureUnit.class)
|
||||
.description("Get weather information for a city")
|
||||
.targetClass(WeatherService.class)
|
||||
.build();
|
||||
var toolMethod = ReflectionUtils.findMethod(WeatherService.class, "getWeather", String.class, TemperatureUnit.class);
|
||||
MethodToolCallback callback = MethodToolCallback.builder()
|
||||
.toolDefinition(ToolDefinition.builder(toolMethod)
|
||||
.description("Get the weather in location")
|
||||
.build())
|
||||
.toolMethod(toolMethod)
|
||||
.toolObject(targetObject)
|
||||
.build();
|
||||
----
|
||||
Instance Method with ToolContext::
|
||||
+
|
||||
@@ -363,19 +363,21 @@ public class DeviceController {
|
||||
|
||||
// Usage
|
||||
DeviceController controller = new DeviceController();
|
||||
|
||||
var toolMethod = ReflectionUtils.findMethod(
|
||||
DeviceController.class, "setDeviceState", String.class, Boolean.class, ToolContext.class);
|
||||
String response = ChatClient.create(chatModel).prompt()
|
||||
.user("Turn on the living room lights")
|
||||
.functions(FunctionCallback.builder()
|
||||
.method("setDeviceState", String.class,boolean.class,ToolContext.class)
|
||||
.description("Control device state")
|
||||
.targetObject(controller)
|
||||
.build())
|
||||
.toolContext(Map.of("location", "home"))
|
||||
.call()
|
||||
.content();
|
||||
.user("Turn on the living room lights")
|
||||
.tools(MethodToolCallback.builder()
|
||||
.toolDefinition(ToolDefinition.builder(toolMethod)
|
||||
.description("Control device state")
|
||||
.build())
|
||||
.toolMethod(toolMethod)
|
||||
.toolObject(controller)
|
||||
.build())
|
||||
.toolContext(Map.of("location", "home"))
|
||||
.call()
|
||||
.content();
|
||||
----
|
||||
|
||||
======
|
||||
|
||||
The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMethodInvokingFunctionCallbackIT.java[OpenAiChatClientMethodInvokingFunctionCallbackIT]
|
||||
@@ -425,8 +427,7 @@ BiFunction<MockWeatherService.Request, ToolContext, MockWeatherService.Response>
|
||||
|
||||
|
||||
ChatResponse response = chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
|
||||
.functions(FunctionCallback.builder()
|
||||
.function("getCurrentWeather", this.weatherFunction)
|
||||
.tools(FunctionToolCallback.builder("getCurrentWeather", this.weatherFunction)
|
||||
.description("Get the weather in location")
|
||||
.inputType(MockWeatherService.Request.class)
|
||||
.build())
|
||||
@@ -452,15 +453,18 @@ public class DeviceController {
|
||||
|
||||
// Usage
|
||||
DeviceController controller = new DeviceController();
|
||||
|
||||
var toolMethod = ReflectionUtils.findMethod(
|
||||
DeviceController.class, "setDeviceState", String.class, Boolean.class, ToolContext.class);
|
||||
String response = ChatClient.create(chatModel).prompt()
|
||||
.user("Turn on the living room lights")
|
||||
.functions(FunctionCallback.builder()
|
||||
.method("setDeviceState", String.class,boolean.class,ToolContext.class)
|
||||
.description("Control device state")
|
||||
.targetObject(controller)
|
||||
.build())
|
||||
.toolContext(Map.of("location", "home"))
|
||||
.call()
|
||||
.content();
|
||||
.user("Turn on the living room lights")
|
||||
.tools(MethodToolCallback.builder()
|
||||
.toolDefinition(ToolDefinition.builder(toolMethod)
|
||||
.description("Control device state")
|
||||
.build())
|
||||
.toolMethod(toolMethod)
|
||||
.toolObject(controller)
|
||||
.build())
|
||||
.toolContext(Map.of("location", "home"))
|
||||
.call()
|
||||
.content();
|
||||
----
|
||||
|
||||
@@ -1,189 +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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.ToolResponseMessage;
|
||||
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;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Helper class that reuses the {@link AbstractToolCallSupport} to implement the function
|
||||
* call handling logic on the client side. Used when the withProxyToolCalls(true) option
|
||||
* is enabled.
|
||||
*
|
||||
* @deprecated Use {@link ToolCallingManager} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public class FunctionCallingHelper extends AbstractToolCallSupport {
|
||||
|
||||
public FunctionCallingHelper() {
|
||||
this(null, FunctionCallingOptions.builder().build(), List.of());
|
||||
}
|
||||
|
||||
public FunctionCallingHelper(FunctionCallbackResolver functionCallbackResolver,
|
||||
FunctionCallingOptions functionCallingOptions, List<FunctionCallback> toolFunctionCallbacks) {
|
||||
super(functionCallbackResolver, functionCallingOptions, toolFunctionCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isToolCall(ChatResponse chatResponse, Set<String> toolCallFinishReasons) {
|
||||
return super.isToolCall(chatResponse, toolCallFinishReasons);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> buildToolCallConversation(List<Message> previousMessages, AssistantMessage assistantMessage,
|
||||
ToolResponseMessage toolResponseMessage) {
|
||||
return super.buildToolCallConversation(previousMessages, assistantMessage, toolResponseMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> handleToolCalls(Prompt prompt, ChatResponse response) {
|
||||
return super.handleToolCalls(prompt, response);
|
||||
}
|
||||
|
||||
public Flux<ChatResponse> processStream(ChatModel chatModel, Prompt prompt, Set<String> finishReasons,
|
||||
Function<AssistantMessage.ToolCall, String> customFunction) {
|
||||
|
||||
Flux<ChatResponse> chatResponses = chatModel.stream(prompt);
|
||||
|
||||
return chatResponses.flatMap(chatResponse -> {
|
||||
|
||||
boolean isToolCall = this.isToolCall(chatResponse, finishReasons);
|
||||
|
||||
if (isToolCall) {
|
||||
|
||||
Optional<Generation> toolCallGeneration = chatResponse.getResults()
|
||||
.stream()
|
||||
.filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
|
||||
.findFirst();
|
||||
|
||||
AssistantMessage assistantMessage = toolCallGeneration.get().getOutput();
|
||||
|
||||
List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();
|
||||
|
||||
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
|
||||
|
||||
String functionResponse = customFunction.apply(toolCall);
|
||||
|
||||
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolCall.name(),
|
||||
ModelOptionsUtils.toJsonString(functionResponse)));
|
||||
}
|
||||
|
||||
ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of());
|
||||
|
||||
List<Message> toolCallConversation = this.buildToolCallConversation(prompt.getInstructions(),
|
||||
assistantMessage, toolMessageResponse);
|
||||
|
||||
var prompt2 = new Prompt(toolCallConversation, prompt.getOptions());
|
||||
|
||||
return processStream(chatModel, prompt2, finishReasons, customFunction);
|
||||
}
|
||||
|
||||
return Flux.just(chatResponse);
|
||||
});
|
||||
}
|
||||
|
||||
public ChatResponse processCall(ChatModel chatModel, Prompt prompt, Set<String> finishReasons,
|
||||
Function<AssistantMessage.ToolCall, String> customFunction) {
|
||||
|
||||
ChatResponse chatResponse = chatModel.call(prompt);
|
||||
|
||||
boolean isToolCall = this.isToolCall(chatResponse, finishReasons);
|
||||
|
||||
if (!isToolCall) {
|
||||
return chatResponse;
|
||||
}
|
||||
|
||||
Optional<Generation> toolCallGeneration = chatResponse.getResults()
|
||||
.stream()
|
||||
.filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
|
||||
.findFirst();
|
||||
|
||||
AssistantMessage assistantMessage = toolCallGeneration.get().getOutput();
|
||||
|
||||
List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();
|
||||
|
||||
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
|
||||
|
||||
String functionResponse = customFunction.apply(toolCall);
|
||||
|
||||
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolCall.name(),
|
||||
ModelOptionsUtils.toJsonString(functionResponse)));
|
||||
}
|
||||
|
||||
ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of());
|
||||
|
||||
List<Message> toolCallConversation = this.buildToolCallConversation(prompt.getInstructions(), assistantMessage,
|
||||
toolMessageResponse);
|
||||
|
||||
var prompt2 = new Prompt(toolCallConversation, prompt.getOptions());
|
||||
|
||||
return processCall(chatModel, prompt2, finishReasons, customFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper used to provide only the function definition, without the actual function
|
||||
* call implementation.
|
||||
*
|
||||
* @param name the function name
|
||||
* @param description the function description
|
||||
* @param inputTypeSchema the input type schema
|
||||
*/
|
||||
public static record FunctionDefinition(String name, String description,
|
||||
String inputTypeSchema) implements FunctionCallback {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return this.description();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInputTypeSchema() {
|
||||
return this.inputTypeSchema();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String functionInput) {
|
||||
throw new UnsupportedOperationException(
|
||||
"FunctionDefinition provides only metadata. It doesn't implement the call method.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -44,10 +44,10 @@ public interface FunctionCallingOptions extends ChatOptions {
|
||||
|
||||
/**
|
||||
* Function Callbacks to be registered 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. You have to use "functions" property to list the function names from the
|
||||
* ChatModel registry to be used in the chat completion requests.
|
||||
* toolCallbacks are automatically enabled for the duration of the prompt execution.
|
||||
* For Default Options the FunctionCallbacks are registered but disabled by default.
|
||||
* You have to use "functions" property to list the function names from the ChatModel
|
||||
* registry to be used in the chat completion requests.
|
||||
* @return Return the Function Callbacks to be registered with the ChatModel.
|
||||
*/
|
||||
List<FunctionCallback> getFunctionCallbacks();
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -37,7 +37,7 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class DefaultToolCallingChatOptions implements ToolCallingChatOptions {
|
||||
|
||||
private List<FunctionCallback> toolCallbacks = new ArrayList<>();
|
||||
private List<ToolCallback> toolCallbacks = new ArrayList<>();
|
||||
|
||||
private Set<String> toolNames = new HashSet<>();
|
||||
|
||||
@@ -71,12 +71,12 @@ public class DefaultToolCallingChatOptions implements ToolCallingChatOptions {
|
||||
private Double topP;
|
||||
|
||||
@Override
|
||||
public List<FunctionCallback> getToolCallbacks() {
|
||||
public List<ToolCallback> getToolCallbacks() {
|
||||
return List.copyOf(this.toolCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setToolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public void setToolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
this.toolCallbacks = new ArrayList<>(toolCallbacks);
|
||||
@@ -118,37 +118,6 @@ public class DefaultToolCallingChatOptions implements ToolCallingChatOptions {
|
||||
this.internalToolExecutionEnabled = internalToolExecutionEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FunctionCallback> getFunctionCallbacks() {
|
||||
return getToolCallbacks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
setToolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getFunctions() {
|
||||
return getToolNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFunctions(Set<String> functions) {
|
||||
setToolNames(functions);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Boolean getProxyToolCalls() {
|
||||
return getInternalToolExecutionEnabled() != null ? !getInternalToolExecutionEnabled() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProxyToolCalls(@Nullable Boolean proxyToolCalls) {
|
||||
setInternalToolExecutionEnabled(proxyToolCalls == null || !proxyToolCalls);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getModel() {
|
||||
@@ -260,13 +229,13 @@ public class DefaultToolCallingChatOptions implements ToolCallingChatOptions {
|
||||
private final DefaultToolCallingChatOptions options = new DefaultToolCallingChatOptions();
|
||||
|
||||
@Override
|
||||
public ToolCallingChatOptions.Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
public ToolCallingChatOptions.Builder toolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
this.options.setToolCallbacks(toolCallbacks);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolCallingChatOptions.Builder toolCallbacks(FunctionCallback... toolCallbacks) {
|
||||
public ToolCallingChatOptions.Builder toolCallbacks(ToolCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
this.options.setToolCallbacks(Arrays.asList(toolCallbacks));
|
||||
return this;
|
||||
@@ -308,37 +277,6 @@ public class DefaultToolCallingChatOptions implements ToolCallingChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated // Use toolCallbacks() instead
|
||||
public ToolCallingChatOptions.Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
return toolCallbacks(functionCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated // Use toolCallbacks() instead
|
||||
public ToolCallingChatOptions.Builder functionCallbacks(FunctionCallback... functionCallbacks) {
|
||||
Assert.notNull(functionCallbacks, "functionCallbacks cannot be null");
|
||||
return functionCallbacks(List.of(functionCallbacks));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated // Use toolNames() instead
|
||||
public ToolCallingChatOptions.Builder functions(Set<String> functions) {
|
||||
return toolNames(functions);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated // Use toolNames() instead
|
||||
public ToolCallingChatOptions.Builder function(String function) {
|
||||
return toolNames(function);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated // Use internalToolExecutionEnabled() instead
|
||||
public ToolCallingChatOptions.Builder proxyToolCalls(@Nullable Boolean proxyToolCalls) {
|
||||
return internalToolExecutionEnabled(proxyToolCalls == null || !proxyToolCalls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolCallingChatOptions.Builder model(@Nullable String model) {
|
||||
this.options.setModel(model);
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.springframework.ai.chat.model.Generation;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallingOptions;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
|
||||
@@ -89,7 +88,7 @@ public class DefaultToolCallingManager implements ToolCallingManager {
|
||||
public List<ToolDefinition> resolveToolDefinitions(ToolCallingChatOptions chatOptions) {
|
||||
Assert.notNull(chatOptions, "chatOptions cannot be null");
|
||||
|
||||
List<FunctionCallback> toolCallbacks = new ArrayList<>(chatOptions.getToolCallbacks());
|
||||
List<ToolCallback> toolCallbacks = new ArrayList<>(chatOptions.getToolCallbacks());
|
||||
for (String toolName : chatOptions.getToolNames()) {
|
||||
// Skip the tool if it is already present in the request toolCallbacks.
|
||||
// That might happen if a tool is defined in the options
|
||||
@@ -97,25 +96,14 @@ public class DefaultToolCallingManager implements ToolCallingManager {
|
||||
if (chatOptions.getToolCallbacks().stream().anyMatch(tool -> tool.getName().equals(toolName))) {
|
||||
continue;
|
||||
}
|
||||
FunctionCallback toolCallback = this.toolCallbackResolver.resolve(toolName);
|
||||
ToolCallback toolCallback = this.toolCallbackResolver.resolve(toolName);
|
||||
if (toolCallback == null) {
|
||||
throw new IllegalStateException("No ToolCallback found for tool name: " + toolName);
|
||||
}
|
||||
toolCallbacks.add(toolCallback);
|
||||
}
|
||||
|
||||
return toolCallbacks.stream().map(functionCallback -> {
|
||||
if (functionCallback instanceof ToolCallback toolCallback) {
|
||||
return toolCallback.getToolDefinition();
|
||||
}
|
||||
else {
|
||||
return ToolDefinition.builder()
|
||||
.name(functionCallback.getName())
|
||||
.description(functionCallback.getDescription())
|
||||
.inputSchema(functionCallback.getInputTypeSchema())
|
||||
.build();
|
||||
}
|
||||
}).toList();
|
||||
return toolCallbacks.stream().map(toolCallback -> toolCallback.getToolDefinition()).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -151,9 +139,9 @@ public class DefaultToolCallingManager implements ToolCallingManager {
|
||||
private static ToolContext buildToolContext(Prompt prompt, AssistantMessage assistantMessage) {
|
||||
Map<String, Object> toolContextMap = Map.of();
|
||||
|
||||
if (prompt.getOptions() instanceof FunctionCallingOptions functionOptions
|
||||
&& !CollectionUtils.isEmpty(functionOptions.getToolContext())) {
|
||||
toolContextMap = new HashMap<>(functionOptions.getToolContext());
|
||||
if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions
|
||||
&& !CollectionUtils.isEmpty(toolCallingChatOptions.getToolContext())) {
|
||||
toolContextMap = new HashMap<>(toolCallingChatOptions.getToolContext());
|
||||
|
||||
List<Message> messageHistory = new ArrayList<>(prompt.copy().getInstructions());
|
||||
messageHistory.add(new AssistantMessage(assistantMessage.getText(), assistantMessage.getMetadata(),
|
||||
@@ -181,13 +169,10 @@ public class DefaultToolCallingManager implements ToolCallingManager {
|
||||
*/
|
||||
private InternalToolExecutionResult executeToolCall(Prompt prompt, AssistantMessage assistantMessage,
|
||||
ToolContext toolContext) {
|
||||
List<FunctionCallback> toolCallbacks = List.of();
|
||||
List<ToolCallback> toolCallbacks = List.of();
|
||||
if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) {
|
||||
toolCallbacks = toolCallingChatOptions.getToolCallbacks();
|
||||
}
|
||||
else if (prompt.getOptions() instanceof FunctionCallingOptions functionOptions) {
|
||||
toolCallbacks = functionOptions.getFunctionCallbacks();
|
||||
}
|
||||
|
||||
List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();
|
||||
|
||||
@@ -200,7 +185,7 @@ public class DefaultToolCallingManager implements ToolCallingManager {
|
||||
String toolName = toolCall.name();
|
||||
String toolInputArguments = toolCall.arguments();
|
||||
|
||||
FunctionCallback toolCallback = toolCallbacks.stream()
|
||||
ToolCallback toolCallback = toolCallbacks.stream()
|
||||
.filter(tool -> toolName.equals(tool.getName()))
|
||||
.findFirst()
|
||||
.orElseGet(() -> this.toolCallbackResolver.resolve(toolName));
|
||||
@@ -209,17 +194,11 @@ public class DefaultToolCallingManager implements ToolCallingManager {
|
||||
throw new IllegalStateException("No ToolCallback found for tool name: " + toolName);
|
||||
}
|
||||
|
||||
if (returnDirect == null && toolCallback instanceof ToolCallback callback) {
|
||||
returnDirect = callback.getToolMetadata().returnDirect();
|
||||
if (returnDirect == null) {
|
||||
returnDirect = toolCallback.getToolMetadata().returnDirect();
|
||||
}
|
||||
else if (toolCallback instanceof ToolCallback callback) {
|
||||
returnDirect = returnDirect && callback.getToolMetadata().returnDirect();
|
||||
}
|
||||
else if (returnDirect == null) {
|
||||
// This is a temporary solution to ensure backward compatibility with
|
||||
// FunctionCallback.
|
||||
// TODO: remove this block when FunctionCallback is removed.
|
||||
returnDirect = false;
|
||||
else {
|
||||
returnDirect = returnDirect && toolCallback.getToolMetadata().returnDirect();
|
||||
}
|
||||
|
||||
String toolResult;
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2025 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.tool;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.ToolResponseMessage;
|
||||
import org.springframework.ai.chat.model.AbstractToolCallSupport;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.model.Generation;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallbackResolver;
|
||||
import org.springframework.ai.model.function.FunctionCallingOptions;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
|
||||
import org.springframework.ai.tool.execution.ToolExecutionException;
|
||||
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link ToolCallingManager} supporting the migration from
|
||||
* {@link AbstractToolCallSupport} to {@link ToolCallingManager} and ensuring AI
|
||||
* compatibility for all the ChatModel implementations.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
* @since 1.0.0
|
||||
* @deprecated Only to help moving away from {@link AbstractToolCallSupport}. It will be
|
||||
* removed in the next milestone.
|
||||
*/
|
||||
@Deprecated
|
||||
public class LegacyToolCallingManager implements ToolCallingManager {
|
||||
|
||||
@Nullable
|
||||
private final FunctionCallbackResolver functionCallbackResolver;
|
||||
|
||||
private final Map<String, FunctionCallback> functionCallbacks = new HashMap<>();
|
||||
|
||||
private final ToolExecutionExceptionProcessor toolExecutionExceptionProcessor = DefaultToolExecutionExceptionProcessor
|
||||
.builder()
|
||||
.build();
|
||||
|
||||
public LegacyToolCallingManager(@Nullable FunctionCallbackResolver functionCallbackResolver,
|
||||
@Nullable List<FunctionCallback> functionCallbacks) {
|
||||
this.functionCallbackResolver = functionCallbackResolver;
|
||||
if (functionCallbacks != null) {
|
||||
functionCallbacks.forEach(toolCallback -> this.functionCallbacks.put(toolCallback.getName(), toolCallback));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ToolDefinition> resolveToolDefinitions(ToolCallingChatOptions chatOptions) {
|
||||
Assert.notNull(chatOptions, "chatOptions cannot be null");
|
||||
|
||||
List<FunctionCallback> toolCallbacks = new ArrayList<>(chatOptions.getToolCallbacks());
|
||||
for (String toolName : chatOptions.getToolNames()) {
|
||||
// Skip the tool if it is already present in the request toolCallbacks.
|
||||
// That might happen if a tool is defined in the options
|
||||
// both as a ToolCallback and as a tool name.
|
||||
if (chatOptions.getToolCallbacks().stream().anyMatch(tool -> tool.getName().equals(toolName))) {
|
||||
continue;
|
||||
}
|
||||
FunctionCallback toolCallback = resolveFunctionCallback(toolName);
|
||||
if (toolCallback == null) {
|
||||
throw new IllegalStateException("No ToolCallback found for tool name: " + toolName);
|
||||
}
|
||||
toolCallbacks.add(toolCallback);
|
||||
}
|
||||
|
||||
return toolCallbacks.stream().map(functionCallback -> {
|
||||
if (functionCallback instanceof ToolCallback toolCallback) {
|
||||
return toolCallback.getToolDefinition();
|
||||
}
|
||||
else {
|
||||
return ToolDefinition.builder()
|
||||
.name(functionCallback.getName())
|
||||
.description(functionCallback.getDescription())
|
||||
.inputSchema(functionCallback.getInputTypeSchema())
|
||||
.build();
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private FunctionCallback resolveFunctionCallback(String toolName) {
|
||||
Assert.hasText(toolName, "toolName cannot be null or empty");
|
||||
if (this.functionCallbacks.get(toolName) != null) {
|
||||
return this.functionCallbacks.get(toolName);
|
||||
}
|
||||
return this.functionCallbackResolver != null ? this.functionCallbackResolver.resolve(toolName) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse) {
|
||||
Assert.notNull(prompt, "prompt cannot be null");
|
||||
Assert.notNull(chatResponse, "chatResponse cannot be null");
|
||||
|
||||
Optional<Generation> toolCallGeneration = chatResponse.getResults()
|
||||
.stream()
|
||||
.filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
|
||||
.findFirst();
|
||||
|
||||
if (toolCallGeneration.isEmpty()) {
|
||||
throw new IllegalStateException("No tool call requested by the chat model");
|
||||
}
|
||||
|
||||
AssistantMessage assistantMessage = toolCallGeneration.get().getOutput();
|
||||
|
||||
ToolContext toolContext = buildToolContext(prompt, assistantMessage);
|
||||
|
||||
ToolResponseMessage toolMessageResponse = executeToolCall(prompt, assistantMessage, toolContext);
|
||||
|
||||
List<Message> conversationHistory = buildConversationHistoryAfterToolExecution(prompt.getInstructions(),
|
||||
assistantMessage, toolMessageResponse);
|
||||
|
||||
return ToolExecutionResult.builder().conversationHistory(conversationHistory).returnDirect(false).build();
|
||||
}
|
||||
|
||||
private static ToolContext buildToolContext(Prompt prompt, AssistantMessage assistantMessage) {
|
||||
Map<String, Object> toolContextMap = Map.of();
|
||||
|
||||
if (prompt.getOptions() instanceof FunctionCallingOptions functionOptions
|
||||
&& !CollectionUtils.isEmpty(functionOptions.getToolContext())) {
|
||||
toolContextMap = new HashMap<>(functionOptions.getToolContext());
|
||||
|
||||
List<Message> messageHistory = new ArrayList<>(prompt.copy().getInstructions());
|
||||
messageHistory.add(new AssistantMessage(assistantMessage.getText(), assistantMessage.getMetadata(),
|
||||
assistantMessage.getToolCalls()));
|
||||
|
||||
toolContextMap.put(ToolContext.TOOL_CALL_HISTORY,
|
||||
buildConversationHistoryBeforeToolExecution(prompt, assistantMessage));
|
||||
}
|
||||
|
||||
return new ToolContext(toolContextMap);
|
||||
}
|
||||
|
||||
private static List<Message> buildConversationHistoryBeforeToolExecution(Prompt prompt,
|
||||
AssistantMessage assistantMessage) {
|
||||
List<Message> messageHistory = new ArrayList<>(prompt.copy().getInstructions());
|
||||
messageHistory.add(new AssistantMessage(assistantMessage.getText(), assistantMessage.getMetadata(),
|
||||
assistantMessage.getToolCalls()));
|
||||
return messageHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the tool call and return the response message. To ensure backward
|
||||
* compatibility, both {@link ToolCallback} and {@link FunctionCallback} are
|
||||
* supported.
|
||||
*/
|
||||
private ToolResponseMessage executeToolCall(Prompt prompt, AssistantMessage assistantMessage,
|
||||
ToolContext toolContext) {
|
||||
List<FunctionCallback> toolCallbacks = List.of();
|
||||
if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) {
|
||||
toolCallbacks = toolCallingChatOptions.getToolCallbacks();
|
||||
}
|
||||
else if (prompt.getOptions() instanceof FunctionCallingOptions functionOptions) {
|
||||
toolCallbacks = functionOptions.getFunctionCallbacks();
|
||||
}
|
||||
|
||||
List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();
|
||||
|
||||
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
|
||||
|
||||
String toolName = toolCall.name();
|
||||
String toolInputArguments = toolCall.arguments();
|
||||
|
||||
FunctionCallback toolCallback = toolCallbacks.stream()
|
||||
.filter(tool -> toolName.equals(tool.getName()))
|
||||
.findFirst()
|
||||
.orElseGet(() -> resolveFunctionCallback(toolName));
|
||||
|
||||
if (toolCallback == null) {
|
||||
throw new IllegalStateException("No ToolCallback found for tool name: " + toolName);
|
||||
}
|
||||
|
||||
String toolResult;
|
||||
try {
|
||||
toolResult = toolCallback.call(toolInputArguments, toolContext);
|
||||
}
|
||||
catch (ToolExecutionException ex) {
|
||||
toolResult = this.toolExecutionExceptionProcessor.process(ex);
|
||||
}
|
||||
|
||||
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, toolResult));
|
||||
}
|
||||
|
||||
return new ToolResponseMessage(toolResponses, Map.of());
|
||||
}
|
||||
|
||||
private List<Message> buildConversationHistoryAfterToolExecution(List<Message> previousMessages,
|
||||
AssistantMessage assistantMessage, ToolResponseMessage toolResponseMessage) {
|
||||
List<Message> messages = new ArrayList<>(previousMessages);
|
||||
messages.add(assistantMessage);
|
||||
messages.add(toolResponseMessage);
|
||||
return messages;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public final static class Builder {
|
||||
|
||||
private FunctionCallbackResolver functionCallbackResolver;
|
||||
|
||||
private List<FunctionCallback> functionCallbacks = new ArrayList<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder functionCallbackResolver(FunctionCallbackResolver functionCallbackResolver) {
|
||||
this.functionCallbackResolver = functionCallbackResolver;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
this.functionCallbacks = functionCallbacks;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LegacyToolCallingManager build() {
|
||||
return new LegacyToolCallingManager(this.functionCallbackResolver, this.functionCallbacks);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,8 +25,7 @@ import java.util.Set;
|
||||
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
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.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.util.ToolUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -40,19 +39,19 @@ import org.springframework.util.CollectionUtils;
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface ToolCallingChatOptions extends FunctionCallingOptions {
|
||||
public interface ToolCallingChatOptions extends ChatOptions {
|
||||
|
||||
boolean DEFAULT_TOOL_EXECUTION_ENABLED = true;
|
||||
|
||||
/**
|
||||
* ToolCallbacks to be registered with the ChatModel.
|
||||
*/
|
||||
List<FunctionCallback> getToolCallbacks();
|
||||
List<ToolCallback> getToolCallbacks();
|
||||
|
||||
/**
|
||||
* Set the ToolCallbacks to be registered with the ChatModel.
|
||||
*/
|
||||
void setToolCallbacks(List<FunctionCallback> toolCallbacks);
|
||||
void setToolCallbacks(List<ToolCallback> toolCallbacks);
|
||||
|
||||
/**
|
||||
* Names of the tools to register with the ChatModel.
|
||||
@@ -87,6 +86,18 @@ public interface ToolCallingChatOptions extends FunctionCallingOptions {
|
||||
*/
|
||||
void setInternalToolExecutionEnabled(@Nullable Boolean internalToolExecutionEnabled);
|
||||
|
||||
/**
|
||||
* Get the configured tool context.
|
||||
* @return the tool context map.
|
||||
*/
|
||||
Map<String, Object> getToolContext();
|
||||
|
||||
/**
|
||||
* Set the tool context values as map.
|
||||
* @param toolContext as map
|
||||
*/
|
||||
void setToolContext(Map<String, Object> toolContext);
|
||||
|
||||
/**
|
||||
* A builder to create a new {@link ToolCallingChatOptions} instance.
|
||||
*/
|
||||
@@ -102,10 +113,6 @@ public interface ToolCallingChatOptions extends FunctionCallingOptions {
|
||||
internalToolExecutionEnabled = Boolean.TRUE
|
||||
.equals(toolCallingChatOptions.getInternalToolExecutionEnabled());
|
||||
}
|
||||
else if (chatOptions instanceof FunctionCallingOptions functionCallingOptions
|
||||
&& functionCallingOptions.getProxyToolCalls() != null) {
|
||||
internalToolExecutionEnabled = Boolean.TRUE.equals(!functionCallingOptions.getProxyToolCalls());
|
||||
}
|
||||
else {
|
||||
internalToolExecutionEnabled = DEFAULT_TOOL_EXECUTION_ENABLED;
|
||||
}
|
||||
@@ -121,8 +128,8 @@ public interface ToolCallingChatOptions extends FunctionCallingOptions {
|
||||
return new HashSet<>(runtimeToolNames);
|
||||
}
|
||||
|
||||
static List<FunctionCallback> mergeToolCallbacks(List<FunctionCallback> runtimeToolCallbacks,
|
||||
List<FunctionCallback> defaultToolCallbacks) {
|
||||
static List<ToolCallback> mergeToolCallbacks(List<ToolCallback> runtimeToolCallbacks,
|
||||
List<ToolCallback> defaultToolCallbacks) {
|
||||
Assert.notNull(runtimeToolCallbacks, "runtimeToolCallbacks cannot be null");
|
||||
Assert.notNull(defaultToolCallbacks, "defaultToolCallbacks cannot be null");
|
||||
if (CollectionUtils.isEmpty(runtimeToolCallbacks)) {
|
||||
@@ -142,7 +149,7 @@ public interface ToolCallingChatOptions extends FunctionCallingOptions {
|
||||
return mergedToolContext;
|
||||
}
|
||||
|
||||
static void validateToolCallbacks(List<FunctionCallback> toolCallbacks) {
|
||||
static void validateToolCallbacks(List<ToolCallback> toolCallbacks) {
|
||||
List<String> duplicateToolNames = ToolUtils.getDuplicateToolNames(toolCallbacks);
|
||||
if (!duplicateToolNames.isEmpty()) {
|
||||
throw new IllegalStateException("Multiple tools with the same name (%s) found in ToolCallingChatOptions"
|
||||
@@ -153,17 +160,17 @@ public interface ToolCallingChatOptions extends FunctionCallingOptions {
|
||||
/**
|
||||
* A builder to create a {@link ToolCallingChatOptions} instance.
|
||||
*/
|
||||
interface Builder extends FunctionCallingOptions.Builder {
|
||||
interface Builder extends ChatOptions.Builder {
|
||||
|
||||
/**
|
||||
* ToolCallbacks to be registered with the ChatModel.
|
||||
*/
|
||||
Builder toolCallbacks(List<FunctionCallback> functionCallbacks);
|
||||
Builder toolCallbacks(List<ToolCallback> toolCallbacks);
|
||||
|
||||
/**
|
||||
* ToolCallbacks to be registered with the ChatModel.
|
||||
*/
|
||||
Builder toolCallbacks(FunctionCallback... functionCallbacks);
|
||||
Builder toolCallbacks(ToolCallback... toolCallbacks);
|
||||
|
||||
/**
|
||||
* Names of the tools to register with the ChatModel.
|
||||
@@ -181,34 +188,21 @@ public interface ToolCallingChatOptions extends FunctionCallingOptions {
|
||||
*/
|
||||
Builder internalToolExecutionEnabled(@Nullable Boolean internalToolExecutionEnabled);
|
||||
|
||||
// FunctionCallingOptions.Builder methods
|
||||
|
||||
@Override
|
||||
/**
|
||||
* Add a {@link Map} of context values into tool context.
|
||||
* @param context the map representing the tool context.
|
||||
* @return the {@link ToolCallingChatOptions} Builder.
|
||||
*/
|
||||
Builder toolContext(Map<String, Object> context);
|
||||
|
||||
@Override
|
||||
/**
|
||||
* Add a specific key/value pair to the tool context.
|
||||
* @param key the key to use.
|
||||
* @param value the corresponding value.
|
||||
* @return the {@link ToolCallingChatOptions} Builder.
|
||||
*/
|
||||
Builder toolContext(String key, Object value);
|
||||
|
||||
@Override
|
||||
@Deprecated // Use toolCallbacks() instead
|
||||
Builder functionCallbacks(List<FunctionCallback> functionCallbacks);
|
||||
|
||||
@Override
|
||||
@Deprecated // Use toolCallbacks() instead
|
||||
Builder functionCallbacks(FunctionCallback... functionCallbacks);
|
||||
|
||||
@Override
|
||||
@Deprecated // Use tools() instead
|
||||
Builder functions(Set<String> functions);
|
||||
|
||||
@Override
|
||||
@Deprecated // Use tools() instead
|
||||
Builder function(String function);
|
||||
|
||||
@Override
|
||||
@Deprecated // Use internalToolExecutionEnabled() instead
|
||||
Builder proxyToolCalls(@Nullable Boolean proxyToolCalls);
|
||||
|
||||
// ChatOptions.Builder methods
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,12 +18,11 @@ package org.springframework.ai.tool;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A simple implementation of {@link ToolCallbackProvider} that maintains a static array
|
||||
* of {@link FunctionCallback} objects. This provider is immutable after construction and
|
||||
* of {@link ToolCallback} objects. This provider is immutable after construction and
|
||||
* provides a straightforward way to supply a fixed set of tool callbacks to AI models.
|
||||
*
|
||||
* <p>
|
||||
@@ -46,11 +45,11 @@ import org.springframework.util.Assert;
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
* @see ToolCallbackProvider
|
||||
* @see FunctionCallback
|
||||
* @see ToolCallback
|
||||
*/
|
||||
public class StaticToolCallbackProvider implements ToolCallbackProvider {
|
||||
|
||||
private final FunctionCallback[] toolCallbacks;
|
||||
private final ToolCallback[] toolCallbacks;
|
||||
|
||||
/**
|
||||
* Constructs a new StaticToolCallbackProvider with the specified array of function
|
||||
@@ -59,7 +58,7 @@ public class StaticToolCallbackProvider implements ToolCallbackProvider {
|
||||
* provider. Must not be null, though an empty array is permitted.
|
||||
* @throws IllegalArgumentException if the toolCallbacks array is null
|
||||
*/
|
||||
public StaticToolCallbackProvider(FunctionCallback... toolCallbacks) {
|
||||
public StaticToolCallbackProvider(ToolCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "ToolCallbacks must not be null");
|
||||
this.toolCallbacks = toolCallbacks;
|
||||
}
|
||||
@@ -72,9 +71,9 @@ public class StaticToolCallbackProvider implements ToolCallbackProvider {
|
||||
* @throws IllegalArgumentException if the toolCallbacks list is null or contains null
|
||||
* elements
|
||||
*/
|
||||
public StaticToolCallbackProvider(List<? extends FunctionCallback> toolCallbacks) {
|
||||
public StaticToolCallbackProvider(List<? extends ToolCallback> toolCallbacks) {
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
this.toolCallbacks = toolCallbacks.toArray(new FunctionCallback[0]);
|
||||
this.toolCallbacks = toolCallbacks.toArray(new ToolCallback[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +83,7 @@ public class StaticToolCallbackProvider implements ToolCallbackProvider {
|
||||
* are expected to be immutable.
|
||||
*/
|
||||
@Override
|
||||
public FunctionCallback[] getToolCallbacks() {
|
||||
public ToolCallback[] getToolCallbacks() {
|
||||
return this.toolCallbacks;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ package org.springframework.ai.tool;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
|
||||
/**
|
||||
* Provides {@link ToolCallback} instances for tools defined in different sources.
|
||||
*
|
||||
@@ -28,13 +26,13 @@ import org.springframework.ai.model.function.FunctionCallback;
|
||||
*/
|
||||
public interface ToolCallbackProvider {
|
||||
|
||||
FunctionCallback[] getToolCallbacks();
|
||||
ToolCallback[] getToolCallbacks();
|
||||
|
||||
static ToolCallbackProvider from(List<? extends FunctionCallback> toolCallbacks) {
|
||||
static ToolCallbackProvider from(List<? extends ToolCallback> toolCallbacks) {
|
||||
return new StaticToolCallbackProvider(toolCallbacks);
|
||||
}
|
||||
|
||||
static ToolCallbackProvider from(FunctionCallback... toolCallbacks) {
|
||||
static ToolCallbackProvider from(ToolCallback... toolCallbacks) {
|
||||
return new StaticToolCallbackProvider(toolCallbacks);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ package org.springframework.ai.tool.resolution;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -41,11 +41,11 @@ public class DelegatingToolCallbackResolver implements ToolCallbackResolver {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public FunctionCallback resolve(String toolName) {
|
||||
public ToolCallback resolve(String toolName) {
|
||||
Assert.hasText(toolName, "toolName cannot be null or empty");
|
||||
|
||||
for (ToolCallbackResolver toolCallbackResolver : this.toolCallbackResolvers) {
|
||||
FunctionCallback toolCallback = toolCallbackResolver.resolve(toolName);
|
||||
ToolCallback toolCallback = toolCallbackResolver.resolve(toolName);
|
||||
if (toolCallback != null) {
|
||||
return toolCallback;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -37,22 +36,18 @@ public class StaticToolCallbackResolver implements ToolCallbackResolver {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(StaticToolCallbackResolver.class);
|
||||
|
||||
private final Map<String, FunctionCallback> toolCallbacks = new HashMap<>();
|
||||
private final Map<String, ToolCallback> toolCallbacks = new HashMap<>();
|
||||
|
||||
public StaticToolCallbackResolver(List<FunctionCallback> toolCallbacks) {
|
||||
public StaticToolCallbackResolver(List<ToolCallback> toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
|
||||
toolCallbacks.forEach(callback -> {
|
||||
if (callback instanceof ToolCallback toolCallback) {
|
||||
this.toolCallbacks.put(toolCallback.getToolDefinition().name(), toolCallback);
|
||||
}
|
||||
this.toolCallbacks.put(callback.getName(), callback);
|
||||
});
|
||||
toolCallbacks
|
||||
.forEach(toolCallback -> this.toolCallbacks.put(toolCallback.getToolDefinition().name(), toolCallback));
|
||||
}
|
||||
|
||||
@Override
|
||||
public FunctionCallback resolve(String toolName) {
|
||||
public ToolCallback resolve(String toolName) {
|
||||
Assert.hasText(toolName, "toolName cannot be null or empty");
|
||||
logger.debug("ToolCallback resolution attempt from static registry");
|
||||
return this.toolCallbacks.get(toolName);
|
||||
|
||||
@@ -32,6 +32,6 @@ public interface ToolCallbackResolver {
|
||||
* Resolve the {@link FunctionCallback} for the given tool name.
|
||||
*/
|
||||
@Nullable
|
||||
FunctionCallback resolve(String toolName);
|
||||
ToolCallback resolve(String toolName);
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.execution.DefaultToolCallResultConverter;
|
||||
import org.springframework.ai.tool.execution.ToolCallResultConverter;
|
||||
@@ -84,10 +84,11 @@ public final class ToolUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static List<String> getDuplicateToolNames(List<FunctionCallback> toolCallbacks) {
|
||||
public static List<String> getDuplicateToolNames(List<ToolCallback> toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
return toolCallbacks.stream()
|
||||
.collect(Collectors.groupingBy(FunctionCallback::getName, Collectors.counting()))
|
||||
.collect(Collectors.groupingBy(toolCallback -> toolCallback.getToolDefinition().name(),
|
||||
Collectors.counting()))
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(entry -> entry.getValue() > 1)
|
||||
@@ -95,7 +96,7 @@ public final class ToolUtils {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static List<String> getDuplicateToolNames(FunctionCallback... toolCallbacks) {
|
||||
public static List<String> getDuplicateToolNames(ToolCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
return getDuplicateToolNames(Arrays.asList(toolCallbacks));
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import java.util.Set;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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.function.FunctionToolCallback;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
|
||||
@@ -96,25 +96,24 @@ public class ChatOptionsBuilderTests {
|
||||
@Test
|
||||
void shouldUpcastToChatOptions() {
|
||||
// Given
|
||||
FunctionCallback callback = FunctionCallback.builder()
|
||||
.function("function1", x -> "result")
|
||||
FunctionToolCallback callback = FunctionToolCallback.builder("function1", x -> "result")
|
||||
.description("Test function")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
|
||||
FunctionCallingOptions functionOptions = FunctionCallingOptions.builder()
|
||||
ToolCallingChatOptions toolCallingChatOptions = ToolCallingChatOptions.builder()
|
||||
.model("gpt-4")
|
||||
.maxTokens(100)
|
||||
.temperature(0.7)
|
||||
.topP(1.0)
|
||||
.topK(40)
|
||||
.stopSequences(List.of("stop1", "stop2"))
|
||||
.functions(Set.of("function1", "function2"))
|
||||
.functionCallbacks(List.of(callback))
|
||||
.toolNames(Set.of("function1", "function2"))
|
||||
.toolCallbacks(List.of(callback))
|
||||
.build();
|
||||
|
||||
// When
|
||||
ChatOptions chatOptions = functionOptions;
|
||||
ChatOptions chatOptions = toolCallingChatOptions;
|
||||
|
||||
// Then
|
||||
assertThat(chatOptions.getModel()).isEqualTo("gpt-4");
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.model.function;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback.FunctionInvokingSpec;
|
||||
import org.springframework.ai.model.function.FunctionCallback.MethodInvokingSpec;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DefaultFunctionCallbackBuilder}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
class DefaultFunctionCallbackBuilderTests {
|
||||
|
||||
// Function
|
||||
@Test
|
||||
void whenFunctionDescriptionIsNullThenThrow() {
|
||||
assertThatThrownBy(
|
||||
() -> FunctionCallback.builder().function("functionName", input -> "output").description(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Description must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionDescriptionIsEmptyThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().function("functionName", input -> "output").description(""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Description must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionInputTypeSchemaIsNullThenThrow() {
|
||||
assertThatThrownBy(
|
||||
() -> FunctionCallback.builder().function("functionName", input -> "output").inputTypeSchema(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("InputTypeSchema must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionInputTypeSchemaIsEmptyThenThrow() {
|
||||
assertThatThrownBy(
|
||||
() -> FunctionCallback.builder().function("functionName", input -> "output").inputTypeSchema(""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("InputTypeSchema must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionSchemaTypeIsNullThenThrow() {
|
||||
assertThatThrownBy(
|
||||
() -> FunctionCallback.builder().function("functionName", input -> "output").schemaType(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("SchemaType must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionResponseConverterIsNullThenThrow() {
|
||||
assertThatThrownBy(
|
||||
() -> FunctionCallback.builder().function("functionName", input -> "output").responseConverter(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("ResponseConverter must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionNameIsNullThenThrow2() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().function(null, (Function) null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Name must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionIsNullThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().function("functionName", (Function) null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Function must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionThenReturn() {
|
||||
FunctionInvokingSpec<?, ?> functionBuilder = FunctionCallback.builder()
|
||||
.function("functionName", input -> "output");
|
||||
assertThat(functionBuilder).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionWithNullInputTypeThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().function("functionName", input -> "output").build())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("InputType must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionWithInputTypeThenReturn() {
|
||||
FunctionCallback functionCallback = FunctionCallback.builder()
|
||||
.function("functionName", input -> "output")
|
||||
.description("description")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
assertThat(functionCallback).isNotNull();
|
||||
assertThat(functionCallback.getDescription()).isEqualTo("description");
|
||||
assertThat(functionCallback.getName()).isEqualTo("functionName");
|
||||
assertThat(functionCallback.getInputTypeSchema()).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionWithGeneratedDescriptionThenReturn() {
|
||||
FunctionCallback functionCallback = FunctionCallback.builder()
|
||||
.function("veryLongDescriptiveFunctionName", input -> "output")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
assertThat(functionCallback.getDescription()).isEqualTo("very long descriptive function name");
|
||||
assertThat(functionCallback.getName()).isEqualTo("veryLongDescriptiveFunctionName");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionWithGenericInputTypeThenReturn() {
|
||||
FunctionCallback functionCallback = FunctionCallback.builder()
|
||||
.function("functionName", input -> "output")
|
||||
.inputType(new ParameterizedTypeReference<GenericsRequest<Request>>() {
|
||||
})
|
||||
.build();
|
||||
assertThat(functionCallback.getName()).isEqualTo("functionName");
|
||||
assertThat(functionCallback.getInputTypeSchema()).isEqualTo("""
|
||||
{
|
||||
"$schema" : "https://json-schema.org/draft/2020-12/schema",
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"datum" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"value" : {
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}""");
|
||||
}
|
||||
|
||||
// BiFunction
|
||||
@Test
|
||||
void whenBiFunctionNameIsNullThenThrow2() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().function(null, (BiFunction) null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Name must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenBiFunctionIsNullThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().function("functionName", (BiFunction) null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("BiFunction must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenBiFunctionThenReturn() {
|
||||
FunctionInvokingSpec<?, ?> functionBuilder = FunctionCallback.builder()
|
||||
.function("functionName", (input, context) -> "output");
|
||||
assertThat(functionBuilder).isNotNull();
|
||||
}
|
||||
|
||||
// Method
|
||||
|
||||
@Test
|
||||
void whenMethodDescriptionIsNullThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName").description(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Description must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodDescriptionIsEmptyThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName").description(""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Description must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodInputTypeSchemaIsNullThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName").inputTypeSchema(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("InputTypeSchema must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodInputTypeSchemaIsEmptyThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName").inputTypeSchema(""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("InputTypeSchema must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodSchemaTypeIsNullThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName").schemaType(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("SchemaType must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodResponseConverterIsNullThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName").responseConverter(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("ResponseConverter must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodNameIsNullThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().method(null)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Method name must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodArgumentTypesIsNullThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName", (Class<?>[]) null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Argument types must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodThenReturn() {
|
||||
MethodInvokingSpec methodInvokeBuilder = FunctionCallback.builder().method("methodName");
|
||||
assertThat(methodInvokeBuilder).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodWithArgumentTypesThenReturn() {
|
||||
MethodInvokingSpec methodInvokeBuilder = FunctionCallback.builder()
|
||||
.method("methodName", String.class, Integer.class);
|
||||
assertThat(methodInvokeBuilder).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodWithMissingTargetObjectOrTargetClassThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName").build())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Target class or object must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodWithMissingTargetObjectThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder()
|
||||
.method("methodName", String.class, Integer.class)
|
||||
.targetClass(TestClass.class)
|
||||
.build()).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Function object must be provided for non-static methods!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodNotExistingThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder().method("methodName").targetClass(TestClass.class).build())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Method: 'methodName' with arguments:[] not found!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodAndNameIsNullThenThrow() {
|
||||
assertThatThrownBy(() -> FunctionCallback.builder()
|
||||
.method("staticMethodName", String.class, Integer.class)
|
||||
.targetClass(TestClass.class)
|
||||
.name(null)
|
||||
.build()).isInstanceOf(IllegalArgumentException.class).hasMessage("Name must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodAndTargetClassThenReturn() {
|
||||
var functionCallback = FunctionCallback.builder()
|
||||
.method("staticMethodName", String.class, Integer.class)
|
||||
.targetClass(TestClass.class)
|
||||
.build();
|
||||
assertThat(functionCallback).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMethodAndTargetObjectThenReturn() {
|
||||
var functionCallback = FunctionCallback.builder()
|
||||
.method("methodName", String.class, Integer.class)
|
||||
.targetObject(new TestClass())
|
||||
.build();
|
||||
assertThat(functionCallback).isNotNull();
|
||||
}
|
||||
|
||||
public static class TestClass {
|
||||
|
||||
public static String staticMethodName(String arg1, Integer arg2) {
|
||||
return arg1 + arg2;
|
||||
}
|
||||
|
||||
public String methodName(String arg1, Integer arg2) {
|
||||
return arg1 + arg2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public record Request(String value) {
|
||||
}
|
||||
|
||||
public static class GenericsRequest<T> {
|
||||
|
||||
private T datum;
|
||||
|
||||
public T getDatum() {
|
||||
return this.datum;
|
||||
}
|
||||
|
||||
public void setDatum(T value) {
|
||||
this.datum = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,580 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.model.function;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DefaultFunctionCallingOptionsBuilder}.
|
||||
*
|
||||
*/
|
||||
class DefaultFunctionCallingOptionsBuilderTests {
|
||||
|
||||
private DefaultFunctionCallingOptionsBuilder builder;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.builder = new DefaultFunctionCallingOptionsBuilder();
|
||||
}
|
||||
|
||||
// Tests for inherited ChatOptions properties
|
||||
|
||||
@Test
|
||||
void shouldBuildWithModel() {
|
||||
// When
|
||||
ChatOptions options = this.builder.model("gpt-4").build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getModel()).isEqualTo("gpt-4");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithFrequencyPenalty() {
|
||||
// When
|
||||
ChatOptions options = this.builder.frequencyPenalty(0.5).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getFrequencyPenalty()).isEqualTo(0.5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithMaxTokens() {
|
||||
// When
|
||||
ChatOptions options = this.builder.maxTokens(100).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getMaxTokens()).isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithPresencePenalty() {
|
||||
// When
|
||||
ChatOptions options = this.builder.presencePenalty(0.7).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getPresencePenalty()).isEqualTo(0.7);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithStopSequences() {
|
||||
// Given
|
||||
List<String> stopSequences = List.of("stop1", "stop2");
|
||||
|
||||
// When
|
||||
ChatOptions options = this.builder.stopSequences(stopSequences).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getStopSequences()).hasSize(2).containsExactlyElementsOf(stopSequences);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithTemperature() {
|
||||
// When
|
||||
ChatOptions options = this.builder.temperature(0.8).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getTemperature()).isEqualTo(0.8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithTopK() {
|
||||
// When
|
||||
ChatOptions options = this.builder.topK(5).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getTopK()).isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithTopP() {
|
||||
// When
|
||||
ChatOptions options = this.builder.topP(0.9).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getTopP()).isEqualTo(0.9);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithAllInheritedOptions() {
|
||||
// When
|
||||
ChatOptions options = this.builder.model("gpt-4")
|
||||
.frequencyPenalty(0.5)
|
||||
.maxTokens(100)
|
||||
.presencePenalty(0.7)
|
||||
.stopSequences(List.of("stop1", "stop2"))
|
||||
.temperature(0.8)
|
||||
.topK(5)
|
||||
.topP(0.9)
|
||||
.build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getModel()).isEqualTo("gpt-4");
|
||||
assertThat(options.getFrequencyPenalty()).isEqualTo(0.5);
|
||||
assertThat(options.getMaxTokens()).isEqualTo(100);
|
||||
assertThat(options.getPresencePenalty()).isEqualTo(0.7);
|
||||
assertThat(options.getStopSequences()).containsExactly("stop1", "stop2");
|
||||
assertThat(options.getTemperature()).isEqualTo(0.8);
|
||||
assertThat(options.getTopK()).isEqualTo(5);
|
||||
assertThat(options.getTopP()).isEqualTo(0.9);
|
||||
}
|
||||
|
||||
// Original FunctionCallingOptions tests
|
||||
|
||||
@Test
|
||||
void shouldBuildWithFunctionCallbacksList() {
|
||||
// Given
|
||||
FunctionCallback callback1 = FunctionCallback.builder()
|
||||
.function("test1", (String input) -> "result1")
|
||||
.description("Test function 1")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
FunctionCallback callback2 = FunctionCallback.builder()
|
||||
.function("test2", (String input) -> "result2")
|
||||
.description("Test function 2")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
List<FunctionCallback> callbacks = List.of(callback1, callback2);
|
||||
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.functionCallbacks(callbacks).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getFunctionCallbacks()).hasSize(2).containsExactlyElementsOf(callbacks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithFunctionCallbacksVarargs() {
|
||||
// Given
|
||||
FunctionCallback callback1 = FunctionCallback.builder()
|
||||
.function("test1", (String input) -> "result1")
|
||||
.description("Test function 1")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
FunctionCallback callback2 = FunctionCallback.builder()
|
||||
.function("test2", (String input) -> "result2")
|
||||
.description("Test function 2")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.functionCallbacks(callback1, callback2).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getFunctionCallbacks()).hasSize(2).containsExactly(callback1, callback2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenFunctionCallbacksVarargsIsNull() {
|
||||
assertThatThrownBy(() -> this.builder.functionCallbacks((FunctionCallback[]) null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("FunctionCallbacks must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithFunctionsSet() {
|
||||
// Given
|
||||
Set<String> functions = Set.of("function1", "function2");
|
||||
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.functions(functions).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getFunctions()).hasSize(2).containsExactlyInAnyOrderElementsOf(functions);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithSingleFunction() {
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.function("function1").function("function2").build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getFunctions()).hasSize(2).containsExactlyInAnyOrder("function1", "function2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenFunctionIsNull() {
|
||||
assertThatThrownBy(() -> this.builder.function(null)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Function must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithProxyToolCalls() {
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.proxyToolCalls(true).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getProxyToolCalls()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithToolContextMap() {
|
||||
// Given
|
||||
Map<String, Object> context = Map.of("key1", "value1", "key2", 42);
|
||||
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.toolContext(context).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getToolContext()).hasSize(2).containsAllEntriesOf(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenToolContextMapIsNull() {
|
||||
assertThatThrownBy(() -> this.builder.toolContext((Map<String, Object>) null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Tool context must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithToolContextKeyValue() {
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.toolContext("key1", "value1").toolContext("key2", 42).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getToolContext()).hasSize(2).containsEntry("key1", "value1").containsEntry("key2", 42);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenToolContextKeyIsNull() {
|
||||
assertThatThrownBy(() -> this.builder.toolContext(null, "value")).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Key must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenToolContextValueIsNull() {
|
||||
assertThatThrownBy(() -> this.builder.toolContext("key", null)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Value must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMergeToolContextMaps() {
|
||||
// Given
|
||||
Map<String, Object> context1 = Map.of("key1", "value1", "key2", 42);
|
||||
Map<String, Object> context2 = Map.of("key2", "updated", "key3", true);
|
||||
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.toolContext(context1).toolContext(context2).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getToolContext()).hasSize(3)
|
||||
.containsEntry("key1", "value1")
|
||||
.containsEntry("key2", "updated")
|
||||
.containsEntry("key3", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithAllOptions() {
|
||||
// Given
|
||||
FunctionCallback callback = FunctionCallback.builder()
|
||||
.function("test", (String input) -> "result")
|
||||
.description("Test function")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
Set<String> functions = Set.of("function1");
|
||||
Map<String, Object> context = Map.of("key1", "value1");
|
||||
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.model("gpt-4")
|
||||
.frequencyPenalty(0.5)
|
||||
.maxTokens(100)
|
||||
.presencePenalty(0.7)
|
||||
.stopSequences(List.of("stop1", "stop2"))
|
||||
.temperature(0.8)
|
||||
.topK(5)
|
||||
.topP(0.9)
|
||||
.functionCallbacks(callback)
|
||||
.functions(functions)
|
||||
.proxyToolCalls(true)
|
||||
.toolContext(context)
|
||||
.build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getFunctionCallbacks()).hasSize(1).containsExactly(callback);
|
||||
assertThat(options.getFunctions()).hasSize(1).containsExactlyElementsOf(functions);
|
||||
assertThat(options.getProxyToolCalls()).isTrue();
|
||||
assertThat(options.getToolContext()).hasSize(1).containsAllEntriesOf(context);
|
||||
|
||||
ChatOptions chatOptions = options;
|
||||
assertThat(chatOptions.getModel()).isEqualTo("gpt-4");
|
||||
assertThat(chatOptions.getFrequencyPenalty()).isEqualTo(0.5);
|
||||
assertThat(chatOptions.getMaxTokens()).isEqualTo(100);
|
||||
assertThat(chatOptions.getPresencePenalty()).isEqualTo(0.7);
|
||||
assertThat(chatOptions.getStopSequences()).containsExactly("stop1", "stop2");
|
||||
assertThat(chatOptions.getTemperature()).isEqualTo(0.8);
|
||||
assertThat(chatOptions.getTopK()).isEqualTo(5);
|
||||
assertThat(chatOptions.getTopP()).isEqualTo(0.9);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithEmptyFunctionCallbacks() {
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.functionCallbacks(List.of()).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getFunctionCallbacks()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithEmptyFunctions() {
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.functions(Set.of()).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getFunctions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildWithEmptyToolContext() {
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.toolContext(Map.of()).build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getToolContext()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeduplicateFunctions() {
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.function("function1")
|
||||
.function("function1") // Duplicate
|
||||
.function("function2")
|
||||
.build();
|
||||
|
||||
// Then
|
||||
assertThat(options.getFunctions()).hasSize(2).containsExactlyInAnyOrder("function1", "function2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCopyAllOptions() {
|
||||
// Given
|
||||
FunctionCallback callback = FunctionCallback.builder()
|
||||
.function("test", (String input) -> "result")
|
||||
.description("Test function")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
FunctionCallingOptions original = this.builder.model("gpt-4")
|
||||
.frequencyPenalty(0.5)
|
||||
.maxTokens(100)
|
||||
.presencePenalty(0.7)
|
||||
.stopSequences(List.of("stop1", "stop2"))
|
||||
.temperature(0.8)
|
||||
.topK(5)
|
||||
.topP(0.9)
|
||||
.functionCallbacks(callback)
|
||||
.function("function1")
|
||||
.proxyToolCalls(true)
|
||||
.toolContext("key1", "value1")
|
||||
.build();
|
||||
|
||||
// When
|
||||
FunctionCallingOptions copy = original.copy();
|
||||
|
||||
// Then
|
||||
assertThat(copy).usingRecursiveComparison().isEqualTo(original);
|
||||
// Verify collections are actually copied
|
||||
assertThat(copy.getFunctionCallbacks()).isNotSameAs(original.getFunctionCallbacks());
|
||||
assertThat(copy.getFunctions()).isNotSameAs(original.getFunctions());
|
||||
assertThat(copy.getToolContext()).isNotSameAs(original.getToolContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMergeWithFunctionCallingOptions() {
|
||||
// Given
|
||||
FunctionCallback callback1 = FunctionCallback.builder()
|
||||
.function("test1", (String input) -> "result1")
|
||||
.description("Test function 1")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
FunctionCallback callback2 = FunctionCallback.builder()
|
||||
.function("test2", (String input) -> "result2")
|
||||
.description("Test function 2")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
|
||||
DefaultFunctionCallingOptions options1 = (DefaultFunctionCallingOptions) this.builder.model("gpt-4")
|
||||
.temperature(0.8)
|
||||
.functionCallbacks(callback1)
|
||||
.function("function1")
|
||||
.proxyToolCalls(true)
|
||||
.toolContext("key1", "value1")
|
||||
.build();
|
||||
|
||||
DefaultFunctionCallingOptions options2 = (DefaultFunctionCallingOptions) FunctionCallingOptions.builder()
|
||||
.model("gpt-3.5")
|
||||
.maxTokens(100)
|
||||
.functionCallbacks(callback2)
|
||||
.function("function2")
|
||||
.proxyToolCalls(false)
|
||||
.toolContext("key2", "value2")
|
||||
.build();
|
||||
|
||||
// When
|
||||
FunctionCallingOptions merged = options1.merge(options2);
|
||||
|
||||
// Then
|
||||
assertThat(merged.getModel()).isEqualTo("gpt-3.5"); // Overridden
|
||||
assertThat(merged.getTemperature()).isEqualTo(0.8); // Kept
|
||||
assertThat(merged.getMaxTokens()).isEqualTo(100); // Added
|
||||
assertThat(merged.getFunctionCallbacks()).containsExactly(callback1, callback2); // Combined
|
||||
assertThat(merged.getFunctions()).containsExactlyInAnyOrder("function1", "function2"); // Combined
|
||||
assertThat(merged.getProxyToolCalls()).isFalse(); // Overridden
|
||||
assertThat(merged.getToolContext()).containsEntry("key1", "value1").containsEntry("key2", "value2"); // Combined
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMergeWithChatOptions() {
|
||||
// Given
|
||||
FunctionCallback callback = FunctionCallback.builder()
|
||||
.function("test", (String input) -> "result")
|
||||
.description("Test function")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
|
||||
DefaultFunctionCallingOptions options1 = (DefaultFunctionCallingOptions) this.builder.model("gpt-4")
|
||||
.temperature(0.8)
|
||||
.functionCallbacks(callback)
|
||||
.function("function1")
|
||||
.proxyToolCalls(true)
|
||||
.toolContext("key1", "value1")
|
||||
.build();
|
||||
|
||||
ChatOptions options2 = ChatOptions.builder().model("gpt-3.5").maxTokens(100).build();
|
||||
|
||||
// When
|
||||
FunctionCallingOptions merged = options1.merge(options2);
|
||||
|
||||
// Then
|
||||
assertThat(merged.getModel()).isEqualTo("gpt-3.5"); // Overridden
|
||||
assertThat(merged.getTemperature()).isEqualTo(0.8); // Kept
|
||||
assertThat(merged.getMaxTokens()).isEqualTo(100); // Added
|
||||
// Function-specific options should be preserved
|
||||
assertThat(merged.getFunctionCallbacks()).containsExactly(callback);
|
||||
assertThat(merged.getFunctions()).containsExactly("function1");
|
||||
assertThat(merged.getProxyToolCalls()).isTrue();
|
||||
assertThat(merged.getToolContext()).containsEntry("key1", "value1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowBuilderReuse() {
|
||||
// Given
|
||||
FunctionCallback callback1 = FunctionCallback.builder()
|
||||
.function("test1", (String input) -> "result1")
|
||||
.description("Test function 1")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
FunctionCallback callback2 = FunctionCallback.builder()
|
||||
.function("test2", (String input) -> "result2")
|
||||
.description("Test function 2")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
|
||||
// When
|
||||
FunctionCallingOptions options1 = this.builder.model("model1")
|
||||
.temperature(0.7)
|
||||
.functionCallbacks(callback1)
|
||||
.build();
|
||||
|
||||
FunctionCallingOptions options2 = this.builder.model("model2").functionCallbacks(callback2).build();
|
||||
|
||||
// Then
|
||||
assertThat(options1.getModel()).isEqualTo("model1");
|
||||
assertThat(options1.getTemperature()).isEqualTo(0.7);
|
||||
assertThat(options1.getFunctionCallbacks()).containsExactly(callback1);
|
||||
|
||||
assertThat(options2.getModel()).isEqualTo("model2");
|
||||
assertThat(options2.getTemperature()).isEqualTo(0.7); // Retains previous value
|
||||
assertThat(options2.getFunctionCallbacks()).containsExactly(callback2); // Replaces
|
||||
// previous
|
||||
// callbacks
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSameBuilderInstanceOnEachMethod() {
|
||||
// When
|
||||
FunctionCallingOptions.Builder returnedBuilder = this.builder.model("test");
|
||||
|
||||
// Then
|
||||
assertThat(returnedBuilder).isSameAs(this.builder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHaveExpectedDefaultValues() {
|
||||
// When
|
||||
FunctionCallingOptions options = this.builder.build();
|
||||
|
||||
// Then
|
||||
// ChatOptions defaults
|
||||
assertThat(options.getModel()).isNull();
|
||||
assertThat(options.getTemperature()).isNull();
|
||||
assertThat(options.getMaxTokens()).isNull();
|
||||
assertThat(options.getTopP()).isNull();
|
||||
assertThat(options.getTopK()).isNull();
|
||||
assertThat(options.getFrequencyPenalty()).isNull();
|
||||
assertThat(options.getPresencePenalty()).isNull();
|
||||
assertThat(options.getStopSequences()).isNull();
|
||||
|
||||
// FunctionCallingOptions specific defaults
|
||||
assertThat(options.getFunctionCallbacks()).isEmpty();
|
||||
assertThat(options.getFunctions()).isEmpty();
|
||||
assertThat(options.getToolContext()).isEmpty();
|
||||
assertThat(options.getProxyToolCalls()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeImmutableAfterBuild() {
|
||||
// Given
|
||||
FunctionCallback callback = FunctionCallback.builder()
|
||||
.function("test", (String input) -> "result")
|
||||
.description("Test function")
|
||||
.inputType(String.class)
|
||||
.build();
|
||||
|
||||
List<String> stopSequences = new ArrayList<>(List.of("stop1", "stop2"));
|
||||
Set<String> functions = new HashSet<>(Set.of("function1", "function2"));
|
||||
Map<String, Object> context = new HashMap<>(Map.of("key1", "value1"));
|
||||
|
||||
FunctionCallingOptions options = this.builder.stopSequences(stopSequences)
|
||||
.functionCallbacks(callback)
|
||||
.functions(functions)
|
||||
.toolContext(context)
|
||||
.build();
|
||||
|
||||
// Then
|
||||
assertThatThrownBy(() -> options.getStopSequences().add("stop3"))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
assertThatThrownBy(() -> options.getFunctionCallbacks().add(callback))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
assertThatThrownBy(() -> options.getFunctions().add("function3"))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
assertThatThrownBy(() -> options.getToolContext().put("key2", "value2"))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,161 +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 java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class MethodInvokingFunctionCallbackTests {
|
||||
|
||||
private static final Map<String, Object> arguments = new ConcurrentHashMap<>();
|
||||
|
||||
String value = """
|
||||
{
|
||||
"unit": "CELSIUS",
|
||||
"city": "Barcelona",
|
||||
"intNumber": 123,
|
||||
"record": {
|
||||
"foo": "foo",
|
||||
"bar": "bar"
|
||||
},
|
||||
"intList": [1, 2, 3]
|
||||
}
|
||||
""";
|
||||
|
||||
@BeforeEach
|
||||
public void beforeEach() {
|
||||
arguments.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staticMethod() throws NoSuchMethodException, SecurityException {
|
||||
|
||||
var functionCallback = FunctionCallback.builder()
|
||||
.method("myStaticMethod", String.class, Unit.class, int.class, MyRecord.class, List.class)
|
||||
.description("weather at location")
|
||||
.objectMapper(new ObjectMapper())
|
||||
.targetClass(TestClassWithFunctionMethods.class)
|
||||
.build();
|
||||
|
||||
String response = functionCallback.call(this.value);
|
||||
|
||||
assertThat(response).isEqualTo("23");
|
||||
|
||||
assertThat(arguments).hasSize(5);
|
||||
assertThat(arguments.get("city")).isEqualTo("Barcelona");
|
||||
assertThat(arguments.get("unit")).isEqualTo(Unit.CELSIUS);
|
||||
assertThat(arguments.get("intNumber")).isEqualTo(123);
|
||||
assertThat(arguments.get("record")).isEqualTo(new MyRecord("foo", "bar"));
|
||||
assertThat(arguments.get("intList")).isEqualTo(List.of(1, 2, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonStaticMethod() throws NoSuchMethodException, SecurityException {
|
||||
|
||||
var object = new TestClassWithFunctionMethods();
|
||||
|
||||
var functionCallback = FunctionCallback.builder()
|
||||
.method("myNonStaticMethod", String.class, Unit.class, int.class, MyRecord.class, List.class)
|
||||
.description("weather at location")
|
||||
.targetObject(object)
|
||||
.build();
|
||||
|
||||
String response = functionCallback.call(this.value);
|
||||
|
||||
assertThat(response).isEqualTo("23");
|
||||
|
||||
assertThat(arguments).hasSize(5);
|
||||
assertThat(arguments.get("city")).isEqualTo("Barcelona");
|
||||
assertThat(arguments.get("unit")).isEqualTo(Unit.CELSIUS);
|
||||
assertThat(arguments.get("intNumber")).isEqualTo(123);
|
||||
assertThat(arguments.get("record")).isEqualTo(new MyRecord("foo", "bar"));
|
||||
assertThat(arguments.get("intList")).isEqualTo(List.of(1, 2, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noArgsNoReturnMethod() throws NoSuchMethodException, SecurityException {
|
||||
|
||||
var functionCallback = FunctionCallback.builder()
|
||||
.method("argumentLessReturnVoid")
|
||||
.description("weather at location")
|
||||
.objectMapper(new ObjectMapper())
|
||||
.targetClass(TestClassWithFunctionMethods.class)
|
||||
.build();
|
||||
|
||||
String response = functionCallback.call(this.value);
|
||||
|
||||
assertThat(response).isEqualTo("Done");
|
||||
|
||||
assertThat(arguments.get("method called")).isEqualTo("argumentLessReturnVoid");
|
||||
}
|
||||
|
||||
record MyRecord(String foo, String bar) {
|
||||
}
|
||||
|
||||
public enum Unit {
|
||||
|
||||
CELSIUS, FAHRENHEIT
|
||||
|
||||
}
|
||||
|
||||
public static class TestClassWithFunctionMethods {
|
||||
|
||||
public static void argumentLessReturnVoid() {
|
||||
arguments.put("method called", "argumentLessReturnVoid");
|
||||
}
|
||||
|
||||
public static String myStaticMethod(String city, Unit unit, int intNumber, MyRecord record,
|
||||
List<Integer> intList) {
|
||||
System.out.println("City: " + city + " Unit: " + unit + " intNumber: " + intNumber + " Record: " + record
|
||||
+ " List: " + intList);
|
||||
|
||||
arguments.put("city", city);
|
||||
arguments.put("unit", unit);
|
||||
arguments.put("intNumber", intNumber);
|
||||
arguments.put("record", record);
|
||||
arguments.put("intList", intList);
|
||||
|
||||
return "23";
|
||||
}
|
||||
|
||||
public String myNonStaticMethod(String city, Unit unit, int intNumber, MyRecord record, List<Integer> intList) {
|
||||
System.out.println("City: " + city + " Unit: " + unit + " intNumber: " + intNumber + " Record: " + record
|
||||
+ " List: " + intList);
|
||||
|
||||
arguments.put("city", city);
|
||||
arguments.put("unit", unit);
|
||||
arguments.put("intNumber", intNumber);
|
||||
arguments.put("record", record);
|
||||
arguments.put("intList", intList);
|
||||
|
||||
return "23";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -42,7 +41,7 @@ class DefaultToolCallingChatOptionsTests {
|
||||
DefaultToolCallingChatOptions options = new DefaultToolCallingChatOptions();
|
||||
ToolCallback callback1 = mock(ToolCallback.class);
|
||||
ToolCallback callback2 = mock(ToolCallback.class);
|
||||
List<FunctionCallback> callbacks = List.of(callback1, callback2);
|
||||
List<ToolCallback> callbacks = List.of(callback1, callback2);
|
||||
|
||||
options.setToolCallbacks(callbacks);
|
||||
|
||||
@@ -221,22 +220,19 @@ class DefaultToolCallingChatOptionsTests {
|
||||
void deprecatedMethodsShouldWorkCorrectly() {
|
||||
DefaultToolCallingChatOptions options = new DefaultToolCallingChatOptions();
|
||||
|
||||
FunctionCallback callback1 = mock(FunctionCallback.class);
|
||||
ToolCallback callback1 = mock(ToolCallback.class);
|
||||
ToolCallback callback2 = mock(ToolCallback.class);
|
||||
options.setFunctionCallbacks(List.of(callback1, callback2));
|
||||
assertThat(options.getFunctionCallbacks()).hasSize(2);
|
||||
options.setToolCallbacks(List.of(callback1, callback2));
|
||||
assertThat(options.getToolCallbacks()).hasSize(2);
|
||||
|
||||
options.setToolNames(Set.of("tool1"));
|
||||
assertThat(options.getFunctions()).containsExactly("tool1");
|
||||
assertThat(options.getToolNames()).containsExactly("tool1");
|
||||
|
||||
options.setFunctions(Set.of("function1"));
|
||||
options.setToolNames(Set.of("function1"));
|
||||
assertThat(options.getToolNames()).containsExactly("function1");
|
||||
|
||||
options.setInternalToolExecutionEnabled(true);
|
||||
assertThat(options.getProxyToolCalls()).isFalse();
|
||||
|
||||
options.setProxyToolCalls(true);
|
||||
assertThat(options.getInternalToolExecutionEnabled()).isFalse();
|
||||
assertThat(options.getInternalToolExecutionEnabled()).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -112,22 +112,6 @@ class DefaultToolExecutionEligibilityPredicateTests {
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionCallingOptionsAndToolExecutionDisabled() {
|
||||
// Create a FunctionCallingOptions with proxy tool calls enabled (which means
|
||||
// internal tool execution is disabled)
|
||||
FunctionCallingOptions options = FunctionCallingOptions.builder().proxyToolCalls(true).build();
|
||||
|
||||
// Create a ChatResponse with tool calls
|
||||
AssistantMessage.ToolCall toolCall = new AssistantMessage.ToolCall("id1", "function", "testTool", "{}");
|
||||
AssistantMessage assistantMessage = new AssistantMessage("test", Map.of(), List.of(toolCall));
|
||||
ChatResponse chatResponse = new ChatResponse(List.of(new Generation(assistantMessage)));
|
||||
|
||||
// Test the predicate
|
||||
boolean result = this.predicate.test(options, chatResponse);
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenRegularChatOptionsAndHasToolCalls() {
|
||||
// Create regular ChatOptions (not ToolCallingChatOptions or
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023-2025 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.tool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.ToolResponseMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.model.Generation;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
import org.springframework.ai.tool.execution.ToolExecutionException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link LegacyToolCallingManager}.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
class LegacyToolCallingManagerTests {
|
||||
|
||||
// RESOLVE TOOL DEFINITIONS
|
||||
|
||||
@Test
|
||||
void whenChatOptionsIsNullThenThrow() {
|
||||
ToolCallingManager toolCallingManager = LegacyToolCallingManager.builder().build();
|
||||
assertThatThrownBy(() -> toolCallingManager.resolveToolDefinitions(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("chatOptions cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenToolCallbackExistsThenResolve() {
|
||||
ToolCallback toolCallback = new TestToolCallback("toolA");
|
||||
ToolCallingManager toolCallingManager = LegacyToolCallingManager.builder()
|
||||
.functionCallbacks(List.of(toolCallback))
|
||||
.build();
|
||||
|
||||
List<ToolDefinition> toolDefinitions = toolCallingManager
|
||||
.resolveToolDefinitions(ToolCallingChatOptions.builder().toolNames("toolA").build());
|
||||
|
||||
assertThat(toolDefinitions).containsExactly(toolCallback.getToolDefinition());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenToolCallbackDoesNotExistThenThrow() {
|
||||
ToolCallingManager toolCallingManager = LegacyToolCallingManager.builder().functionCallbacks(List.of()).build();
|
||||
|
||||
assertThatThrownBy(() -> toolCallingManager
|
||||
.resolveToolDefinitions(ToolCallingChatOptions.builder().toolNames("toolB").build()))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("No ToolCallback found for tool name: toolB");
|
||||
}
|
||||
|
||||
// EXECUTE TOOL CALLS
|
||||
|
||||
@Test
|
||||
void whenPromptIsNullThenThrow() {
|
||||
ToolCallingManager toolCallingManager = LegacyToolCallingManager.builder().build();
|
||||
assertThatThrownBy(() -> toolCallingManager.executeToolCalls(null, mock(ChatResponse.class)))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("prompt cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenChatResponseIsNullThenThrow() {
|
||||
ToolCallingManager toolCallingManager = LegacyToolCallingManager.builder().build();
|
||||
assertThatThrownBy(() -> toolCallingManager.executeToolCalls(mock(Prompt.class), null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("chatResponse cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoToolCallInChatResponseThenThrow() {
|
||||
ToolCallingManager toolCallingManager = LegacyToolCallingManager.builder().build();
|
||||
assertThatThrownBy(() -> toolCallingManager.executeToolCalls(mock(Prompt.class),
|
||||
ChatResponse.builder().generations(List.of()).build()))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("No tool call requested by the chat model");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenSingleToolCallInChatResponseThenExecute() {
|
||||
ToolCallback toolCallback = new TestToolCallback("toolA");
|
||||
ToolCallingManager toolCallingManager = LegacyToolCallingManager.builder()
|
||||
.functionCallbacks(List.of(toolCallback))
|
||||
.build();
|
||||
|
||||
Prompt prompt = new Prompt(new UserMessage("Hello"), ToolCallingChatOptions.builder().build());
|
||||
ChatResponse chatResponse = ChatResponse.builder()
|
||||
.generations(List.of(new Generation(new AssistantMessage("", Map.of(),
|
||||
List.of(new AssistantMessage.ToolCall("toolA", "function", "toolA", "{}"))))))
|
||||
.build();
|
||||
|
||||
ToolResponseMessage expectedToolResponse = new ToolResponseMessage(
|
||||
List.of(new ToolResponseMessage.ToolResponse("toolA", "toolA", "Mission accomplished!")));
|
||||
|
||||
ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, chatResponse);
|
||||
|
||||
assertThat(toolExecutionResult.conversationHistory()).contains(expectedToolResponse);
|
||||
assertThat(toolExecutionResult.returnDirect()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleToolCallsInChatResponseThenExecute() {
|
||||
ToolCallback toolCallbackA = new TestToolCallback("toolA");
|
||||
ToolCallback toolCallbackB = new TestToolCallback("toolB");
|
||||
ToolCallingManager toolCallingManager = LegacyToolCallingManager.builder()
|
||||
.functionCallbacks(List.of(toolCallbackA, toolCallbackB))
|
||||
.build();
|
||||
|
||||
Prompt prompt = new Prompt(new UserMessage("Hello"), ToolCallingChatOptions.builder().build());
|
||||
ChatResponse chatResponse = ChatResponse.builder()
|
||||
.generations(List.of(new Generation(new AssistantMessage("", Map.of(),
|
||||
List.of(new AssistantMessage.ToolCall("toolA", "function", "toolA", "{}"),
|
||||
new AssistantMessage.ToolCall("toolB", "function", "toolB", "{}"))))))
|
||||
.build();
|
||||
|
||||
ToolResponseMessage expectedToolResponse = new ToolResponseMessage(
|
||||
List.of(new ToolResponseMessage.ToolResponse("toolA", "toolA", "Mission accomplished!"),
|
||||
new ToolResponseMessage.ToolResponse("toolB", "toolB", "Mission accomplished!")));
|
||||
|
||||
ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, chatResponse);
|
||||
|
||||
assertThat(toolExecutionResult.conversationHistory()).contains(expectedToolResponse);
|
||||
assertThat(toolExecutionResult.returnDirect()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenToolCallWithExceptionThenReturnError() {
|
||||
ToolCallback toolCallback = new FailingToolCallback("toolC");
|
||||
ToolCallingManager toolCallingManager = LegacyToolCallingManager.builder()
|
||||
.functionCallbacks(List.of(toolCallback))
|
||||
.build();
|
||||
|
||||
Prompt prompt = new Prompt(new UserMessage("Hello"), ToolCallingChatOptions.builder().build());
|
||||
ChatResponse chatResponse = ChatResponse.builder()
|
||||
.generations(List.of(new Generation(new AssistantMessage("", Map.of(),
|
||||
List.of(new AssistantMessage.ToolCall("toolC", "function", "toolC", "{}"))))))
|
||||
.build();
|
||||
|
||||
ToolResponseMessage expectedToolResponse = new ToolResponseMessage(
|
||||
List.of(new ToolResponseMessage.ToolResponse("toolC", "toolC", "You failed this city!")));
|
||||
|
||||
ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, chatResponse);
|
||||
|
||||
assertThat(toolExecutionResult.conversationHistory()).contains(expectedToolResponse);
|
||||
assertThat(toolExecutionResult.returnDirect()).isFalse();
|
||||
}
|
||||
|
||||
static class TestToolCallback implements ToolCallback {
|
||||
|
||||
private final ToolDefinition toolDefinition;
|
||||
|
||||
TestToolCallback(String name) {
|
||||
this.toolDefinition = ToolDefinition.builder().name(name).inputSchema("{}").build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return this.toolDefinition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String toolInput) {
|
||||
return "Mission accomplished!";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class FailingToolCallback implements ToolCallback {
|
||||
|
||||
private final ToolDefinition toolDefinition;
|
||||
|
||||
FailingToolCallback(String name) {
|
||||
this.toolDefinition = ToolDefinition.builder().name(name).inputSchema("{}").build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return this.toolDefinition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String toolInput) {
|
||||
throw new ToolExecutionException(this.toolDefinition, new IllegalStateException("You failed this city!"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallingOptions;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
@@ -64,19 +63,6 @@ class ToolCallingChatOptionsTests {
|
||||
assertThat(ToolCallingChatOptions.isInternalToolExecutionEnabled(options)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionCallingOptionsAndExecutionEnabledFalse() {
|
||||
FunctionCallingOptions options = FunctionCallingOptions.builder().build();
|
||||
options.setProxyToolCalls(true);
|
||||
assertThat(ToolCallingChatOptions.isInternalToolExecutionEnabled(options)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFunctionCallingOptionsAndExecutionEnabledDefault() {
|
||||
FunctionCallingOptions options = FunctionCallingOptions.builder().build();
|
||||
assertThat(ToolCallingChatOptions.isInternalToolExecutionEnabled(options)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMergeRuntimeAndDefaultToolNames() {
|
||||
Set<String> runtimeToolNames = Set.of("toolA");
|
||||
@@ -111,9 +97,9 @@ class ToolCallingChatOptionsTests {
|
||||
|
||||
@Test
|
||||
void whenMergeRuntimeAndDefaultToolCallbacks() {
|
||||
List<FunctionCallback> runtimeToolCallbacks = List.of(new TestToolCallback("toolA"));
|
||||
List<FunctionCallback> defaultToolCallbacks = List.of(new TestToolCallback("toolB"));
|
||||
List<FunctionCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
|
||||
List<ToolCallback> runtimeToolCallbacks = List.of(new TestToolCallback("toolA"));
|
||||
List<ToolCallback> defaultToolCallbacks = List.of(new TestToolCallback("toolB"));
|
||||
List<ToolCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
|
||||
defaultToolCallbacks);
|
||||
assertThat(mergedToolCallbacks).hasSize(1);
|
||||
assertThat(mergedToolCallbacks.get(0).getName()).isEqualTo("toolA");
|
||||
@@ -121,9 +107,9 @@ class ToolCallingChatOptionsTests {
|
||||
|
||||
@Test
|
||||
void whenMergeRuntimeAndEmptyDefaultToolCallbacks() {
|
||||
List<FunctionCallback> runtimeToolCallbacks = List.of(new TestToolCallback("toolA"));
|
||||
List<FunctionCallback> defaultToolCallbacks = List.of();
|
||||
List<FunctionCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
|
||||
List<ToolCallback> runtimeToolCallbacks = List.of(new TestToolCallback("toolA"));
|
||||
List<ToolCallback> defaultToolCallbacks = List.of();
|
||||
List<ToolCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
|
||||
defaultToolCallbacks);
|
||||
assertThat(mergedToolCallbacks).hasSize(1);
|
||||
assertThat(mergedToolCallbacks.get(0).getName()).isEqualTo("toolA");
|
||||
@@ -131,9 +117,9 @@ class ToolCallingChatOptionsTests {
|
||||
|
||||
@Test
|
||||
void whenMergeEmptyRuntimeAndDefaultToolCallbacks() {
|
||||
List<FunctionCallback> runtimeToolCallbacks = List.of();
|
||||
List<FunctionCallback> defaultToolCallbacks = List.of(new TestToolCallback("toolB"));
|
||||
List<FunctionCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
|
||||
List<ToolCallback> runtimeToolCallbacks = List.of();
|
||||
List<ToolCallback> defaultToolCallbacks = List.of(new TestToolCallback("toolB"));
|
||||
List<ToolCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
|
||||
defaultToolCallbacks);
|
||||
assertThat(mergedToolCallbacks).hasSize(1);
|
||||
assertThat(mergedToolCallbacks.get(0).getName()).isEqualTo("toolB");
|
||||
@@ -141,9 +127,9 @@ class ToolCallingChatOptionsTests {
|
||||
|
||||
@Test
|
||||
void whenMergeEmptyRuntimeAndEmptyDefaultToolCallbacks() {
|
||||
List<FunctionCallback> runtimeToolCallbacks = List.of();
|
||||
List<FunctionCallback> defaultToolCallbacks = List.of();
|
||||
List<FunctionCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
|
||||
List<ToolCallback> runtimeToolCallbacks = List.of();
|
||||
List<ToolCallback> defaultToolCallbacks = List.of();
|
||||
List<ToolCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
|
||||
defaultToolCallbacks);
|
||||
assertThat(mergedToolCallbacks).hasSize(0);
|
||||
}
|
||||
@@ -191,7 +177,7 @@ class ToolCallingChatOptionsTests {
|
||||
|
||||
@Test
|
||||
void shouldEnsureUniqueToolNames() {
|
||||
List<FunctionCallback> toolCallbacks = List.of(new TestToolCallback("toolA"), new TestToolCallback("toolA"));
|
||||
List<ToolCallback> toolCallbacks = List.of(new TestToolCallback("toolA"), new TestToolCallback("toolA"));
|
||||
assertThatThrownBy(() -> ToolCallingChatOptions.validateToolCallbacks(toolCallbacks))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Multiple tools with the same name (toolA)");
|
||||
|
||||
Reference in New Issue
Block a user