Skip to content

Commit 449d6ec

Browse files
committed
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 2f14597 commit 449d6ec

File tree

43 files changed

+2031
-243
lines changed

Some content is hidden

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

43 files changed

+2031
-243
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: 24 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
import org.springframework.ai.tool.ToolCallback;
2222
import org.springframework.lang.Nullable;
2323
import org.springframework.util.Assert;
24-
import org.springframework.util.CollectionUtils;
25-
import org.springframework.util.StringUtils;
2624

2725
import java.util.ArrayList;
2826
import java.util.HashMap;
@@ -46,7 +44,7 @@ public class DefaultToolCallingChatOptions implements ToolCallingChatOptions {
4644
private Map<String, Object> toolContext = new HashMap<>();
4745

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

5149
@Nullable
5250
private String model;
@@ -123,13 +121,13 @@ public void setToolContext(Map<String, Object> toolContext) {
123121

124122
@Override
125123
@Nullable
126-
public Boolean getToolCallReturnDirect() {
127-
return this.toolCallReturnDirect;
124+
public Boolean isToolExecutionEnabled() {
125+
return this.toolExecutionEnabled;
128126
}
129127

130128
@Override
131-
public void setToolCallReturnDirect(@Nullable Boolean toolCallReturnDirect) {
132-
this.toolCallReturnDirect = toolCallReturnDirect;
129+
public void setToolExecutionEnabled(@Nullable Boolean toolExecutionEnabled) {
130+
this.toolExecutionEnabled = toolExecutionEnabled;
133131
}
134132

135133
@Override
@@ -139,7 +137,12 @@ public List<FunctionCallback> getFunctionCallbacks() {
139137

140138
@Override
141139
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
142-
throw new UnsupportedOperationException("Not supported. Call setToolCallbacks instead.");
140+
if (functionCallbacks.stream().allMatch(ToolCallback.class::isInstance)) {
141+
setToolCallbacks(functionCallbacks.stream().map(ToolCallback.class::cast).toList());
142+
}
143+
else {
144+
throw new IllegalArgumentException("functionCallbacks must be instances of ToolCallback");
145+
}
143146
}
144147

145148
@Override
@@ -155,12 +158,12 @@ public void setFunctions(Set<String> functions) {
155158
@Override
156159
@Nullable
157160
public Boolean getProxyToolCalls() {
158-
return getToolCallReturnDirect();
161+
return !isToolExecutionEnabled();
159162
}
160163

161164
@Override
162165
public void setProxyToolCalls(@Nullable Boolean proxyToolCalls) {
163-
setToolCallReturnDirect(proxyToolCalls != null && proxyToolCalls);
166+
setToolExecutionEnabled(proxyToolCalls == null || !proxyToolCalls);
164167
}
165168

166169
@Override
@@ -250,7 +253,7 @@ public <T extends ChatOptions> T copy() {
250253
options.setToolCallbacks(getToolCallbacks());
251254
options.setTools(getTools());
252255
options.setToolContext(getToolContext());
253-
options.setToolCallReturnDirect(getToolCallReturnDirect());
256+
options.setToolExecutionEnabled(isToolExecutionEnabled());
254257
options.setModel(getModel());
255258
options.setFrequencyPenalty(getFrequencyPenalty());
256259
options.setMaxTokens(getMaxTokens());
@@ -262,55 +265,6 @@ public <T extends ChatOptions> T copy() {
262265
return (T) options;
263266
}
264267

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-
314268
public static Builder builder() {
315269
return new Builder();
316270
}
@@ -363,16 +317,21 @@ public ToolCallingChatOptions.Builder toolContext(String key, Object value) {
363317
}
364318

365319
@Override
366-
public ToolCallingChatOptions.Builder toolCallReturnDirect(@Nullable Boolean toolCallReturnDirect) {
367-
this.options.setToolCallReturnDirect(toolCallReturnDirect);
320+
public ToolCallingChatOptions.Builder toolExecutionEnabled(@Nullable Boolean toolExecutionEnabled) {
321+
this.options.setToolExecutionEnabled(toolExecutionEnabled);
368322
return this;
369323
}
370324

371325
@Override
372326
@Deprecated // Use toolCallbacks() instead
373327
public ToolCallingChatOptions.Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
374328
Assert.notNull(functionCallbacks, "functionCallbacks cannot be null");
375-
return toolCallbacks(functionCallbacks.stream().map(ToolCallback.class::cast).toList());
329+
if (functionCallbacks.stream().allMatch(ToolCallback.class::isInstance)) {
330+
return toolCallbacks(functionCallbacks.stream().map(ToolCallback.class::cast).toList());
331+
}
332+
else {
333+
throw new IllegalArgumentException("functionCallbacks must be instances of ToolCallback");
334+
}
376335
}
377336

378337
@Override
@@ -395,9 +354,9 @@ public ToolCallingChatOptions.Builder function(String function) {
395354
}
396355

397356
@Override
398-
@Deprecated // Use toolCallReturnDirect() instead
357+
@Deprecated // Use toolExecutionEnabled() instead
399358
public ToolCallingChatOptions.Builder proxyToolCalls(@Nullable Boolean proxyToolCalls) {
400-
return toolCallReturnDirect(proxyToolCalls != null && proxyToolCalls);
359+
return toolExecutionEnabled(proxyToolCalls == null || !proxyToolCalls);
401360
}
402361

403362
@Override

0 commit comments

Comments
 (0)