Skip to content

Commit 8057b49

Browse files
ThomasVitaletzolov
authored andcommitted
Advancing Tool Support - Part 3
* Introduced ToolCallingManager to manage the tool calling activities for resolving and executing tools. A default implementation is provided. It can be used to handle explicit tool execution on the client-side, superseding the previous FunctionCallingHelper class. It’s ready to be instrumented via Micrometer, and support exception handling when tool calls fail. * Introduced ToolCallExceptionConverter to handle exceptions in tool calling, and provided a default implementation propagating the error message to the chat morel. * Introduced ToolCallbackResolver to resolve ToolCallback instances. A default implementation is provided (DelegatingToolCallbackResolver), capable of delegating the resolution to a series of resolvers, including static resolution (StaticToolCallbackResolver) and dynamic resolution from the Spring context (SpringBeanToolCallbackResolver). * Improved configuration in ToolCallingChatOptions to enable/disable the tool execution within a ChatModel (superseding the previous proxyToolCalls option). * Added unit and integration tests to cover all the new use cases and existing functionality which was not covered by autotests (tool resolution from Spring context). * Deprecated FunctionCallbackResolver, AbstractToolCallSupport, and FunctionCallingHelper. Relates to gh-2049 Signed-off-by: Thomas Vitale <[email protected]>
1 parent b03a6c9 commit 8057b49

File tree

46 files changed

+2124
-287
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2124
-287
lines changed

spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.ai.model.function.FunctionCallback;
3333
import org.springframework.ai.model.function.FunctionCallbackResolver;
3434
import org.springframework.ai.model.function.FunctionCallingOptions;
35+
import org.springframework.ai.model.tool.ToolCallingManager;
3536
import org.springframework.util.Assert;
3637
import org.springframework.util.CollectionUtils;
3738

