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
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user