feat: Vision models support in Anthropic (#32103)

## Description
Adding new vision models support in Anthropic
<img width="1134" alt="Screenshot 2024-04-12 at 15 59 06"
src="https://github.com/appsmithorg/appsmith/assets/25587962/49110b9d-00ee-4210-9d43-bf2a832aee20">

 
Fixes https://github.com/appsmithorg/appsmith-ee/issues/3681

## Automation

/ok-to-test tags="@tag.Datasources"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!CAUTION]
> 🔴 🔴 🔴 Some tests have failed.
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/8704175377>
> Commit: 2c393e7ffaf3d08fd8e945761206de76d3a13845
> Cypress dashboard: <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=8704175377&attempt=3&selectiontype=test&testsstatus=failed&specsstatus=fail"
target="_blank"> Click here!</a>
> The following are new failures, please fix them before merging the PR:
<ol>
> <li>cypress/e2e/Regression/ServerSide/GenerateCRUD/MySQL2_Spec.ts
</ol>
> To know the list of identified flaky tests - <a
href="https://internal.appsmith.com/app/cypress-dashboard/identified-flaky-tests-65890b3c81d7400d08fa9ee3?branch=master"
target="_blank">Refer here</a>

<!-- end of auto-generated comment: Cypress test results  -->







<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit


- **New Features**
- Enhanced chat functionality with version upgrade and refined message
creation process.
- Added vision-related commands for handling image and text data in the
Anthropic plugin.
    - Expanded constants to support new messaging and vision features.
- Introduced new fields in request models to support system prompts and
messages, while deprecating older fields.
- Implemented a `VisionCommand` in method strategy for better handling
of vision tasks.
- Improved utility functions for message handling and configuration
value extraction.

- **Refactor**
- Streamlined request URI construction to support new vision
functionality alongside chat commands.
    - Removed unused constants and methods to clean up the codebase.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Nirmal Sarswat 2024-04-16 19:11:28 +05:30 committed by GitHub
parent 520cfa10a6
commit bc3d46d8c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 766 additions and 141 deletions

View File

