Anthropic - Adopt ToolCallingManager API

- Update AnthropicChatModel to use the new ToolCallingManager API, while ensuring full API backward compatibility.
- Introduce Builder to instantiate a new AnthropicChatModel since the number of overloaded constructors is growing too big.
- Update documentation about tool calling and Anthropic support for that.

Part of the #2207 epic

Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
This commit is contained in:
Thomas Vitale
2025-02-13 00:30:14 +01:00
committed by Christian Tzolov
parent 8463454a87
commit b7dcfc1198
10 changed files with 450 additions and 139 deletions

View File

@@ -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.
@@ -18,17 +18,24 @@ package org.springframework.ai.anthropic;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.type.TypeReference;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.model.tool.LegacyToolCallingManager;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.tool.ToolExecutionResult;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.util.json.JsonParser;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -96,6 +103,8 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
private static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention();
private static final ToolCallingManager DEFAULT_TOOL_CALLING_MANAGER = ToolCallingManager.builder().build();
/**
* The retry template used to retry the OpenAI API calls.
*/
@@ -116,6 +125,8 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
*/
private final ObservationRegistry observationRegistry;
private final ToolCallingManager toolCallingManager;
/**
* Conventions to use for generating observations.
*/
@@ -124,7 +135,9 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
/**
* Construct a new {@link AnthropicChatModel} instance.
* @param anthropicApi the lower-level API for the Anthropic service.
* @deprecated Use {@link AnthropicChatModel.Builder}.
*/
@Deprecated
public AnthropicChatModel(AnthropicApi anthropicApi) {
this(anthropicApi,
AnthropicChatOptions.builder()
@@ -138,7 +151,9 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
* Construct a new {@link AnthropicChatModel} instance.
* @param anthropicApi the lower-level API for the Anthropic service.
* @param defaultOptions the default options used for the chat completion requests.
* @deprecated Use {@link AnthropicChatModel.Builder}.
*/
@Deprecated
public AnthropicChatModel(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions) {
this(anthropicApi, defaultOptions, RetryUtils.DEFAULT_RETRY_TEMPLATE);
}
@@ -148,7 +163,9 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
* @param anthropicApi the lower-level API for the Anthropic service.
* @param defaultOptions the default options used for the chat completion requests.
* @param retryTemplate the retry template used to retry the Anthropic API calls.
* @deprecated Use {@link AnthropicChatModel.Builder}.
*/
@Deprecated
public AnthropicChatModel(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions,
RetryTemplate retryTemplate) {
this(anthropicApi, defaultOptions, retryTemplate, null);
@@ -161,10 +178,11 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
* @param retryTemplate the retry template used to retry the Anthropic API calls.
* @param functionCallbackResolver the function callback resolver used to resolve the
* function by its name.
* @deprecated Use {@link AnthropicChatModel.Builder}.
*/
@Deprecated
public AnthropicChatModel(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions,
RetryTemplate retryTemplate, FunctionCallbackResolver functionCallbackResolver) {
this(anthropicApi, defaultOptions, retryTemplate, functionCallbackResolver, List.of());
}
@@ -177,7 +195,9 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
* function by its name.
* @param toolFunctionCallbacks the tool function callbacks used to handle the tool
* calls.
* @deprecated Use {@link AnthropicChatModel.Builder}.
*/
@Deprecated
public AnthropicChatModel(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions,
RetryTemplate retryTemplate, FunctionCallbackResolver functionCallbackResolver,
List<FunctionCallback> toolFunctionCallbacks) {
@@ -194,27 +214,49 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
* function by its name.
* @param toolFunctionCallbacks the tool function callbacks used to handle the tool
* calls.
* @deprecated Use {@link AnthropicChatModel.Builder}.
*/
@Deprecated
public AnthropicChatModel(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions,
RetryTemplate retryTemplate, FunctionCallbackResolver functionCallbackResolver,
List<FunctionCallback> toolFunctionCallbacks, ObservationRegistry observationRegistry) {
RetryTemplate retryTemplate, @Nullable FunctionCallbackResolver functionCallbackResolver,
@Nullable List<FunctionCallback> toolFunctionCallbacks, ObservationRegistry observationRegistry) {
this(anthropicApi, defaultOptions,
LegacyToolCallingManager.builder()
.functionCallbackResolver(functionCallbackResolver)
.functionCallbacks(toolFunctionCallbacks)
.build(),
retryTemplate, observationRegistry);
logger.warn("This constructor is deprecated and will be removed in the next milestone. "
+ "Please use the MistralAiChatModel.Builder or the new constructor accepting ToolCallingManager instead.");
}
super(functionCallbackResolver, defaultOptions, toolFunctionCallbacks);
public AnthropicChatModel(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions,
ToolCallingManager toolCallingManager, RetryTemplate retryTemplate,
ObservationRegistry observationRegistry) {
// We do not pass the 'defaultOptions' to the AbstractToolSupport,
// because it modifies them. We are using ToolCallingManager instead,
// so we just pass empty options here.
super(null, AnthropicChatOptions.builder().build(), List.of());
Assert.notNull(anthropicApi, "AnthropicApi must not be null");
Assert.notNull(defaultOptions, "DefaultOptions must not be null");
Assert.notNull(retryTemplate, "RetryTemplate must not be null");
Assert.notNull(observationRegistry, "ObservationRegistry must not be null");
Assert.notNull(anthropicApi, "anthropicApi cannot be null");
Assert.notNull(defaultOptions, "defaultOptions cannot be null");
Assert.notNull(toolCallingManager, "toolCallingManager cannot be null");
Assert.notNull(retryTemplate, "retryTemplate cannot be null");
Assert.notNull(observationRegistry, "observationRegistry cannot be null");
this.anthropicApi = anthropicApi;
this.defaultOptions = defaultOptions;
this.toolCallingManager = toolCallingManager;
this.retryTemplate = retryTemplate;
this.observationRegistry = observationRegistry;
}
@Override
public ChatResponse call(Prompt prompt) {
return this.internalCall(prompt, null);
// Before moving any further, build the final request Prompt,
// merging runtime and default options.
Prompt requestPrompt = buildRequestPrompt(prompt);
return this.internalCall(requestPrompt, null);
}
public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatResponse) {
@@ -223,7 +265,7 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.provider(AnthropicApi.PROVIDER_NAME)
.requestOptions(buildRequestOptions(request))
.requestOptions(prompt.getOptions())
.build();
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
@@ -247,10 +289,21 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
return chatResponse;
});
if (!isProxyToolCalls(prompt, this.defaultOptions) && response != null
&& this.isToolCall(response, Set.of("tool_use"))) {
var toolCallConversation = handleToolCalls(prompt, response);
return this.internalCall(new Prompt(toolCallConversation, prompt.getOptions()), response);
if (ToolCallingChatOptions.isInternalToolExecutionEnabled(prompt.getOptions()) && response != null
&& response.hasToolCalls()) {
var toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response);
if (toolExecutionResult.returnDirect()) {
// Return tool execution result directly to the client.
return ChatResponse.builder()
.from(response)
.generations(ToolExecutionResult.buildGenerations(toolExecutionResult))
.build();
}
else {
// Send the tool execution result back to the model.
return this.internalCall(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()),
response);
}
}
return response;
@@ -263,7 +316,10 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return this.internalStream(prompt, null);
// Before moving any further, build the final request Prompt,
// merging runtime and default options.
Prompt requestPrompt = buildRequestPrompt(prompt);
return this.internalStream(requestPrompt, null);
}
public Flux<ChatResponse> internalStream(Prompt prompt, ChatResponse previousChatResponse) {
@@ -273,7 +329,7 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.provider(AnthropicApi.PROVIDER_NAME)
.requestOptions(buildRequestOptions(request))
.requestOptions(prompt.getOptions())
.build();
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
@@ -291,9 +347,18 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
Usage accumulatedUsage = UsageUtils.getCumulativeUsage(currentChatResponseUsage, previousChatResponse);
ChatResponse chatResponse = toChatResponse(chatCompletionResponse, accumulatedUsage);
if (!isProxyToolCalls(prompt, this.defaultOptions) && this.isToolCall(chatResponse, Set.of("tool_use"))) {
var toolCallConversation = handleToolCalls(prompt, chatResponse);
return this.internalStream(new Prompt(toolCallConversation, prompt.getOptions()), chatResponse);
if (ToolCallingChatOptions.isInternalToolExecutionEnabled(prompt.getOptions()) && chatResponse.hasToolCalls() && chatResponse.hasFinishReasons(Set.of("tool_use"))) {
var toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, chatResponse);
if (toolExecutionResult.returnDirect()) {
// Return tool execution result directly to the client.
return Flux.just(ChatResponse.builder().from(chatResponse)
.generations(ToolExecutionResult.buildGenerations(toolExecutionResult))
.build());
} else {
// Send the tool execution result back to the model.
return this.internalStream(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()),
chatResponse);
}
}
return Mono.just(chatResponse);
@@ -341,7 +406,7 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
var functionCallId = toolToUse.id();
var functionName = toolToUse.name();
var functionArguments = ModelOptionsUtils.toJsonString(toolToUse.input());
var functionArguments = JsonParser.toJson(toolToUse.input());
toolCalls
.add(new AssistantMessage.ToolCall(functionCallId, "function", functionName, functionArguments));
@@ -397,9 +462,55 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
+ ". Supported types are: images (image/*) and PDF documents (application/pdf)");
}
ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
Prompt buildRequestPrompt(Prompt prompt) {
// Process runtime options
AnthropicChatOptions runtimeOptions = null;
if (prompt.getOptions() != null) {
if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) {
runtimeOptions = ModelOptionsUtils.copyToTarget(toolCallingChatOptions, ToolCallingChatOptions.class,
AnthropicChatOptions.class);
}
else if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOptions) {
runtimeOptions = ModelOptionsUtils.copyToTarget(functionCallingOptions, FunctionCallingOptions.class,
AnthropicChatOptions.class);
}
else {
runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,
AnthropicChatOptions.class);
}
}
Set<String> functionsForThisRequest = new HashSet<>();
// Define request options by merging runtime options and default options
AnthropicChatOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions,
AnthropicChatOptions.class);
// Merge @JsonIgnore-annotated options explicitly since they are ignored by
// Jackson, used by ModelOptionsUtils.
if (runtimeOptions != null) {
requestOptions.setInternalToolExecutionEnabled(
ModelOptionsUtils.mergeOption(runtimeOptions.isInternalToolExecutionEnabled(),
this.defaultOptions.isInternalToolExecutionEnabled()));
requestOptions.setToolNames(ToolCallingChatOptions.mergeToolNames(runtimeOptions.getToolNames(),
this.defaultOptions.getToolNames()));
requestOptions.setToolCallbacks(ToolCallingChatOptions.mergeToolCallbacks(runtimeOptions.getToolCallbacks(),
this.defaultOptions.getToolCallbacks()));
requestOptions.setToolContext(ToolCallingChatOptions.mergeToolContext(runtimeOptions.getToolContext(),
this.defaultOptions.getToolContext()));
}
else {
requestOptions.setInternalToolExecutionEnabled(this.defaultOptions.isInternalToolExecutionEnabled());
requestOptions.setToolNames(this.defaultOptions.getToolNames());
requestOptions.setToolCallbacks(this.defaultOptions.getToolCallbacks());
requestOptions.setToolContext(this.defaultOptions.getToolContext());
}
ToolCallingChatOptions.validateToolCallbacks(requestOptions.getToolCallbacks());
return new Prompt(prompt.getInstructions(), requestOptions);
}
ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
List<AnthropicMessage> userMessages = prompt.getInstructions()
.stream()
@@ -457,58 +568,29 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
ChatCompletionRequest request = new ChatCompletionRequest(this.defaultOptions.getModel(), userMessages,
systemPrompt, this.defaultOptions.getMaxTokens(), this.defaultOptions.getTemperature(), stream);
if (prompt.getOptions() != null) {
AnthropicChatOptions updatedRuntimeOptions;
if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOptions) {
updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(functionCallingOptions,
FunctionCallingOptions.class, AnthropicChatOptions.class);
}
else {
updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,
AnthropicChatOptions.class);
}
AnthropicChatOptions requestOptions = (AnthropicChatOptions) prompt.getOptions();
request = ModelOptionsUtils.merge(requestOptions, request, ChatCompletionRequest.class);
functionsForThisRequest.addAll(this.runtimeFunctionCallbackConfigurations(updatedRuntimeOptions));
request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, ChatCompletionRequest.class);
}
if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) {
functionsForThisRequest.addAll(this.defaultOptions.getFunctions());
}
request = ModelOptionsUtils.merge(request, this.defaultOptions, ChatCompletionRequest.class);
if (!CollectionUtils.isEmpty(functionsForThisRequest)) {
List<AnthropicApi.Tool> tools = getFunctionTools(functionsForThisRequest);
request = ChatCompletionRequest.from(request).withTools(tools).build();
// Add the tool definitions to the request's tools parameter.
List<ToolDefinition> toolDefinitions = this.toolCallingManager.resolveToolDefinitions(requestOptions);
if (!CollectionUtils.isEmpty(toolDefinitions)) {
request = ModelOptionsUtils.merge(request, this.defaultOptions, ChatCompletionRequest.class);
request = ChatCompletionRequest.from(request).withTools(getFunctionTools(toolDefinitions)).build();
}
return request;
}
private List<AnthropicApi.Tool> getFunctionTools(Set<String> functionNames) {
return this.resolveFunctionCallbacks(functionNames).stream().map(functionCallback -> {
var description = functionCallback.getDescription();
var name = functionCallback.getName();
String inputSchema = functionCallback.getInputTypeSchema();
return new AnthropicApi.Tool(name, description, ModelOptionsUtils.jsonToMap(inputSchema));
private List<AnthropicApi.Tool> getFunctionTools(List<ToolDefinition> toolDefinitions) {
return toolDefinitions.stream().map(toolDefinition -> {
var name = toolDefinition.name();
var description = toolDefinition.description();
String inputSchema = toolDefinition.inputSchema();
return new AnthropicApi.Tool(name, description, JsonParser.fromJson(inputSchema, new TypeReference<>() {
}));
}).toList();
}
private ChatOptions buildRequestOptions(AnthropicApi.ChatCompletionRequest request) {
return ChatOptions.builder()
.model(request.model())
.maxTokens(request.maxTokens())
.stopSequences(request.stopSequences())
.temperature(request.temperature())
.topK(request.topK())
.topP(request.topP())
.build();
}
@Override
public ChatOptions getDefaultOptions() {
return AnthropicChatOptions.fromOptions(this.defaultOptions);
@@ -523,4 +605,92 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
this.observationConvention = observationConvention;
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private AnthropicApi anthropicApi;
private AnthropicChatOptions defaultOptions = AnthropicChatOptions.builder()
.model(DEFAULT_MODEL_NAME)
.maxTokens(DEFAULT_MAX_TOKENS)
.temperature(DEFAULT_TEMPERATURE)
.build();
private RetryTemplate retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE;
private FunctionCallbackResolver functionCallbackResolver;
private List<FunctionCallback> toolCallbacks;
private ToolCallingManager toolCallingManager;
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
private Builder() {
}
public Builder anthropicApi(AnthropicApi anthropicApi) {
this.anthropicApi = anthropicApi;
return this;
}
public Builder defaultOptions(AnthropicChatOptions defaultOptions) {
this.defaultOptions = defaultOptions;
return this;
}
public Builder retryTemplate(RetryTemplate retryTemplate) {
this.retryTemplate = retryTemplate;
return this;
}
public Builder toolCallingManager(ToolCallingManager toolCallingManager) {
this.toolCallingManager = toolCallingManager;
return this;
}
@Deprecated
public Builder functionCallbackResolver(FunctionCallbackResolver functionCallbackResolver) {
this.functionCallbackResolver = functionCallbackResolver;
return this;
}
@Deprecated
public Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
this.toolCallbacks = toolCallbacks;
return this;
}
public Builder observationRegistry(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
return this;
}
public AnthropicChatModel build() {
if (toolCallingManager != null) {
Assert.isNull(functionCallbackResolver,
"functionCallbackResolver cannot be set when toolCallingManager is set");
Assert.isNull(toolCallbacks, "toolCallbacks cannot be set when toolCallingManager is set");
return new AnthropicChatModel(anthropicApi, defaultOptions, toolCallingManager, retryTemplate,
observationRegistry);
}
if (functionCallbackResolver != null) {
Assert.isNull(toolCallingManager,
"toolCallingManager cannot be set when functionCallbackResolver is set");
List<FunctionCallback> toolCallbacks = this.toolCallbacks != null ? this.toolCallbacks : List.of();
return new AnthropicChatModel(anthropicApi, defaultOptions, retryTemplate, functionCallbackResolver,
toolCallbacks, observationRegistry);
}
return new AnthropicChatModel(anthropicApi, defaultOptions, DEFAULT_TOOL_CALLING_MANAGER, retryTemplate,
observationRegistry);
}
}
}

View File

@@ -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.
@@ -17,6 +17,8 @@
package org.springframework.ai.anthropic;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -30,7 +32,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -42,7 +46,7 @@ import org.springframework.util.Assert;
* @since 1.0.0
*/
@JsonInclude(Include.NON_NULL)
public class AnthropicChatOptions implements FunctionCallingOptions {
public class AnthropicChatOptions implements ToolCallingChatOptions {
// @formatter:off
private @JsonProperty("model") String model;
@@ -54,34 +58,27 @@ public class AnthropicChatOptions implements FunctionCallingOptions {
private @JsonProperty("top_k") Integer topK;
/**
* Tool Function Callbacks to register with the ChatModel. For Prompt
* Options the functionCallbacks are automatically enabled for the duration of the
* prompt execution. For Default Options the functionCallbacks are registered but
* disabled by default. Use the enableFunctions to set the functions from the registry
* to be used by the ChatModel chat completion requests.
* Collection of {@link ToolCallback}s to be used for tool calling in the chat
* completion requests.
*/
@JsonIgnore
private List<FunctionCallback> functionCallbacks = new ArrayList<>();
private List<FunctionCallback> toolCallbacks = new ArrayList<>();
/**
* List of functions, identified by their names, to configure for function calling in
* the chat completion requests. Functions with those names must exist in the
* functionCallbacks registry. The {@link #functionCallbacks} from the PromptOptions
* are automatically enabled for the duration of the prompt execution.
*
* Note that function enabled with the default options are enabled for all chat
* completion requests. This could impact the token count and the billing. If the
* functions is set in a prompt options, then the enabled functions are only active
* for the duration of this prompt execution.
* Collection of tool names to be resolved at runtime and used for tool calling in the
* chat completion requests.
*/
@JsonIgnore
private Set<String> functions = new HashSet<>();
private Set<String> toolNames = new HashSet<>();
/**
* Whether to enable the tool execution lifecycle internally in ChatModel.
*/
@JsonIgnore
private Boolean internalToolExecutionEnabled;
@JsonIgnore
private Boolean proxyToolCalls;
@JsonIgnore
private Map<String, Object> toolContext;
private Map<String, Object> toolContext = new HashMap<>();
// @formatter:on
@@ -97,9 +94,9 @@ public class AnthropicChatOptions implements FunctionCallingOptions {
.temperature(fromOptions.getTemperature())
.topP(fromOptions.getTopP())
.topK(fromOptions.getTopK())
.functionCallbacks(fromOptions.getFunctionCallbacks())
.functions(fromOptions.getFunctions())
.proxyToolCalls(fromOptions.getProxyToolCalls())
.toolCallbacks(fromOptions.getToolCallbacks())
.toolNames(fromOptions.getToolNames())
.internalToolExecutionEnabled(fromOptions.isInternalToolExecutionEnabled())
.toolContext(fromOptions.getToolContext())
.build();
}
@@ -167,25 +164,73 @@ public class AnthropicChatOptions implements FunctionCallingOptions {
}
@Override
@JsonIgnore
public List<FunctionCallback> getToolCallbacks() {
return this.toolCallbacks;
}
@Override
@JsonIgnore
public void setToolCallbacks(List<FunctionCallback> toolCallbacks) {
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
this.toolCallbacks = toolCallbacks;
}
@Override
@JsonIgnore
public Set<String> getToolNames() {
return this.toolNames;
}
@Override
@JsonIgnore
public void setToolNames(Set<String> toolNames) {
Assert.notNull(toolNames, "toolNames cannot be null");
Assert.noNullElements(toolNames, "toolNames cannot contain null elements");
toolNames.forEach(tool -> Assert.hasText(tool, "toolNames cannot contain empty elements"));
this.toolNames = toolNames;
}
@Override
@Nullable
@JsonIgnore
public Boolean isInternalToolExecutionEnabled() {
return internalToolExecutionEnabled;
}
@Override
@JsonIgnore
public void setInternalToolExecutionEnabled(@Nullable Boolean internalToolExecutionEnabled) {
this.internalToolExecutionEnabled = internalToolExecutionEnabled;
}
@Override
@Deprecated
@JsonIgnore
public List<FunctionCallback> getFunctionCallbacks() {
return this.functionCallbacks;
return this.getToolCallbacks();
}
@Override
@Deprecated
@JsonIgnore
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
Assert.notNull(functionCallbacks, "FunctionCallbacks must not be null");
this.functionCallbacks = functionCallbacks;
this.setToolCallbacks(functionCallbacks);
}
@Override
@Deprecated
@JsonIgnore
public Set<String> getFunctions() {
return this.functions;
return this.getToolNames();
}
@Override
public void setFunctions(Set<String> functions) {
Assert.notNull(functions, "Function must not be null");
this.functions = functions;
@Deprecated
@JsonIgnore
public void setFunctions(Set<String> functionNames) {
this.setToolNames(functionNames);
}
@Override
@@ -201,20 +246,26 @@ public class AnthropicChatOptions implements FunctionCallingOptions {
}
@Override
@Deprecated
@JsonIgnore
public Boolean getProxyToolCalls() {
return this.proxyToolCalls;
return this.internalToolExecutionEnabled != null ? !this.internalToolExecutionEnabled : null;
}
@Deprecated
@JsonIgnore
public void setProxyToolCalls(Boolean proxyToolCalls) {
this.proxyToolCalls = proxyToolCalls;
this.internalToolExecutionEnabled = proxyToolCalls != null ? !proxyToolCalls : null;
}
@Override
@JsonIgnore
public Map<String, Object> getToolContext() {
return this.toolContext;
}
@Override
@JsonIgnore
public void setToolContext(Map<String, Object> toolContext) {
this.toolContext = toolContext;
}
@@ -268,25 +319,54 @@ public class AnthropicChatOptions implements FunctionCallingOptions {
return this;
}
public Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
this.options.setToolCallbacks(toolCallbacks);
return this;
}
public Builder toolCallbacks(FunctionCallback... toolCallbacks) {
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
this.options.toolCallbacks.addAll(Arrays.asList(toolCallbacks));
return this;
}
public Builder toolNames(Set<String> toolNames) {
Assert.notNull(toolNames, "toolNames cannot be null");
this.options.setToolNames(toolNames);
return this;
}
public Builder toolNames(String... toolNames) {
Assert.notNull(toolNames, "toolNames cannot be null");
this.options.toolNames.addAll(Set.of(toolNames));
return this;
}
public Builder internalToolExecutionEnabled(@Nullable Boolean internalToolExecutionEnabled) {
this.options.setInternalToolExecutionEnabled(internalToolExecutionEnabled);
return this;
}
@Deprecated
public Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
this.options.functionCallbacks = functionCallbacks;
return this;
return toolCallbacks(functionCallbacks);
}
@Deprecated
public Builder functions(Set<String> functionNames) {
Assert.notNull(functionNames, "Function names must not be null");
this.options.functions = functionNames;
return this;
return toolNames(functionNames);
}
@Deprecated
public Builder function(String functionName) {
Assert.hasText(functionName, "Function name must not be empty");
this.options.functions.add(functionName);
return this;
return toolNames(functionName);
}
@Deprecated
public Builder proxyToolCalls(Boolean proxyToolCalls) {
this.options.proxyToolCalls = proxyToolCalls;
if (proxyToolCalls != null) {
this.options.setInternalToolExecutionEnabled(!proxyToolCalls);
}
return this;
}

View File

@@ -238,7 +238,7 @@ public class AnthropicApi {
CLAUDE_3_OPUS("claude-3-opus-latest"),
/**
* The CLAUDE_3_SONNET
* The CLAUDE_3_SONNET (Deprecated. To be removed on July 21, 2025)
*/
CLAUDE_3_SONNET("claude-3-sonnet-20240229"),
@@ -254,12 +254,12 @@ public class AnthropicApi {
// Legacy models
/**
* The CLAUDE_2_1
* The CLAUDE_2_1 (Deprecated. To be removed on July 21, 2025)
*/
CLAUDE_2_1("claude-2.1"),
/**
* The CLAUDE_2_0
* The CLAUDE_2_0 (Deprecated. To be removed on July 21, 2025)
*/
CLAUDE_2("claude-2.0");

View File

@@ -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.
@@ -26,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
* @author Alexandros Pappas
* @author Thomas Vitale
*/
public class ChatCompletionRequestTests {
@@ -35,7 +36,9 @@ public class ChatCompletionRequestTests {
var client = new AnthropicChatModel(new AnthropicApi("TEST"),
AnthropicChatOptions.builder().model("DEFAULT_MODEL").temperature(66.6).build());
var request = client.createRequest(new Prompt("Test message content"), false);
var prompt = client.buildRequestPrompt(new Prompt("Test message content"));
var request = client.createRequest(prompt, false);
assertThat(request.messages()).hasSize(1);
assertThat(request.stream()).isFalse();
@@ -43,8 +46,10 @@ public class ChatCompletionRequestTests {
assertThat(request.model()).isEqualTo("DEFAULT_MODEL");
assertThat(request.temperature()).isEqualTo(66.6);
request = client.createRequest(new Prompt("Test message content",
AnthropicChatOptions.builder().model("PROMPT_MODEL").temperature(99.9).build()), true);
prompt = client.buildRequestPrompt(new Prompt("Test message content",
AnthropicChatOptions.builder().model("PROMPT_MODEL").temperature(99.9).build()));
request = client.createRequest(prompt, true);
assertThat(request.messages()).hasSize(1);
assertThat(request.stream()).isTrue();

View File

@@ -23,6 +23,7 @@ import java.util.Set;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.model.ModelResponse;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
@@ -111,6 +112,21 @@ public class ChatResponse implements ModelResponse<Generation> {
return generations.stream().anyMatch(generation -> generation.getOutput().hasToolCalls());
}
/**
* Whether the model has finished with any of the given finish reasons.
*/
public boolean hasFinishReasons(Set<String> finishReasons) {
Assert.notNull(finishReasons, "finishReasons cannot be null");
if (CollectionUtils.isEmpty(generations)) {
return false;
}
return generations.stream().anyMatch(generation -> {
var finishReason = (generation.getMetadata().getFinishReason() != null)
? generation.getMetadata().getFinishReason() : "";
return finishReasons.stream().map(String::toLowerCase).toList().contains(finishReason.toLowerCase());
});
}
@Override
public String toString() {
return "ChatResponse [metadata=" + this.chatResponseMetadata + ", generations=" + this.generations + "]";

View File

@@ -18,11 +18,14 @@ package org.springframework.ai.chat.model;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link ChatResponse}.
@@ -48,4 +51,32 @@ class ChatResponseTests {
assertThat(chatResponse.hasToolCalls()).isFalse();
}
@Test
void whenFinishReasonIsNullThenThrow() {
var chatResponse = ChatResponse.builder()
.generations(List.of(new Generation(new AssistantMessage("Result"),
ChatGenerationMetadata.builder().finishReason("completed").build())))
.build();
assertThatThrownBy(() -> chatResponse.hasFinishReasons(null)).isInstanceOf(IllegalArgumentException.class)
.hasMessage("finishReasons cannot be null");
}
@Test
void whenFinishReasonIsPresent() {
ChatResponse chatResponse = ChatResponse.builder()
.generations(List.of(new Generation(new AssistantMessage("Result"),
ChatGenerationMetadata.builder().finishReason("completed").build())))
.build();
assertThat(chatResponse.hasFinishReasons(Set.of("completed"))).isTrue();
}
@Test
void whenFinishReasonIsNotPresent() {
ChatResponse chatResponse = ChatResponse.builder()
.generations(List.of(new Generation(new AssistantMessage("Result"),
ChatGenerationMetadata.builder().finishReason("failed").build())))
.build();
assertThat(chatResponse.hasFinishReasons(Set.of("completed"))).isFalse();
}
}

View File

@@ -8,7 +8,7 @@
*** xref:api/chat/comparison.adoc[Chat Models Comparison]
*** xref:api/chat/bedrock-converse.adoc[Amazon Bedrock Converse]
*** xref:api/chat/anthropic-chat.adoc[Anthropic 3]
**** xref:api/chat/functions/anthropic-chat-functions.adoc[Anthropic Function Calling]
**** xref:api/chat/functions/anthropic-chat-functions.adoc[Anthropic Function Calling (Deprecated)]
*** xref:api/chat/azure-openai-chat.adoc[Azure OpenAI]
**** xref:api/chat/functions/azure-open-ai-chat-functions.adoc[Azure OpenAI Function Calling]
*** xref:api/chat/deepseek-chat.adoc[DeepSeek AI]
@@ -17,19 +17,19 @@
*** xref:api/chat/groq-chat.adoc[Groq]
*** xref:api/chat/huggingface.adoc[Hugging Face]
*** xref:api/chat/mistralai-chat.adoc[Mistral AI]
**** xref:api/chat/functions/mistralai-chat-functions.adoc[Mistral Function Calling]
**** xref:api/chat/functions/mistralai-chat-functions.adoc[Mistral Function Calling (Deprecated)]
*** xref:api/chat/minimax-chat.adoc[MiniMax]
**** xref:api/chat/functions/minimax-chat-functions.adoc[MinmaxFunction Calling]
*** xref:api/chat/moonshot-chat.adoc[Moonshot AI]
//// **** xref:api/chat/functions/moonshot-chat-functions.adoc[Moonshot Function Calling]
*** xref:api/chat/nvidia-chat.adoc[NVIDIA]
*** xref:api/chat/ollama-chat.adoc[Ollama]
**** xref:api/chat/functions/ollama-chat-functions.adoc[Ollama Function Calling]
**** xref:api/chat/functions/ollama-chat-functions.adoc[Ollama Function Calling (Deprecated)]
*** xref:api/chat/perplexity-chat.adoc[Perplexity AI]
*** OCI Generative AI
**** xref:api/chat/oci-genai/cohere-chat.adoc[Cohere]
*** xref:api/chat/openai-chat.adoc[OpenAI]
**** xref:api/chat/functions/openai-chat-functions.adoc[OpenAI Function Calling]
**** xref:api/chat/functions/openai-chat-functions.adoc[OpenAI Function Calling (Deprecated)]
*** xref:api/chat/qianfan-chat.adoc[QianFan]
*** xref:api/chat/zhipuai-chat.adoc[ZhiPu AI]
// **** xref:api/chat/functions/zhipuai-chat-functions.adoc[Function Calling]

View File

@@ -108,9 +108,12 @@ The prefix `spring.ai.anthropic.chat` is the property prefix that lets you confi
| spring.ai.anthropic.chat.options.stop-sequence | Custom text sequences that will cause the model to stop generating. Our models will normally stop when they have naturally completed their turn, which will result in a response stop_reason of "end_turn". If you want the model to stop generating when it encounters custom strings of text, you can use the stop_sequences parameter. If the model encounters one of the custom sequences, the response stop_reason value will be "stop_sequence" and the response stop_sequence value will contain the matched stop sequence. | -
| spring.ai.anthropic.chat.options.top-p | Use nucleus sampling. In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both. Recommended for advanced use cases only. You usually only need to use temperature. | -
| spring.ai.anthropic.chat.options.top-k | Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Learn more technical details here. Recommended for advanced use cases only. You usually only need to use temperature. | -
| spring.ai.anthropic.chat.options.functions | List of functions, identified by their names, to enable for function calling in a single prompt requests. Functions with those names must exist in the functionCallbacks registry. | -
| spring.ai.anthropic.chat.options.functionCallbacks | Tool Function Callbacks to register with the ChatModel. | -
| spring.ai.anthropic.chat.options.proxy-tool-calls | If true, the Spring AI will not handle the function calls internally, but will proxy them to the client. Then is the client's responsibility to handle the function calls, dispatch them to the appropriate function, and return the results. If false (the default), the Spring AI will handle the function calls internally. Applicable only for chat models with function calling support | false
| spring.ai.anthropic.chat.options.toolNames | List of tools, identified by their names, to enable for tool calling in a single prompt requests. Tools with those names must exist in the toolCallbacks registry. | -
| spring.ai.anthropic.chat.options.toolCallbacks | Tool Callbacks to register with the ChatModel. | -
| spring.ai.anthropic.chat.options.internal-tool-execution-enabled | If false, the Spring AI will not handle the tool calls internally, but will proxy them to the client. Then it is the client's responsibility to handle the tool calls, dispatch them to the appropriate function, and return the results. If true (the default), the Spring AI will handle the function calls internally. Applicable only for chat models with function calling support | true
| (**deprecated** - replaced by `toolNames`) spring.ai.anthropic.chat.options.functions | List of functions, identified by their names, to enable for function calling in a single prompt requests. Functions with those names must exist in the functionCallbacks registry. | -
| (**deprecated** - replaced by `toolCallbacks`) spring.ai.anthropic.chat.options.functionCallbacks | Tool Function Callbacks to register with the ChatModel. | -
| (**deprecated** - replaced by a negated `internal-tool-execution-enabled`) spring.ai.anthropic.chat.options.proxy-tool-calls | If true, the Spring AI will not handle the function calls internally, but will proxy them to the client. Then is the client's responsibility to handle the function calls, dispatch them to the appropriate function, and return the results. If false (the default), the Spring AI will handle the function calls internally. Applicable only for chat models with function calling support | false
|====
TIP: All properties prefixed with `spring.ai.anthropic.chat.options` can be overridden at runtime by adding a request specific <<chat-options>> to the `Prompt` call.
@@ -138,9 +141,9 @@ ChatResponse response = chatModel.call(
TIP: In addition to the model specific https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatOptions.java[AnthropicChatOptions] you can use a portable https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptions.java[ChatOptions] instance, created with the https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptionsBuilder.java[ChatOptionsBuilder#builder()].
== Function Calling
== Tool/Function Calling
You can register custom Java functions with the `AnthropicChatModel` and have the Anthropic Claude model intelligently choose to output a JSON object containing arguments to call one or many of the registered functions.
You can register custom Java Tools with the `AnthropicChatModel` and have the Anthropic Claude model intelligently choose to output a JSON object containing arguments to call one or many of the registered functions.
This is a powerful technique to connect the LLM capabilities with external tools and APIs.
Read more about xref:api/chat/functions/anthropic-chat-functions.adoc[Anthropic Function Calling].

View File

@@ -1,4 +1,6 @@
= Anthropic Function Calling
= Anthropic Function Calling (Deprecated)
WARNING: This page describes the previous version of the Function Calling API, which has been deprecated and marked for remove in the next release. The current version is available at xref:api/tools.adoc[Tool Calling]. See the xref:api/tools-migration.adoc[Migration Guide] for more information.
TIP: Starting of Jul 1st, 2024, streaming function calling and Tool use is supported.

View File

@@ -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.
@@ -16,17 +16,16 @@
package org.springframework.ai.autoconfigure.anthropic;
import java.util.List;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.autoconfigure.chat.model.ToolCallingAutoConfiguration;
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
@@ -50,13 +49,14 @@ import org.springframework.web.reactive.function.client.WebClient;
* @author Thomas Vitale
* @since 1.0.0
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
ToolCallingAutoConfiguration.class })
@EnableConfigurationProperties({ AnthropicChatProperties.class, AnthropicConnectionProperties.class })
@ConditionalOnClass(AnthropicApi.class)
@ConditionalOnProperty(prefix = AnthropicChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
WebClientAutoConfiguration.class })
ToolCallingAutoConfiguration.class, WebClientAutoConfiguration.class })
public class AnthropicAutoConfiguration {
@Bean
@@ -74,13 +74,17 @@ public class AnthropicAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AnthropicChatModel anthropicChatModel(AnthropicApi anthropicApi, AnthropicChatProperties chatProperties,
RetryTemplate retryTemplate, FunctionCallbackResolver functionCallbackResolver,
List<FunctionCallback> toolFunctionCallbacks, ObjectProvider<ObservationRegistry> observationRegistry,
RetryTemplate retryTemplate, ToolCallingManager toolCallingManager,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var chatModel = new AnthropicChatModel(anthropicApi, chatProperties.getOptions(), retryTemplate,
functionCallbackResolver, toolFunctionCallbacks,
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
var chatModel = AnthropicChatModel.builder()
.anthropicApi(anthropicApi)
.defaultOptions(chatProperties.getOptions())
.toolCallingManager(toolCallingManager)
.retryTemplate(retryTemplate)
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.build();
observationConvention.ifAvailable(chatModel::setObservationConvention);