Implement Anthropic Claude3 Message API client support (direct)
This commit introduces support for the Anthropic Claude3 Message API (https://api.anthropic.com), enabling direct interaction with its services. This is not a Bedrock Anthropic Claude3 implemenation. Changes include: - Implementation of a low-level client, AnthropicApi, to interact with the message API endpoints specified in the Anthropic documentation (https://docs.anthropic.com/claude/reference/messages_post), including support for streaming. - Addition of AnthropicApi tests to ensure functionality and reliability. - Support for multimodal requests within AnthropicApi. - Adding the spring-ai-anthropic and boot starter into BOM and parent POM modules for streamlined usage. - Add Anthropic Auto-configuration and Boot Starter for seamless integration into existing projects. - Implementation of AnthropicChatClient with capabilities for synchronous and streaming communication, including support for multimodal messages. - Inclusion of both unit and integration tests to validate functionality across various scenarios. - Add Antora documentation with comprehensive guidance on using AnthropicApi and AnthropicChatClient. - Add of Ahead-of-Time (AOT) hints for AnthropicApi. - update anthropic diagram
This commit is contained in:
committed by
Mark Pollack
parent
8503078088
commit
ce2bb131e5
79
models/spring-ai-anthropic/pom.xml
Normal file
79
models/spring-ai-anthropic/pom.xml
Normal file
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-ai-anthropic</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring AI Anthropic Chat Client</name>
|
||||
<description>Anthropic support</description>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
<connection>git://github.com/spring-projects/spring-ai.git</connection>
|
||||
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
|
||||
</scm>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- production dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-retry</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- NOTE: Required only by the @ConstructorBinding. -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>json-path</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.victools</groupId>
|
||||
<artifactId>jsonschema-generator</artifactId>
|
||||
<version>${victools.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.victools</groupId>
|
||||
<artifactId>jsonschema-module-jackson</artifactId>
|
||||
<version>${victools.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context-support</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-test</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.Role;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.StreamResponse;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.Usage;
|
||||
import org.springframework.ai.anthropic.metadata.AnthropicChatResponseMetadata;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.chat.StreamingChatClient;
|
||||
import org.springframework.ai.chat.messages.MessageType;
|
||||
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.retry.RetryUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* The {@link ChatClient} implementation for the Anthropic service.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class AnthropicChatClient implements ChatClient, StreamingChatClient {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AnthropicChatClient.class);
|
||||
|
||||
public static final String DEFAULT_MODEL_NAME = AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue();
|
||||
|
||||
public static final Integer DEFAULT_MAX_TOKENS = 500;
|
||||
|
||||
public static final Float DEFAULT_TEMPERATURE = 0.8f;
|
||||
|
||||
/**
|
||||
* The lower-level API for the Anthropic service.
|
||||
*/
|
||||
public final AnthropicApi anthropicApi;
|
||||
|
||||
/**
|
||||
* The default options used for the chat completion requests.
|
||||
*/
|
||||
private AnthropicChatOptions defaultOptions;
|
||||
|
||||
/**
|
||||
* The retry template used to retry the OpenAI API calls.
|
||||
*/
|
||||
public final RetryTemplate retryTemplate;
|
||||
|
||||
/**
|
||||
* Construct a new {@link AnthropicChatClient} instance.
|
||||
* @param anthropicApi the lower-level API for the Anthropic service.
|
||||
*/
|
||||
public AnthropicChatClient(AnthropicApi anthropicApi) {
|
||||
this(anthropicApi,
|
||||
AnthropicChatOptions.builder()
|
||||
.withModel(DEFAULT_MODEL_NAME)
|
||||
.withMaxTokens(DEFAULT_MAX_TOKENS)
|
||||
.withTemperature(DEFAULT_TEMPERATURE)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public AnthropicChatClient(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions) {
|
||||
this(anthropicApi, defaultOptions, RetryUtils.DEFAULT_RETRY_TEMPLATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public AnthropicChatClient(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions,
|
||||
RetryTemplate retryTemplate) {
|
||||
Assert.notNull(anthropicApi, "AnthropicApi must not be null");
|
||||
Assert.notNull(defaultOptions, "DefaultOptions must not be null");
|
||||
Assert.notNull(retryTemplate, "RetryTemplate must not be null");
|
||||
|
||||
this.anthropicApi = anthropicApi;
|
||||
this.defaultOptions = defaultOptions;
|
||||
this.retryTemplate = retryTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatResponse call(Prompt prompt) {
|
||||
|
||||
ChatCompletionRequest request = createRequest(prompt, false);
|
||||
|
||||
return this.retryTemplate.execute(ctx -> {
|
||||
ResponseEntity<ChatCompletion> completionEntity = this.anthropicApi.chatCompletionEntity(request);
|
||||
return toChatResponse(completionEntity.getBody());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ChatResponse> stream(Prompt prompt) {
|
||||
|
||||
ChatCompletionRequest request = createRequest(prompt, true);
|
||||
|
||||
Flux<StreamResponse> response = this.anthropicApi.chatCompletionStream(request);
|
||||
|
||||
AtomicReference<ChatCompletionBuilder> chatCompletionReference = new AtomicReference<>();
|
||||
|
||||
// https://docs.anthropic.com/claude/reference/messages-streaming
|
||||
|
||||
return response.map(chunk -> {
|
||||
|
||||
if (chunk.type().equals("message_start")) {
|
||||
chatCompletionReference.set(new ChatCompletionBuilder());
|
||||
chatCompletionReference.get()
|
||||
.withType(chunk.type())
|
||||
.withId(chunk.message().id())
|
||||
.withRole(chunk.message().role())
|
||||
.withModel(chunk.message().model())
|
||||
.withUsage(chunk.message().usage())
|
||||
.withContent(new ArrayList<>());
|
||||
}
|
||||
else if (chunk.type().equals("content_block_start")) {
|
||||
var content = new MediaContent(chunk.contentBlock().type(), null, chunk.contentBlock().text(),
|
||||
chunk.index());
|
||||
chatCompletionReference.get().withType(chunk.type()).withContent(List.of(content));
|
||||
}
|
||||
else if (chunk.type().equals("content_block_delta")) {
|
||||
var content = new MediaContent("text_delta", null, (String) chunk.delta().get("text"), chunk.index());
|
||||
chatCompletionReference.get().withType(chunk.type()).withContent(List.of(content));
|
||||
}
|
||||
else if (chunk.type().equals("message_delta")) {
|
||||
|
||||
ChatCompletion delta = ModelOptionsUtils.mapToClass(chunk.delta(), ChatCompletion.class);
|
||||
|
||||
chatCompletionReference.get().withType(chunk.type());
|
||||
if (delta.id() != null) {
|
||||
chatCompletionReference.get().withId(delta.id());
|
||||
}
|
||||
if (delta.role() != null) {
|
||||
chatCompletionReference.get().withRole(delta.role());
|
||||
}
|
||||
if (delta.model() != null) {
|
||||
chatCompletionReference.get().withModel(delta.model());
|
||||
}
|
||||
if (delta.usage() != null) {
|
||||
chatCompletionReference.get().withUsage(delta.usage());
|
||||
}
|
||||
if (delta.content() != null) {
|
||||
chatCompletionReference.get().withContent(delta.content());
|
||||
}
|
||||
if (delta.stopReason() != null) {
|
||||
chatCompletionReference.get().withStopReason(delta.stopReason());
|
||||
}
|
||||
if (delta.stopSequence() != null) {
|
||||
chatCompletionReference.get().withStopSequence(delta.stopSequence());
|
||||
}
|
||||
}
|
||||
else {
|
||||
chatCompletionReference.get().withType(chunk.type()).withContent(List.of());
|
||||
}
|
||||
return chatCompletionReference.get().build();
|
||||
|
||||
}).map(this::toChatResponse);
|
||||
}
|
||||
|
||||
private ChatResponse toChatResponse(ChatCompletion chatCompletion) {
|
||||
if (chatCompletion == null) {
|
||||
logger.warn("Null chat completion returned");
|
||||
return new ChatResponse(List.of());
|
||||
}
|
||||
|
||||
List<Generation> generations = chatCompletion.content().stream().map(content -> {
|
||||
return new Generation(content.text(), Map.of())
|
||||
.withGenerationMetadata(ChatGenerationMetadata.from(chatCompletion.stopReason(), null));
|
||||
}).toList();
|
||||
|
||||
return new ChatResponse(generations, AnthropicChatResponseMetadata.from(chatCompletion));
|
||||
}
|
||||
|
||||
private String fromMediaData(Object mediaData) {
|
||||
if (mediaData instanceof byte[] bytes) {
|
||||
return Base64.getEncoder().encodeToString(bytes);
|
||||
}
|
||||
else if (mediaData instanceof String text) {
|
||||
return text;
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Unsupported media data type: " + mediaData.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
|
||||
|
||||
List<RequestMessage> userMessages = prompt.getInstructions()
|
||||
.stream()
|
||||
.filter(m -> m.getMessageType() != MessageType.SYSTEM)
|
||||
.map(m -> {
|
||||
List<MediaContent> contents = new ArrayList<>(List.of(new MediaContent(m.getContent())));
|
||||
if (!CollectionUtils.isEmpty(m.getMedia())) {
|
||||
List<MediaContent> mediaContent = m.getMedia()
|
||||
.stream()
|
||||
.map(media -> new MediaContent(media.getMimeType().toString(),
|
||||
this.fromMediaData(media.getData())))
|
||||
.toList();
|
||||
contents.addAll(mediaContent);
|
||||
}
|
||||
return new RequestMessage(contents, Role.valueOf(m.getMessageType().name()));
|
||||
})
|
||||
.toList();
|
||||
|
||||
String systemPrompt = prompt.getInstructions()
|
||||
.stream()
|
||||
.filter(m -> m.getMessageType() == MessageType.SYSTEM)
|
||||
.map(m -> m.getContent())
|
||||
.collect(Collectors.joining(System.lineSeparator()));
|
||||
|
||||
ChatCompletionRequest request = new ChatCompletionRequest(this.defaultOptions.getModel(), userMessages,
|
||||
systemPrompt, this.defaultOptions.getMaxTokens(), this.defaultOptions.getTemperature(), stream);
|
||||
|
||||
if (prompt.getOptions() != null) {
|
||||
if (prompt.getOptions() instanceof ChatOptions runtimeOptions) {
|
||||
AnthropicChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions,
|
||||
ChatOptions.class, AnthropicChatOptions.class);
|
||||
|
||||
request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, ChatCompletionRequest.class);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Prompt options are not of type ChatOptions: "
|
||||
+ prompt.getOptions().getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
if (this.defaultOptions != null) {
|
||||
request = ModelOptionsUtils.merge(request, this.defaultOptions, ChatCompletionRequest.class);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private static class ChatCompletionBuilder {
|
||||
|
||||
private String type;
|
||||
|
||||
private String id;
|
||||
|
||||
private Role role;
|
||||
|
||||
private List<MediaContent> content;
|
||||
|
||||
private String model;
|
||||
|
||||
private String stopReason;
|
||||
|
||||
private String stopSequence;
|
||||
|
||||
private Usage usage;
|
||||
|
||||
public ChatCompletionBuilder() {
|
||||
}
|
||||
|
||||
public ChatCompletionBuilder withType(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionBuilder withId(String id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionBuilder withRole(Role role) {
|
||||
this.role = role;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionBuilder withContent(List<MediaContent> content) {
|
||||
this.content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionBuilder withModel(String model) {
|
||||
this.model = model;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionBuilder withStopReason(String stopReason) {
|
||||
this.stopReason = stopReason;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionBuilder withStopSequence(String stopSequence) {
|
||||
this.stopSequence = stopSequence;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletionBuilder withUsage(Usage usage) {
|
||||
this.usage = usage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatCompletion build() {
|
||||
return new ChatCompletion(this.id, this.type, this.role, this.content, this.model, this.stopReason,
|
||||
this.stopSequence, this.usage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* The options to be used when sending a chat request to the Anthropic API.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public class AnthropicChatOptions implements ChatOptions {
|
||||
|
||||
// @formatter:off
|
||||
private @JsonProperty("model") String model;
|
||||
private @JsonProperty("max_tokens") Integer maxTokens;
|
||||
private @JsonProperty("metadata") ChatCompletionRequest.Metadata metadata;
|
||||
private @JsonProperty("stop_sequences") List<String> stopSequences;
|
||||
private @JsonProperty("temperature") Float temperature;
|
||||
private @JsonProperty("top_p") Float topP;
|
||||
private @JsonProperty("top_k") Integer topK;
|
||||
// @formatter:on
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final AnthropicChatOptions options = new AnthropicChatOptions();
|
||||
|
||||
public Builder withModel(String model) {
|
||||
this.options.model = model;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withMaxTokens(Integer maxTokens) {
|
||||
this.options.maxTokens = maxTokens;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withMetadata(ChatCompletionRequest.Metadata metadata) {
|
||||
this.options.metadata = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withStopSequences(List<String> stopSequences) {
|
||||
this.options.stopSequences = stopSequences;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withTemperature(Float temperature) {
|
||||
this.options.temperature = temperature;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withTopP(Float topP) {
|
||||
this.options.topP = topP;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withTopK(Integer topK) {
|
||||
this.options.topK = topK;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AnthropicChatOptions build() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public Integer getMaxTokens() {
|
||||
return this.maxTokens;
|
||||
}
|
||||
|
||||
public void setMaxTokens(Integer maxTokens) {
|
||||
this.maxTokens = maxTokens;
|
||||
}
|
||||
|
||||
public ChatCompletionRequest.Metadata getMetadata() {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
public void setMetadata(ChatCompletionRequest.Metadata metadata) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public List<String> getStopSequences() {
|
||||
return this.stopSequences;
|
||||
}
|
||||
|
||||
public void setStopSequences(List<String> stopSequences) {
|
||||
this.stopSequences = stopSequences;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float getTemperature() {
|
||||
return this.temperature;
|
||||
}
|
||||
|
||||
public void setTemperature(Float temperature) {
|
||||
this.temperature = temperature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float getTopP() {
|
||||
return this.topP;
|
||||
}
|
||||
|
||||
public void setTopP(Float topP) {
|
||||
this.topP = topP;
|
||||
}
|
||||
|
||||
public Integer getTopK() {
|
||||
return this.topK;
|
||||
}
|
||||
|
||||
public void setTopK(Integer topK) {
|
||||
this.topK = topK;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.aot;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage;
|
||||
|
||||
/**
|
||||
* The AnthropicRuntimeHints class is responsible for registering runtime hints for
|
||||
* Anthropic API classes.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class AnthropicRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(@NonNull RuntimeHints hints, @Nullable ClassLoader classLoader) {
|
||||
var mcs = MemberCategory.values();
|
||||
for (var tr : findJsonAnnotatedClassesInPackage(AnthropicApi.class))
|
||||
hints.reflection().registerType(tr, mcs);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.retry.RetryUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class AnthropicApi {
|
||||
|
||||
public static final String DEFAULT_BASE_URL = "https://api.anthropic.com";
|
||||
|
||||
public static final String DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
|
||||
|
||||
private static final Predicate<String> SSE_DONE_PREDICATE = "[DONE]"::equals;
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
private WebClient webClient;
|
||||
|
||||
/**
|
||||
* Create a new client api with DEFAULT_BASE_URL
|
||||
* @param anthropicApiKey Anthropic api Key.
|
||||
*/
|
||||
public AnthropicApi(String anthropicApiKey) {
|
||||
this(DEFAULT_BASE_URL, anthropicApiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new client api.
|
||||
* @param baseUrl api base URL.
|
||||
* @param anthropicApiKey Anthropic api Key.
|
||||
*/
|
||||
public AnthropicApi(String baseUrl, String anthropicApiKey) {
|
||||
this(baseUrl, anthropicApiKey, DEFAULT_ANTHROPIC_VERSION, RestClient.builder(),
|
||||
RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new client api.
|
||||
* @param baseUrl api base URL.
|
||||
* @param anthropicApiKey Anthropic api Key.
|
||||
* @param restClientBuilder RestClient builder.
|
||||
* @param responseErrorHandler Response error handler.
|
||||
*/
|
||||
public AnthropicApi(String baseUrl, String anthropicApiKey, String anthropicVersion,
|
||||
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
|
||||
|
||||
Consumer<HttpHeaders> jsonContentHeaders = headers -> {
|
||||
headers.add("x-api-key", anthropicApiKey);
|
||||
headers.add("anthropic-version", anthropicVersion);
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
};
|
||||
|
||||
this.restClient = restClientBuilder.baseUrl(baseUrl)
|
||||
.defaultHeaders(jsonContentHeaders)
|
||||
.defaultStatusHandler(responseErrorHandler)
|
||||
.build();
|
||||
|
||||
this.webClient = WebClient.builder().baseUrl(baseUrl).defaultHeaders(jsonContentHeaders).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the <a href="https://docs.anthropic.com/claude/docs/models-overview">Models
|
||||
* overview</a> and <a href=
|
||||
* "https://docs.anthropic.com/claude/docs/models-overview#model-comparison">model
|
||||
* comparison</a> for additional details and options.
|
||||
*/
|
||||
public enum ChatModel {
|
||||
|
||||
// @formatter:off
|
||||
CLAUDE_3_OPUS("claude-3-opus-20240229"),
|
||||
CLAUDE_3_SONNET("claude-3-sonnet-20240229"),
|
||||
CLAUDE_3_HAIKU("claude-3-haiku-20240307"),
|
||||
|
||||
// Legacy models
|
||||
CLAUDE_2_1("claude-2.1"),
|
||||
CLAUDE_2("claude-2.0"),
|
||||
|
||||
CLAUDE_INSTANT_1_2("claude-instant-1.2");
|
||||
// @formatter:on
|
||||
|
||||
public final String value;
|
||||
|
||||
ChatModel(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param model The model that will complete your prompt. See the list of
|
||||
* <a href="https://docs.anthropic.com/claude/docs/models-overview">models</a> for
|
||||
* additional details and options.
|
||||
* @param messages Input messages.
|
||||
* @param system System prompt. A system prompt is a way of providing context and
|
||||
* instructions to Claude, such as specifying a particular goal or role. See our
|
||||
* <a href="https://docs.anthropic.com/claude/docs/system-prompts">guide</a> to system
|
||||
* prompts.
|
||||
* @param maxTokens The maximum number of tokens to generate before stopping. Note
|
||||
* that our models may stop before reaching this maximum. This parameter only
|
||||
* specifies the absolute maximum number of tokens to generate. Different models have
|
||||
* different maximum values for this parameter.
|
||||
* @param metadata An object describing metadata about the request.
|
||||
* @param stopSequences 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.
|
||||
* @param stream Whether to incrementally stream the response using server-sent
|
||||
* events.
|
||||
* @param temperature Amount of randomness injected into the response.Defaults to 1.0.
|
||||
* Ranges from 0.0 to 1.0. Use temperature closer to 0.0 for analytical / multiple
|
||||
* choice, and closer to 1.0 for creative and generative tasks. Note that even with
|
||||
* temperature of 0.0, the results will not be fully deterministic.
|
||||
* @param topP 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.
|
||||
* @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.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ChatCompletionRequest( // @formatter:off
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("messages") List<RequestMessage> messages,
|
||||
@JsonProperty("system") String system,
|
||||
@JsonProperty("max_tokens") Integer maxTokens,
|
||||
@JsonProperty("metadata") Metadata metadata,
|
||||
@JsonProperty("stop_sequences") List<String> stopSequences,
|
||||
@JsonProperty("stream") Boolean stream,
|
||||
@JsonProperty("temperature") Float temperature,
|
||||
@JsonProperty("top_p") Float topP,
|
||||
@JsonProperty("top_k") Integer topK) {
|
||||
// @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param userId An external identifier for the user who is associated with the
|
||||
* request. This should be a uuid, hash value, or other opaque identifier.
|
||||
* Anthropic may use this id to help detect abuse. Do not include any identifying
|
||||
* information such as name, email address, or phone number.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record Metadata(@JsonProperty("user_id") String userId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Input messages.
|
||||
*
|
||||
* Our models are trained to operate on alternating user and assistant conversational
|
||||
* turns. When creating a new Message, you specify the prior conversational turns with
|
||||
* the messages parameter, and the model then generates the next Message in the
|
||||
* conversation. Each input message must be an object with a role and content. You can
|
||||
* specify a single user-role message, or you can include multiple user and assistant
|
||||
* messages. The first message must always use the user role. If the final message
|
||||
* uses the assistant role, the response content will continue immediately from the
|
||||
* content in that message. This can be used to constrain part of the model's
|
||||
* response.
|
||||
*
|
||||
* @param content The contents of the message. Can be of one of String or
|
||||
* MultiModalContent.
|
||||
* @param role The role of the messages author. Could be one of the {@link Role}
|
||||
* types.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record RequestMessage( // @formatter:off
|
||||
@JsonProperty("content") List<MediaContent> content,
|
||||
@JsonProperty("role") Role role) {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type the content type can be "text" or "image".
|
||||
* @param source The source of the media content. Applicable for "image" types only.
|
||||
* @param text The text of the message. Applicable for "text" types only.
|
||||
* @param index The index of the content block. Applicable only for streaming
|
||||
* responses.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record MediaContent( // @formatter:off
|
||||
@JsonProperty("type") String type,
|
||||
@JsonProperty("source") Source source,
|
||||
@JsonProperty("text") String text,
|
||||
@JsonProperty("index") Integer index // applicable only for streaming responses.
|
||||
) {
|
||||
// @formatter:on
|
||||
|
||||
public MediaContent(String mediaType, String data) {
|
||||
this(new Source(mediaType, data));
|
||||
}
|
||||
|
||||
public MediaContent(Source source) {
|
||||
this("image", source, null, null);
|
||||
}
|
||||
|
||||
public MediaContent(String text) {
|
||||
this("text", null, text, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* The source of the media content. (Applicable for "image" types only)
|
||||
*
|
||||
* @param type The type of the media content. Only "base64" is supported at the
|
||||
* moment.
|
||||
* @param mediaType The media type of the content. For example, "image/png" or
|
||||
* "image/jpeg".
|
||||
* @param data The base64-encoded data of the content.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record Source( // @formatter:off
|
||||
@JsonProperty("type") String type,
|
||||
@JsonProperty("media_type") String mediaType,
|
||||
@JsonProperty("data") String data) {
|
||||
// @formatter:on
|
||||
|
||||
public Source(String mediaType, String data) {
|
||||
this("base64", mediaType, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id Unique object identifier. The format and length of IDs may change over
|
||||
* time.
|
||||
* @param type Object type. For Messages, this is always "message".
|
||||
* @param role Conversational role of the generated message. This will always be
|
||||
* "assistant".
|
||||
* @param content Content generated by the model. This is an array of content blocks.
|
||||
* @param model The model that handled the request.
|
||||
* @param stopReason The reason the model stopped generating tokens. This will be one
|
||||
* of "end_turn", "max_tokens", "stop_sequence", or "timeout".
|
||||
* @param stopSequence Which custom stop sequence was generated, if any.
|
||||
* @param usage Input and output token usage.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ChatCompletion( // @formatter:off
|
||||
@JsonProperty("id") String id,
|
||||
@JsonProperty("type") String type,
|
||||
@JsonProperty("role") Role role,
|
||||
@JsonProperty("content") List<MediaContent> content,
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("stop_reason") String stopReason,
|
||||
@JsonProperty("stop_sequence") String stopSequence,
|
||||
@JsonProperty("usage") Usage usage) {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage statistics.
|
||||
*
|
||||
* @param inputTokens The number of input tokens which were used.
|
||||
* @param outputTokens The number of output tokens which were used. completion).
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record Usage( // @formatter:off
|
||||
@JsonProperty("input_tokens") Integer inputTokens,
|
||||
@JsonProperty("output_tokens") Integer outputTokens) {
|
||||
// @formatter:off
|
||||
}
|
||||
|
||||
/**
|
||||
* The role of the author of this message.
|
||||
*/
|
||||
public enum Role { // @formatter:off
|
||||
@JsonProperty("user") USER,
|
||||
@JsonProperty("assistant") ASSISTANT
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Streaming chat completion response. Provides partial information for either the
|
||||
* ResponseMessage or its MediaContent. The event type defines what partial
|
||||
* information is provided.
|
||||
*
|
||||
* @param type The server event type of the stream response. Each stream uses the
|
||||
* following event flow: (1) 'message_start': contains a Message object with empty
|
||||
* content; (2) A series of content blocks, each of which have a
|
||||
* 'content_block_start', one or more 'content_block_delta events', and a
|
||||
* 'content_block_stop' event. Each content block will have an 'index' that
|
||||
* corresponds to its index in the final Message content array.
|
||||
* @param index The index of the content block. Applicable only for "content_block"
|
||||
* type.
|
||||
* @param message The message object. Applicable only for "message_start" type.
|
||||
* @param contentBlock The content block object. Applicable only for "content_block"
|
||||
* type.
|
||||
* @param delta The delta object. Applicable only for "content_block_delta" and
|
||||
* "message_delta" types.
|
||||
*
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record StreamResponse( // @formatter:off
|
||||
@JsonProperty("type") String type,
|
||||
@JsonProperty("index") Integer index,
|
||||
@JsonProperty("message") ChatCompletion message,
|
||||
@JsonProperty("content_block") MediaContent contentBlock,
|
||||
@JsonProperty("delta") Map<String, Object> delta) {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a model response for the given chat conversation.
|
||||
* @param chatRequest The chat completion request.
|
||||
* @return Entity response with {@link ChatCompletion} as a body and HTTP status code
|
||||
* and headers.
|
||||
*/
|
||||
public ResponseEntity<ChatCompletion> chatCompletionEntity(ChatCompletionRequest chatRequest) {
|
||||
|
||||
Assert.notNull(chatRequest, "The request body can not be null.");
|
||||
Assert.isTrue(!chatRequest.stream(), "Request must set the steam property to false.");
|
||||
|
||||
return this.restClient.post().uri("/v1/messages").body(chatRequest).retrieve().toEntity(ChatCompletion.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a streaming chat response for the given chat conversation.
|
||||
* @param chatRequest The chat completion request. Must have the stream property set
|
||||
* to true.
|
||||
* @return Returns a {@link Flux} stream from chat completion chunks.
|
||||
*/
|
||||
public Flux<StreamResponse> chatCompletionStream(ChatCompletionRequest chatRequest) {
|
||||
|
||||
Assert.notNull(chatRequest, "The request body can not be null.");
|
||||
Assert.isTrue(chatRequest.stream(), "Request must set the steam property to true.");
|
||||
|
||||
return this.webClient.post()
|
||||
.uri("/v1/messages")
|
||||
.body(Mono.just(chatRequest), ChatCompletionRequest.class)
|
||||
.retrieve()
|
||||
.bodyToFlux(String.class)
|
||||
.takeUntil(SSE_DONE_PREDICATE)
|
||||
.filter(SSE_DONE_PREDICATE.negate())
|
||||
.map(content -> ModelOptionsUtils.jsonToObject(content, StreamResponse.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.metadata;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.chat.metadata.*;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link ChatResponseMetadata} implementation for {@literal AnthropicApi}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @see ChatResponseMetadata
|
||||
* @see RateLimit
|
||||
* @see Usage
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class AnthropicChatResponseMetadata implements ChatResponseMetadata {
|
||||
|
||||
protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, usage: %3$s, rateLimit: %4$s }";
|
||||
|
||||
public static AnthropicChatResponseMetadata from(AnthropicApi.ChatCompletion result) {
|
||||
Assert.notNull(result, "Anthropic ChatCompletionResult must not be null");
|
||||
AnthropicUsage usage = AnthropicUsage.from(result.usage());
|
||||
return new AnthropicChatResponseMetadata(result.id(), usage);
|
||||
}
|
||||
|
||||
private final String id;
|
||||
|
||||
@Nullable
|
||||
private RateLimit rateLimit;
|
||||
|
||||
private final Usage usage;
|
||||
|
||||
protected AnthropicChatResponseMetadata(String id, AnthropicUsage usage) {
|
||||
this(id, usage, null);
|
||||
}
|
||||
|
||||
protected AnthropicChatResponseMetadata(String id, AnthropicUsage usage, @Nullable AnthropicRateLimit rateLimit) {
|
||||
this.id = id;
|
||||
this.usage = usage;
|
||||
this.rateLimit = rateLimit;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public RateLimit getRateLimit() {
|
||||
RateLimit rl = this.rateLimit;
|
||||
return rl != null ? rl : new EmptyRateLimit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Usage getUsage() {
|
||||
Usage usage = this.usage;
|
||||
return usage != null ? usage : new EmptyUsage();
|
||||
}
|
||||
|
||||
public AnthropicChatResponseMetadata withRateLimit(RateLimit rateLimit) {
|
||||
this.rateLimit = rateLimit;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return AI_METADATA_STRING.formatted(getClass().getName(), getId(), getUsage(), getRateLimit());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.metadata;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.ai.chat.metadata.RateLimit;
|
||||
|
||||
/**
|
||||
* {@link RateLimit} implementation for {@literal OpenAI}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class AnthropicRateLimit implements RateLimit {
|
||||
|
||||
private static final String RATE_LIMIT_STRING = "{ @type: %1$s, requestsLimit: %2$s, requestsRemaining: %3$s, requestsReset: %4$s, tokensLimit: %5$s; tokensRemaining: %6$s; tokensReset: %7$s }";
|
||||
|
||||
private final Long requestsLimit;
|
||||
|
||||
private final Long requestsRemaining;
|
||||
|
||||
private final Long tokensLimit;
|
||||
|
||||
private final Long tokensRemaining;
|
||||
|
||||
private final Duration requestsReset;
|
||||
|
||||
private final Duration tokensReset;
|
||||
|
||||
public AnthropicRateLimit(Long requestsLimit, Long requestsRemaining, Duration requestsReset, Long tokensLimit,
|
||||
Long tokensRemaining, Duration tokensReset) {
|
||||
|
||||
this.requestsLimit = requestsLimit;
|
||||
this.requestsRemaining = requestsRemaining;
|
||||
this.requestsReset = requestsReset;
|
||||
this.tokensLimit = tokensLimit;
|
||||
this.tokensRemaining = tokensRemaining;
|
||||
this.tokensReset = tokensReset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getRequestsLimit() {
|
||||
return this.requestsLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getTokensLimit() {
|
||||
return this.tokensLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getRequestsRemaining() {
|
||||
return this.requestsRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getTokensRemaining() {
|
||||
return this.tokensRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getRequestsReset() {
|
||||
return this.requestsReset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getTokensReset() {
|
||||
return this.tokensReset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return RATE_LIMIT_STRING.formatted(getClass().getName(), getRequestsLimit(), getRequestsRemaining(),
|
||||
getRequestsReset(), getTokensLimit(), getTokensRemaining(), getTokensReset());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.metadata;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.chat.metadata.Usage;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link Usage} implementation for {@literal AnthropicApi}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class AnthropicUsage implements Usage {
|
||||
|
||||
public static AnthropicUsage from(AnthropicApi.Usage usage) {
|
||||
return new AnthropicUsage(usage);
|
||||
}
|
||||
|
||||
private final AnthropicApi.Usage usage;
|
||||
|
||||
protected AnthropicUsage(AnthropicApi.Usage usage) {
|
||||
Assert.notNull(usage, "AnthropicApi Usage must not be null");
|
||||
this.usage = usage;
|
||||
}
|
||||
|
||||
protected AnthropicApi.Usage getUsage() {
|
||||
return this.usage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getPromptTokens() {
|
||||
return getUsage().inputTokens().longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGenerationTokens() {
|
||||
return getUsage().outputTokens().longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getTotalTokens() {
|
||||
return this.getPromptTokens() + this.getGenerationTokens();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getUsage().toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.aot.hint.RuntimeHintsRegistrar=\
|
||||
org.springframework.ai.anthropic.aot.AnthropicRuntimeHints
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.chat.StreamingChatClient;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Media;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
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.parser.BeanOutputParser;
|
||||
import org.springframework.ai.parser.ListOutputParser;
|
||||
import org.springframework.ai.parser.MapOutputParser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest(classes = AnthropicTestConfiguration.class)
|
||||
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+")
|
||||
class AnthropicChatClientIT {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AnthropicChatClientIT.class);
|
||||
|
||||
@Autowired
|
||||
protected ChatClient chatClient;
|
||||
|
||||
@Autowired
|
||||
protected StreamingChatClient streamingChatClient;
|
||||
|
||||
@Value("classpath:/prompts/system-message.st")
|
||||
private Resource systemResource;
|
||||
|
||||
@Test
|
||||
void roleTest() {
|
||||
UserMessage userMessage = new UserMessage(
|
||||
"Tell me about 3 famous pirates from the Golden Age of Piracy and why they did.");
|
||||
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
|
||||
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", "Bob", "voice", "pirate"));
|
||||
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
|
||||
ChatResponse response = chatClient.call(prompt);
|
||||
assertThat(response.getResults()).hasSize(1);
|
||||
assertThat(response.getMetadata().getUsage().getGenerationTokens()).isGreaterThan(0);
|
||||
assertThat(response.getMetadata().getUsage().getPromptTokens()).isGreaterThan(0);
|
||||
assertThat(response.getMetadata().getUsage().getTotalTokens())
|
||||
.isEqualTo(response.getMetadata().getUsage().getPromptTokens()
|
||||
+ response.getMetadata().getUsage().getGenerationTokens());
|
||||
Generation generation = response.getResults().get(0);
|
||||
assertThat(generation.getOutput().getContent()).contains("Blackbeard");
|
||||
assertThat(generation.getMetadata().getFinishReason()).isEqualTo("end_turn");
|
||||
logger.info(response.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void outputParser() {
|
||||
DefaultConversionService conversionService = new DefaultConversionService();
|
||||
ListOutputParser outputParser = new ListOutputParser(conversionService);
|
||||
|
||||
String format = outputParser.getFormat();
|
||||
String template = """
|
||||
List five {subject}
|
||||
{format}
|
||||
""";
|
||||
PromptTemplate promptTemplate = new PromptTemplate(template,
|
||||
Map.of("subject", "ice cream flavors", "format", format));
|
||||
Prompt prompt = new Prompt(promptTemplate.createMessage());
|
||||
Generation generation = this.chatClient.call(prompt).getResult();
|
||||
|
||||
List<String> list = outputParser.parse(generation.getOutput().getContent());
|
||||
assertThat(list).hasSize(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapOutputParser() {
|
||||
MapOutputParser outputParser = new MapOutputParser();
|
||||
|
||||
String format = outputParser.getFormat();
|
||||
String template = """
|
||||
Provide me a List of {subject}
|
||||
{format}
|
||||
""";
|
||||
PromptTemplate promptTemplate = new PromptTemplate(template,
|
||||
Map.of("subject", "an array of numbers from 1 to 9 under they key name 'numbers'", "format", format));
|
||||
Prompt prompt = new Prompt(promptTemplate.createMessage());
|
||||
Generation generation = chatClient.call(prompt).getResult();
|
||||
|
||||
Map<String, Object> result = outputParser.parse(generation.getOutput().getContent());
|
||||
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
|
||||
|
||||
}
|
||||
|
||||
record ActorsFilmsRecord(String actor, List<String> movies) {
|
||||
}
|
||||
|
||||
@Test
|
||||
void beanOutputParserRecords() {
|
||||
|
||||
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
|
||||
|
||||
String format = outputParser.getFormat();
|
||||
String template = """
|
||||
Generate the filmography of 5 movies for Tom Hanks.
|
||||
{format}
|
||||
""";
|
||||
PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format));
|
||||
Prompt prompt = new Prompt(promptTemplate.createMessage());
|
||||
Generation generation = chatClient.call(prompt).getResult();
|
||||
|
||||
ActorsFilmsRecord actorsFilms = outputParser.parse(generation.getOutput().getContent());
|
||||
logger.info("" + actorsFilms);
|
||||
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
|
||||
assertThat(actorsFilms.movies()).hasSize(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void beanStreamOutputParserRecords() {
|
||||
|
||||
BeanOutputParser<ActorsFilmsRecord> outputParser = new BeanOutputParser<>(ActorsFilmsRecord.class);
|
||||
|
||||
String format = outputParser.getFormat();
|
||||
String template = """
|
||||
Generate the filmography of 5 movies for Tom Hanks.
|
||||
{format}
|
||||
""";
|
||||
PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format));
|
||||
Prompt prompt = new Prompt(promptTemplate.createMessage());
|
||||
|
||||
String generationTextFromStream = streamingChatClient.stream(prompt)
|
||||
.collectList()
|
||||
.block()
|
||||
.stream()
|
||||
.map(ChatResponse::getResults)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getOutput)
|
||||
.map(AssistantMessage::getContent)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
ActorsFilmsRecord actorsFilms = outputParser.parse(generationTextFromStream);
|
||||
logger.info("" + actorsFilms);
|
||||
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
|
||||
assertThat(actorsFilms.movies()).hasSize(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiModalityTest() throws IOException {
|
||||
|
||||
byte[] imageData = new ClassPathResource("/test.png").getContentAsByteArray();
|
||||
|
||||
var userMessage = new UserMessage("Explain what do you see o this picture?",
|
||||
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage)));
|
||||
|
||||
logger.info(response.getResult().getOutput().getContent());
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("bananas", "apple", "basket");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@SpringBootConfiguration
|
||||
public class AnthropicTestConfiguration {
|
||||
|
||||
@Bean
|
||||
public AnthropicApi anthropicApi() {
|
||||
return new AnthropicApi(getApiKey());
|
||||
}
|
||||
|
||||
private String getApiKey() {
|
||||
String apiKey = System.getenv("ANTHROPIC_API_KEY");
|
||||
if (!StringUtils.hasText(apiKey)) {
|
||||
throw new IllegalArgumentException(
|
||||
"You must provide an API key. Put it in an environment variable under the name ANTHROPIC_API_KEY");
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AnthropicChatClient openAiChatClient(AnthropicApi api) {
|
||||
AnthropicChatClient anthropicChatClient = new AnthropicChatClient(api);
|
||||
return anthropicChatClient;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class ChatCompletionRequestTests {
|
||||
|
||||
@Test
|
||||
public void createRequestWithChatOptions() {
|
||||
|
||||
var client = new AnthropicChatClient(new AnthropicApi("TEST"),
|
||||
AnthropicChatOptions.builder().withModel("DEFAULT_MODEL").withTemperature(66.6f).build());
|
||||
|
||||
var request = client.createRequest(new Prompt("Test message content"), false);
|
||||
|
||||
assertThat(request.messages()).hasSize(1);
|
||||
assertThat(request.stream()).isFalse();
|
||||
|
||||
assertThat(request.model()).isEqualTo("DEFAULT_MODEL");
|
||||
assertThat(request.temperature()).isEqualTo(66.6f);
|
||||
|
||||
request = client.createRequest(new Prompt("Test message content",
|
||||
AnthropicChatOptions.builder().withModel("PROMPT_MODEL").withTemperature(99.9f).build()), true);
|
||||
|
||||
assertThat(request.messages()).hasSize(1);
|
||||
assertThat(request.stream()).isTrue();
|
||||
|
||||
assertThat(request.model()).isEqualTo("PROMPT_MODEL");
|
||||
assertThat(request.temperature()).isEqualTo(99.9f);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.aot;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.TypeReference;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import static org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage;
|
||||
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
|
||||
|
||||
class AnthropicRuntimeHintsTests {
|
||||
|
||||
@Test
|
||||
void registerHints() {
|
||||
RuntimeHints runtimeHints = new RuntimeHints();
|
||||
AnthropicRuntimeHints anthropicRuntimeHints = new AnthropicRuntimeHints();
|
||||
anthropicRuntimeHints.registerHints(runtimeHints, null);
|
||||
|
||||
Set<TypeReference> jsonAnnotatedClasses = findJsonAnnotatedClassesInPackage(AnthropicApi.class);
|
||||
for (TypeReference jsonAnnotatedClass : jsonAnnotatedClasses) {
|
||||
assertThat(runtimeHints).matches(reflection().onType(jsonAnnotatedClass));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletion;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.RequestMessage;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.MediaContent;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi.StreamResponse;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+")
|
||||
public class AnthropicApiIT {
|
||||
|
||||
AnthropicApi anthropicApi = new AnthropicApi(System.getenv("ANTHROPIC_API_KEY"));
|
||||
|
||||
@Test
|
||||
void chatCompletionEntity() {
|
||||
|
||||
RequestMessage chatCompletionMessage = new RequestMessage(List.of(new MediaContent("Tell me a Joke?")),
|
||||
Role.USER);
|
||||
ResponseEntity<ChatCompletion> response = anthropicApi
|
||||
.chatCompletionEntity(new ChatCompletionRequest(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(),
|
||||
List.of(chatCompletionMessage), null, 100, 0.8f, false));
|
||||
|
||||
System.out.println(response);
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getBody()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatCompletionStream() {
|
||||
|
||||
RequestMessage chatCompletionMessage = new RequestMessage(List.of(new MediaContent("Tell me a Joke?")),
|
||||
Role.USER);
|
||||
|
||||
Flux<StreamResponse> response = anthropicApi
|
||||
.chatCompletionStream(new ChatCompletionRequest(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(),
|
||||
List.of(chatCompletionMessage), null, 100, 0.8f, true));
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
|
||||
List<StreamResponse> bla = response.collectList().block();
|
||||
assertThat(bla).isNotNull();
|
||||
|
||||
bla.stream().forEach(r -> System.out.println(r));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
"You are a helpful AI assistant. Your name is {name}.
|
||||
You are an AI assistant that helps people find information.
|
||||
Your name is {name}
|
||||
You should reply to the user's request with your name and also in the style of a {voice}.
|
||||
BIN
models/spring-ai-anthropic/src/test/resources/test.png
Normal file
BIN
models/spring-ai-anthropic/src/test/resources/test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
3
pom.xml
3
pom.xml
@@ -26,6 +26,8 @@
|
||||
<module>models/spring-ai-mistral-ai</module>
|
||||
<module>models/spring-ai-vertex-ai-palm2</module>
|
||||
<module>models/spring-ai-vertex-ai-gemini</module>
|
||||
<module>models/spring-ai-anthropic</module>
|
||||
|
||||
<module>spring-ai-test</module>
|
||||
<module>spring-ai-spring-boot-autoconfigure</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-openai</module>
|
||||
@@ -63,6 +65,7 @@
|
||||
<module>vector-stores/spring-ai-mongodb-atlas-store</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-mongodb-atlas-store</module>
|
||||
<module>spring-ai-spring-boot-testcontainers</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-anthropic</module>
|
||||
</modules>
|
||||
|
||||
<organization>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
@@ -33,18 +32,18 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-retry</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-retry</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Document Readers -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-pdf-document-reader</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<!-- Document Readers -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-pdf-document-reader</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
@@ -119,6 +118,13 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-anthropic</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Vector Databses -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
@@ -313,6 +319,11 @@
|
||||
<artifactId>spring-ai-mongodb-atlas-store-spring-boot-starter</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 574 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
@@ -21,6 +21,7 @@
|
||||
***** xref:api/chat/functions/vertexai-gemini-chat-functions.adoc[Function Calling]
|
||||
*** 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]
|
||||
** xref:api/embeddings.adoc[]
|
||||
*** xref:api/embeddings/openai-embeddings.adoc[OpenAI]
|
||||
*** xref:api/embeddings/ollama-embeddings.adoc[Ollama]
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
= Anthropic Chat
|
||||
|
||||
link:https://www.anthropic.com/[Anthropic Claude] is a family of foundational AI models that can be used in a variety of applications.
|
||||
For developers and businesses, you can leverage the API access and build directly on top of link:https://www.anthropic.com/api[Anthropic's AI infrastructure].
|
||||
|
||||
Spring AI supports the Anthropic link:https://docs.anthropic.com/claude/reference/messages_post[Messaging API] for sync and streaming text generations.
|
||||
|
||||
TIP: Anthropic’s Claude models are also available through Amazon Bedrock.
|
||||
Spring AI provides dedicated xref:api/chat/bedrock/bedrock-anthropic.adoc[Amazon Bedrock Anthropic] client implementations as well.
|
||||
|
||||
== Prerequisites
|
||||
|
||||
You will need to create an API key on Anthropic portal.
|
||||
Create an account at https://console.anthropic.com/dashboard[Anthropic API dashboard] and generate the api key on the https://console.anthropic.com/settings/keys[Get API Keys] page.
|
||||
The Spring AI project defines a configuration property named `spring.ai.anthropic.api-key` that you should set to the value of the `API Key` obtained from anthropic.com.
|
||||
Exporting an environment variable is one way to set that configuration property:
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
export SPRING_AI_ANTHROPIC_API_KEY=<INSERT KEY HERE>
|
||||
----
|
||||
|
||||
=== Add Repositories and BOM
|
||||
|
||||
Spring AI artifacts are published in Spring Milestone and Snapshot repositories.
|
||||
Refer to the xref:getting-started.adoc#repositories[Repositories] section to add these repositories to your build system.
|
||||
|
||||
To help with dependency management, Spring AI provides a BOM (bill of materials) to ensure that a consistent version of Spring AI is used throughout the entire project. Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build system.
|
||||
|
||||
|
||||
== Auto-configuration
|
||||
|
||||
Spring AI provides Spring Boot auto-configuration for the Anthropic Chat Client.
|
||||
To enable it add the following dependency to your project's Maven `pom.xml` file:
|
||||
|
||||
[source, xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
or to your Gradle `build.gradle` build file.
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
dependencies {
|
||||
implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter'
|
||||
}
|
||||
----
|
||||
|
||||
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
|
||||
|
||||
=== Chat Properties
|
||||
|
||||
==== Retry Properties
|
||||
|
||||
The prefix `spring.ai.retry` is used as the property prefix that lets you configure the retry mechanism for the Anthropic Chat client.
|
||||
|
||||
[cols="3,5,1"]
|
||||
|====
|
||||
| Property | Description | Default
|
||||
|
||||
| spring.ai.retry.max-attempts | Maximum number of retry attempts. | 10
|
||||
| spring.ai.retry.backoff.initial-interval | Initial sleep duration for the exponential backoff policy. | 2 sec.
|
||||
| spring.ai.retry.backoff.multiplier | Backoff interval multiplier. | 5
|
||||
| spring.ai.retry.backoff.max-interval | Maximum backoff duration. | 3 min.
|
||||
| spring.ai.retry.on-client-errors | If false, throw a NonTransientAiException, and do not attempt retry for `4xx` client error codes | false
|
||||
| spring.ai.retry.exclude-on-http-codes | List of HTTP status codes that should not trigger a retry (e.g. to throw NonTransientAiException). | empty
|
||||
|====
|
||||
|
||||
NOTE: currently the retry policies are not applicable for the streaming API.
|
||||
|
||||
==== Connection Properties
|
||||
|
||||
The prefix `spring.ai.anthropic` is used as the property prefix that lets you connect to Anthropic.
|
||||
|
||||
[cols="3,5,1"]
|
||||
|====
|
||||
| Property | Description | Default
|
||||
|
||||
| spring.ai.anthropic.base-url | The URL to connect to | https://api.anthropic.com
|
||||
| spring.ai.anthropic.version | Anthropic API version | 2023-06-01
|
||||
| spring.ai.anthropic.api-key | The API Key | -
|
||||
|====
|
||||
|
||||
==== Configuration Properties
|
||||
|
||||
The prefix `spring.ai.anthropic.chat` is the property prefix that lets you configure the chat client implementation for Anthropic.
|
||||
|
||||
[cols="3,5,1"]
|
||||
|====
|
||||
| Property | Description | Default
|
||||
|
||||
| spring.ai.anthropic.chat.enabled | Enable Anthropic chat client. | true
|
||||
| spring.ai.anthropic.chat.options.model | This is the Anthropic Chat model to use. Supports `claude-3-opus-20240229`, `claude-3-sonnet-20240229`, `claude-3-haiku-20240307` and the legacy `claude-2.1`, `claude-2.0` and `claude-instant-1.2` models. | `claude-3-opus-20240229`
|
||||
| spring.ai.anthropic.chat.options.temperature | The sampling temperature to use that controls the apparent creativity of generated completions. Higher values will make output more random while lower values will make results more focused and deterministic. It is not recommended to modify temperature and top_p for the same completions request as the interaction of these two settings is difficult to predict. | 0.8
|
||||
| spring.ai.anthropic.chat.options.max-tokens | The maximum number of tokens to generate in the chat completion. The total length of input tokens and generated tokens is limited by the model's context length. | 500
|
||||
| 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. | -
|
||||
|====
|
||||
|
||||
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.
|
||||
|
||||
== Runtime Options [[chat-options]]
|
||||
|
||||
The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatOptions.java[AnthropicChatOptions.java] provides model configurations, such as the model to use, the temperature, the max token count, etc.
|
||||
|
||||
On start-up, the default options can be configured with the `AnthropicChatClient(api, options)` constructor or the `spring.ai.anthropic.chat.options.*` properties.
|
||||
|
||||
At run-time you can override the default options by adding new, request specific, options to the `Prompt` call.
|
||||
For example to override the default model and temperature for a specific request:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
ChatResponse response = chatClient.call(
|
||||
new Prompt(
|
||||
"Generate the names of 5 famous pirates.",
|
||||
AnthropicChatOptions.builder()
|
||||
.withModel("claude-2.1")
|
||||
.withTemperature(0.4)
|
||||
.build()
|
||||
));
|
||||
----
|
||||
|
||||
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()].
|
||||
|
||||
|
||||
== Multimodal
|
||||
|
||||
Multimodality refers to a model's ability to simultaneously understand and process information from various sources, including text, images, audio, and other data formats. This paradigm represents a significant advancement in AI models.
|
||||
|
||||
Currently, Anthropic Claude 3 supports the `base64` source type for `images`, and the `image/jpeg`, `image/png`, `image/gif`, and `image/webp` media types.
|
||||
Check the link:https://docs.anthropic.com/claude/docs/vision[Vision guide] for more information.
|
||||
|
||||
Spring AI's `Message` interface supports multimodal AI models by introducing the Media type.
|
||||
This type contains data and information about media attachments in messages, using Spring's `org.springframework.util.MimeType` and a `java.lang.Object` for the raw media data.
|
||||
|
||||
Below is a simple code example extracted from https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatClientIT.java[AnthropicChatClientIT.java], demonstrating the combination of user text with an image.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
byte[] imageData = new ClassPathResource("/multimodal.test.png").getContentAsByteArray();
|
||||
|
||||
var userMessage = new UserMessage("Explain what do you see o this picture?",
|
||||
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage)));
|
||||
|
||||
logger.info(response.getResult().getOutput().getContent());
|
||||
----
|
||||
|
||||
It takes as an input the `multimodal.test.png` image:
|
||||
|
||||
image::multimodal.test.png[Multimodal Test Image, 200, 200, align="left"]
|
||||
|
||||
along with the text message "Explain what do you see on this picture?", and generates a response something like:
|
||||
|
||||
----
|
||||
The image shows a close-up view of a wire fruit basket containing several pieces of fruit.
|
||||
The basket appears to be made of thin metal wires formed into a round shape with an elevated handle.
|
||||
|
||||
Inside the basket, there are a few yellow bananas and a couple of red apples or possibly tomatoes.
|
||||
The vibrant colors of the fruit contrast nicely against the metallic tones of the wire basket.
|
||||
|
||||
The shallow depth of field in the photograph puts the focus squarely on the fruit in the foreground, while the basket handle extending upwards is slightly blurred, creating a pleasing bokeh effect in the background.
|
||||
|
||||
The composition and lighting give the image a clean, minimalist aesthetic that highlights the natural beauty and freshness of the fruit displayed in this elegant wire basket.
|
||||
----
|
||||
|
||||
== Sample Controller
|
||||
|
||||
https://start.spring.io/[Create] a new Spring Boot project and add the `spring-ai-anthropic-spring-boot-starter` to your pom (or gradle) dependencies.
|
||||
|
||||
Add a `application.properties` file, under the `src/main/resources` directory, to enable and configure the Anthropic Chat client:
|
||||
|
||||
[source,application.properties]
|
||||
----
|
||||
spring.ai.anthropic.api-key=YOUR_API_KEY
|
||||
spring.ai.anthropic.chat.options.model=claude-3-opus-20240229
|
||||
spring.ai.anthropic.chat.options.temperature=0.7
|
||||
spring.ai.anthropic.chat.options.max-tokens=450
|
||||
----
|
||||
|
||||
TIP: replace the `api-key` with your Anthropic credentials.
|
||||
|
||||
This will create a `AnthropicChatClient` implementation that you can inject into your class.
|
||||
Here is an example of a simple `@Controller` class that uses the chat client for text generations.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@RestController
|
||||
public class ChatController {
|
||||
|
||||
private final AnthropicChatClient chatClient;
|
||||
|
||||
@Autowired
|
||||
public ChatController(AnthropicChatClient chatClient) {
|
||||
this.chatClient = chatClient;
|
||||
}
|
||||
|
||||
@GetMapping("/ai/generate")
|
||||
public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
return Map.of("generation", chatClient.call(message));
|
||||
}
|
||||
|
||||
@GetMapping("/ai/generateStream")
|
||||
public Flux<ChatResponse> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
Prompt prompt = new Prompt(new UserMessage(message));
|
||||
return chatClient.stream(prompt);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
== Manual Configuration
|
||||
|
||||
The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatClient.java[AnthropicChatClient] implements the `ChatClient` and `StreamingChatClient` and uses the <<low-level-api>> to connect to the Anthropic service.
|
||||
|
||||
Add the `spring-ai-anthropic` dependency to your project's Maven `pom.xml` file:
|
||||
|
||||
[source, xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-anthropic</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
or to your Gradle `build.gradle` build file.
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
dependencies {
|
||||
implementation 'org.springframework.ai:spring-ai-anthropic'
|
||||
}
|
||||
----
|
||||
|
||||
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
|
||||
|
||||
Next, create a `AnthropicChatClient` and use it for text generations:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
var anthropicApi = new AnthropicApi(System.getenv("ANTHROPIC_API_KEY"));
|
||||
|
||||
var chatClient = new AnthropicChatClient(anthropicApi,
|
||||
AnthropicChatOptions.builder()
|
||||
.withModel("claude-3-opus-20240229")
|
||||
.withTemperature(0.4)
|
||||
.withMaxTokens(200)
|
||||
.build());
|
||||
|
||||
ChatResponse response = chatClient.call(
|
||||
new Prompt("Generate the names of 5 famous pirates."));
|
||||
|
||||
// Or with streaming responses
|
||||
Flux<ChatResponse> response = chatClient.stream(
|
||||
new Prompt("Generate the names of 5 famous pirates."));
|
||||
----
|
||||
|
||||
The `AnthropicChatOptions` provides the configuration information for the chat requests.
|
||||
The `AnthropicChatOptions.Builder` is fluent options builder.
|
||||
|
||||
== Low-level AnthropicApi Client [[low-level-api]]
|
||||
|
||||
The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java[AnthropicApi] provides is lightweight Java client for link:https://docs.anthropic.com/claude/reference/messages_post[Anthropic Message API].
|
||||
|
||||
Following class diagram illustrates the `AnthropicApi` chat interfaces and building blocks:
|
||||
|
||||
image::anthropic-claude3-class-diagram.jpg[AnthropicApi Chat API Diagram, 800, 600, align="center"]
|
||||
|
||||
Here is a simple snippet how to use the api programmatically:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
AnthropicApi anthropicApi =
|
||||
new AnthropicApi(System.getenv("ANTHROPIC_API_KEY"));
|
||||
|
||||
RequestMessage chatCompletionMessage = new RequestMessage(
|
||||
List.of(new MediaContent("Tell me a Joke?")), Role.USER);
|
||||
|
||||
// Sync request
|
||||
ResponseEntity<ChatCompletion> response = anthropicApi
|
||||
.chatCompletionEntity(new ChatCompletionRequest(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(),
|
||||
List.of(chatCompletionMessage), null, 100, 0.8f, false));
|
||||
|
||||
// Streaming request
|
||||
Flux<StreamResponse> response = anthropicApi
|
||||
.chatCompletionStream(new ChatCompletionRequest(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(),
|
||||
List.of(chatCompletionMessage), null, 100, 0.8f, true));
|
||||
----
|
||||
|
||||
Follow the https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java[AnthropicApi.java]'s JavaDoc for further information.
|
||||
|
||||
=== Low-level API Examples
|
||||
* The link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/chat/api/AnthropicApiIT.java[AnthropicApiIT.java] test provides some general examples how to use the lightweight library.
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ This is a powerful technique to connect the LLM capabilities with external tools
|
||||
Read more about xref:api/chat/functions/vertexai-gemini-chat-functions.adoc[Vertex AI Gemini Function Calling].
|
||||
|
||||
== Multimodal
|
||||
|
||||
Multimodality refers to a model's ability to simultaneously understand and process information from various sources, including text, images, audio, and other data formats. This paradigm represents a significant advancement in AI models.
|
||||
|
||||
Google's Gemini AI models support this capability by comprehending and integrating text, code, audio, images, and video. For more details, refer to the blog post https://blog.google/technology/ai/google-gemini-ai/#introducing-gemini[Introducing Gemini].
|
||||
|
||||
@@ -188,6 +188,7 @@ image::spring-ai-chat-completions-clients.jpg[align="center", width="800px"]
|
||||
** xref:api/chat/bedrock/bedrock-anthropic.adoc[Anthropic Chat Completion]
|
||||
** xref:api/chat/bedrock/bedrock-jurassic2.adoc[Jurassic2 Chat Completion]
|
||||
* xref:api/chat/mistralai-chat.adoc[Mistral AI Chat Completion] (streaming & function-calling support)
|
||||
* xref:api/chat/anthropic-chat.adoc[Anthropic Chat Completion] (streaming)
|
||||
|
||||
== Chat Model API
|
||||
|
||||
|
||||
@@ -223,13 +223,18 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- MongoDB Atlas Vector Store-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mongodb-atlas-store</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-anthropic</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- test dependencies -->
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.springframework.ai.anthropic.AnthropicChatClient;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
|
||||
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.annotation.Bean;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
|
||||
@EnableConfigurationProperties({ AnthropicChatProperties.class, AnthropicConnectionProperties.class })
|
||||
@ConditionalOnClass(AnthropicApi.class)
|
||||
@ConditionalOnProperty(prefix = AnthropicChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public class AnthropicAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public AnthropicApi anthropicApi(AnthropicConnectionProperties connectionProperties,
|
||||
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
|
||||
|
||||
return new AnthropicApi(connectionProperties.getBaseUrl(), connectionProperties.getApiKey(),
|
||||
connectionProperties.getVersion(), restClientBuilder, responseErrorHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public AnthropicChatClient anthropicChatClient(AnthropicApi anthropicApi, AnthropicChatProperties chatProperties,
|
||||
RetryTemplate retryTemplate) {
|
||||
return new AnthropicChatClient(anthropicApi, chatProperties.getOptions(), retryTemplate);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.springframework.ai.anthropic.AnthropicChatClient;
|
||||
import org.springframework.ai.anthropic.AnthropicChatOptions;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
|
||||
/**
|
||||
* Anthropic Chat autoconfiguration properties.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@ConfigurationProperties(AnthropicChatProperties.CONFIG_PREFIX)
|
||||
public class AnthropicChatProperties {
|
||||
|
||||
public static final String CONFIG_PREFIX = "spring.ai.anthropic.chat";
|
||||
|
||||
/**
|
||||
* Enable Anthropic chat client.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* Client lever Ollama options. Use this property to configure generative temperature,
|
||||
* topK and topP and alike parameters. The null values are ignored defaulting to the
|
||||
* generative's defaults.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private AnthropicChatOptions options = AnthropicChatOptions.builder()
|
||||
.withModel(AnthropicChatClient.DEFAULT_MODEL_NAME)
|
||||
.withMaxTokens(AnthropicChatClient.DEFAULT_MAX_TOKENS)
|
||||
.withTemperature(AnthropicChatClient.DEFAULT_TEMPERATURE)
|
||||
.build();
|
||||
|
||||
public AnthropicChatOptions getOptions() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Anthropic API connection properties.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@ConfigurationProperties(AnthropicConnectionProperties.CONFIG_PREFIX)
|
||||
public class AnthropicConnectionProperties {
|
||||
|
||||
public static final String CONFIG_PREFIX = "spring.ai.anthropic";
|
||||
|
||||
/**
|
||||
* Anthropic API access key.
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* Anthropic API base URL.
|
||||
*/
|
||||
private String baseUrl = AnthropicApi.DEFAULT_BASE_URL;
|
||||
|
||||
/**
|
||||
* Anthropic API version.
|
||||
*/
|
||||
private String version = AnthropicApi.DEFAULT_ANTHROPIC_VERSION;
|
||||
|
||||
public String getApiKey() {
|
||||
return this.apiKey;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
public String getBaseUrl() {
|
||||
return this.baseUrl;
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,3 +26,4 @@ org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoCon
|
||||
org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration
|
||||
org.springframework.ai.autoconfigure.postgresml.PostgresMlAutoConfiguration
|
||||
org.springframework.ai.autoconfigure.vectorstore.mongo.MongoDBAtlasVectorStoreAutoConfiguration
|
||||
org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.anthropic.AnthropicChatClient;
|
||||
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".*")
|
||||
public class AnthropicAutoConfigurationIT {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(AnthropicAutoConfigurationIT.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 generate() {
|
||||
contextRunner.run(context -> {
|
||||
AnthropicChatClient chatClient = context.getBean(AnthropicChatClient.class);
|
||||
String response = chatClient.call("Hello");
|
||||
assertThat(response).isNotEmpty();
|
||||
logger.info("Response: " + response);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateStreaming() {
|
||||
contextRunner.run(context -> {
|
||||
AnthropicChatClient chatClient = context.getBean(AnthropicChatClient.class);
|
||||
Flux<ChatResponse> responseFlux = chatClient.stream(new Prompt(new UserMessage("Hello")));
|
||||
|
||||
String response = responseFlux.collectList()
|
||||
.block()
|
||||
.stream()
|
||||
.map(ChatResponse::getResults)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getOutput)
|
||||
.map(AssistantMessage::getContent)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
assertThat(response).isNotEmpty();
|
||||
logger.info("Response: " + response);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.anthropic.AnthropicChatClient;
|
||||
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Unit Tests for {@link AnthropicChatProperties}, {@link AnthropicConnectionProperties}.
|
||||
*/
|
||||
public class AnthropicPropertiesTests {
|
||||
|
||||
@Test
|
||||
public void connectionProperties() {
|
||||
|
||||
new ApplicationContextRunner().withPropertyValues(
|
||||
// @formatter:off
|
||||
"spring.ai.anthropic.base-url=TEST_BASE_URL",
|
||||
"spring.ai.anthropic.api-key=abc123",
|
||||
"spring.ai.anthropic.chat.options.model=MODEL_XYZ",
|
||||
"spring.ai.anthropic.chat.options.temperature=0.55")
|
||||
// @formatter:on
|
||||
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
|
||||
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
|
||||
.run(context -> {
|
||||
var chatProperties = context.getBean(AnthropicChatProperties.class);
|
||||
var connectionProperties = context.getBean(AnthropicConnectionProperties.class);
|
||||
|
||||
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
|
||||
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
|
||||
|
||||
assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
|
||||
assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55f);
|
||||
// enabled is true by default
|
||||
assertThat(chatProperties.isEnabled()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chatOptionsTest() {
|
||||
|
||||
new ApplicationContextRunner().withPropertyValues(
|
||||
// @formatter:off
|
||||
"spring.ai.anthropic.api-key=API_KEY",
|
||||
"spring.ai.anthropic.base-url=TEST_BASE_URL",
|
||||
|
||||
"spring.ai.anthropic.chat.options.model=MODEL_XYZ",
|
||||
"spring.ai.anthropic.chat.options.max-tokens=123",
|
||||
"spring.ai.anthropic.chat.options.metadata.user-id=MyUserId",
|
||||
"spring.ai.anthropic.chat.options.stop_sequences=boza,koza",
|
||||
|
||||
"spring.ai.anthropic.chat.options.temperature=0.55",
|
||||
"spring.ai.anthropic.chat.options.top-p=0.56",
|
||||
"spring.ai.anthropic.chat.options.top-k=100"
|
||||
)
|
||||
// @formatter:on
|
||||
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
|
||||
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
|
||||
.run(context -> {
|
||||
var chatProperties = context.getBean(AnthropicChatProperties.class);
|
||||
var connectionProperties = context.getBean(AnthropicConnectionProperties.class);
|
||||
|
||||
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
|
||||
assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
|
||||
assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
|
||||
assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
|
||||
assertThat(chatProperties.getOptions().getStopSequences()).contains("boza", "koza");
|
||||
assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55f);
|
||||
assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56f);
|
||||
assertThat(chatProperties.getOptions().getTopK()).isEqualTo(100);
|
||||
|
||||
assertThat(chatProperties.getOptions().getMetadata().userId()).isEqualTo("MyUserId");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chatCompletionDisabled() {
|
||||
|
||||
// It is enabled by default
|
||||
new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
|
||||
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
|
||||
.run(context -> {
|
||||
assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty();
|
||||
assertThat(context.getBeansOfType(AnthropicChatClient.class)).isNotEmpty();
|
||||
});
|
||||
|
||||
// Explicitly enable the chat auto-configuration.
|
||||
new ApplicationContextRunner().withPropertyValues("spring.ai.anthropic.chat.enabled=true")
|
||||
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
|
||||
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
|
||||
.run(context -> {
|
||||
assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty();
|
||||
assertThat(context.getBeansOfType(AnthropicChatClient.class)).isNotEmpty();
|
||||
});
|
||||
|
||||
// Explicitly disable the chat auto-configuration.
|
||||
new ApplicationContextRunner().withPropertyValues("spring.ai.anthropic.chat.enabled=false")
|
||||
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
|
||||
RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
|
||||
.run(context -> {
|
||||
assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isEmpty();
|
||||
assertThat(context.getBeansOfType(AnthropicChatClient.class)).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring AI Starter - Anthropic</name>
|
||||
<description>Spring AI Anthropic Auto Configuration</description>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
<connection>git://github.com/spring-projects/spring-ai.git</connection>
|
||||
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
|
||||
</scm>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-spring-boot-autoconfigure</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-anthropic</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
Reference in New Issue
Block a user