diff --git a/README.md b/README.md index 06f018cf..6167141a 100644 --- a/README.md +++ b/README.md @@ -40,29 +40,31 @@ Interweb can be used in a wide range of applications, but was designed with the Interweb currently supports the following data providers: -1. **Bing**: +1. **Anthropic**: + - Interact with Anthropic's LLMs for natural language understanding and generation. +2. **Bing**: - Search: the web for images, videos, news, and more. - Suggest: get related queries to enhance user search experience. -2. **Flickr**: +3. **Flickr**: - Search: for photos and images. - Describe: a media resource by url. -3. **Giphy**: +4. **Giphy**: - Search: for variety of gifs in one of the largest gif libraries. -4. **Google**: +5. **Google**: - Suggest: access related search queries from one of the world's leading search engines. -5. **Ipernity**: +6. **Ipernity**: - Search: discover photos and images within one of the largest non-commercial clubs. - Describe: obtain photo information using its url. -6. **OpenAI**: +7. **OpenAI**: - Interact with OpenAI's ChatGPT for natural language understanding and generation. -7. **SlideShare**: +8. **SlideShare**: - Search: find presentations and documents for various topics. -8. **Vimeo**: +9. **Vimeo**: - Search: locate videos created by creative content creators. - Describe: obtain video information using its url. -9. **YouTube**: - - Search: for videos in the largest video hosting platform. - - Describe: obtain detailed information about a video using its url. +10. **YouTube**: + - Search: for videos in the largest video hosting platform. + - Describe: obtain detailed information about a video using its url. ## How to Use Interweb 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..85e7f0e5 --- /dev/null +++ b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/AnthropicClient.java @@ -0,0 +1,36 @@ +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}") +@ClientHeaderParam(name = "anthropic-version", value = "2023-06-01") +public interface AnthropicClient { + + /** + * Anthropic Completion API + * https://docs.anthropic.com/en/api/messages + */ + @POST + @Path("/v1/messages") + Uni chatCompletions(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..e9306b81 --- /dev/null +++ b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/AnthropicConnector.java @@ -0,0 +1,80 @@ +package de.l3s.interweb.connector.anthropic; + +import java.time.Instant; +import java.util.Map; + +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.AnthropicUsage; +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 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://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(new CompletionBody(query)).map(response -> { + AnthropicUsage anthropicUsage = response.getUsage(); + Usage usage = new Usage( + anthropicUsage.getInputTokens(), + anthropicUsage.getOutputTokens() + ); + + + AnthropicContent content = response.getContent().get(0); + Message message = new Message(Message.Role.user, content.getText()); + Choice choice = new Choice(0, response.getStopReason(), message); + + + CompletionResults results = new CompletionResults( + query.getModel(), + usage, + choice, + Instant.now() + ); + + 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..808d4650 --- /dev/null +++ b/connectors/AnthropicConnector/src/main/java/de/l3s/interweb/connector/anthropic/entity/CompletionBody.java @@ -0,0 +1,77 @@ +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(CompletionQuery query) { + this.model = query.getModel(); + + 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) { + this.maxTokens = 800; + } + } + + 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..219f4c0f --- /dev/null +++ b/connectors/AnthropicConnector/src/test/java/de/l3s/interweb/connector/anthropic/AnthropicConnectorTest.java @@ -0,0 +1,73 @@ +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.setModel("claude-3-haiku-20240307"); + 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(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/connectors/OpenaiConnector/src/main/java/de/l3s/interweb/connector/openai/OpenaiConnector.java b/connectors/OpenaiConnector/src/main/java/de/l3s/interweb/connector/openai/OpenaiConnector.java index e3b34efc..9a318d95 100644 --- a/connectors/OpenaiConnector/src/main/java/de/l3s/interweb/connector/openai/OpenaiConnector.java +++ b/connectors/OpenaiConnector/src/main/java/de/l3s/interweb/connector/openai/OpenaiConnector.java @@ -53,11 +53,13 @@ public UsagePrice getPrice(String model) { @Override public Uni complete(CompletionQuery query) throws ConnectorException { return openai.chatCompletions(query.getModel(), version, new CompletionBody(query)).map(response -> { - CompletionResults results = new CompletionResults(); - results.setModel(query.getModel()); - results.setCreated(response.getCreated()); - results.setChoices(response.getChoices()); - results.setUsage(response.getUsage()); + CompletionResults results = new CompletionResults( + query.getModel(), + response.getUsage(), + response.getChoices(), + response.getCreated() + ); + return results; }); } diff --git a/interweb-core/src/main/java/de/l3s/interweb/core/completion/Choice.java b/interweb-core/src/main/java/de/l3s/interweb/core/completion/Choice.java index 66f05082..cbfa16b8 100644 --- a/interweb-core/src/main/java/de/l3s/interweb/core/completion/Choice.java +++ b/interweb-core/src/main/java/de/l3s/interweb/core/completion/Choice.java @@ -15,6 +15,12 @@ public class Choice extends ConnectorResults { private String finishReason; private Message message; + public Choice(int index, String finishReason, Message message) { + this.index = index; + this.finishReason = finishReason; + this.message = message; + } + public int getIndex() { return index; } diff --git a/interweb-core/src/main/java/de/l3s/interweb/core/completion/CompletionResults.java b/interweb-core/src/main/java/de/l3s/interweb/core/completion/CompletionResults.java index ad81202e..ed516a21 100644 --- a/interweb-core/src/main/java/de/l3s/interweb/core/completion/CompletionResults.java +++ b/interweb-core/src/main/java/de/l3s/interweb/core/completion/CompletionResults.java @@ -26,6 +26,32 @@ public class CompletionResults extends Results { private UsageCost cost; private Instant created; + public CompletionResults( + String model, + Usage usage, + Choice choice, + Instant created + ) { + this.model = model; + this.usage = usage; + this.created = created; + + add(choice); + } + + public CompletionResults( + String model, + Usage usage, + List choices, + Instant created + ) { + this.model = model; + this.usage = usage; + this.created = created; + + add(choices); + } + public UUID getChatId() { return chatId; } diff --git a/interweb-core/src/main/java/de/l3s/interweb/core/completion/Usage.java b/interweb-core/src/main/java/de/l3s/interweb/core/completion/Usage.java index 8563a8b4..50a1f55d 100644 --- a/interweb-core/src/main/java/de/l3s/interweb/core/completion/Usage.java +++ b/interweb-core/src/main/java/de/l3s/interweb/core/completion/Usage.java @@ -13,6 +13,12 @@ public class Usage { @JsonProperty("total_tokens") private int totalTokens; + public Usage(int promptTokens, int completionTokens) { + this.promptTokens = promptTokens; + this.completionTokens = completionTokens; + this.totalTokens = promptTokens + completionTokens; + } + public int getPromptTokens() { return promptTokens; } 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