Skip to content

Commit d992600

Browse files
committed
refactor: migrate from functions to tools terminology
Refactors the test codebase to use tools instead of functions. - Rename FunctionCallback to FunctionToolCallback - Rename FunctionCallingOptions to ToolCallingChatOptions - Update API methods from functions() to tools() - Deprecate function-related methods in favor of tool alternatives - Refactor MethodToolCallback implementation with improved builder pattern - Update all tests to use new tool-based APIs
1 parent 2f14597 commit d992600

File tree

25 files changed

+219
-174
lines changed

25 files changed

+219
-174
lines changed

models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
import org.springframework.ai.converter.ListOutputConverter;
5050
import org.springframework.ai.converter.MapOutputConverter;
5151
import org.springframework.ai.model.Media;
52-
import org.springframework.ai.model.function.FunctionCallback;
53-
import org.springframework.ai.model.function.FunctionCallingOptions;
52+
import org.springframework.ai.model.tool.ToolCallingChatOptions;
53+
import org.springframework.ai.tool.function.FunctionToolCallback;
5454
import org.springframework.beans.factory.annotation.Autowired;
5555
import org.springframework.beans.factory.annotation.Value;
5656
import org.springframework.boot.SpringBootConfiguration;
@@ -259,7 +259,7 @@ void multiModalityPdfTest() throws IOException {
259259
List.of(new Media(new MimeType("application", "pdf"), pdfData)));
260260

261261
var response = this.chatModel.call(new Prompt(List.of(userMessage),
262-
FunctionCallingOptions.builder().model(AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getName()).build()));
262+
ToolCallingChatOptions.builder().model(AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getName()).build()));
263263

