From 09c92956c3b6dce33555385deafed05481132e91 Mon Sep 17 00:00:00 2001 From: ChristophScn Date: Tue, 11 Jun 2024 20:52:49 +0200 Subject: [PATCH] Adds Anthropic Connector for Anthropic LLM support --- connectors/AnthropicConnector/pom.xml | 51 +++++++++++ .../connector/anthropic/AnthropicClient.java | 35 ++++++++ .../anthropic/AnthropicConnector.java | 84 ++++++++++++++++++ .../anthropic/entity/AnthropicContent.java | 26 ++++++ .../anthropic/entity/AnthropicUsage.java | 30 +++++++ .../anthropic/entity/CompletionBody.java | 78 +++++++++++++++++ .../anthropic/entity/CompletionMessage.java | 45 ++++++++++ .../anthropic/entity/CompletionResponse.java | 85 +++++++++++++++++++ .../src/main/resources/application.properties | 4 + .../anthropic/AnthropicConnectorTest.java | 72 ++++++++++++++++ .../core/completion/CompletionQuery.java | 3 +- interweb-server/pom.xml | 5 ++ pom.xml | 1 + 13 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 connectors/AnthropicConnector/pom.xml create mode 100644 connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/AnthropicClient.java create mode 100644 connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/AnthropicConnector.java create mode 100644 connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/AnthropicContent.java create mode 100644 connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/AnthropicUsage.java create mode 100644 connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionBody.java create mode 100644 connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionMessage.java create mode 100644 connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionResponse.java create mode 100644 connectors/AnthropicConnector/src/main/resources/application.properties create mode 100644 connectors/AnthropicConnector/src/test/java/de/l3s/interweb/connector/anthropic/AnthropicConnectorTest.java diff --git a/connectors/AnthropicConnector/pom.xml b/connectors/AnthropicConnector/pom.xml new file mode 100644 index 00000000..6022f372 --- /dev/null +++ b/connectors/AnthropicConnector/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + de.l3s.interweb + interweb-parent + 4.0.0-SNAPSHOT + ../../pom.xml + + + connector-anthropic + 4.0.0-SNAPSHOT + jar + + + + de.l3s.interweb + interweb-core + + + io.quarkus + quarkus-rest-client-reactive-jackson + + + + io.quarkus + quarkus-junit5 + test + + + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + jandex + + + + + + + diff --git a/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/AnthropicClient.java b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/AnthropicClient.java new file mode 100644 index 00000000..f1a26316 --- /dev/null +++ b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/AnthropicClient.java @@ -0,0 +1,35 @@ +package de.l3s.interweb.connector.anthropic; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import io.quarkus.rest.client.reactive.ClientExceptionMapper; +import io.smallrye.mutiny.Uni; +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import de.l3s.interweb.connector.anthropic.entity.CompletionResponse; +import de.l3s.interweb.connector.anthropic.entity.CompletionBody; +import de.l3s.interweb.core.ConnectorException; + +@Path("") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@RegisterRestClient(configKey = "anthropic") +@ClientHeaderParam(name = "x-api-key", value = "${connector.anthropic.apikey}") +public interface AnthropicClient { + + /** + * Anthropic Completion API + * https://docs.anthropic.com/en/api/messages + */ + @POST + @Path("/v1/messages") + Uni chatCompletions(@HeaderParam("anthropic-version") String version, CompletionBody body); + + @ClientExceptionMapper + static RuntimeException toException(Response response) { + return new ConnectorException("Remote service responded with HTTP " + response.getStatus(), response.readEntity(String.class)); + } +} diff --git a/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/AnthropicConnector.java b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/AnthropicConnector.java new file mode 100644 index 00000000..19fba097 --- /dev/null +++ b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/AnthropicConnector.java @@ -0,0 +1,84 @@ +package de.l3s.interweb.connector.anthropic; + +import java.time.Instant; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; + +import jakarta.enterprise.context.Dependent; + +import io.smallrye.mutiny.Uni; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import de.l3s.interweb.connector.anthropic.entity.AnthropicContent; +import de.l3s.interweb.connector.anthropic.entity.CompletionBody; +import de.l3s.interweb.core.ConnectorException; +import de.l3s.interweb.core.completion.CompletionConnector; +import de.l3s.interweb.core.completion.CompletionQuery; +import de.l3s.interweb.core.completion.CompletionResults; +import de.l3s.interweb.core.completion.Message; +import de.l3s.interweb.core.completion.UsagePrice; +import de.l3s.interweb.core.completion.Usage; +import de.l3s.interweb.core.completion.Choice; + +@Dependent +public class AnthropicConnector implements CompletionConnector { + + private static final String version = "2023-06-01"; + private static final Map models = Map.of( + "claude-3-opus-20240229", new UsagePrice(0.015, 0.075), + "claude-3-sonnet-20240229", new UsagePrice(0.003, 0.015), + "claude-3-haiku-20240307", new UsagePrice(0.00025, 0.00125) + ); + + @Override + public String getName() { + return "Anthropic"; + } + + @Override + public String getBaseUrl() { + return "https://api.anthropic.com/"; + } + + @Override + public String[] getModels() { + return models.keySet().toArray(new String[0]); + } + + @Override + public UsagePrice getPrice(String model) { + return models.get(model); + } + + @RestClient + AnthropicClient anthropic; + + @Override + public Uni complete(CompletionQuery query) throws ConnectorException { + return anthropic.chatCompletions(version, new CompletionBody(query.getModel(), query)).map(response -> { + CompletionResults results = new CompletionResults(); + results.setModel(query.getModel()); + results.setCreated(Instant.now()); + + List choices = new ArrayList<>(); + Choice choice = new Choice(); + AnthropicContent content = response.getContent().get(0); + Message message = new Message(); + message.setContent(content.getText()); + message.setRole(Message.Role.assistant); + choice.setMessage(message); + choice.setIndex(0); + choice.setFinishReason(response.getStopReason()); + choices.add(choice); + results.setChoices(choices); + + Usage usage = new Usage(); + usage.setPromptTokens(response.getUsage().getInputTokens()); + usage.setCompletionTokens(response.getUsage().getOutputTokens()); + usage.setTotalTokens(response.getUsage().getInputTokens() + response.getUsage().getOutputTokens()); + results.setUsage(usage); + return results; + }); + } +} diff --git a/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/AnthropicContent.java b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/AnthropicContent.java new file mode 100644 index 00000000..85a18d98 --- /dev/null +++ b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/AnthropicContent.java @@ -0,0 +1,26 @@ +package de.l3s.interweb.connector.anthropic.entity; + +import io.quarkus.runtime.annotations.RegisterForReflection; + + +@RegisterForReflection +public class AnthropicContent { + private String type; + private String text; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/AnthropicUsage.java b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/AnthropicUsage.java new file mode 100644 index 00000000..bf2f089b --- /dev/null +++ b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/AnthropicUsage.java @@ -0,0 +1,30 @@ +package de.l3s.interweb.connector.anthropic.entity; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +import com.fasterxml.jackson.annotation.JsonProperty; + +@RegisterForReflection +public class AnthropicUsage { + + @JsonProperty("input_tokens") + private int inputTokens; + @JsonProperty("output_tokens") + private int outputTokens; + + public int getInputTokens() { + return inputTokens; + } + + public void setInputTokens(int inputTokens) { + this.inputTokens = inputTokens; + } + + public int getOutputTokens() { + return outputTokens; + } + + public void setOutputTokens(int outputTokens) { + this.outputTokens = outputTokens; + } +} diff --git a/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionBody.java b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionBody.java new file mode 100644 index 00000000..79b6a93e --- /dev/null +++ b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionBody.java @@ -0,0 +1,78 @@ +package de.l3s.interweb.connector.anthropic.entity; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +import de.l3s.interweb.core.completion.CompletionQuery; +import de.l3s.interweb.core.completion.Message; +import de.l3s.interweb.core.completion.Message.Role; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@RegisterForReflection +public final class CompletionBody { + + private List messages; + + private String model; + + private String system; + + private Double temperature; + + @JsonProperty("top_p") + private Double topP; + + @JsonProperty("max_tokens") + private Integer maxTokens; + + public CompletionBody(String model, CompletionQuery query) { + this.model = model; + System.out.println("model: " + model); + if (this.model == null) { + throw new IllegalArgumentException("model must be set"); + } + + this.messages = query.getMessages().stream().filter( + m -> m.getRole() != Role.system + ).map(CompletionMessage::new).toList(); + this.system = query.getMessages().stream().filter( + m -> m.getRole() == Role.system + ).findFirst().map(Message::getContent).orElse(null); + + this.temperature = query.getTemperature(); + this.topP = query.getTopP(); + this.maxTokens = query.getMaxTokens(); + + if (this.maxTokens == null) { + throw new IllegalArgumentException("maxTokens must be set"); + } + } + + public String getModel() { + return model; + } + + public List getMessages() { + return messages; + } + + public String getSystem() { + return system; + } + + public Double getTemperature() { + return temperature; + } + + public Double getTopP() { + return topP; + } + + public Integer getMaxTokens() { + return maxTokens; + } +} diff --git a/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionMessage.java b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionMessage.java new file mode 100644 index 00000000..7c987096 --- /dev/null +++ b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionMessage.java @@ -0,0 +1,45 @@ +package de.l3s.interweb.connector.anthropic.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +import de.l3s.interweb.core.completion.Message; + +@RegisterForReflection +public final class CompletionMessage { + private String role; + @JsonIgnore + private String name; + private String content; + + public CompletionMessage(Message message) { + this.role = message.getRole().name(); + this.name = message.getName(); + this.content = message.getContent(); + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionResponse.java b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionResponse.java new file mode 100644 index 00000000..d9cbc06f --- /dev/null +++ b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionResponse.java @@ -0,0 +1,85 @@ +package de.l3s.interweb.connector.anthropic.entity; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection +public class CompletionResponse { + private String id; + private String type; + private String role; + private String model; + private List content; + @JsonProperty("stop_reason") + private String stopReason; + @JsonProperty("stop_sequence") + private Integer stopSequence; + private AnthropicUsage usage; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getContent() { + return content; + } + + public void setContent(List content) { + this.content = content; + } + + public String getStopReason() { + return stopReason; + } + + public void setStopReason(String stopReason) { + this.stopReason = stopReason; + } + + public Integer getStopSequence() { + return stopSequence; + } + + public void setStopSequence(Integer stopSequence) { + this.stopSequence = stopSequence; + } + + public AnthropicUsage getUsage() { + return usage; + } + + public void setUsage(AnthropicUsage usage) { + this.usage = usage; + } +} diff --git a/connectors/AnthropicConnector/src/main/resources/application.properties b/connectors/AnthropicConnector/src/main/resources/application.properties new file mode 100644 index 00000000..43f32ee1 --- /dev/null +++ b/connectors/AnthropicConnector/src/main/resources/application.properties @@ -0,0 +1,4 @@ +# Required properties, recommended to set via environment variables (for tests, create .env in the root of the module) +connector.anthropic.url= +connector.anthropic.apikey= +quarkus.rest-client.anthropic.url=${connector.anthropic.url} diff --git a/connectors/AnthropicConnector/src/test/java/de/l3s/interweb/connector/anthropic/AnthropicConnectorTest.java b/connectors/AnthropicConnector/src/test/java/de/l3s/interweb/connector/anthropic/AnthropicConnectorTest.java new file mode 100644 index 00000000..d64392fa --- /dev/null +++ b/connectors/AnthropicConnector/src/test/java/de/l3s/interweb/connector/anthropic/AnthropicConnectorTest.java @@ -0,0 +1,72 @@ +package de.l3s.interweb.connector.anthropic; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import io.quarkus.test.junit.QuarkusTest; +import org.jboss.logging.Logger; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.l3s.interweb.core.ConnectorException; +import de.l3s.interweb.core.completion.Choice; +import de.l3s.interweb.core.completion.CompletionQuery; +import de.l3s.interweb.core.completion.CompletionResults; +import de.l3s.interweb.core.completion.Message; + +import de.l3s.interweb.connector.anthropic.entity.CompletionBody; + +@Disabled +@QuarkusTest +class AnthropicConnectorTest { + private static final Logger log = Logger.getLogger(AnthropicConnectorTest.class); + + @Inject + AnthropicConnector connector; + + @Test + void complete() throws ConnectorException { + CompletionQuery query = new CompletionQuery(); + query.addMessage("You are Interweb Assistant, a helpful chat bot. Your name is not Claude it is Interweb Assistant.", Message.Role.system); + query.addMessage("What is your name?.", Message.Role.user); + query.setMaxTokens(100); + query.setModel("claude-3-haiku-20240307"); + + + CompletionResults results = connector.complete(query).await().indefinitely(); + + + assertEquals(1, results.getChoices().size()); + System.out.println("Results for '" + query.getMessages().get(query.getMessages().size() - 1).getContent() + "':"); + for (Choice result : results.getChoices()) { + System.out.println(result.getMessage().getContent()); + } + } + + @Test + void jsonBody() { + CompletionQuery query = new CompletionQuery(); + query.addMessage("You are Interweb Assistant, a helpful chat bot.", Message.Role.system); + query.addMessage("What is your name?.", Message.Role.user); + query.addMessage("My name is Interweb Assistant.", Message.Role.assistant); + query.addMessage("Hi Interweb Assistant, I am a user.", Message.Role.user); + + CompletionBody body = new CompletionBody("haiku", query); + + // Print body as json + ObjectMapper mapper = new ObjectMapper(); + + try { + String jsonString = mapper.writeValueAsString(body); + System.out.println(jsonString); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + +} diff --git a/interweb-core/src/main/java/de/l3s/interweb/core/completion/CompletionQuery.java b/interweb-core/src/main/java/de/l3s/interweb/core/completion/CompletionQuery.java index 20d4cb6c..2abea14b 100644 --- a/interweb-core/src/main/java/de/l3s/interweb/core/completion/CompletionQuery.java +++ b/interweb-core/src/main/java/de/l3s/interweb/core/completion/CompletionQuery.java @@ -89,8 +89,9 @@ public class CompletionQuery { *
* The total length of input tokens and generated tokens is limited by the model's context length. */ + @NotEmpty @JsonProperty("max_tokens") - private Integer maxTokens; + private Integer maxTokens = 800; /** * Whether the conversation should be summarized into a title. Defaults to false. diff --git a/interweb-server/pom.xml b/interweb-server/pom.xml index 7fb538d5..b6fae108 100644 --- a/interweb-server/pom.xml +++ b/interweb-server/pom.xml @@ -19,6 +19,11 @@ de.l3s.interweb interweb-core + + de.l3s.interweb + connector-anthropic + 4.0.0-SNAPSHOT + de.l3s.interweb connector-bing diff --git a/pom.xml b/pom.xml index be1d1c9f..76dd6087 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,7 @@ interweb-core interweb-server interweb-client + connectors/AnthropicConnector connectors/BingConnector connectors/FlickrConnector connectors/GiphyConnector