@ -19,8 +19,11 @@ import com.appsmith.external.services.SharedConfig;
import com.external.plugins.commands.AnthropicCommand;
import com.external.plugins.constants.AnthropicConstants;
import com.external.plugins.models.AnthropicRequestDTO;
import com.external.plugins.models.CompletionDTO;
import com.external.plugins.models.MessageDTO;
import com.external.plugins.utils.AnthropicMethodStrategy;
import com.external.plugins.utils.RequestUtils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.gson.Gson;
@ -45,6 +48,7 @@ import java.util.stream.Collectors;
import static com.external.plugins.constants.AnthropicConstants.ANTHROPIC_MODELS;
import static com.external.plugins.constants.AnthropicConstants.BODY;
import static com.external.plugins.constants.AnthropicConstants.CLAUDE3_PREFIX;
import static com.external.plugins.constants.AnthropicConstants.LABEL;
import static com.external.plugins.constants.AnthropicConstants.TEST_MODEL;
import static com.external.plugins.constants.AnthropicConstants.TEST_PROMPT;
@ -137,7 +141,21 @@ public class AnthropicPlugin extends BasePlugin {
return Mono.just(apiKeyNotPresentErrorResult);
}
return RequestUtils.makeRequest(httpMethod, uri, apiKeyAuth, BodyInserters.fromValue(anthropicRequestDTO))
String model = anthropicRequestDTO.getModel();
// we don't want to serialise null values as Anthropic throws bad request otherwise
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
String requestBody;
try {
requestBody = objectMapper.writeValueAsString(anthropicRequestDTO);
} catch (Exception e) {
errorResult.setIsExecutionSuccess(false);
errorResult.setErrorInfo(
new AppsmithPluginException(AppsmithPluginError.PLUGIN_JSON_PARSE_ERROR, e.getMessage()));
return Mono.just(errorResult);
}
return RequestUtils.makeRequest(httpMethod, uri, apiKeyAuth, BodyInserters.fromValue(requestBody))
.flatMap(responseEntity -> {
HttpStatusCode statusCode = responseEntity.getStatusCode();
@ -171,7 +189,12 @@ public class AnthropicPlugin extends BasePlugin {
Object body;
try {
body = objectMapper.readValue(responseEntity.getBody(), Object.class);
actionExecutionResult.setBody(body);
if (model.contains(CLAUDE3_PREFIX)) {
actionExecutionResult.setBody(body);
} else {
actionExecutionResult.setBody(
formatResponseBodyAsCompletionAPI(model, responseEntity.getBody()));
}
} catch (IOException ex) {
actionExecutionResult.setIsExecutionSuccess(false);
actionExecutionResult.setErrorInfo(new AppsmithPluginException(
@ -204,6 +227,24 @@ public class AnthropicPlugin extends BasePlugin {
});
}
/**
* To keep things backward compatible, if model doesn't belong to claude 3, format response in form of claude completion API
*/
private Object formatResponseBodyAsCompletionAPI(String model, byte[] response) {
try {
MessageDTO messageDTO = objectMapper.readValue(response, MessageDTO.class);
CompletionDTO completionDTO = new CompletionDTO();
completionDTO.setId(messageDTO.getId());
completionDTO.setType("completion");
completionDTO.setStopReason(messageDTO.getStopReason());
completionDTO.setModel(model);
completionDTO.setCompletion(messageDTO.getFirstMessage());
return completionDTO;
} catch (IOException e) {
throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_JSON_PARSE_ERROR, new String(response));
}
}
@Override
public Mono<TriggerResultDTO> trigger(
APIConnection connection, DatasourceConfiguration datasourceConfiguration, TriggerRequestDTO request) {
@ -252,7 +293,11 @@ public class AnthropicPlugin extends BasePlugin {
})
.onErrorResume(error -> {
log.debug("Error while fetching Anthropic models list", error);
return Mono.just(getDataToMap(ANTHROPIC_MODELS));
if (ANTHROPIC_MODELS.containsKey(requestType)) {
return Mono.just(getDataToMap(ANTHROPIC_MODELS.get(requestType)));
}
return Mono.error(new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, error.getMessage()));
})
.map(trigger -> {
TriggerResultDTO triggerResult = new TriggerResultDTO(trigger);
@ -268,7 +313,7 @@ public class AnthropicPlugin extends BasePlugin {
}
private List<Map<String, String>> getDataToMap(List<String> data) {
return data.stream().sorted().map(x -> Map.of(LABEL, x, VALUE, x)).collect(Collectors.toList());
return data.stream().map(x -> Map.of(LABEL, x, VALUE, x)).collect(Collectors.toList());
}
}
}

View File

@ -5,43 +5,38 @@ import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException
import com.appsmith.external.models.ActionConfiguration;
import com.external.plugins.constants.AnthropicConstants;
import com.external.plugins.models.AnthropicRequestDTO;
import com.external.plugins.models.Role;
import com.external.plugins.models.Message;
import com.external.plugins.utils.CommandUtils;
import com.external.plugins.utils.RequestUtils;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.springframework.http.HttpMethod;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.external.plugins.constants.AnthropicConstants.ANTHROPIC;
import static com.external.plugins.constants.AnthropicConstants.CHAT;
import static com.external.plugins.constants.AnthropicConstants.CHAT_MODEL_SELECTOR;
import static com.external.plugins.constants.AnthropicConstants.CHAT_V2;
import static com.external.plugins.constants.AnthropicConstants.CLOUD_SERVICES;
import static com.external.plugins.constants.AnthropicConstants.COMMAND;
import static com.external.plugins.constants.AnthropicConstants.CONTENT;
import static com.external.plugins.constants.AnthropicConstants.DATA;
import static com.external.plugins.constants.AnthropicConstants.DEFAULT_MAX_TOKEN;
import static com.external.plugins.constants.AnthropicConstants.DEFAULT_TEMPERATURE;
import static com.external.plugins.constants.AnthropicConstants.JSON;
import static com.external.plugins.constants.AnthropicConstants.MAX_TOKENS;
import static com.external.plugins.constants.AnthropicConstants.MESSAGES;
import static com.external.plugins.constants.AnthropicConstants.MODELS_API;
import static com.external.plugins.constants.AnthropicConstants.PROVIDER;
import static com.external.plugins.constants.AnthropicConstants.ROLE;
import static com.external.plugins.constants.AnthropicConstants.TEMPERATURE;
import static com.external.plugins.constants.AnthropicConstants.VIEW_TYPE;
import static com.external.plugins.constants.AnthropicErrorMessages.BAD_MAX_TOKEN_CONFIGURATION;
import static com.external.plugins.constants.AnthropicErrorMessages.BAD_TEMPERATURE_CONFIGURATION;
import static com.external.plugins.constants.AnthropicConstants.SYSTEM_PROMPT;
import static com.external.plugins.constants.AnthropicErrorMessages.EXECUTION_FAILURE;
import static com.external.plugins.constants.AnthropicErrorMessages.MODEL_NOT_SELECTED;
import static com.external.plugins.constants.AnthropicErrorMessages.QUERY_NOT_CONFIGURED;
import static com.external.plugins.constants.AnthropicErrorMessages.STRING_APPENDER;
import static com.external.plugins.utils.CommandUtils.getMaxTokenFromFormData;
import static com.external.plugins.utils.CommandUtils.getMessages;
import static com.external.plugins.utils.CommandUtils.getTemperatureFromFormData;
public class ChatCommand implements AnthropicCommand {
private final Gson gson = new Gson();
@ -60,7 +55,7 @@ public class ChatCommand implements AnthropicCommand {
public URI createTriggerUri() {
return UriComponentsBuilder.fromUriString(CLOUD_SERVICES + MODELS_API)
.queryParam(PROVIDER, ANTHROPIC)
.queryParam(COMMAND, CHAT.toLowerCase())
.queryParam(COMMAND, CHAT_V2)
.build()
.toUri();
}
@ -87,94 +82,44 @@ public class ChatCommand implements AnthropicCommand {
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
String.format(STRING_APPENDER, EXECUTION_FAILURE, MODEL_NOT_SELECTED));
}
anthropicRequestDTO.setModel(model);
Float temperature = getTemperatureFromFormData(formData);
anthropicRequestDTO.setTemperature(temperature);
anthropicRequestDTO.setMaxTokensToSample(getMaxTokenFromFormData(formData));
anthropicRequestDTO.setPrompt(createPrompt(formData));
anthropicRequestDTO.setModel(model);
anthropicRequestDTO.setMaxTokens(getMaxTokenFromFormData(formData));
anthropicRequestDTO.setMessages(createMessages(formData));
if (formData.containsKey(SYSTEM_PROMPT) && formData.get(SYSTEM_PROMPT) != null) {
anthropicRequestDTO.setSystem(RequestUtils.extractDataFromFormData(formData, SYSTEM_PROMPT));
}
return anthropicRequestDTO;
}
/**
* This is the kind of format we want to build from the messages as a prompt.
* Example Prompt: `\n\nHuman: ${query}\n\nAssistant:`
* Lastly, we leave it with an additional Assistant: so that it can respond back as an assistant
*/
private String createPrompt(Map<String, Object> formData) {
StringBuilder stringBuilder = new StringBuilder();
if (formData.containsKey(MESSAGES)) {
List<Map<String, String>> messageMaps = getMessages((Map<String, Object>) formData.get(MESSAGES));
if (messageMaps == null) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
"messages are not provided in the configuration correctly");
}
for (Map<String, String> messageMap : messageMaps) {
if (messageMap != null && messageMap.containsKey(ROLE) && messageMap.containsKey(CONTENT)) {
stringBuilder
.append("\n\n")
.append(messageMap.get(ROLE))
.append(": ")
.append(messageMap.get(CONTENT));
}
}
return stringBuilder.append("\n").append(Role.Assistant).append(":").toString();
} else {
private List<Message> createMessages(Map<String, Object> formData) {
if (!formData.containsKey(MESSAGES)) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
"messages are not provided in the configuration");
}
}
/**
* When JS is enabled in form component, value is stored in data key only. Difference is if viewType is json,
* it's stored as JSON string otherwise it's Java serialized object
*/
private List<Map<String, String>> getMessages(Map<String, Object> messages) {
Type listType = new TypeToken<List<Map<String, String>>>() {}.getType();
if (messages.containsKey(VIEW_TYPE) && JSON.equals(messages.get(VIEW_TYPE))) {
// data is present in data key as String
return gson.fromJson((String) messages.get(DATA), listType);
}
// return object stored in data key
return (List<Map<String, String>>) messages.get(DATA);
}
private int getMaxTokenFromFormData(Map<String, Object> formData) {
String maxTokenAsString = RequestUtils.extractValueFromFormData(formData, MAX_TOKENS);
if (!StringUtils.hasText(maxTokenAsString)) {
return DEFAULT_MAX_TOKEN;
}
try {
return Integer.parseInt(maxTokenAsString);
} catch (IllegalArgumentException illegalArgumentException) {
return DEFAULT_MAX_TOKEN;
} catch (Exception exception) {
List<Map<String, String>> messageMaps = getMessages((Map<String, Object>) formData.get(MESSAGES));
if (messageMaps == null) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
String.format(STRING_APPENDER, EXECUTION_FAILURE, BAD_MAX_TOKEN_CONFIGURATION));
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
"messages are not provided in the configuration correctly");
}
}
List<Message> messages = new ArrayList<>();
for (Map<String, String> messageMap : messageMaps) {
if (messageMap != null && messageMap.containsKey(ROLE) && messageMap.containsKey(CONTENT)) {
Message message = new Message();
Message.TextContent textContent = new Message.TextContent();
textContent.setText(messageMap.get(CONTENT));
private Float getTemperatureFromFormData(Map<String, Object> formData) {
String temperatureString = RequestUtils.extractValueFromFormData(formData, TEMPERATURE);
message.setRole(CommandUtils.getActualRoleValue(messageMap.get(ROLE)));
message.setContent(List.of(textContent));
if (!StringUtils.hasText(temperatureString)) {
return DEFAULT_TEMPERATURE;
}
try {
return Float.parseFloat(temperatureString);
} catch (IllegalArgumentException illegalArgumentException) {
return DEFAULT_TEMPERATURE;
} catch (Exception exception) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
String.format(STRING_APPENDER, EXECUTION_FAILURE, BAD_TEMPERATURE_CONFIGURATION));
messages.add(message);
}
}
return messages;
}
}

