Add Function Calling Support for Anthropic Features
- expanded the AnthropicApi to include Tool, facilitating request and response abstractions. - extended AnthropicChatClient to inherit AbstractFunctionCallSupport, with implementation of all necessary methods and function registration protocols. - implemented FunctionCallingOptions interface in AnthropicChatOptions. - added tools integration tests for AnthropoicApi and AnthropicChatClient. - extended the auto-configuration with functional calling functionality. - added ITs for tools auto-config. - updated documentation on anthropic function calling and relevant pages for comprehensive coverage.
This commit is contained in:
committed by
Mark Pollack
parent
e268975a80
commit
f249e64651
@@ -17,8 +17,10 @@ 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.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -28,13 +30,13 @@ import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletion;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.RequestMessage;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.MediaContent;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.MediaContent;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.MediaContent.Type;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.RequestMessage;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.StreamResponse;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.Usage;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.MediaContent.Type;
|
||||
import org.springframework.ai.anthropic.metadata.AnthropicChatResponseMetadata;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
@@ -45,6 +47,8 @@ import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.model.function.AbstractFunctionCallSupport;
|
||||
import org.springframework.ai.model.function.FunctionCallbackContext;
|
||||
import org.springframework.ai.retry.RetryUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
@@ -57,7 +61,9 @@ import org.springframework.util.CollectionUtils;
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class AnthropicChatClient implements ChatClient, StreamingChatClient {
|
||||
public class AnthropicChatClient extends
|
||||
AbstractFunctionCallSupport<AnthropicApi.RequestMessage, AnthropicApi.ChatCompletionRequest, ResponseEntity<AnthropicApi.ChatCompletion>>
|
||||
implements ChatClient, StreamingChatClient {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AnthropicChatClient.class);
|
||||
|
||||
@@ -112,6 +118,22 @@ public class AnthropicChatClient implements ChatClient, StreamingChatClient {
|
||||
*/
|
||||
public AnthropicChatClient(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions,
|
||||
RetryTemplate retryTemplate) {
|
||||
this(anthropicApi, defaultOptions, retryTemplate, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@link AnthropicChatClient} instance.
|
||||
* @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.
|
||||
* @param functionCallbackContext the function callback context used to store the
|
||||
* state of the function calls.
|
||||
*/
|
||||
public AnthropicChatClient(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions,
|
||||
RetryTemplate retryTemplate, FunctionCallbackContext functionCallbackContext) {
|
||||
|
||||
super(functionCallbackContext);
|
||||
|
||||
Assert.notNull(anthropicApi, "AnthropicApi must not be null");
|
||||
Assert.notNull(defaultOptions, "DefaultOptions must not be null");
|
||||
Assert.notNull(retryTemplate, "RetryTemplate must not be null");
|
||||
@@ -127,7 +149,7 @@ public class AnthropicChatClient implements ChatClient, StreamingChatClient {
|
||||
ChatCompletionRequest request = createRequest(prompt, false);
|
||||
|
||||
return this.retryTemplate.execute(ctx -> {
|
||||
ResponseEntity<ChatCompletion> completionEntity = this.anthropicApi.chatCompletionEntity(request);
|
||||
ResponseEntity<ChatCompletion> completionEntity = this.callWithFunctionSupport(request);
|
||||
return toChatResponse(completionEntity.getBody());
|
||||
});
|
||||
}
|
||||
@@ -229,6 +251,8 @@ public class AnthropicChatClient implements ChatClient, StreamingChatClient {
|
||||
|
||||
ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
|
||||
|
||||
Set<String> functionsForThisRequest = new HashSet<>();
|
||||
|
||||
List<RequestMessage> userMessages = prompt.getInstructions()
|
||||
.stream()
|
||||
.filter(m -> m.getMessageType() != MessageType.SYSTEM)
|
||||
@@ -260,6 +284,10 @@ public class AnthropicChatClient implements ChatClient, StreamingChatClient {
|
||||
AnthropicChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions,
|
||||
ChatOptions.class, AnthropicChatOptions.class);
|
||||
|
||||
Set<String> promptEnabledFunctions = this.handleFunctionCallbackConfigurations(updatedRuntimeOptions,
|
||||
IS_RUNTIME_CALL);
|
||||
functionsForThisRequest.addAll(promptEnabledFunctions);
|
||||
|
||||
request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, ChatCompletionRequest.class);
|
||||
}
|
||||
else {
|
||||
@@ -269,12 +297,32 @@ public class AnthropicChatClient implements ChatClient, StreamingChatClient {
|
||||
}
|
||||
|
||||
if (this.defaultOptions != null) {
|
||||
Set<String> defaultEnabledFunctions = this.handleFunctionCallbackConfigurations(this.defaultOptions,
|
||||
!IS_RUNTIME_CALL);
|
||||
functionsForThisRequest.addAll(defaultEnabledFunctions);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private static class ChatCompletionBuilder {
|
||||
|
||||
private String type;
|
||||
@@ -343,4 +391,63 @@ public class AnthropicChatClient implements ChatClient, StreamingChatClient {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ChatCompletionRequest doCreateToolResponseRequest(ChatCompletionRequest previousRequest,
|
||||
RequestMessage responseMessage, List<RequestMessage> conversationHistory) {
|
||||
|
||||
List<MediaContent> toolToUseList = responseMessage.content()
|
||||
.stream()
|
||||
.filter(c -> c.type() == MediaContent.Type.TOOL_USE)
|
||||
.toList();
|
||||
|
||||
List<MediaContent> toolResults = new ArrayList<>();
|
||||
|
||||
for (MediaContent toolToUse : toolToUseList) {
|
||||
|
||||
var functionCallId = toolToUse.id();
|
||||
var functionName = toolToUse.name();
|
||||
var functionArguments = toolToUse.input();
|
||||
|
||||
if (!this.functionCallbackRegister.containsKey(functionName)) {
|
||||
throw new IllegalStateException("No function callback found for function name: " + functionName);
|
||||
}
|
||||
|
||||
String functionResponse = this.functionCallbackRegister.get(functionName)
|
||||
.call(ModelOptionsUtils.toJsonString(functionArguments));
|
||||
|
||||
toolResults.add(new MediaContent(Type.TOOL_RESULT, functionCallId, functionResponse));
|
||||
}
|
||||
|
||||
// Add the function response to the conversation.
|
||||
conversationHistory.add(new RequestMessage(toolResults, Role.USER));
|
||||
|
||||
// Recursively call chatCompletionWithTools until the model doesn't call a
|
||||
// functions anymore.
|
||||
return ChatCompletionRequest.from(previousRequest).withMessages(conversationHistory).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<RequestMessage> doGetUserMessages(ChatCompletionRequest request) {
|
||||
return request.messages();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RequestMessage doGetToolResponseMessage(ResponseEntity<ChatCompletion> response) {
|
||||
return new RequestMessage(response.getBody().content(), Role.ASSISTANT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResponseEntity<ChatCompletion> doChatCompletion(ChatCompletionRequest request) {
|
||||
return this.anthropicApi.chatCompletionEntity(request);
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Override
|
||||
protected boolean isToolFunctionCall(ResponseEntity<ChatCompletion> response) {
|
||||
if (response == null || response.getBody() == null || CollectionUtils.isEmpty(response.getBody().content())) {
|
||||
return false;
|
||||
}
|
||||
return response.getBody().content().stream().anyMatch(content -> content.type() == MediaContent.Type.TOOL_USE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,14 +15,22 @@
|
||||
*/
|
||||
package org.springframework.ai.anthropic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallingOptions;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The options to be used when sending a chat request to the Anthropic API.
|
||||
@@ -31,7 +39,7 @@ import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public class AnthropicChatOptions implements ChatOptions {
|
||||
public class AnthropicChatOptions implements ChatOptions, FunctionCallingOptions {
|
||||
|
||||
// @formatter:off
|
||||
private @JsonProperty("model") String model;
|
||||
@@ -41,6 +49,32 @@ public class AnthropicChatOptions implements ChatOptions {
|
||||
private @JsonProperty("temperature") Float temperature;
|
||||
private @JsonProperty("top_p") Float topP;
|
||||
private @JsonProperty("top_k") Integer topK;
|
||||
|
||||
/**
|
||||
* Tool Function Callbacks to register with the ChatClient. 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 ChatClient chat completion requests.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
@JsonIgnore
|
||||
private List<FunctionCallback> functionCallbacks = 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.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
@JsonIgnore
|
||||
private Set<String> functions = new HashSet<>();
|
||||
// @formatter:on
|
||||
|
||||
public static Builder builder() {
|
||||
@@ -86,6 +120,23 @@ public class AnthropicChatOptions implements ChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
this.options.functionCallbacks = functionCallbacks;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withFunctions(Set<String> functionNames) {
|
||||
Assert.notNull(functionNames, "Function names must not be null");
|
||||
this.options.functions = functionNames;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withFunction(String functionName) {
|
||||
Assert.hasText(functionName, "Function name must not be empty");
|
||||
this.options.functions.add(functionName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AnthropicChatOptions build() {
|
||||
return this.options;
|
||||
}
|
||||
@@ -150,4 +201,26 @@ public class AnthropicChatOptions implements ChatOptions {
|
||||
this.topK = topK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FunctionCallback> getFunctionCallbacks() {
|
||||
return this.functionCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
Assert.notNull(functionCallbacks, "FunctionCallbacks must not be null");
|
||||
this.functionCallbacks = functionCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getFunctions() {
|
||||
return this.functions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFunctions(Set<String> functions) {
|
||||
Assert.notNull(functions, "Function must not be null");
|
||||
this.functions = functions;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,10 +46,14 @@ public class AnthropicApi {
|
||||
|
||||
private static final String HEADER_ANTHROPIC_VERSION = "anthropic-version";
|
||||
|
||||
private static final String HEADER_ANTHROPIC_BETA = "anthropic-beta";
|
||||
|
||||
public static final String DEFAULT_BASE_URL = "https://api.anthropic.com";
|
||||
|
||||
public static final String DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
|
||||
|
||||
public static final String DEFAULT_ANTHROPIC_BETA_VERSION = "tools-2024-04-04";
|
||||
|
||||
private static final Predicate<String> SSE_DONE_PREDICATE = "[DONE]"::equals;
|
||||
|
||||
private final RestClient restClient;
|
||||
@@ -87,6 +91,7 @@ public class AnthropicApi {
|
||||
Consumer<HttpHeaders> jsonContentHeaders = headers -> {
|
||||
headers.add(HEADER_X_API_KEY, anthropicApiKey);
|
||||
headers.add(HEADER_ANTHROPIC_VERSION, anthropicVersion);
|
||||
headers.add(HEADER_ANTHROPIC_BETA, DEFAULT_ANTHROPIC_BETA_VERSION);
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
};
|
||||
|
||||
@@ -165,6 +170,10 @@ public class AnthropicApi {
|
||||
* @param topK 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.
|
||||
* @param tools Definitions of tools that the model may use. If provided the model may
|
||||
* return tool_use content blocks that represent the model's use of those tools. You
|
||||
* can then run those tools using the tool input generated by the model and then
|
||||
* optionally return results back to the model using tool_result content blocks.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ChatCompletionRequest( // @formatter:off
|
||||
@@ -177,17 +186,18 @@ public class AnthropicApi {
|
||||
@JsonProperty("stream") Boolean stream,
|
||||
@JsonProperty("temperature") Float temperature,
|
||||
@JsonProperty("top_p") Float topP,
|
||||
@JsonProperty("top_k") Integer topK) {
|
||||
@JsonProperty("top_k") Integer topK,
|
||||
@JsonProperty("tools") List<Tool> tools) {
|
||||
// @formatter:on
|
||||
|
||||
public ChatCompletionRequest(String model, List<RequestMessage> messages, String system, Integer maxTokens,
|
||||
Float temperature, Boolean stream) {
|
||||
this(model, messages, system, maxTokens, null, null, stream, temperature, null, null);
|
||||
this(model, messages, system, maxTokens, null, null, stream, temperature, null, null, null);
|
||||
}
|
||||
|
||||
public ChatCompletionRequest(String model, List<RequestMessage> messages, String system, Integer maxTokens,
|
||||
List<String> stopSequences, Float temperature, Boolean stream) {
|
||||
this(model, messages, system, maxTokens, null, stopSequences, stream, temperature, null, null);
|
||||
this(model, messages, system, maxTokens, null, stopSequences, stream, temperature, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,6 +209,122 @@ public class AnthropicApi {
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record Metadata(@JsonProperty("user_id") String userId) {
|
||||
}
|
||||
|
||||
public static ChatCompletionRequestBuilder builder() {
|
||||
return new ChatCompletionRequestBuilder();
|
||||
}
|
||||
|
||||
public static ChatCompletionRequestBuilder from(ChatCompletionRequest request) {
|
||||
return new ChatCompletionRequestBuilder(request);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChatCompletionRequestBuilder {
|
||||
|
||||
private String model;
|
||||
|
||||
private List<RequestMessage> messages;
|
||||
|
||||
private String system;
|
||||
|
||||
private Integer maxTokens;
|
||||
|
||||
private ChatCompletionRequest.Metadata metadata;
|
||||
|
||||
private List<String> stopSequences;
|
||||
|
||||
private Boolean stream = false;
|
||||
|
||||
private Float temperature;
|
||||
|
||||
private Float topP;
|
||||
|
||||
private Integer topK;
|
||||
|
||||
private List<Tool> tools;
|
||||
|
||||
private ChatCompletionRequestBuilder() {
|
||||
}
|
||||
|
||||
private ChatCompletionRequestBuilder(ChatCompletionRequest request) {
|
||||
this.model = request.model;
|
||||
this.messages = request.messages;
|
||||
this.system = request.system;
|
||||
this.maxTokens = request.maxTokens;
|
||||
this.metadata = request.metadata;
|
||||
this.stopSequences = request.stopSequences;
|
||||
this.stream = request.stream;
|
||||
this.temperature = request.temperature;
|
||||
this.topP = request.topP;
|
||||
this.topK = request.topK;
|
||||
this.tools = request.tools;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withModel(ChatModel model) {
|
||||
this.model = model.getValue();
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withModel(String model) {
|
||||
this.model = model;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withMessages(List<RequestMessage> messages) {
|
||||
this.messages = messages;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withSystem(String system) {
|
||||
this.system = system;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withMaxTokens(Integer maxTokens) {
|
||||
this.maxTokens = maxTokens;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withMetadata(ChatCompletionRequest.Metadata metadata) {
|
||||
this.metadata = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withStopSequences(List<String> stopSequences) {
|
||||
this.stopSequences = stopSequences;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withStream(Boolean stream) {
|
||||
this.stream = stream;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withTemperature(Float temperature) {
|
||||
this.temperature = temperature;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withTopP(Float topP) {
|
||||
this.topP = topP;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withTopK(Integer topK) {
|
||||
this.topK = topK;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequestBuilder withTools(List<Tool> tools) {
|
||||
this.tools = tools;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionRequest build() {
|
||||
return new ChatCompletionRequest(model, messages, system, maxTokens, metadata, stopSequences, stream,
|
||||
temperature, topP, topK, tools);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,7 +364,18 @@ public class AnthropicApi {
|
||||
@JsonProperty("type") Type type,
|
||||
@JsonProperty("source") Source source,
|
||||
@JsonProperty("text") String text,
|
||||
@JsonProperty("index") Integer index // applicable only for streaming responses.
|
||||
|
||||
// applicable only for streaming responses.
|
||||
@JsonProperty("index") Integer index,
|
||||
|
||||
// tool_use response only
|
||||
@JsonProperty("id") String id,
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("input") Map<String, Object> input,
|
||||
|
||||
// tool_result response only
|
||||
@JsonProperty("tool_use_id") String toolUseId,
|
||||
@JsonProperty("content") String content
|
||||
) {
|
||||
// @formatter:on
|
||||
|
||||
@@ -247,11 +384,20 @@ public class AnthropicApi {
|
||||
}
|
||||
|
||||
public MediaContent(Source source) {
|
||||
this(Type.IMAGE, source, null, null);
|
||||
this(Type.IMAGE, source, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public MediaContent(String text) {
|
||||
this(Type.TEXT, null, text, null);
|
||||
this(Type.TEXT, null, text, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
// Tool result
|
||||
public MediaContent(Type type, String toolUseId, String content) {
|
||||
this(type, null, null, null, null, null, null, toolUseId, content);
|
||||
}
|
||||
|
||||
public MediaContent(Type type, Source source, String text, Integer index) {
|
||||
this(type, source, text, index, null, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,6 +405,18 @@ public class AnthropicApi {
|
||||
*/
|
||||
public enum Type {
|
||||
|
||||
/**
|
||||
* Tool request
|
||||
*/
|
||||
@JsonProperty("tool_use")
|
||||
TOOL_USE,
|
||||
|
||||
/**
|
||||
* Send tool result back to LLM.
|
||||
*/
|
||||
@JsonProperty("tool_result")
|
||||
TOOL_RESULT,
|
||||
|
||||
/**
|
||||
* Text message.
|
||||
*/
|
||||
@@ -301,6 +459,14 @@ public class AnthropicApi {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record Tool(// @formatter:off
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("description") String description,
|
||||
@JsonProperty("input_schema") Map<String, Object> inputSchema) {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id Unique object identifier. The format and length of IDs may change over
|
||||
* time.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.ai.anthropic;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -26,6 +27,8 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.anthropic.api.tool.MockWeatherService;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
@@ -37,6 +40,7 @@ import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.model.function.FunctionCallbackWrapper;
|
||||
import org.springframework.ai.parser.BeanOutputParser;
|
||||
import org.springframework.ai.parser.ListOutputParser;
|
||||
import org.springframework.ai.parser.MapOutputParser;
|
||||
@@ -189,4 +193,30 @@ class AnthropicChatClientIT {
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("bananas", "apple", "basket");
|
||||
}
|
||||
|
||||
@Test
|
||||
void functionCallTest() {
|
||||
|
||||
UserMessage userMessage = new UserMessage(
|
||||
"What's the weather like in San Francisco, Tokyo and Paris? Return the result in Celsius.");
|
||||
|
||||
List<Message> messages = new ArrayList<>(List.of(userMessage));
|
||||
|
||||
var promptOptions = AnthropicChatOptions.builder()
|
||||
.withModel(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue())
|
||||
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
|
||||
.withName("getCurrentWeather")
|
||||
.withDescription("Get the weather in location")
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(messages, promptOptions));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
Generation generation = response.getResults().get(0);
|
||||
assertThat(generation.getOutput().getContent()).containsAnyOf("30.0", "30");
|
||||
assertThat(generation.getOutput().getContent()).containsAnyOf("10.0", "10");
|
||||
assertThat(generation.getOutput().getContent()).containsAnyOf("15.0", "15");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2023 - 2024 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.anthropic.api.tool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletion;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.MediaContent;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.RequestMessage;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
|
||||
import org.springframework.ai.anthropic.api.tool.XmlHelper.FunctionCalls;
|
||||
import org.springframework.ai.anthropic.api.tool.XmlHelper.Tools;
|
||||
import org.springframework.ai.anthropic.api.tool.XmlHelper.Tools.ToolDescription;
|
||||
import org.springframework.ai.anthropic.api.tool.XmlHelper.Tools.ToolDescription.Parameter;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Experiments with
|
||||
* <a href="https://docs.anthropic.com/claude/docs/functions-external-tools">Anthropic
|
||||
* Functions & external tools</a>.
|
||||
*
|
||||
* <p>
|
||||
* <a href=
|
||||
* "https://www.linkedin.com/pulse/tool-usefunction-calling-anthropics-claude-3-opus-llm-micky-multani-fsmrc">Tool
|
||||
* Use(Function Calling) with Anthropic's Claude 3 Opus LLM</a>
|
||||
* <p>
|
||||
* <a href=
|
||||
* "https://www.codeproject.com/Articles/5379174/Csharp-Anthropic-Claude-Library-You-Can-Call-Claud">Anthropic
|
||||
* Functions & external tools</a>
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+")
|
||||
@SuppressWarnings("null")
|
||||
public class AnthropicApiLegacyToolIT {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AnthropicApiLegacyToolIT.class);
|
||||
|
||||
AnthropicApi anthropicApi = new AnthropicApi(System.getenv("ANTHROPIC_API_KEY"));
|
||||
|
||||
public static final String TOO_SYSTEM_PROMPT_TEMPLATE = """
|
||||
In this environment you have access to a set of tools you can use to answer the user's question.
|
||||
|
||||
You may call them like this:
|
||||
<function_calls>
|
||||
<invoke>
|
||||
<tool_name>$TOOL_NAME</tool_name>
|
||||
<parameters>
|
||||
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
|
||||
...
|
||||
</parameters>
|
||||
</invoke>
|
||||
</function_calls>
|
||||
|
||||
Here are the tools available:
|
||||
<tools>%s</tools>
|
||||
""";
|
||||
|
||||
public static final ConcurrentHashMap<String, Function> FUNCTIONS = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
FUNCTIONS.put("getCurrentWeather", new MockWeatherService());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toolCalls() {
|
||||
|
||||
String toolDescription = XmlHelper.toXml(new Tools(List.of(new ToolDescription("getCurrentWeather",
|
||||
"Get the weather in location. Return temperature in 30°F or 30°C format.",
|
||||
List.of(new Parameter("location", "string", "The city and state e.g. San Francisco, CA"),
|
||||
new Parameter("unit", "enum", "Temperature unit. Use only C or F. Default is C."))))));
|
||||
|
||||
logger.info("TOOLS: " + toolDescription);
|
||||
|
||||
String systemPrompt = String.format(TOO_SYSTEM_PROMPT_TEMPLATE, toolDescription);
|
||||
|
||||
RequestMessage chatCompletionMessage = new RequestMessage(
|
||||
List.of(new MediaContent("What's the weather like in Paris? Show the temperature in Celsius.")),
|
||||
// "What's the weather like in San Francisco, Tokyo, and Paris? Show the
|
||||
// temperature in Celsius.")),
|
||||
Role.USER);
|
||||
|
||||
ChatCompletionRequest chatCompletionRequest = new ChatCompletionRequest(
|
||||
AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(), List.of(chatCompletionMessage), systemPrompt, 500,
|
||||
0.8f, false);
|
||||
|
||||
ResponseEntity<ChatCompletion> chatCompletion = doCall(chatCompletionRequest);
|
||||
|
||||
var responseText = chatCompletion.getBody().content().get(0).text();
|
||||
logger.info("FINAL RESPONSE: " + responseText);
|
||||
|
||||
assertThat(responseText).contains("15");
|
||||
}
|
||||
|
||||
private ResponseEntity<ChatCompletion> doCall(ChatCompletionRequest chatCompletionRequest) {
|
||||
|
||||
ResponseEntity<ChatCompletion> response = anthropicApi.chatCompletionEntity(chatCompletionRequest);
|
||||
|
||||
FunctionCalls functionCalls = XmlHelper.extractFunctionCalls(response.getBody().content().get(0).text());
|
||||
|
||||
if (functionCalls == null) {
|
||||
return response;
|
||||
}
|
||||
|
||||
logger.info("FunctionCalls from the LLM: " + functionCalls);
|
||||
|
||||
MockWeatherService.Request request = ModelOptionsUtils.mapToClass(functionCalls.invoke().parameters(),
|
||||
MockWeatherService.Request.class);
|
||||
|
||||
logger.info("Resolved function request param: " + request);
|
||||
|
||||
Object functionCallResponseData = FUNCTIONS.get(functionCalls.invoke().toolName()).apply(request);
|
||||
|
||||
XmlHelper.FunctionResults functionResults = new XmlHelper.FunctionResults(List
|
||||
.of(new XmlHelper.FunctionResults.Result(functionCalls.invoke().toolName(), functionCallResponseData)));
|
||||
|
||||
String content = XmlHelper.toXml(functionResults);
|
||||
|
||||
logger.info("Function response XML : " + content);
|
||||
|
||||
RequestMessage chatCompletionMessage2 = new RequestMessage(List.of(new MediaContent(content)), Role.USER);
|
||||
|
||||
return doCall(new ChatCompletionRequest(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(),
|
||||
List.of(chatCompletionMessage2), null, 500, 0.8f, false));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.ai.anthropic.api.tool;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
@@ -28,125 +29,130 @@ import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletion;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.MediaContent;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.MediaContent.Type;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.RequestMessage;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
|
||||
import org.springframework.ai.anthropic.api.tool.XmlHelper.FunctionCalls;
|
||||
import org.springframework.ai.anthropic.api.tool.XmlHelper.Tools;
|
||||
import org.springframework.ai.anthropic.api.tool.XmlHelper.Tools.ToolDescription;
|
||||
import org.springframework.ai.anthropic.api.tool.XmlHelper.Tools.ToolDescription.Parameter;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.Tool;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Experiments with
|
||||
* <a href="https://docs.anthropic.com/claude/docs/functions-external-tools">Anthropic
|
||||
* Functions & external tools</a>.
|
||||
*
|
||||
* <p>
|
||||
* <a href=
|
||||
* "https://www.linkedin.com/pulse/tool-usefunction-calling-anthropics-claude-3-opus-llm-micky-multani-fsmrc">Tool
|
||||
* Use(Function Calling) with Anthropic's Claude 3 Opus LLM</a>
|
||||
* <p>
|
||||
* <a href=
|
||||
* "https://www.codeproject.com/Articles/5379174/Csharp-Anthropic-Claude-Library-You-Can-Call-Claud">Anthropic
|
||||
* Functions & external tools</a>
|
||||
* <a href="https://docs.anthropic.com/claude/docs/tool-use-examples">Tool use
|
||||
* examples</a> <br/>
|
||||
* <a href="https://docs.anthropic.com/claude/docs/tool-use">Tool use (function
|
||||
* calling)</a>
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+")
|
||||
@SuppressWarnings("null")
|
||||
public class AnthropicApiToolIT {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AnthropicApiToolIT.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(AnthropicApiLegacyToolIT.class);
|
||||
|
||||
AnthropicApi anthropicApi = new AnthropicApi(System.getenv("ANTHROPIC_API_KEY"));
|
||||
|
||||
public static final String TOO_SYSTEM_PROMPT_TEMPLATE = """
|
||||
In this environment you have access to a set of tools you can use to answer the user's question.
|
||||
|
||||
You may call them like this:
|
||||
<function_calls>
|
||||
<invoke>
|
||||
<tool_name>$TOOL_NAME</tool_name>
|
||||
<parameters>
|
||||
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
|
||||
...
|
||||
</parameters>
|
||||
</invoke>
|
||||
</function_calls>
|
||||
|
||||
Here are the tools available:
|
||||
<tools>%s</tools>
|
||||
""";
|
||||
|
||||
public static final ConcurrentHashMap<String, Function> FUNCTIONS = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
FUNCTIONS.put("getCurrentWeather", new MockWeatherService());
|
||||
}
|
||||
|
||||
List<Tool> tools = List.of(new Tool("getCurrentWeather",
|
||||
"Get the weather in location. Return temperature in 30°F or 30°C format.", ModelOptionsUtils.jsonToMap("""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state e.g. San Francisco, CA"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": ["C", "F"]
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
}
|
||||
""")));
|
||||
|
||||
@Test
|
||||
void toolCalls() {
|
||||
|
||||
String toolDescription = XmlHelper.toXml(new Tools(List.of(new ToolDescription("getCurrentWeather",
|
||||
"Get the weather in location. Return temperature in 30°F or 30°C format.",
|
||||
List.of(new Parameter("location", "string", "The city and state e.g. San Francisco, CA"),
|
||||
new Parameter("unit", "enum", "Temperature unit. Use only C or F. Default is C."))))));
|
||||
List<RequestMessage> messageConversation = new ArrayList<>();
|
||||
|
||||
logger.info("TOOLS: " + toolDescription);
|
||||
|
||||
String systemPrompt = String.format(TOO_SYSTEM_PROMPT_TEMPLATE, toolDescription);
|
||||
|
||||
RequestMessage chatCompletionMessage = new RequestMessage(
|
||||
List.of(new MediaContent("What's the weather like in Paris? Show the temperature in Celsius.")),
|
||||
// "What's the weather like in San Francisco, Tokyo, and Paris? Show the
|
||||
// temperature in Celsius.")),
|
||||
RequestMessage chatCompletionMessage = new RequestMessage(List.of(new MediaContent(
|
||||
"What's the weather like in San Francisco, Tokyo, and Paris? Show the temperature in Celsius.")),
|
||||
Role.USER);
|
||||
|
||||
ChatCompletionRequest chatCompletionRequest = new ChatCompletionRequest(
|
||||
AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(), List.of(chatCompletionMessage), systemPrompt, 500,
|
||||
0.8f, false);
|
||||
messageConversation.add(chatCompletionMessage);
|
||||
|
||||
ResponseEntity<ChatCompletion> chatCompletion = doCall(chatCompletionRequest);
|
||||
ResponseEntity<ChatCompletion> chatCompletion = doCall(messageConversation);
|
||||
|
||||
var responseText = chatCompletion.getBody().content().get(0).text();
|
||||
logger.info("FINAL RESPONSE: " + responseText);
|
||||
|
||||
assertThat(responseText).contains("15");
|
||||
assertThat(responseText).contains("10");
|
||||
assertThat(responseText).contains("30");
|
||||
}
|
||||
|
||||
private ResponseEntity<ChatCompletion> doCall(ChatCompletionRequest chatCompletionRequest) {
|
||||
private ResponseEntity<ChatCompletion> doCall(List<RequestMessage> messageConversation) {
|
||||
|
||||
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
|
||||
.withModel(AnthropicApi.ChatModel.CLAUDE_3_OPUS)
|
||||
.withMessages(messageConversation)
|
||||
.withMaxTokens(1500)
|
||||
.withTemperature(0.8f)
|
||||
.withTools(tools)
|
||||
.build();
|
||||
|
||||
ResponseEntity<ChatCompletion> response = anthropicApi.chatCompletionEntity(chatCompletionRequest);
|
||||
|
||||
FunctionCalls functionCalls = XmlHelper.extractFunctionCalls(response.getBody().content().get(0).text());
|
||||
List<MediaContent> toolToUseList = response.getBody()
|
||||
.content()
|
||||
.stream()
|
||||
.filter(c -> c.type() == MediaContent.Type.TOOL_USE)
|
||||
.toList();
|
||||
|
||||
if (functionCalls == null) {
|
||||
if (CollectionUtils.isEmpty(toolToUseList)) {
|
||||
return response;
|
||||
}
|
||||
// Add use tool message to the conversation history
|
||||
messageConversation.add(new RequestMessage(response.getBody().content(), Role.ASSISTANT));
|
||||
|
||||
logger.info("FunctionCalls from the LLM: " + functionCalls);
|
||||
List<MediaContent> toolResults = new ArrayList<>();
|
||||
|
||||
MockWeatherService.Request request = ModelOptionsUtils.mapToClass(functionCalls.invoke().parameters(),
|
||||
MockWeatherService.Request.class);
|
||||
for (MediaContent toolToUse : toolToUseList) {
|
||||
|
||||
logger.info("Resolved function request param: " + request);
|
||||
var id = toolToUse.id();
|
||||
var name = toolToUse.name();
|
||||
var input = toolToUse.input();
|
||||
|
||||
Object functionCallResponseData = FUNCTIONS.get(functionCalls.invoke().toolName()).apply(request);
|
||||
logger.info("FunctionCalls from the LLM: " + name);
|
||||
|
||||
XmlHelper.FunctionResults functionResults = new XmlHelper.FunctionResults(List
|
||||
.of(new XmlHelper.FunctionResults.Result(functionCalls.invoke().toolName(), functionCallResponseData)));
|
||||
MockWeatherService.Request request = ModelOptionsUtils.mapToClass(input, MockWeatherService.Request.class);
|
||||
|
||||
String content = XmlHelper.toXml(functionResults);
|
||||
logger.info("Resolved function request param: " + request);
|
||||
|
||||
logger.info("Function response XML : " + content);
|
||||
Object functionCallResponseData = FUNCTIONS.get(name).apply(request);
|
||||
|
||||
RequestMessage chatCompletionMessage2 = new RequestMessage(List.of(new MediaContent(content)), Role.USER);
|
||||
String content = ModelOptionsUtils.toJsonString(functionCallResponseData);
|
||||
|
||||
return doCall(new ChatCompletionRequest(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(),
|
||||
List.of(chatCompletionMessage2), null, 500, 0.8f, false));
|
||||
logger.info("Function response : " + content);
|
||||
|
||||
toolResults.add(new MediaContent(Type.TOOL_RESULT, id, content));
|
||||
}
|
||||
|
||||
// Add function response message to the conversation history
|
||||
messageConversation.add(new RequestMessage(toolResults, Role.USER));
|
||||
|
||||
return doCall(messageConversation);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public abstract class AbstractFunctionCallSupport<Msg, Req, Resp> {
|
||||
*/
|
||||
protected final FunctionCallbackContext functionCallbackContext;
|
||||
|
||||
public AbstractFunctionCallSupport(FunctionCallbackContext functionCallbackContext) {
|
||||
protected AbstractFunctionCallSupport(FunctionCallbackContext functionCallbackContext) {
|
||||
this.functionCallbackContext = functionCallbackContext;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
*** xref:api/chat/mistralai-chat.adoc[Mistral AI]
|
||||
**** xref:api/chat/functions/mistralai-chat-functions.adoc[Function Calling]
|
||||
*** xref:api/chat/anthropic-chat.adoc[Anthropic 3]
|
||||
***** xref:api/chat/functions/anthropic-chat-functions.adoc[Function Calling]
|
||||
*** xref:api/chat/watsonx-ai-chat.adoc[Watsonx.AI]
|
||||
** xref:api/embeddings.adoc[]
|
||||
*** xref:api/embeddings/openai-embeddings.adoc[OpenAI]
|
||||
|
||||
@@ -101,6 +101,8 @@ 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.mistralai.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.mistralai.chat.options.functionCallbacks | MistralAI Tool Function Callbacks to register with the ChatClient. | -
|
||||
|====
|
||||
|
||||
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.
|
||||
@@ -128,6 +130,11 @@ ChatResponse response = chatClient.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/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/ChatOptionsBuilder.java[ChatOptionsBuilder#builder()].
|
||||
|
||||
== Function Calling
|
||||
|
||||
You can register custom Java functions with the `AnthropicChatClient` 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].
|
||||
|
||||
== Multimodal
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
= Anthropic Function Calling
|
||||
|
||||
You can register custom Java functions with the `AnthropicChatClient` and have the Anthropic models intelligently choose to output a JSON object containing arguments to call one or many of the registered functions.
|
||||
This allows you to connect the LLM capabilities with external tools and APIs.
|
||||
The `claude-3-opus`, `claude-3-sonnet` and `claude-3-haiku` link:https://docs.anthropic.com/claude/docs/tool-use#tool-use-best-practices-and-limitations[models are trained to detect when a function should be called] and to respond with JSON that adheres to the function signature.
|
||||
|
||||
The Anthropic API does not call the function directly; instead, the model generates JSON that you can use to call the function in your code and return the result back to the model to complete the conversation.
|
||||
|
||||
NOTE: As of April 4th, 2024, streaming is not yet supported for function calling and Tool use is not yet available on third-party platforms like Vertex AI or AWS Bedrock, but is coming soon.
|
||||
|
||||
Spring AI provides flexible and user-friendly ways to register and call custom functions.
|
||||
In general, the custom functions need to provide a function `name`, `description`, and the function call `signature` (as JSON schema) to let the model know what arguments the function expects.
|
||||
The `description` helps the model to understand when to call the function.
|
||||
|
||||
As a developer, you need to implement a functions that takes the function call arguments sent from the AI model, and respond with the result back to the model.
|
||||
Your function can in turn invoke other 3rd party services to provide the results.
|
||||
|
||||
Spring AI makes this as easy as defining a `@Bean` definition that returns a `java.util.Function` and supplying the bean name as an option when invoking the `ChatClient`.
|
||||
|
||||
Under the hood, Spring wraps your POJO (the function) with the appropriate adapter code that enables interaction with the AI Model, saving you from writing tedious boilerplate code.
|
||||
The basis of the underlying infrastructure is the link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java[FunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java[FunctionCallbackWrapper.java] utility class to simplify the implementation and registration of Java callback functions.
|
||||
|
||||
== How it works
|
||||
|
||||
Suppose we want the AI model to respond with information that it does not have, for example the current temperature at a given location.
|
||||
|
||||
We can provide the AI model with metadata about our own functions that it can use to retrieve that information as it processes your prompt.
|
||||
|
||||
For example, if during the processing of a prompt, the AI Model determines that it needs additional information about the temperature in a given location, it will start a server side generated request/response interaction. The AI Model invokes a client side function.
|
||||
The AI Model provides method invocation details as JSON and it is the responsibility of the client to execute that function and return the response.
|
||||
|
||||
Spring AI greatly simplifies code you need to write to support function invocation.
|
||||
It brokers the function invocation conversation for you.
|
||||
You can simply provide your function definition as a `@Bean` and then provide the bean name of the function in your prompt options.
|
||||
You can also reference multiple function bean names in your prompt.
|
||||
|
||||
== Quick Start
|
||||
|
||||
Let's create a chatbot that answer questions by calling our own function.
|
||||
To support the response of the chatbot, we will register our own function that takes a location and returns the current weather in that location.
|
||||
|
||||
When the response to the prompt to the model needs to answer a question such as `"What’s the weather like in Boston?"` the AI model will invoke the client providing the location value as an argument to be passed to the function. This RPC-like data is passed as JSON.
|
||||
|
||||
Our function can some SaaS based weather service API and returns the weather response back to the model to complete the conversation.
|
||||
In this example we will use a simple implementation named `MockWeatherService` that hard codes the temperature for various locations.
|
||||
|
||||
The following `MockWeatherService.java` represents the weather service API:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class MockWeatherService implements Function<Request, Response> {
|
||||
|
||||
public enum Unit { C, F }
|
||||
public record Request(String location, Unit unit) {}
|
||||
public record Response(double temp, Unit unit) {}
|
||||
|
||||
public Response apply(Request request) {
|
||||
return new Response(30.0, Unit.C);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
=== Registering Functions as Beans
|
||||
|
||||
With the link:../anthropic-chat.html#_auto_configuration[AnthropicChatClient Auto-Configuration] you have multiple ways to register custom functions as beans in the Spring context.
|
||||
|
||||
We start with describing the most POJO friendly options.
|
||||
|
||||
==== Plain Java Functions
|
||||
|
||||
In this approach you define `@Beans` in your application context as you would any other Spring managed object.
|
||||
|
||||
Internally, Spring AI `ChatClient` will create an instance of a `FunctionCallbackWrapper` wrapper that adds the logic for it being invoked via the AI model.
|
||||
The name of the `@Bean` is passed as a `ChatOption`.
|
||||
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
@Description("Get the weather in location") // function description
|
||||
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction1() {
|
||||
return new MockWeatherService();
|
||||
}
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
The `@Description` annotation is optional and provides a function description (2) that helps the model to understand when to call the function.
|
||||
It is an important property to set to help the AI model determine what client side function to invoke.
|
||||
|
||||
Another option to provide the description of the function is to the `@JacksonDescription` annotation on the `MockWeatherService.Request` to provide the function description:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public Function<Request, Response> currentWeather3() { // (1) bean name as function name.
|
||||
return new MockWeatherService();
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
@JsonClassDescription("Get the weather in location") // (2) function description
|
||||
public record Request(String location, Unit unit) {}
|
||||
----
|
||||
|
||||
It is a best practice to annotate the request object with information such that the generates JSON schema of that function is as descriptive as possible to help the AI model pick the correct function to invoke.
|
||||
|
||||
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithFunctionBeanIT.java.java[FunctionCallWithFunctionBeanIT.java] demonstrates this approach.
|
||||
|
||||
|
||||
==== FunctionCallback Wrapper
|
||||
|
||||
Another way register a function is to create `FunctionCallbackWrapper` wrapper like this:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public FunctionCallback weatherFunctionInfo() {
|
||||
|
||||
return new FunctionCallbackWrapper<>("CurrentWeather", // (1) function name
|
||||
"Get the weather in location", // (2) function description
|
||||
(response) -> "" + response.temp() + response.unit(), // (3) Response Converter
|
||||
new MockWeatherService()); // function code
|
||||
}
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
It wraps the 3rd party, `MockWeatherService` function and registers it as a `CurrentWeather` function with the `AnthropicChatClient`.
|
||||
It also provides a description (2) and an optional response converter (3) to convert the response into a text as expected by the model.
|
||||
|
||||
NOTE: By default, the response converter does a JSON serialization of the Response object.
|
||||
|
||||
NOTE: The `FunctionCallbackWrapper` internally resolves the function call signature based on the `MockWeatherService.Request` class.
|
||||
|
||||
=== Specifying functions in Chat Options
|
||||
|
||||
To let the model know and call your `CurrentWeather` function you need to enable it in your prompt requests:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
AnthropicChatClient chatClient = ...
|
||||
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in Paris?");
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage),
|
||||
AnthropicChatOptions.builder().withFunction("CurrentWeather").build())); // (1) Enable the function
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
----
|
||||
|
||||
// NOTE: You can can have multiple functions registered in your `ChatClient` but only those enabled in the prompt request will be considered for the function calling.
|
||||
|
||||
Above user question will trigger 3 calls to `CurrentWeather` function (one for each city) and produce the final response.
|
||||
|
||||
=== Register/Call Functions with Prompt Options
|
||||
|
||||
In addition to the auto-configuration you can register callback functions, dynamically, with your Prompt requests:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
AnthropicChatClient chatClient = ...
|
||||
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in Paris?");
|
||||
|
||||
var promptOptions = AnthropicChatOptions.builder()
|
||||
.withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(
|
||||
"CurrentWeather", // name
|
||||
"Get the weather in location", // function description
|
||||
new MockWeatherService()))) // function code
|
||||
.build();
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage), promptOptions));
|
||||
----
|
||||
|
||||
NOTE: The in-prompt registered functions are enabled by default for the duration of this request.
|
||||
|
||||
This approach allows to dynamically chose different functions to be called based on the user input.
|
||||
|
||||
The https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithPromptFunctionIT.java[FunctionCallWithPromptFunctionIT.java] integration test provides a complete example of how to register a function with the `AnthropicChatClient` and use it in a prompt request.
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[[Function]]
|
||||
= Function Calling API
|
||||
|
||||
The integration of function support in AI models, such as ChatGPT, permits the model to request the execution of client-side functions, thereby accessing necessary information or performing tasks dynamically as required.
|
||||
The integration of function support in AI models, permits the model to request the execution of client-side functions, thereby accessing necessary information or performing tasks dynamically as required.
|
||||
|
||||
Spring AI currently supports Function invocation for the following AI Models
|
||||
|
||||
* OpenAI: Refer to the xref:api/chat/functions/openai-chat-functions.adoc[Open AI function invocation docs].
|
||||
* VertexAI Gemini: Refer to the xref:api/chat/functions/vertexai-gemini-chat-functions.adoc[Vertex AI Gemini function invocation docs].
|
||||
* Azure OpenAI: Refer to the xref:api/chat/functions/azure-open-ai-chat-functions.adoc[Azure OpenAI function invocation docs].
|
||||
* Mistral AI: Refer to the xref:api/chat/functions/mistralai-chat-functions.adoc[Mistral AI function invocation docs].
|
||||
* Mistral AI: Refer to the xref:api/chat/functions/mistralai-chat-functions.adoc[Mistral AI function invocation docs].
|
||||
* Anthropic Claude: Refer to the xref:api/chat/functions/anthropic-chat-functions.adoc[Anthropic Claude function invocation docs].
|
||||
@@ -15,17 +15,23 @@
|
||||
*/
|
||||
package org.springframework.ai.autoconfigure.anthropic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.anthropic.AnthropicChatClient;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallbackContext;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@@ -52,8 +58,23 @@ public class AnthropicAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public AnthropicChatClient anthropicChatClient(AnthropicApi anthropicApi, AnthropicChatProperties chatProperties,
|
||||
RetryTemplate retryTemplate) {
|
||||
return new AnthropicChatClient(anthropicApi, chatProperties.getOptions(), retryTemplate);
|
||||
RetryTemplate retryTemplate, FunctionCallbackContext functionCallbackContext,
|
||||
List<FunctionCallback> toolFunctionCallbacks) {
|
||||
|
||||
if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) {
|
||||
chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks);
|
||||
}
|
||||
|
||||
return new AnthropicChatClient(anthropicApi, chatProperties.getOptions(), retryTemplate,
|
||||
functionCallbackContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public FunctionCallbackContext springAiFunctionManager(ApplicationContext context) {
|
||||
FunctionCallbackContext manager = new FunctionCallbackContext();
|
||||
manager.setApplicationContext(context);
|
||||
return manager;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2023 - 2024 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.autoconfigure.anthropic.tool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.anthropic.AnthropicChatClient;
|
||||
import org.springframework.ai.anthropic.AnthropicChatOptions;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.anthropic.tool.MockWeatherService.Request;
|
||||
import org.springframework.ai.autoconfigure.anthropic.tool.MockWeatherService.Response;
|
||||
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Description;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".*")
|
||||
class FunctionCallWithFunctionBeanIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionBeanIT.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"))
|
||||
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
|
||||
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
|
||||
.withUserConfiguration(Config.class);
|
||||
|
||||
@Test
|
||||
void functionCallTest() {
|
||||
|
||||
contextRunner
|
||||
.withPropertyValues(
|
||||
"spring.ai.anthropic.chat.options.model=" + AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue())
|
||||
.run(context -> {
|
||||
|
||||
AnthropicChatClient chatClient = context.getBean(AnthropicChatClient.class);
|
||||
|
||||
var userMessage = new UserMessage(
|
||||
"What's the weather like in San Francisco, in Paris, France and in Tokyo, Japan? Return the temperature in Celsius.");
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage),
|
||||
AnthropicChatOptions.builder().withFunction("weatherFunction").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
|
||||
|
||||
response = chatClient.call(new Prompt(List.of(userMessage),
|
||||
AnthropicChatOptions.builder().withFunction("weatherFunction3").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
@Description("Get the weather in location. Return temperature in 36°F or 36°C format.")
|
||||
public Function<Request, Response> weatherFunction() {
|
||||
return new MockWeatherService();
|
||||
}
|
||||
|
||||
// Relies on the Request's JsonClassDescription annotation to provide the
|
||||
// function description.
|
||||
@Bean
|
||||
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction3() {
|
||||
MockWeatherService weatherService = new MockWeatherService();
|
||||
return (weatherService::apply);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2023 - 2024 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.autoconfigure.anthropic.tool;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.anthropic.AnthropicChatClient;
|
||||
import org.springframework.ai.anthropic.AnthropicChatOptions;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.function.FunctionCallbackWrapper;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".*")
|
||||
public class FunctionCallWithPromptFunctionIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FunctionCallWithPromptFunctionIT.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"))
|
||||
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
|
||||
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void functionCallTest() {
|
||||
contextRunner
|
||||
.withPropertyValues(
|
||||
"spring.ai.anthropic.chat.options.model=" + AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue())
|
||||
.run(context -> {
|
||||
|
||||
AnthropicChatClient chatClient = context.getBean(AnthropicChatClient.class);
|
||||
|
||||
UserMessage userMessage = new UserMessage(
|
||||
"What's the weather like in San Francisco, in Paris and in Tokyo? Return the temperature in Celsius.");
|
||||
|
||||
var promptOptions = AnthropicChatOptions.builder()
|
||||
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
|
||||
.withName("CurrentWeatherService")
|
||||
.withDescription("Get the weather in location. Return temperature in 36°F or 36°C format.")
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage), promptOptions));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2023 - 2024 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.autoconfigure.anthropic.tool;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonClassDescription;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||
|
||||
/**
|
||||
* Mock 3rd party weather service.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> {
|
||||
|
||||
/**
|
||||
* Weather Function request.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
@JsonClassDescription("Weather API request")
|
||||
public record Request(@JsonProperty(required = true,
|
||||
value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
|
||||
@JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Temperature units.
|
||||
*/
|
||||
public enum Unit {
|
||||
|
||||
/**
|
||||
* Celsius.
|
||||
*/
|
||||
C("metric"),
|
||||
/**
|
||||
* Fahrenheit.
|
||||
*/
|
||||
F("imperial");
|
||||
|
||||
/**
|
||||
* Human readable unit name.
|
||||
*/
|
||||
public final String unitName;
|
||||
|
||||
private Unit(String text) {
|
||||
this.unitName = text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Weather Function response.
|
||||
*/
|
||||
public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
|
||||
Unit unit) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response apply(Request request) {
|
||||
|
||||
double temperature = 0;
|
||||
if (request.location().contains("Paris")) {
|
||||
temperature = 15;
|
||||
}
|
||||
else if (request.location().contains("Tokyo")) {
|
||||
temperature = 10;
|
||||
}
|
||||
else if (request.location().contains("San Francisco")) {
|
||||
temperature = 30;
|
||||
}
|
||||
|
||||
return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user