@@ -44,7 +45,9 @@
4445
* @author Thomas Vitale
4546
* @author Jihoon Kim
4647
* @since 1.0.0
48+
* @deprecated Use {@link ToolCallingManager} instead.
4749
*/
50+
@Deprecated
4851
public abstract class AbstractToolCallSupport {
4952

5053
protected static final boolean IS_RUNTIME_CALL = true;

spring-ai-core/src/main/java/org/springframework/ai/chat/model/ChatResponse.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@
3333
* @author Soby Chacko
3434
* @author John Blum
3535
* @author Alexandros Pappas
36+
* @author Thomas Vitale
3637
*/
3738
public class ChatResponse implements ModelResponse<Generation> {
3839

@@ -100,6 +101,16 @@ public ChatResponseMetadata getMetadata() {
100101
return this.chatResponseMetadata;
101102
}
102103

104+
/**
105+
* Whether the model has requested the execution of a tool.
106+
*/
107+
public boolean hasToolCalls() {
108+
if (CollectionUtils.isEmpty(generations)) {
109+
return false;
110+
}
111+
return generations.stream().anyMatch(generation -> generation.getOutput().hasToolCalls());
112+
}
113+
103114
@Override
104115
public String toString() {
105116
return "ChatResponse [metadata=" + this.chatResponseMetadata + ", generations=" + this.generations + "]";

spring-ai-core/src/main/java/org/springframework/ai/model/function/DefaultFunctionCallbackResolver.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828

2929
import org.springframework.ai.chat.model.ToolContext;
3030
import org.springframework.ai.model.function.FunctionCallback.SchemaType;
31+
import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
32+
import org.springframework.ai.tool.resolution.TypeResolverHelper;
3133
import org.springframework.beans.BeansException;
3234
import org.springframework.context.ApplicationContext;
3335
import org.springframework.context.ApplicationContextAware;
@@ -55,7 +57,9 @@
5557
* @author Christian Tzolov
5658
* @author Christopher Smith
5759
* @author Sebastien Deleuze
60+
* @deprecated Use {@link SpringBeanToolCallbackResolver} instead.
5861
*/
62+
@Deprecated
5963
public class DefaultFunctionCallbackResolver implements ApplicationContextAware, FunctionCallbackResolver {
6064

6165
private GenericApplicationContext applicationContext;

spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackResolver.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616

1717
package org.springframework.ai.model.function;
1818

19+
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
1920
import org.springframework.lang.NonNull;
2021

2122
/**
2223
* Strategy interface for resolving {@link FunctionCallback} instances.
2324
*
2425
* @author Christian Tzolov
2526
* @since 1.0.0
27+
* @deprecated Use {@link ToolCallbackResolver} instead.
2628
*/
29+
@Deprecated
2730
public interface FunctionCallbackResolver {
2831

2932
/**

spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingHelper.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Set;
2424
import java.util.function.Function;
2525

26+
import org.springframework.ai.model.tool.ToolCallingManager;
2627
import reactor.core.publisher.Flux;
2728

2829
import org.springframework.ai.chat.messages.AssistantMessage;
@@ -40,7 +41,10 @@
4041
* Helper class that reuses the {@link AbstractToolCallSupport} to implement the function
4142
* call handling logic on the client side. Used when the withProxyToolCalls(true) option
4243
* is enabled.
44+
*
45+
* @deprecated Use {@link ToolCallingManager} instead.
4346
*/
47+
@Deprecated
4448
public class FunctionCallingHelper extends AbstractToolCallSupport {
4549

4650
public FunctionCallingHelper() {

spring-ai-core/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingChatOptions.java

Lines changed: 26 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,11 @@
1818

1919
import org.springframework.ai.chat.prompt.ChatOptions;
2020
import org.springframework.ai.model.function.FunctionCallback;
21-
import org.springframework.ai.tool.ToolCallback;
2221
import org.springframework.lang.Nullable;
2322
import org.springframework.util.Assert;
24-
import org.springframework.util.CollectionUtils;
25-
import org.springframework.util.StringUtils;
2623

2724
import java.util.ArrayList;
25+
import java.util.Arrays;
2826
import java.util.HashMap;
2927
import java.util.HashSet;
3028
import java.util.List;
@@ -39,14 +37,14 @@
3937
*/
4038
public class DefaultToolCallingChatOptions implements ToolCallingChatOptions {
4139

42-
private List<ToolCallback> toolCallbacks = new ArrayList<>();
40+
private List<FunctionCallback> toolCallbacks = new ArrayList<>();
4341

4442
private Set<String> tools = new HashSet<>();
4543

4644
private Map<String, Object> toolContext = new HashMap<>();
4745

4846
@Nullable
49-
private Boolean toolCallReturnDirect;
47+
private Boolean internalToolExecutionEnabled;
5048

5149
@Nullable
5250
private String model;
@@ -73,23 +71,17 @@ public class DefaultToolCallingChatOptions implements ToolCallingChatOptions {
7371
private Double topP;
7472

7573
@Override
76-
public List<ToolCallback> getToolCallbacks() {
74+
public List<FunctionCallback> getToolCallbacks() {
7775
return List.copyOf(this.toolCallbacks);
7876
}
7977

8078
@Override
81-
public void setToolCallbacks(List<ToolCallback> toolCallbacks) {
79+
public void setToolCallbacks(List<FunctionCallback> toolCallbacks) {
8280
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
8381
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
8482
this.toolCallbacks = new ArrayList<>(toolCallbacks);
8583
}
8684

87-
@Override
88-
public void setToolCallbacks(ToolCallback... toolCallbacks) {
89-
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
90-
setToolCallbacks(List.of(toolCallbacks));
91-
}
92-
9385
@Override
9486
public Set<String> getTools() {
9587
return Set.copyOf(this.tools);
@@ -103,12 +95,6 @@ public void setTools(Set<String> tools) {
10395
this.tools = new HashSet<>(tools);
10496
}
10597

106-
@Override
107-
public void setTools(String... tools) {
108-
Assert.notNull(tools, "tools cannot be null");
109-
setTools(Set.of(tools));
110-
}
111-
11298
@Override
11399
public Map<String, Object> getToolContext() {
114100
return Map.copyOf(this.toolContext);
@@ -123,23 +109,23 @@ public void setToolContext(Map<String, Object> toolContext) {
123109

124110
@Override
125111
@Nullable
126-
public Boolean getToolCallReturnDirect() {
127-
return this.toolCallReturnDirect;
112+
public Boolean isInternalToolExecutionEnabled() {
113+
return this.internalToolExecutionEnabled;
128114
}
129115

130116
@Override
131-
public void setToolCallReturnDirect(@Nullable Boolean toolCallReturnDirect) {
132-
this.toolCallReturnDirect = toolCallReturnDirect;
117+
public void setInternalToolExecutionEnabled(@Nullable Boolean internalToolExecutionEnabled) {
118+
this.internalToolExecutionEnabled = internalToolExecutionEnabled;
133119
}
134120

135121
@Override
136122
public List<FunctionCallback> getFunctionCallbacks() {
137-
return getToolCallbacks().stream().map(FunctionCallback.class::cast).toList();
123+
return getToolCallbacks();
138124
}
139125

140126
@Override
141127
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
142-
throw new UnsupportedOperationException("Not supported. Call setToolCallbacks instead.");
128+
setToolCallbacks(functionCallbacks);
143129
}
144130

145131
@Override
@@ -155,12 +141,12 @@ public void setFunctions(Set<String> functions) {
155141
@Override
156142
@Nullable
157143
public Boolean getProxyToolCalls() {
158-
return getToolCallReturnDirect();
144+
return isInternalToolExecutionEnabled() != null ? !isInternalToolExecutionEnabled() : null;
159145
}
160146

161147
@Override
162148
public void setProxyToolCalls(@Nullable Boolean proxyToolCalls) {
163-
setToolCallReturnDirect(proxyToolCalls != null && proxyToolCalls);
149+
setInternalToolExecutionEnabled(proxyToolCalls == null || !proxyToolCalls);
164150
}
165151

166152
@Override
@@ -250,7 +236,7 @@ public <T extends ChatOptions> T copy() {
250236
options.setToolCallbacks(getToolCallbacks());
251237
options.setTools(getTools());
252238
options.setToolContext(getToolContext());
253-
options.setToolCallReturnDirect(getToolCallReturnDirect());
239+
options.setInternalToolExecutionEnabled(isInternalToolExecutionEnabled());
254240
options.setModel(getModel());
255241
options.setFrequencyPenalty(getFrequencyPenalty());
256242
options.setMaxTokens(getMaxTokens());
@@ -262,55 +248,6 @@ public <T extends ChatOptions> T copy() {
262248
return (T) options;
263249
}
264250

265-
/**
266-
* Merge the given {@link ChatOptions} into this instance.
267-
*/
268-
public ToolCallingChatOptions merge(ChatOptions options) {
269-
ToolCallingChatOptions.Builder builder = ToolCallingChatOptions.builder();
270-
builder.model(StringUtils.hasText(options.getModel()) ? options.getModel() : this.getModel());
271-
builder.frequencyPenalty(
272-
options.getFrequencyPenalty() != null ? options.getFrequencyPenalty() : this.getFrequencyPenalty());
273-
builder.maxTokens(options.getMaxTokens() != null ? options.getMaxTokens() : this.getMaxTokens());
274-
builder.presencePenalty(
275-
options.getPresencePenalty() != null ? options.getPresencePenalty() : this.getPresencePenalty());
276-
builder.stopSequences(options.getStopSequences() != null ? new ArrayList<>(options.getStopSequences())
277-
: this.getStopSequences());
278-
builder.temperature(options.getTemperature() != null ? options.getTemperature() : this.getTemperature());
279-
builder.topK(options.getTopK() != null ? options.getTopK() : this.getTopK());
280-
builder.topP(options.getTopP() != null ? options.getTopP() : this.getTopP());
281-
282-
if (options instanceof ToolCallingChatOptions toolOptions) {
283-
List<ToolCallback> toolCallbacks = new ArrayList<>(this.toolCallbacks);
284-
if (!CollectionUtils.isEmpty(toolOptions.getToolCallbacks())) {
285-
toolCallbacks.addAll(toolOptions.getToolCallbacks());
286-
}
287-
builder.toolCallbacks(toolCallbacks);
288-
289-
Set<String> tools = new HashSet<>(this.tools);
290-
if (!CollectionUtils.isEmpty(toolOptions.getTools())) {
291-
tools.addAll(toolOptions.getTools());
292-
}
293-
builder.tools(tools);
294-
295-
Map<String, Object> toolContext = new HashMap<>(this.toolContext);
296-
if (!CollectionUtils.isEmpty(toolOptions.getToolContext())) {
297-
toolContext.putAll(toolOptions.getToolContext());
298-
}
299-
builder.toolContext(toolContext);
300-
301-
builder.toolCallReturnDirect(toolOptions.getToolCallReturnDirect() != null
302-
? toolOptions.getToolCallReturnDirect() : this.getToolCallReturnDirect());
303-
}
304-
else {
305-
builder.toolCallbacks(this.toolCallbacks);
306-
builder.tools(this.tools);
307-
builder.toolContext(this.toolContext);
308-
builder.toolCallReturnDirect(this.toolCallReturnDirect);
309-
}
310-
311-
return builder.build();
312-
}
313-
314251
public static Builder builder() {
315252
return new Builder();
316253
}
@@ -323,14 +260,15 @@ public static class Builder implements ToolCallingChatOptions.Builder {
323260
private final DefaultToolCallingChatOptions options = new DefaultToolCallingChatOptions();
324261

325262
@Override
326-
public ToolCallingChatOptions.Builder toolCallbacks(List<ToolCallback> toolCallbacks) {
263+
public ToolCallingChatOptions.Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
327264
this.options.setToolCallbacks(toolCallbacks);
328265
return this;
329266
}
330267

331268
@Override
332-
public ToolCallingChatOptions.Builder toolCallbacks(ToolCallback... toolCallbacks) {
333-
this.options.setToolCallbacks(toolCallbacks);
269+
public ToolCallingChatOptions.Builder toolCallbacks(FunctionCallback... toolCallbacks) {
270+
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
271+
this.options.setToolCallbacks(Arrays.asList(toolCallbacks));
334272
return this;
335273
}
336274

@@ -342,7 +280,8 @@ public ToolCallingChatOptions.Builder tools(Set<String> toolNames) {
342280

343281
@Override
344282
public ToolCallingChatOptions.Builder tools(String... toolNames) {
345-
this.options.setTools(toolNames);
283+
Assert.notNull(toolNames, "toolNames cannot be null");
284+
this.options.setTools(Set.of(toolNames));
346285
return this;
347286
}
348287

@@ -363,16 +302,16 @@ public ToolCallingChatOptions.Builder toolContext(String key, Object value) {
363302
}
364303

365304
@Override
366-
public ToolCallingChatOptions.Builder toolCallReturnDirect(@Nullable Boolean toolCallReturnDirect) {
367-
this.options.setToolCallReturnDirect(toolCallReturnDirect);
305+
public ToolCallingChatOptions.Builder internalToolExecutionEnabled(
306+
@Nullable Boolean internalToolExecutionEnabled) {
307+
this.options.setInternalToolExecutionEnabled(internalToolExecutionEnabled);
368308
return this;
369309
}
370310

371311
@Override
372312
@Deprecated // Use toolCallbacks() instead
373313
public ToolCallingChatOptions.Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
374-
Assert.notNull(functionCallbacks, "functionCallbacks cannot be null");
375-
return toolCallbacks(functionCallbacks.stream().map(ToolCallback.class::cast).toList());
314+
return toolCallbacks(functionCallbacks);
376315
}
377316

378317
@Override
@@ -395,9 +334,9 @@ public ToolCallingChatOptions.Builder function(String function) {
395334
}
396335

397336
@Override
398-
@Deprecated // Use toolCallReturnDirect() instead
337+
@Deprecated // Use internalToolExecutionEnabled() instead
399338
public ToolCallingChatOptions.Builder proxyToolCalls(@Nullable Boolean proxyToolCalls) {
400-
return toolCallReturnDirect(proxyToolCalls != null && proxyToolCalls);
339+
return internalToolExecutionEnabled(proxyToolCalls == null || !proxyToolCalls);
401340
}
402341

403342
@Override

0 commit comments

Comments
 (0)