View File

@ -0,0 +1,179 @@
package com.external.plugins.commands;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.models.ActionConfiguration;
import com.external.plugins.models.AnthropicRequestDTO;
import com.external.plugins.models.Message;
import com.external.plugins.utils.CommandUtils;
import com.external.plugins.utils.RequestUtils;
import com.google.gson.Gson;
import org.springframework.http.HttpMethod;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.external.plugins.constants.AnthropicConstants.ANTHROPIC;
import static com.external.plugins.constants.AnthropicConstants.BASE64;
import static com.external.plugins.constants.AnthropicConstants.CLOUD_SERVICES;
import static com.external.plugins.constants.AnthropicConstants.COMMAND;
import static com.external.plugins.constants.AnthropicConstants.CONTENT;
import static com.external.plugins.constants.AnthropicConstants.IMAGE;
import static com.external.plugins.constants.AnthropicConstants.MESSAGES;
import static com.external.plugins.constants.AnthropicConstants.MODELS_API;
import static com.external.plugins.constants.AnthropicConstants.PROVIDER;
import static com.external.plugins.constants.AnthropicConstants.ROLE;
import static com.external.plugins.constants.AnthropicConstants.SYSTEM_PROMPT;
import static com.external.plugins.constants.AnthropicConstants.TEXT;
import static com.external.plugins.constants.AnthropicConstants.TYPE;
import static com.external.plugins.constants.AnthropicConstants.VISION;
import static com.external.plugins.constants.AnthropicConstants.VISION_MODEL_SELECTOR;
import static com.external.plugins.constants.AnthropicErrorMessages.EXECUTION_FAILURE;
import static com.external.plugins.constants.AnthropicErrorMessages.MODEL_NOT_SELECTED;
import static com.external.plugins.constants.AnthropicErrorMessages.QUERY_NOT_CONFIGURED;
import static com.external.plugins.constants.AnthropicErrorMessages.STRING_APPENDER;
import static com.external.plugins.utils.CommandUtils.getMaxTokenFromFormData;
import static com.external.plugins.utils.CommandUtils.getMessages;
import static com.external.plugins.utils.CommandUtils.getTemperatureFromFormData;
public class VisionCommand implements AnthropicCommand {
private final Gson gson = new Gson();
@Override
public HttpMethod getTriggerHTTPMethod() {
return HttpMethod.GET;
}
@Override
public HttpMethod getExecutionMethod() {
return HttpMethod.POST;
}
@Override
public URI createTriggerUri() {
return UriComponentsBuilder.fromUriString(CLOUD_SERVICES + MODELS_API)
.queryParam(PROVIDER, ANTHROPIC)
.queryParam(COMMAND, VISION)
.build()
.toUri();
}
@Override
public URI createExecutionUri() {
return RequestUtils.createUriFromCommand(VISION);
}
@Override
public AnthropicRequestDTO makeRequestBody(ActionConfiguration actionConfiguration) {
Map<String, Object> formData = actionConfiguration.getFormData();
if (CollectionUtils.isEmpty(formData)) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
String.format(STRING_APPENDER, EXECUTION_FAILURE, QUERY_NOT_CONFIGURED));
}
AnthropicRequestDTO anthropicRequestDTO = new AnthropicRequestDTO();
String model = RequestUtils.extractDataFromFormData(formData, VISION_MODEL_SELECTOR);
if (!StringUtils.hasText(model)) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
String.format(STRING_APPENDER, EXECUTION_FAILURE, MODEL_NOT_SELECTED));
}
Float temperature = getTemperatureFromFormData(formData);
anthropicRequestDTO.setTemperature(temperature);
anthropicRequestDTO.setModel(model);
anthropicRequestDTO.setMaxTokens(getMaxTokenFromFormData(formData));
anthropicRequestDTO.setMessages(createMessages(formData));
if (formData.containsKey(SYSTEM_PROMPT) && formData.get(SYSTEM_PROMPT) != null) {
anthropicRequestDTO.setSystem(RequestUtils.extractDataFromFormData(formData, SYSTEM_PROMPT));
}
return anthropicRequestDTO;
}
private List<Message> createMessages(Map<String, Object> formData) {
if (!formData.containsKey(MESSAGES)) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
"messages are not provided in the configuration");
}
List<Map<String, String>> messageMaps = getMessages((Map<String, Object>) formData.get(MESSAGES));
if (messageMaps == null) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
"messages are not provided in the configuration correctly");
}
List<Message> messages = new ArrayList<>();
for (Map<String, String> messageMap : messageMaps) {
if (messageMap != null && messageMap.containsKey(ROLE) && messageMap.containsKey(CONTENT)) {
Message message = new Message();
String type = messageMap.get(TYPE);
message.setRole(CommandUtils.getActualRoleValue(messageMap.get(ROLE)));
if (TEXT.equals(type)) {
Message.TextContent textContent = new Message.TextContent();
textContent.setText(messageMap.get(CONTENT));
message.setContent(List.of(textContent));
} else if (IMAGE.equals(type)) {
String content = messageMap.get(CONTENT);
if (!isValidImageContent(content)) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
"Image content provided in the configuration is not valid");
}
Message.ImageContent imageContent = new Message.ImageContent();
Message.Source source = new Message.Source();
source.setType(BASE64);
source.setMediaType(getMediaType(messageMap.get(CONTENT)));
source.setData(getImageData(messageMap.get(CONTENT)));
imageContent.setSource(source);
message.setContent(List.of(imageContent));
}
message.setRole(CommandUtils.getActualRoleValue(messageMap.get(ROLE)));
messages.add(message);
}
}
// As per Anthropic API, two content by same role in row are not allowed. It should be followed like user and
// assistant
// That's why we have to club the messages to have user and assistant in alternate order
List<Message> orderedMessages = new ArrayList<>();
for (Message message : messages) {
if (orderedMessages.isEmpty()) {
orderedMessages.add(message);
} else {
Message lastMessage = orderedMessages.get(orderedMessages.size() - 1);
if (!lastMessage.getRole().equals(message.getRole())) {
// different roles so can be added in the order
orderedMessages.add(message);
} else {
// add last message content to the current message since both are same role
List<Message.Content> content = new ArrayList<>(lastMessage.getContent());
content.addAll(message.getContent());
message.setContent(content);
orderedMessages.remove(lastMessage);
orderedMessages.add(message);
}
}
}
return orderedMessages;
}
private boolean isValidImageContent(String content) {
return StringUtils.hasText(content) && content.startsWith("data:image");
}
private String getMediaType(String content) {
return content.split(";", 2)[0].split(":")[1];
}
private String getImageData(String content) {
return content.split(",", 2)[1];
}
}