264264
assertThat(response.getResult().getOutput().getText()).containsAnyOf("Spring AI", "portable API");
265265
}
@@ -274,8 +274,7 @@ void functionCallTest() {
274274

275275
var promptOptions = AnthropicChatOptions.builder()
276276
.model(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getName())
277-
.functionCallbacks(List.of(FunctionCallback.builder()
278-
.function("getCurrentWeather", new MockWeatherService())
277+
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
279278
.description(
280279
"Get the weather in location. Return temperature in 36°F or 36°C format. Use multi-turn if needed.")
281280
.inputType(MockWeatherService.Request.class)
@@ -307,8 +306,7 @@ void streamFunctionCallTest() {
307306

308307
var promptOptions = AnthropicChatOptions.builder()
309308
.model(AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getName())
310-
.functionCallbacks(List.of(FunctionCallback.builder()
311-
.function("getCurrentWeather", new MockWeatherService())
309+
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
312310
.description(
313311
"Get the weather in location. Return temperature in 36°F or 36°C format. Use multi-turn if needed.")
314312
.inputType(MockWeatherService.Request.class)
@@ -338,8 +336,7 @@ void streamFunctionCallUsageTest() {
338336

339337
var promptOptions = AnthropicChatOptions.builder()
340338
.model(AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getName())
341-
.functionCallbacks(List.of(FunctionCallback.builder()
342-
.function("getCurrentWeather", new MockWeatherService())
339+
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
343340
.description(
344341
"Get the weather in location. Return temperature in 36°F or 36°C format. Use multi-turn if needed.")
345342
.inputType(MockWeatherService.Request.class)

models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientIT.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
import org.springframework.ai.chat.model.ChatResponse;
4343
import org.springframework.ai.converter.BeanOutputConverter;
4444
import org.springframework.ai.converter.ListOutputConverter;
45-
import org.springframework.ai.model.function.FunctionCallback;
45+
import org.springframework.ai.tool.function.FunctionToolCallback;
4646
import org.springframework.beans.factory.annotation.Autowired;
4747
import org.springframework.beans.factory.annotation.Value;
4848
import org.springframework.boot.test.context.SpringBootTest;
@@ -212,8 +212,7 @@ void functionCallTest() {
212212
// @formatter:off
213213
String response = ChatClient.create(this.chatModel).prompt()
214214
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
215-
.functions(FunctionCallback.builder()
216-
.function("getCurrentWeather", new MockWeatherService())
215+
.tools(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
217216
.inputType(MockWeatherService.Request.class)
218217
.build())
219218
.call()
@@ -231,8 +230,7 @@ void functionCallWithGeneratedDescription() {
231230
// @formatter:off
232231
String response = ChatClient.create(this.chatModel).prompt()
233232
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
234-
.functions(FunctionCallback.builder()
235-
.function("getCurrentWeatherInLocation", new MockWeatherService())
233+
.tools(FunctionToolCallback.builder("getCurrentWeatherInLocation", new MockWeatherService())
236234
.inputType(MockWeatherService.Request.class)
237235
.build())
238236
.call()
@@ -249,8 +247,7 @@ void defaultFunctionCallTest() {
249247

250248
// @formatter:off
251249
String response = ChatClient.builder(this.chatModel)
252-
.defaultFunctions(FunctionCallback.builder()
253-
.function("getCurrentWeather", new MockWeatherService())
250+
.defaultTools(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
254251
.description("Get the weather in location")
255252
.inputType(MockWeatherService.Request.class)
256253
.build())
@@ -272,8 +269,7 @@ void streamFunctionCallTest() {
272269
// @formatter:off
273270
Flux<String> response = ChatClient.create(this.chatModel).prompt()
274271
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
275-
.functions(FunctionCallback.builder()
276-
.function("getCurrentWeather", new MockWeatherService())
272+
.tools(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
277273
.description("Get the weather in location")
278274
.inputType(MockWeatherService.Request.class)
279275
.build())

models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientMethodInvokingFunctionCallbackIT.java

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,20 @@
3131
import org.springframework.ai.chat.messages.Message;
3232
import org.springframework.ai.chat.model.ChatModel;
3333
import org.springframework.ai.chat.model.ToolContext;
34-
import org.springframework.ai.model.function.FunctionCallback;
34+
import org.springframework.ai.tool.definition.ToolDefinition;
35+
import org.springframework.ai.tool.method.MethodToolCallback;
3536
import org.springframework.beans.factory.annotation.Autowired;
3637
import org.springframework.boot.test.context.SpringBootTest;
3738
import org.springframework.test.context.ActiveProfiles;
39+
import org.springframework.util.ReflectionUtils;
3840

3941
import static org.assertj.core.api.Assertions.assertThat;
4042
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
4143

4244
@SpringBootTest(classes = AnthropicTestConfiguration.class, properties = "spring.ai.retry.on-http-codes=429")
4345
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+")
4446
@ActiveProfiles("logging-test")
47+
@SuppressWarnings("null")
4548
class AnthropicChatClientMethodInvokingFunctionCallbackIT {
4649

4750
private static final Logger logger = LoggerFactory
@@ -58,11 +61,14 @@ void beforeEach() {
5861
void methodGetWeatherGeneratedDescription() {
5962

6063
// @formatter:off
64+
var toolMethod = ReflectionUtils.findMethod(
65+
TestFunctionClass.class, "getWeatherInLocation", String.class, Unit.class);
66+
6167
String response = ChatClient.create(this.chatModel).prompt()
6268
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
63-
.functions(FunctionCallback.builder()
64-
.method("getWeatherInLocation", String.class, Unit.class)
65-
.targetClass(TestFunctionClass.class)
69+
.tools(MethodToolCallback.builder()
70+
.toolDefinition(ToolDefinition.builder(toolMethod).build())
71+
.toolMethod(toolMethod)
6672
.build())
6773
.call()
6874
.content();
@@ -77,12 +83,16 @@ void methodGetWeatherGeneratedDescription() {
7783
void methodGetWeatherStatic() {
7884

7985
// @formatter:off
86+
var toolMethod = ReflectionUtils.findMethod(
87+
TestFunctionClass.class, "getWeatherStatic", String.class, Unit.class);
88+
8089
String response = ChatClient.create(this.chatModel).prompt()
8190
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
82-
.functions(FunctionCallback.builder()
83-
.method("getWeatherStatic", String.class, Unit.class)
84-
.description("Get the weather in location")
85-
.targetClass(TestFunctionClass.class)
91+
.tools(MethodToolCallback.builder()
92+
.toolDefinition(ToolDefinition.builder(toolMethod)
93+
.description("Get the weather in location")
94+
.build())
95+
.toolMethod(toolMethod)
8696
.build())
8797
.call()
8898
.content();
@@ -99,12 +109,18 @@ void methodTurnLightNoResponse() {
99109
TestFunctionClass targetObject = new TestFunctionClass();
100110

101111
// @formatter:off
112+
113+
var turnLightMethod = ReflectionUtils.findMethod(
114+
TestFunctionClass.class, "turnLight", String.class, boolean.class);
115+
102116
String response = ChatClient.create(this.chatModel).prompt()
103117
.user("Turn light on in the living room.")
104-
.functions(FunctionCallback.builder()
105-
.method("turnLight", String.class, boolean.class)
106-
.description("Turn light on in the living room.")
107-
.targetObject(targetObject)
118+
.tools(MethodToolCallback.builder()
119+
.toolDefinition(ToolDefinition.builder(turnLightMethod)
120+
.description("Turn light on in the living room.")
121+
.build())
122+
.toolMethod(turnLightMethod)
123+
.toolObject(targetObject)
108124
.build())
109125
.call()
110126
.content();
@@ -122,12 +138,17 @@ void methodGetWeatherNonStatic() {
122138
TestFunctionClass targetObject = new TestFunctionClass();
123139

124140
// @formatter:off
141+
var toolMethod = ReflectionUtils.findMethod(
142+
TestFunctionClass.class, "getWeatherNonStatic", String.class, Unit.class);
143+
125144
String response = ChatClient.create(this.chatModel).prompt()
126145
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
127-
.functions(FunctionCallback.builder()
128-
.method("getWeatherNonStatic", String.class, Unit.class)
129-
.description("Get the weather in location")
130-
.targetObject(targetObject)
146+
.tools(MethodToolCallback.builder()
147+
.toolDefinition(ToolDefinition.builder(toolMethod)
148+
.description("Get the weather in location")
149+
.build())
150+
.toolMethod(toolMethod)
151+
.toolObject(targetObject)
131152
.build())
132153
.call()
133154
.content();
@@ -144,17 +165,21 @@ void methodGetWeatherToolContext() {
144165
TestFunctionClass targetObject = new TestFunctionClass();
145166

146167
// @formatter:off
168+
var toolMethod = ReflectionUtils.findMethod(
169+
TestFunctionClass.class, "getWeatherWithContext", String.class, Unit.class, ToolContext.class);
170+
147171
String response = ChatClient.create(this.chatModel).prompt()
148172
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
149-
.functions(FunctionCallback.builder()
150-
.method("getWeatherWithContext", String.class, Unit.class, ToolContext.class)
151-
.description("Get the weather in location")
152-
.targetObject(targetObject)
173+
.tools(MethodToolCallback.builder()
174+
.toolDefinition(ToolDefinition.builder(toolMethod)
175+
.description("Get the weather in location")
176+
.build())
177+
.toolMethod(toolMethod)
178+
.toolObject(targetObject)
153179
.build())
154180
.toolContext(Map.of("tool", "value"))
155181
.call()
156182
.content();
157-
// @formatter:on
158183

159184
logger.info("Response: {}", response);
160185

@@ -171,18 +196,23 @@ void methodGetWeatherToolContextButNonContextMethod() {
171196
TestFunctionClass targetObject = new TestFunctionClass();
172197

173198
// @formatter:off
199+
var toolMethod = ReflectionUtils.findMethod(
200+
TestFunctionClass.class, "getWeatherNonStatic", String.class, Unit.class);
201+
174202
assertThatThrownBy(() -> ChatClient.create(this.chatModel).prompt()
175203
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
176-
.functions(FunctionCallback.builder()
177-
.method("getWeatherNonStatic", String.class, Unit.class)
178-
.description("Get the weather in location")
179-
.targetObject(targetObject)
204+
.tools(MethodToolCallback.builder()
205+
.toolDefinition(ToolDefinition.builder(toolMethod)
206+
.description("Get the weather in location")
207+
.build())
208+
.toolMethod(toolMethod)
209+
.toolObject(targetObject)
180210
.build())
181211
.toolContext(Map.of("tool", "value"))
182212
.call()
183213
.content())
184214
.isInstanceOf(IllegalArgumentException.class)
185-
.hasMessage("Configured method does not accept ToolContext as input parameter!");
215+
.hasMessage("ToolContext is not supported by the method as an argument");
186216
// @formatter:on
187217
}
188218

@@ -191,13 +221,18 @@ void methodNoParameters() {
191221

192222
TestFunctionClass targetObject = new TestFunctionClass();
193223

194-
// @formatter:off
224+
// @formatter:off
225+
var toolMethod = ReflectionUtils.findMethod(
226+
TestFunctionClass.class, "turnLivingRoomLightOn");
227+
195228
String response = ChatClient.create(this.chatModel).prompt()
196229
.user("Turn light on in the living room.")
197-
.functions(FunctionCallback.builder()
198-
.method("turnLivingRoomLightOn")
199-
.description("Can turn lights on in the Living Room")
200-
.targetObject(targetObject)
230+
.functions(MethodToolCallback.builder()
231+
.toolMethod(toolMethod)
232+
.toolDefinition(ToolDefinition.builder(toolMethod)
233+
.description("Can turn lights on in the Living Room")
234+
.build())
235+
.toolObject(targetObject)
201236
.build())
202237
.call()
203238
.content();

models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockConverseChatClientIT.java

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
import org.springframework.ai.chat.model.ChatResponse;
3737
import org.springframework.ai.converter.BeanOutputConverter;
3838
import org.springframework.ai.converter.ListOutputConverter;
39-
import org.springframework.ai.model.function.FunctionCallback;
40-
import org.springframework.ai.model.function.FunctionCallingOptions;
39+
import org.springframework.ai.model.tool.ToolCallingChatOptions;
40+
import org.springframework.ai.tool.function.FunctionToolCallback;
4141
import org.springframework.beans.factory.annotation.Autowired;
4242
import org.springframework.beans.factory.annotation.Value;
4343
import org.springframework.boot.test.context.SpringBootTest;
@@ -211,8 +211,7 @@ void functionCallTest() {
211211
// @formatter:off
212212
String response = ChatClient.create(this.chatModel)
213213
.prompt("What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.")
214-
.functions(FunctionCallback.builder()
215-
.function("getCurrentWeather", new MockWeatherService())
214+
.tools(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
216215
.description("Get the weather in location")
217216
.inputType(MockWeatherService.Request.class)
218217
.build())
@@ -231,8 +230,7 @@ void functionCallWithUsageMetadataTest() {
231230
// @formatter:off
232231
ChatResponse response = ChatClient.create(this.chatModel)
233232
.prompt("What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.")
234-
.functions(FunctionCallback.builder()
235-
.function("getCurrentWeather", new MockWeatherService())
233+
.tools(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
236234
.description("Get the weather in location")
237235
.inputType(MockWeatherService.Request.class)
238236
.build())
@@ -266,8 +264,7 @@ void functionCallWithAdvisorTest() {
266264
// @formatter:off
267265
String response = ChatClient.create(this.chatModel)
268266
.prompt("What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.")
269-
.functions(FunctionCallback.builder()
270-
.function("getCurrentWeather", new MockWeatherService())
267+
.tools(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
271268
.description("Get the weather in location")
272269
.inputType(MockWeatherService.Request.class)
273270
.build())
@@ -286,8 +283,7 @@ void defaultFunctionCallTest() {
286283

287284
// @formatter:off
288285
String response = ChatClient.builder(this.chatModel)
289-
.defaultFunctions(FunctionCallback.builder()
290-
.function("getCurrentWeather", new MockWeatherService())
286+
.defaultTools(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
291287
.description("Get the weather in location")
292288
.inputType(MockWeatherService.Request.class)
293289
.build())
@@ -309,8 +305,7 @@ void streamFunctionCallTest() {
309305
// @formatter:off
310306
Flux<ChatResponse> response = ChatClient.create(this.chatModel).prompt()
311307
.user("What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.")
312-
.functions(FunctionCallback.builder()
313-
.function("getCurrentWeather", new MockWeatherService())
308+
.tools(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
314309
.description("Get the weather in location")
315310
.inputType(MockWeatherService.Request.class)
316311
.build())
@@ -351,8 +346,7 @@ void singularStreamFunctionCallTest() {
351346
// @formatter:off
352347
Flux<String> response = ChatClient.create(this.chatModel).prompt()
353348
.user("What's the weather like in Paris? Return the temperature in Celsius.")
354-
.functions(FunctionCallback.builder()
355-
.function("getCurrentWeather", new MockWeatherService())
349+
.tools(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
356350
.description("Get the weather in location")
357351
.inputType(MockWeatherService.Request.class)
358352
.build())
@@ -372,7 +366,7 @@ void multiModalityEmbeddedImage(String modelName) throws IOException {
372366

373367
// @formatter:off
374368
String response = ChatClient.create(this.chatModel).prompt()
375-
.options(FunctionCallingOptions.builder().model(modelName).build())
369+
.options(ToolCallingChatOptions.builder().model(modelName).build())
376370
.user(u -> u.text("Explain what do you see on this picture?")
377371
.media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("/test.png")))
378372
.call()
@@ -394,7 +388,7 @@ void multiModalityImageUrl(String modelName) throws IOException {
394388
// @formatter:off
395389
String response = ChatClient.create(this.chatModel).prompt()
396390
// TODO consider adding model(...) method to ChatClient as a shortcut to
397-
.options(FunctionCallingOptions.builder().model(modelName).build())
391+
.options(ToolCallingChatOptions.builder().model(modelName).build())
398392
.user(u -> u.text("Explain what do you see on this picture?").media(MimeTypeUtils.IMAGE_PNG, url))
399393
.call()
400394
.content();

0 commit comments

Comments
 (0)