Advancing Tool Support - Part 4
* Adopted new tool calling logic in OllamaChatModel, while maintaining full API backward compatibility thanks to the LegacyToolCallingManager. * Improved efficiency and robustness of merging options in prompts for Ollama. * Update Ollama Autoconfiguration to use the new ToolCallingManager. * Improved troubleshooting for new tool calling APIs and finalised changes for full backward compatibility. * Updated Ollama Testcontainers dependency to 0.5.7. Relates to gh-2049 Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
This commit is contained in:
committed by
Christian Tzolov
parent
76ab91fab8
commit
b902ca2afb
@@ -290,8 +290,7 @@ public interface ChatClient {
|
||||
Builder defaultFunctions(String... functionNames);
|
||||
|
||||
/**
|
||||
* @deprecated in favor of {@link #defaultTools(FunctionCallback...)} or
|
||||
* {@link #defaultToolCallbacks(FunctionCallback...)}
|
||||
* @deprecated in favor of {@link #defaultTools(Object...)}
|
||||
*/
|
||||
@Deprecated
|
||||
Builder defaultFunctions(FunctionCallback... functionCallbacks);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -64,10 +64,12 @@ public abstract class AbstractToolCallSupport {
|
||||
*/
|
||||
protected final FunctionCallbackResolver functionCallbackResolver;
|
||||
|
||||
@Deprecated
|
||||
protected AbstractToolCallSupport(FunctionCallbackResolver functionCallbackResolver) {
|
||||
this(functionCallbackResolver, FunctionCallingOptions.builder().build(), List.of());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
protected AbstractToolCallSupport(FunctionCallbackResolver functionCallbackResolver,
|
||||
FunctionCallingOptions functionCallingOptions, List<FunctionCallback> toolFunctionCallbacks) {
|
||||
|
||||
@@ -97,6 +99,7 @@ public abstract class AbstractToolCallSupport {
|
||||
return toolFunctionCallbacksCopy;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Map<String, FunctionCallback> getFunctionCallbackRegister() {
|
||||
return this.functionCallbackRegister;
|
||||
}
|
||||
@@ -107,6 +110,7 @@ public abstract class AbstractToolCallSupport {
|
||||
* @param runtimeFunctionOptions FunctionCallingOptions to handle.
|
||||
* @return Set of function names to call.
|
||||
*/
|
||||
@Deprecated
|
||||
protected Set<String> runtimeFunctionCallbackConfigurations(FunctionCallingOptions runtimeFunctionOptions) {
|
||||
|
||||
Set<String> enabledFunctionsToCall = new HashSet<>();
|
||||
@@ -133,6 +137,7 @@ public abstract class AbstractToolCallSupport {
|
||||
return enabledFunctionsToCall;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
protected List<Message> handleToolCalls(Prompt prompt, ChatResponse response) {
|
||||
Optional<Generation> toolCallGeneration = response.getResults()
|
||||
.stream()
|
||||
@@ -165,6 +170,7 @@ public abstract class AbstractToolCallSupport {
|
||||
return toolConversationHistory;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
protected List<Message> buildToolCallConversation(List<Message> previousMessages, AssistantMessage assistantMessage,
|
||||
ToolResponseMessage toolResponseMessage) {
|
||||
List<Message> messages = new ArrayList<>(previousMessages);
|
||||
@@ -179,6 +185,7 @@ public abstract class AbstractToolCallSupport {
|
||||
* @param functionNames Name of function callbacks to retrieve.
|
||||
* @return list of resolved FunctionCallbacks.
|
||||
*/
|
||||
@Deprecated
|
||||
protected List<FunctionCallback> resolveFunctionCallbacks(Set<String> functionNames) {
|
||||
|
||||
List<FunctionCallback> retrievedFunctionCallbacks = new ArrayList<>();
|
||||
@@ -208,6 +215,7 @@ public abstract class AbstractToolCallSupport {
|
||||
return retrievedFunctionCallbacks;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
protected ToolResponseMessage executeFunctions(AssistantMessage assistantMessage, ToolContext toolContext) {
|
||||
|
||||
List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();
|
||||
@@ -230,6 +238,7 @@ public abstract class AbstractToolCallSupport {
|
||||
return new ToolResponseMessage(toolResponses, Map.of());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
protected boolean isToolCall(ChatResponse chatResponse, Set<String> toolCallFinishReasons) {
|
||||
Assert.isTrue(!CollectionUtils.isEmpty(toolCallFinishReasons), "Tool call finish reasons cannot be empty!");
|
||||
|
||||
@@ -252,6 +261,7 @@ public abstract class AbstractToolCallSupport {
|
||||
* @param toolCallFinishReasons the tool call finish reasons to check.
|
||||
* @return true if the generation is a tool call, false otherwise.
|
||||
*/
|
||||
@Deprecated
|
||||
protected boolean isToolCall(Generation generation, Set<String> toolCallFinishReasons) {
|
||||
var finishReason = (generation.getMetadata().getFinishReason() != null)
|
||||
? generation.getMetadata().getFinishReason() : "";
|
||||
@@ -271,6 +281,7 @@ public abstract class AbstractToolCallSupport {
|
||||
* @param defaultOptions the default tool call options to check.
|
||||
* @return true if the proxyToolCalls is enabled, false otherwise.
|
||||
*/
|
||||
@Deprecated
|
||||
protected boolean isProxyToolCalls(Prompt prompt, FunctionCallingOptions defaultOptions) {
|
||||
if (prompt.getOptions() instanceof FunctionCallingOptions functionCallOptions
|
||||
&& functionCallOptions.getProxyToolCalls() != null) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -71,7 +71,8 @@ public class ToolContext {
|
||||
|
||||
/**
|
||||
* Returns the tool call history from the context map.
|
||||
* @return The tool call history.
|
||||
* @return The tool call history. TODO: review whether we still need this or
|
||||
* ToolCallingManager solves the original issue
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Message> getToolCallHistory() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -29,6 +29,7 @@ import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.ToolResponseMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.model.ModelRequest;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* The Prompt class represents a prompt used in AI model requests. A prompt consists of
|
||||
@@ -36,11 +37,13 @@ import org.springframework.ai.model.ModelRequest;
|
||||
*
|
||||
* @author Mark Pollack
|
||||
* @author luocongqiu
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
public class Prompt implements ModelRequest<List<Message>> {
|
||||
|
||||
private final List<Message> messages;
|
||||
|
||||
@Nullable
|
||||
private ChatOptions chatOptions;
|
||||
|
||||
public Prompt(String contents) {
|
||||
@@ -81,6 +84,7 @@ public class Prompt implements ModelRequest<List<Message>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ChatOptions getOptions() {
|
||||
return this.chatOptions;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.springframework.ai.model.tool;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.ToolResponseMessage;
|
||||
@@ -50,6 +52,8 @@ import java.util.Optional;
|
||||
*/
|
||||
public class DefaultToolCallingManager implements ToolCallingManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DefaultToolCallingManager.class);
|
||||
|
||||
// @formatter:off
|
||||
|
||||
private static final ObservationRegistry DEFAULT_OBSERVATION_REGISTRY
|
||||
@@ -86,7 +90,7 @@ public class DefaultToolCallingManager implements ToolCallingManager {
|
||||
|
||||
List<FunctionCallback> toolCallbacks = new ArrayList<>(chatOptions.getToolCallbacks());
|
||||
for (String toolName : chatOptions.getTools()) {
|
||||
ToolCallback toolCallback = toolCallbackResolver.resolve(toolName);
|
||||
FunctionCallback toolCallback = toolCallbackResolver.resolve(toolName);
|
||||
if (toolCallback == null) {
|
||||
throw new IllegalStateException("No ToolCallback found for tool name: " + toolName);
|
||||
}
|
||||
@@ -176,13 +180,15 @@ public class DefaultToolCallingManager implements ToolCallingManager {
|
||||
|
||||
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
|
||||
|
||||
logger.debug("Executing tool call: {}", toolCall.name());
|
||||
|
||||
String toolName = toolCall.name();
|
||||
String toolInputArguments = toolCall.arguments();
|
||||
|
||||
FunctionCallback toolCallback = toolCallbacks.stream()
|
||||
.filter(tool -> toolName.equals(tool.getName()))
|
||||
.findFirst()
|
||||
.orElse(toolCallbackResolver.resolve(toolName));
|
||||
.orElseGet(() -> toolCallbackResolver.resolve(toolName));
|
||||
|
||||
if (toolCallback == null) {
|
||||
throw new IllegalStateException("No ToolCallback found for tool name: " + toolName);
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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 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.DefaultToolCallExceptionConverter;
|
||||
import org.springframework.ai.tool.execution.ToolCallExceptionConverter;
|
||||
import org.springframework.ai.tool.execution.ToolExecutionException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
private final FunctionCallbackResolver functionCallbackResolver;
|
||||
|
||||
private final Map<String, FunctionCallback> functionCallbacks = new HashMap<>();
|
||||
|
||||
private final ToolCallExceptionConverter toolCallExceptionConverter = DefaultToolCallExceptionConverter.builder()
|
||||
.build();
|
||||
|
||||
public LegacyToolCallingManager(@Nullable FunctionCallbackResolver functionCallbackResolver,
|
||||
List<FunctionCallback> functionCallbacks) {
|
||||
Assert.notNull(functionCallbacks, "functionCallbacks cannot be null");
|
||||
Assert.noNullElements(functionCallbacks.toArray(), "functionCallbacks cannot contain null elements");
|
||||
this.functionCallbackResolver = functionCallbackResolver;
|
||||
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.getTools()) {
|
||||
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 (functionCallbacks.get(toolName) != null) {
|
||||
return functionCallbacks.get(toolName);
|
||||
}
|
||||
return functionCallbackResolver != null ? functionCallbackResolver.resolve(toolName) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> 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);
|
||||
|
||||
return buildConversationHistoryAfterToolExecution(prompt.getInstructions(), assistantMessage,
|
||||
toolMessageResponse);
|
||||
}
|
||||
|
||||
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 = toolCallExceptionConverter.convert(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 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(functionCallbackResolver, functionCallbacks);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,6 +23,8 @@ import org.springframework.ai.model.function.FunctionCallingOptions;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -185,4 +187,21 @@ public interface ToolCallingChatOptions extends FunctionCallingOptions {
|
||||
return internalToolExecutionEnabled;
|
||||
}
|
||||
|
||||
static Set<String> mergeToolNames(Set<String> runtimeToolNames, Set<String> defaultToolNames) {
|
||||
Assert.notNull(runtimeToolNames, "runtimeToolNames cannot be null");
|
||||
Assert.notNull(defaultToolNames, "defaultToolNames cannot be null");
|
||||
var mergedToolNames = new HashSet<>(runtimeToolNames);
|
||||
mergedToolNames.addAll(defaultToolNames);
|
||||
return mergedToolNames;
|
||||
}
|
||||
|
||||
static List<FunctionCallback> mergeToolCallbacks(List<FunctionCallback> runtimeToolCallbacks,
|
||||
List<FunctionCallback> defaultToolCallbacks) {
|
||||
Assert.notNull(runtimeToolCallbacks, "runtimeToolCallbacks cannot be null");
|
||||
Assert.notNull(defaultToolCallbacks, "defaultToolCallbacks cannot be null");
|
||||
var mergedToolCallbacks = new ArrayList<>(runtimeToolCallbacks);
|
||||
mergedToolCallbacks.addAll(defaultToolCallbacks);
|
||||
return mergedToolCallbacks;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.ai.tool.definition;
|
||||
|
||||
import org.springframework.ai.tool.util.ToolUtils;
|
||||
import org.springframework.ai.util.json.JsonSchemaGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@@ -55,6 +56,7 @@ public interface ToolDefinition {
|
||||
* Create a default {@link ToolDefinition} builder from a {@link Method}.
|
||||
*/
|
||||
static DefaultToolDefinition.Builder builder(Method method) {
|
||||
Assert.notNull(method, "method cannot be null");
|
||||
return DefaultToolDefinition.builder()
|
||||
.name(ToolUtils.getToolName(method))
|
||||
.description(ToolUtils.getToolDescription(method))
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.ai.tool.execution;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -26,6 +28,8 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class DefaultToolCallExceptionConverter implements ToolCallExceptionConverter {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(DefaultToolCallExceptionConverter.class);
|
||||
|
||||
private static final boolean DEFAULT_ALWAYS_THROW = false;
|
||||
|
||||
private final boolean alwaysThrow;
|
||||
@@ -40,6 +44,8 @@ public class DefaultToolCallExceptionConverter implements ToolCallExceptionConve
|
||||
if (alwaysThrow) {
|
||||
throw exception;
|
||||
}
|
||||
logger.debug("Exception thrown by tool: {}. Message: {}", exception.getToolDefinition().name(),
|
||||
exception.getMessage());
|
||||
return exception.getMessage();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ public final class DefaultToolCallResultConverter implements ToolCallResultConve
|
||||
return "Done";
|
||||
}
|
||||
else {
|
||||
logger.debug("Converting tool result to JSON.");
|
||||
return JsonParser.toJson(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,11 @@ public class FunctionToolCallback<I, O> implements ToolCallback {
|
||||
return toolCallResultConverter.apply(response, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FunctionToolCallback{" + "toolDefinition=" + toolDefinition + ", toolMetadata=" + toolMetadata + '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link FunctionToolCallback} from a {@link BiFunction}.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.ai.tool.metadata;
|
||||
|
||||
import org.springframework.ai.tool.util.ToolUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@@ -46,6 +47,7 @@ public interface ToolMetadata {
|
||||
* Create a default {@link ToolMetadata} instance from a {@link Method}.
|
||||
*/
|
||||
static ToolMetadata from(Method method) {
|
||||
Assert.notNull(method, "method cannot be null");
|
||||
return DefaultToolMetadata.builder().returnDirect(ToolUtils.getToolReturnDirect(method)).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ public class MethodToolCallback implements ToolCallback {
|
||||
|
||||
private final Method toolMethod;
|
||||
|
||||
@Nullable
|
||||
private final Object toolObject;
|
||||
|
||||
private final ToolCallResultConverter toolCallResultConverter;
|
||||
@@ -174,6 +175,11 @@ public class MethodToolCallback implements ToolCallback {
|
||||
return !Modifier.isPublic(toolMethod.getModifiers());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MethodToolCallback{" + "toolDefinition=" + toolDefinition + ", toolMetadata=" + toolMetadata + '}';
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package org.springframework.ai.tool.resolution;
|
||||
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -41,9 +41,11 @@ public class DelegatingToolCallbackResolver implements ToolCallbackResolver {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ToolCallback resolve(String toolName) {
|
||||
public FunctionCallback resolve(String toolName) {
|
||||
Assert.hasText(toolName, "toolName cannot be null or empty");
|
||||
|
||||
for (ToolCallbackResolver toolCallbackResolver : toolCallbackResolvers) {
|
||||
ToolCallback toolCallback = toolCallbackResolver.resolve(toolName);
|
||||
FunctionCallback toolCallback = toolCallbackResolver.resolve(toolName);
|
||||
if (toolCallback != null) {
|
||||
return toolCallback;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonClassDescription;
|
||||
import kotlin.jvm.functions.Function0;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import kotlin.jvm.functions.Function2;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
@@ -55,6 +57,8 @@ import java.util.function.Supplier;
|
||||
*/
|
||||
public class SpringBeanToolCallbackResolver implements ToolCallbackResolver {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SpringBeanToolCallbackResolver.class);
|
||||
|
||||
private static final Map<String, ToolCallback> toolCallbacksCache = new HashMap<>();
|
||||
|
||||
private static final SchemaType DEFAULT_SCHEMA_TYPE = SchemaType.JSON_SCHEMA;
|
||||
@@ -75,6 +79,8 @@ public class SpringBeanToolCallbackResolver implements ToolCallbackResolver {
|
||||
public ToolCallback resolve(String toolName) {
|
||||
Assert.hasText(toolName, "toolName cannot be null or empty");
|
||||
|
||||
logger.debug("ToolCallback resolution attempt from Spring application context");
|
||||
|
||||
ToolCallback resolvedToolCallback = toolCallbacksCache.get(toolName);
|
||||
|
||||
if (resolvedToolCallback != null) {
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.ai.tool.resolution;
|
||||
|
||||
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;
|
||||
|
||||
@@ -31,18 +34,26 @@ import java.util.Map;
|
||||
*/
|
||||
public class StaticToolCallbackResolver implements ToolCallbackResolver {
|
||||
|
||||
private final Map<String, ToolCallback> toolCallbacks = new HashMap<>();
|
||||
private static final Logger logger = LoggerFactory.getLogger(StaticToolCallbackResolver.class);
|
||||
|
||||
public StaticToolCallbackResolver(List<ToolCallback> toolCallbacks) {
|
||||
private final Map<String, FunctionCallback> toolCallbacks = new HashMap<>();
|
||||
|
||||
public StaticToolCallbackResolver(List<FunctionCallback> toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
|
||||
toolCallbacks
|
||||
.forEach(toolCallback -> this.toolCallbacks.put(toolCallback.getToolDefinition().name(), toolCallback));
|
||||
toolCallbacks.forEach(callback -> {
|
||||
if (callback instanceof ToolCallback toolCallback) {
|
||||
this.toolCallbacks.put(toolCallback.getToolDefinition().name(), toolCallback);
|
||||
}
|
||||
this.toolCallbacks.put(callback.getName(), callback);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolCallback resolve(String toolName) {
|
||||
public FunctionCallback resolve(String toolName) {
|
||||
Assert.hasText(toolName, "toolName cannot be null or empty");
|
||||
logger.debug("ToolCallback resolution attempt from static registry");
|
||||
return toolCallbacks.get(toolName);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.ai.tool.resolution;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -28,9 +29,9 @@ import org.springframework.lang.Nullable;
|
||||
public interface ToolCallbackResolver {
|
||||
|
||||
/**
|
||||
* Resolve the {@link ToolCallback} for the given tool name.
|
||||
* Resolve the {@link FunctionCallback} for the given tool name.
|
||||
*/
|
||||
@Nullable
|
||||
ToolCallback resolve(String toolName);
|
||||
FunctionCallback resolve(String toolName);
|
||||
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ public final class ToolUtils {
|
||||
}
|
||||
|
||||
public static String getToolName(Method method) {
|
||||
Assert.notNull(method, "method cannot be null");
|
||||
var tool = method.getAnnotation(Tool.class);
|
||||
if (tool == null) {
|
||||
return method.getName();
|
||||
@@ -49,12 +50,13 @@ public final class ToolUtils {
|
||||
return StringUtils.hasText(tool.name()) ? tool.name() : method.getName();
|
||||
}
|
||||
|
||||
public static String getToolDescriptionFromName(@Nullable String toolName) {
|
||||
public static String getToolDescriptionFromName(String toolName) {
|
||||
Assert.hasText(toolName, "toolName cannot be null or empty");
|
||||
return ParsingUtils.reConcatenateCamelCase(toolName, " ");
|
||||
}
|
||||
|
||||
public static String getToolDescription(Method method) {
|
||||
Assert.notNull(method, "method cannot be null");
|
||||
var tool = method.getAnnotation(Tool.class);
|
||||
if (tool == null) {
|
||||
return ParsingUtils.reConcatenateCamelCase(method.getName(), " ");
|
||||
@@ -63,11 +65,13 @@ public final class ToolUtils {
|
||||
}
|
||||
|
||||
public static boolean getToolReturnDirect(Method method) {
|
||||
Assert.notNull(method, "method cannot be null");
|
||||
var tool = method.getAnnotation(Tool.class);
|
||||
return tool != null && tool.returnDirect();
|
||||
}
|
||||
|
||||
public static ToolCallResultConverter getToolCallResultConverter(Method method) {
|
||||
Assert.notNull(method, "method cannot be null");
|
||||
var tool = method.getAnnotation(Tool.class);
|
||||
if (tool == null) {
|
||||
return new DefaultToolCallResultConverter();
|
||||
@@ -81,8 +85,9 @@ public final class ToolUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static List<String> getDuplicateToolNames(FunctionCallback... functionCallbacks) {
|
||||
return Stream.of(functionCallbacks)
|
||||
public static List<String> getDuplicateToolNames(FunctionCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
|
||||
return Stream.of(toolCallbacks)
|
||||
.collect(Collectors.groupingBy(FunctionCallback::getName, Collectors.counting()))
|
||||
.entrySet()
|
||||
.stream()
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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 org.junit.jupiter.api.Test;
|
||||
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.chat.prompt.Prompt;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
import org.springframework.ai.tool.execution.ToolExecutionException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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().tools("toolA").build());
|
||||
|
||||
assertThat(toolDefinitions).containsExactly(toolCallback.getToolDefinition());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenToolCallbackDoesNotExistThenThrow() {
|
||||
ToolCallingManager toolCallingManager = LegacyToolCallingManager.builder().functionCallbacks(List.of()).build();
|
||||
|
||||
assertThatThrownBy(() -> toolCallingManager
|
||||
.resolveToolDefinitions(ToolCallingChatOptions.builder().tools("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 LegacyToolCallingManagerTests.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!")));
|
||||
|
||||
List<Message> toolCallHistory = toolCallingManager.executeToolCalls(prompt, chatResponse);
|
||||
|
||||
assertThat(toolCallHistory).contains(expectedToolResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleToolCallsInChatResponseThenExecute() {
|
||||
ToolCallback toolCallbackA = new LegacyToolCallingManagerTests.TestToolCallback("toolA");
|
||||
ToolCallback toolCallbackB = new LegacyToolCallingManagerTests.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!")));
|
||||
|
||||
List<Message> toolCallHistory = toolCallingManager.executeToolCalls(prompt, chatResponse);
|
||||
|
||||
assertThat(toolCallHistory).contains(expectedToolResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenToolCallWithExceptionThenReturnError() {
|
||||
ToolCallback toolCallback = new LegacyToolCallingManagerTests.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!")));
|
||||
|
||||
List<Message> toolCallHistory = toolCallingManager.executeToolCalls(prompt, chatResponse);
|
||||
|
||||
assertThat(toolCallHistory).contains(expectedToolResponse);
|
||||
}
|
||||
|
||||
static class TestToolCallback implements ToolCallback {
|
||||
|
||||
private final ToolDefinition toolDefinition;
|
||||
|
||||
public TestToolCallback(String name) {
|
||||
this.toolDefinition = ToolDefinition.builder().name(name).inputSchema("{}").build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return toolDefinition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String toolInput) {
|
||||
return "Mission accomplished!";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class FailingToolCallback implements ToolCallback {
|
||||
|
||||
private final ToolDefinition toolDefinition;
|
||||
|
||||
public FailingToolCallback(String name) {
|
||||
this.toolDefinition = ToolDefinition.builder().name(name).inputSchema("{}").build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return toolDefinition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String toolInput) {
|
||||
throw new ToolExecutionException(toolDefinition, new IllegalStateException("You failed this city!"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,9 +16,15 @@
|
||||
package org.springframework.ai.model.tool;
|
||||
|
||||
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;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ToolCallingChatOptions}.
|
||||
@@ -67,4 +73,92 @@ class ToolCallingChatOptionsTests {
|
||||
assertThat(ToolCallingChatOptions.isInternalToolExecutionEnabled(options)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMergeRuntimeAndDefaultToolNames() {
|
||||
Set<String> runtimeToolNames = Set.of("toolA");
|
||||
Set<String> defaultToolNames = Set.of("toolB");
|
||||
Set<String> mergedToolNames = ToolCallingChatOptions.mergeToolNames(runtimeToolNames, defaultToolNames);
|
||||
assertThat(mergedToolNames).containsExactlyInAnyOrder("toolA", "toolB");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMergeRuntimeAndEmptyDefaultToolNames() {
|
||||
Set<String> runtimeToolNames = Set.of("toolA");
|
||||
Set<String> defaultToolNames = Set.of();
|
||||
Set<String> mergedToolNames = ToolCallingChatOptions.mergeToolNames(runtimeToolNames, defaultToolNames);
|
||||
assertThat(mergedToolNames).containsExactlyInAnyOrder("toolA");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMergeEmptyRuntimeAndDefaultToolNames() {
|
||||
Set<String> runtimeToolNames = Set.of();
|
||||
Set<String> defaultToolNames = Set.of("toolB");
|
||||
Set<String> mergedToolNames = ToolCallingChatOptions.mergeToolNames(runtimeToolNames, defaultToolNames);
|
||||
assertThat(mergedToolNames).containsExactlyInAnyOrder("toolB");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMergeEmptyRuntimeAndEmptyDefaultToolNames() {
|
||||
Set<String> runtimeToolNames = Set.of();
|
||||
Set<String> defaultToolNames = Set.of();
|
||||
Set<String> mergedToolNames = ToolCallingChatOptions.mergeToolNames(runtimeToolNames, defaultToolNames);
|
||||
assertThat(mergedToolNames).containsExactlyInAnyOrder();
|
||||
}
|
||||
|
||||
@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,
|
||||
defaultToolCallbacks);
|
||||
assertThat(mergedToolCallbacks).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMergeRuntimeAndEmptyDefaultToolCallbacks() {
|
||||
List<FunctionCallback> runtimeToolCallbacks = List.of(new TestToolCallback("toolA"));
|
||||
List<FunctionCallback> defaultToolCallbacks = List.of();
|
||||
List<FunctionCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
|
||||
defaultToolCallbacks);
|
||||
assertThat(mergedToolCallbacks).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMergeEmptyRuntimeAndDefaultToolCallbacks() {
|
||||
List<FunctionCallback> runtimeToolCallbacks = List.of();
|
||||
List<FunctionCallback> defaultToolCallbacks = List.of(new TestToolCallback("toolB"));
|
||||
List<FunctionCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
|
||||
defaultToolCallbacks);
|
||||
assertThat(mergedToolCallbacks).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMergeEmptyRuntimeAndEmptyDefaultToolCallbacks() {
|
||||
List<FunctionCallback> runtimeToolCallbacks = List.of();
|
||||
List<FunctionCallback> defaultToolCallbacks = List.of();
|
||||
List<FunctionCallback> mergedToolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(runtimeToolCallbacks,
|
||||
defaultToolCallbacks);
|
||||
assertThat(mergedToolCallbacks).hasSize(0);
|
||||
}
|
||||
|
||||
static class TestToolCallback implements ToolCallback {
|
||||
|
||||
private final ToolDefinition toolDefinition;
|
||||
|
||||
public TestToolCallback(String name) {
|
||||
this.toolDefinition = ToolDefinition.builder().name(name).inputSchema("{}").build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return toolDefinition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String toolInput) {
|
||||
return "Mission accomplished!";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.ai.tool.resolution;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
|
||||
@@ -42,7 +43,7 @@ class StaticToolCallbackResolverTests {
|
||||
|
||||
@Test
|
||||
void whenToolCallbacksContainNullElementsThenThrowException() {
|
||||
var toolCallbacks = new ArrayList<ToolCallback>();
|
||||
var toolCallbacks = new ArrayList<FunctionCallback>();
|
||||
toolCallbacks.add(null);
|
||||
assertThatThrownBy(() -> new StaticToolCallbackResolver(toolCallbacks))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
|
||||
Reference in New Issue
Block a user