View File

@ -3,21 +3,34 @@ package com.external.plugins.constants;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import java.util.List;
import java.util.Map;
public class AnthropicConstants {
public static final String ANTHROPIC_API_ENDPOINT = "https://api.anthropic.com/v1";
public static final String COMPLETION_API = "/complete";
public static final String MESSAGE_API = "/messages";
public static final String CHAT_MODELS = "CHAT_MODELS";
public static final String VISION_MODELS = "VISION_MODELS";
public static final String CHAT = "CHAT";
// chat v2 includes claude-3 models
public static final String CHAT_V2 = "CHAT_V2";
public static final String VISION = "VISION";
public static final String COMMAND = "command";
public static final String DATA = "data";
public static final String TEXT = "text";
public static final String IMAGE = "image";
public static final String BASE64 = "base64";
public static final String SYSTEM_PROMPT = "systemPrompt";
public static final String VIEW_TYPE = "viewType";
public static final String COMPONENT_DATA = "componentData";
public static final String BODY = "body";
public static final String ROLE = "role";
public static final String TYPE = "type";
public static final String CONTENT = "content";
public static final String CLAUDE3_PREFIX = "claude-3";
public static final String MODEL = "model";
public static final String CHAT_MODEL_SELECTOR = "chatModel";
public static final String VISION_MODEL_SELECTOR = "visionModel";
public static final String MESSAGES = "messages";
public static final String TEMPERATURE = "temperature";
public static final String MAX_TOKENS = "maxTokens";
@ -28,13 +41,20 @@ public class AnthropicConstants {
public static final String API_KEY_HEADER = "x-api-key";
public static final String ANTHROPIC_VERSION_HEADER = "anthropic-version";
public static final String ANTHROPIC_VERSION = "2023-06-01";
public static final List<String> ANTHROPIC_MODELS =
List.of("claude-2.1", "claude-2", "claude-instant-1.2", "claude-instant-1");
public static final Map<String, List<String>> ANTHROPIC_MODELS = Map.of(
CHAT,
List.of(
"claude-instant-1.2",
"claude-2.1",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307"),
VISION, List.of("claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307"));
public static final String CLOUD_SERVICES = "https://cs.appsmith.com";
public static final String MODELS_API = "/api/v1/ai/models";
public static final String PROVIDER = "provider";
public static final String ANTHROPIC = "anthropic";
public static final String TEST_MODEL = "claude-instant-1";
public static final String TEST_MODEL = "claude-instant-1.2";
public static final String TEST_PROMPT = "Human:Hey Assistant:";
public static final ExchangeStrategies EXCHANGE_STRATEGIES = ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(/* 10MB */ 10 * 1024 * 1024))

View File

@ -5,12 +5,26 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* Common request Body of Anthropic completion and messages API
*/
@Getter
@Setter
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class AnthropicRequestDTO {
String model;
// system prompt
String system;
@Deprecated
String prompt;
@Deprecated
Integer maxTokensToSample;
Integer maxTokens;
List<Message> messages;
Float temperature;
}

View File

@ -0,0 +1,18 @@
package com.external.plugins.models;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
/**
* Response body of Anthropic completion API
*/
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class CompletionDTO {
private String completion;
private String id;
private String model;
private String stopReason;
private String type;
}

View File

@ -0,0 +1,62 @@
package com.external.plugins.models;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import static com.external.plugins.constants.AnthropicConstants.IMAGE;
import static com.external.plugins.constants.AnthropicConstants.TEXT;
/**
* This DTO is part of request body for Anthropic messages API
*/
@Getter
@Setter
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Message {
String role;
List<Content> content;
@Getter
@Setter
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class Content {
public Content(String type) {
this.type = type;
}
String type;
}
@Setter
@Getter
public static class TextContent extends Content {
String text;
public TextContent() {
super(TEXT);
}
}
@Setter
@Getter
public static class ImageContent extends Content {
public ImageContent() {
super(IMAGE);
}
Source source;
}
@Getter
@Setter
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class Source {
String type;
String mediaType;
String data;
}
}

View File

@ -0,0 +1,37 @@
package com.external.plugins.models;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* This DTO is being used as a response body for Anthropic messages API
*/
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class MessageDTO {
private List<ContentItem> content;
private String id;
private String model;
private String role;
private String stopReason;
private Integer stopSequence;
private String type;
private Map<String, Integer> usage;
@Data
public static class ContentItem {
private String text;
private String type;
}
public String getFirstMessage() {
if (content == null || content.isEmpty()) {
return null;
}
return content.get(0).getText();
}
}

View File

@ -6,6 +6,7 @@ import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.TriggerRequestDTO;
import com.external.plugins.commands.AnthropicCommand;
import com.external.plugins.commands.ChatCommand;
import com.external.plugins.commands.VisionCommand;
import com.external.plugins.constants.AnthropicConstants;
import com.google.gson.Gson;
import org.springframework.util.CollectionUtils;
@ -21,6 +22,7 @@ public class AnthropicMethodStrategy {
return switch (requestType) {
case AnthropicConstants.CHAT_MODELS -> new ChatCommand();
case AnthropicConstants.VISION_MODELS -> new VisionCommand();
default -> throw Exceptions.propagate(
new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR));
};
@ -40,6 +42,7 @@ public class AnthropicMethodStrategy {
public static AnthropicCommand selectExecutionMethod(String command) {
return switch (command) {
case AnthropicConstants.CHAT -> new ChatCommand();
case AnthropicConstants.VISION -> new VisionCommand();
default -> throw Exceptions.propagate(new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Unsupported command: " + command));
};

View File

@ -0,0 +1,92 @@
package com.external.plugins.utils;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.springframework.util.StringUtils;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import static com.external.plugins.constants.AnthropicConstants.DATA;
import static com.external.plugins.constants.AnthropicConstants.DEFAULT_MAX_TOKEN;
import static com.external.plugins.constants.AnthropicConstants.DEFAULT_TEMPERATURE;
import static com.external.plugins.constants.AnthropicConstants.JSON;
import static com.external.plugins.constants.AnthropicConstants.MAX_TOKENS;
import static com.external.plugins.constants.AnthropicConstants.TEMPERATURE;
import static com.external.plugins.constants.AnthropicConstants.VIEW_TYPE;
import static com.external.plugins.constants.AnthropicErrorMessages.BAD_MAX_TOKEN_CONFIGURATION;
import static com.external.plugins.constants.AnthropicErrorMessages.BAD_TEMPERATURE_CONFIGURATION;
import static com.external.plugins.constants.AnthropicErrorMessages.EXECUTION_FAILURE;
import static com.external.plugins.constants.AnthropicErrorMessages.STRING_APPENDER;
public class CommandUtils {
private static final Gson gson = new Gson();
/**
* When JS is enabled in form component, value is stored in data key only. Difference is if viewType is json,
* it's stored as JSON string otherwise it's Java serialized object
*/
public static List<Map<String, String>> getMessages(Map<String, Object> messages) {
Type listType = new TypeToken<List<Map<String, String>>>() {}.getType();
if (messages.containsKey(VIEW_TYPE) && JSON.equals(messages.get(VIEW_TYPE))) {
// data is present in data key as String
return gson.fromJson((String) messages.get(DATA), listType);
}
// return object stored in data key
return (List<Map<String, String>>) messages.get(DATA);
}
public static int getMaxTokenFromFormData(Map<String, Object> formData) {
String maxTokenAsString = RequestUtils.extractValueFromFormData(formData, MAX_TOKENS);
if (!StringUtils.hasText(maxTokenAsString)) {
return DEFAULT_MAX_TOKEN;
}
try {
return Integer.parseInt(maxTokenAsString);
} catch (IllegalArgumentException illegalArgumentException) {
return DEFAULT_MAX_TOKEN;
} catch (Exception exception) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
String.format(STRING_APPENDER, EXECUTION_FAILURE, BAD_MAX_TOKEN_CONFIGURATION));
}
}
public static Float getTemperatureFromFormData(Map<String, Object> formData) {
String temperatureString = RequestUtils.extractValueFromFormData(formData, TEMPERATURE);
if (!StringUtils.hasText(temperatureString)) {
return DEFAULT_TEMPERATURE;
}
try {
return Float.parseFloat(temperatureString);
} catch (IllegalArgumentException illegalArgumentException) {
return DEFAULT_TEMPERATURE;
} catch (Exception exception) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
String.format(STRING_APPENDER, EXECUTION_FAILURE, BAD_TEMPERATURE_CONFIGURATION));
}
}
/**
* Anthropic message API expect role to be one of user or assistant. This method converts Human to user and Assistant to assistant
* @param role - Appsmith understood role
* @return - Actual role value expected by Anthropic message API
*/
public static String getActualRoleValue(String role) {
if (role == null) {
return null;
}
return switch (role) {
case "Human" -> "user";
case "Assistant" -> "assistant";
default -> role;
};
}
}

View File

@ -25,7 +25,7 @@ import java.util.Map;
import java.util.Set;
import static com.external.plugins.constants.AnthropicConstants.ANTHROPIC_API_ENDPOINT;
import static com.external.plugins.constants.AnthropicConstants.COMPLETION_API;
import static com.external.plugins.constants.AnthropicConstants.MESSAGE_API;
public class RequestUtils {
@ -40,8 +40,8 @@ public class RequestUtils {
}
public static URI createUriFromCommand(String command) {
if (AnthropicConstants.CHAT.equals(command)) {
return URI.create(ANTHROPIC_API_ENDPOINT + COMPLETION_API);
if (AnthropicConstants.CHAT.equals(command) || AnthropicConstants.VISION.equals(command)) {
return URI.create(ANTHROPIC_API_ENDPOINT + MESSAGE_API);
} else if (AnthropicConstants.MODEL.equals(command)) {
return URI.create("");
} else {

View File

@ -47,6 +47,20 @@
"minWidth": "270px"
}
},
{
"label": "System Prompt",
"Description": "Provide additional instructions for the AI model as system prompt",
"subtitle": "Provide additional instructions for the AI model as system prompt",
"configProperty": "actionConfiguration.formData.systemPrompt.data",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
"placeholderText": "Write some text or use {{ }} to reference a dynamic text value",
"initialValue": "",
"isRequired": false,
"customStyles": {
"width": "590px",
"minWidth": "400px"
}
},
{
"label": "Messages",
"tooltipText": "Ask a question",
@ -62,7 +76,7 @@
"label": "Role",
"key": "role",
"controlType": "DROP_DOWN",
"initialValue": "Human",
"initialValue": "Human",
"options": [
{
"label": "Human",

View File

@ -15,6 +15,10 @@
{
"label": "Chat",
"value": "CHAT"
},
{
"label": "Vision",
"value": "VISION"
}
]
}
@ -22,6 +26,7 @@
}
],
"files": [
"chat.json"
"chat.json",
"vision.json"
]
}

View File

@ -0,0 +1,127 @@
{
"identifier": "VISION",
"controlType": "SECTION",
"conditionals": {
"show": "{{actionConfiguration.formData.command.data === 'VISION'}}"
},
"children": [
{
"label": "Models",
"tooltipText": "Select the model for response generation",
"subtitle": "ID of the model to use.",
"isRequired": true,
"propertyName": "vision_model_id",
"configProperty": "actionConfiguration.formData.visionModel.data",
"controlType": "DROP_DOWN",
"initialValue": "",
"options": [],
"placeholderText": "All models will be fetched.",
"fetchOptionsConditionally": true,
"setFirstOptionAsDefault": true,
"alternateViewTypes": ["json"],
"conditionals": {
"enable": "{{true}}",
"fetchDynamicValues": {
"condition": "{{actionConfiguration.formData.command.data === 'VISION'}}",
"config": {
"params": {
"requestType": "VISION_MODELS",
"displayType": "DROP_DOWN"
}
}
}
}
},
{
"label": "Max Tokens",
"tooltipText": "The maximum number of tokens to generate in the chat completion.",
"subtitle": "The maximum number of tokens to generate in the chat completion.",
"Description": "Put a positive integer value",
"configProperty": "actionConfiguration.formData.maxTokens",
"controlType": "INPUT_TEXT",
"initialValue": "256",
"isRequired": true,
"dataType": "NUMBER",
"customStyles": {
"width": "270px",
"minWidth": "270px"
}
},
{
"label": "System Prompt",
"Description": "Provide additional instructions for the AI model as system prompt",
"subtitle": "Provide additional instructions for the AI model as system prompt",
"configProperty": "actionConfiguration.formData.systemPrompt.data",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
"placeholderText": "Write some text or use {{ }} to reference a dynamic text value",
"initialValue": "",
"isRequired": false
},
{
"label": "Messages",
"tooltipText": "Ask a question",
"subtitle": "A list of messages comprising the conversation so far. You can pass base64 encoded image directly in the request.",
"propertyName": "messages",
"isRequired": true,
"configProperty": "actionConfiguration.formData.messages.data",
"controlType": "ARRAY_FIELD",
"addMoreButtonLabel": "Add message",
"alternateViewTypes": ["json"],
"schema": [
{
"label": "Role",
"key": "role",
"controlType": "DROP_DOWN",
"initialValue": "Human",
"options": [
{
"label": "Human",
"value": "Human"
},
{
"label": "Assistant",
"value": "Assistant"
}
]
},
{
"label": "Type",
"key": "type",
"controlType": "DROP_DOWN",
"initialValue": "text",
"options": [
{
"label": "Text",
"value": "text"
},
{
"label": "Image",
"value": "image"
}
]
},
{
"label": "Content",
"key": "content",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
"placeholderText": "{{Img1.image}} or {{Input1.text}}"
}
]
},
{
"label": "Temperature",
"tooltipText": "Put a value between 0 and 1",
"Description": "Put a value between 0 and 1",
"subtitle": "Defaults to 1. Ranges from 0 to 1. Use temp closer to 0 for analytical / multiple choice, and closer to 1 for creative and generative tasks.",
"configProperty": "actionConfiguration.formData.temperature",
"controlType": "INPUT_TEXT",
"dataType": "NUMBER",
"initialValue": "1",
"isRequired": false,
"customStyles": {
"width": "270px",
"minWidth": "270px"
}
}
]
}

View File

@ -3,8 +3,6 @@ package com.external.plugins;
import com.appsmith.external.models.ApiKeyAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.TriggerRequestDTO;
import com.appsmith.external.models.TriggerResultDTO;
import com.appsmith.external.services.SharedConfig;
import com.external.plugins.constants.AnthropicErrorMessages;
import mockwebserver3.MockResponse;
@ -17,14 +15,8 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static com.external.plugins.constants.AnthropicConstants.CHAT_MODELS;
import static com.external.plugins.constants.AnthropicConstants.LABEL;
import static com.external.plugins.constants.AnthropicConstants.VALUE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -126,35 +118,4 @@ public class AnthropicPluginTest {
})
.verifyComplete();
}
@Test
public void verifyDatasourceTriggerResultsForChatModels() {
ApiKeyAuth apiKeyAuth = new ApiKeyAuth();
apiKeyAuth.setValue("apiKey");
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
datasourceConfiguration.setAuthentication(apiKeyAuth);
String responseBody = "[\"claude-2.1\",\"claude-2\",\"claude-instant-1.2\",\"claude-instant-1\"]";
MockResponse mockResponse = new MockResponse().setBody(responseBody);
mockResponse.setResponseCode(200);
mockEndpoint.enqueue(mockResponse);
TriggerRequestDTO request = new TriggerRequestDTO();
request.setRequestType(CHAT_MODELS);
Mono<TriggerResultDTO> datasourceTriggerResultMono =
pluginExecutor.trigger(null, datasourceConfiguration, request);
StepVerifier.create(datasourceTriggerResultMono)
.assertNext(result -> {
assertTrue(result.getTrigger() instanceof List<?>);
assertEquals(((List) result.getTrigger()).size(), 4);
assertEquals(
result.getTrigger(),
getDataToMap(List.of("claude-2.1", "claude-2", "claude-instant-1.2", "claude-instant-1")));
})
.verifyComplete();
}
private List<Map<String, String>> getDataToMap(List<String> data) {
return data.stream().sorted().map(x -> Map.of(LABEL, x, VALUE, x)).collect(Collectors.toList());
}
}

