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 |
Reference in New Issue
Block a user