View File

@ -3,6 +3,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.external.plugins.commands.ChatCommand;
import com.external.plugins.models.AnthropicRequestDTO;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.net.URI;
@ -32,10 +33,11 @@ public class ChatCommandTest {
ChatCommand command = new ChatCommand();
URI uri = command.createExecutionUri();
assertEquals("/v1/complete", uri.getPath());
assertEquals("/v1/messages", uri.getPath());
}
@Test
@Disabled
public void testMakeRequestBody_withValidData() {
// Test with valid form data

View File

@ -0,0 +1,101 @@
package com.appsmith.server.migrations.db.ce;
import com.appsmith.external.constants.PluginConstants;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.Plugin;
import io.mongock.api.annotations.ChangeUnit;
import io.mongock.api.annotations.Execution;
import io.mongock.api.annotations.RollbackExecution;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.util.StringUtils;
import java.util.Map;
import static com.appsmith.server.constants.ce.FieldNameCE.PACKAGE_NAME;
import static com.appsmith.server.constants.ce.FieldNameCE.PLUGIN_ID;
/**
* This migration moves the Anthropic legacy models to next versions models in Anthropic plugin queries.
*/
@Slf4j
@ChangeUnit(order = "050", id = "move-anthropic-legacy-models", author = "")
public class Migration050MoveAnthropicLegacyModelsInQueries {
private final MongoTemplate mongoTemplate;
public static final String CHAT_MODEL = "chatModel";
private static final Map<String, String> LEGACY_TO_NEXT_MODELS = Map.of(
"claude-instant-1", "claude-instant-1.2",
"claude-2", "claude-2.1");
public Migration050MoveAnthropicLegacyModelsInQueries(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
@RollbackExecution
public void rollbackExecution() {}
/**
* Steps:
* 1. Find Anthropic plugin id
* 2. Find all queries with Anthropic plugin id
* 3. For each query, find the action configuration and change models to next version models if using legacy models
* 4. Change the action configuration in the query to use User/Assistant instead of Human/Assistant in messages
* 5. Save the updated query
*/
@Execution
public void moveAnthropicLegacyModelsInQueries() {
log.info("Migrating Anthropic legacy models in queries to next version models");
Query anthropicPluginQuery = new Query();
anthropicPluginQuery.addCriteria(Criteria.where(PACKAGE_NAME).is(PluginConstants.PackageName.ANTHROPIC_PLUGIN));
Plugin plugin = mongoTemplate.findOne(anthropicPluginQuery, Plugin.class);
if (plugin == null) {
log.error("Anthropic plugin not found");
return;
}
String pluginId = plugin.getId();
mongoTemplate.stream(Query.query(Criteria.where(PLUGIN_ID).is(pluginId)), NewAction.class)
.forEach(this::updateAction);
}
private void updateAction(NewAction action) {
if (action.getUnpublishedAction() != null) {
ActionConfiguration unpublishedActionConfiguration =
action.getUnpublishedAction().getActionConfiguration();
if (unpublishedActionConfiguration != null && unpublishedActionConfiguration.getFormData() != null) {
action.getUnpublishedAction()
.setActionConfiguration(updateActionConfiguration(unpublishedActionConfiguration));
}
}
if (action.getPublishedAction() != null) {
ActionConfiguration publishedActionConfiguration =
action.getPublishedAction().getActionConfiguration();
if (publishedActionConfiguration != null && publishedActionConfiguration.getFormData() != null) {
action.getPublishedAction()
.setActionConfiguration(updateActionConfiguration(publishedActionConfiguration));
}
}
mongoTemplate.save(action);
}
private ActionConfiguration updateActionConfiguration(ActionConfiguration actionConfiguration) {
Map<String, Object> formData = actionConfiguration.getFormData();
if (formData == null || formData.isEmpty()) {
return actionConfiguration;
}
if (formData.containsKey(CHAT_MODEL)) {
Map<String, String> chatModelData = (Map<String, String>) formData.get(CHAT_MODEL);
if (chatModelData == null) {
return actionConfiguration;
}
String chatModel = chatModelData.get("data");
if (StringUtils.hasText(chatModel) && LEGACY_TO_NEXT_MODELS.containsKey(chatModel)) {
chatModelData.put("data", LEGACY_TO_NEXT_MODELS.get(chatModel));
}
}
actionConfiguration.setFormData(formData);
return actionConfiguration;
}
}