From bc3d46d8c147a8a4dc42501d6510fa34125e7515 Mon Sep 17 00:00:00 2001 From: Nirmal Sarswat <25587962+vivonk@users.noreply.github.com> Date: Tue, 16 Apr 2024 19:11:28 +0530 Subject: [PATCH] feat: Vision models support in Anthropic (#32103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Adding new vision models support in Anthropic Screenshot 2024-04-12 at 15 59 06 Fixes https://github.com/appsmithorg/appsmith-ee/issues/3681 ## Automation /ok-to-test tags="@tag.Datasources" ### :mag: Cypress test results > [!CAUTION] > 🔴 🔴 🔴 Some tests have failed. > Workflow run: > Commit: 2c393e7ffaf3d08fd8e945761206de76d3a13845 > Cypress dashboard: Click here! > The following are new failures, please fix them before merging the PR:
    >
  1. cypress/e2e/Regression/ServerSide/GenerateCRUD/MySQL2_Spec.ts
> To know the list of identified flaky tests - Refer here ## 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. --- .../com/external/plugins/AnthropicPlugin.java | 53 +++++- .../plugins/commands/ChatCommand.java | 123 ++++-------- .../plugins/commands/VisionCommand.java | 179 ++++++++++++++++++ .../plugins/constants/AnthropicConstants.java | 26 ++- .../plugins/models/AnthropicRequestDTO.java | 14 ++ .../plugins/models/CompletionDTO.java | 18 ++ .../com/external/plugins/models/Message.java | 62 ++++++ .../external/plugins/models/MessageDTO.java | 37 ++++ .../utils/AnthropicMethodStrategy.java | 3 + .../external/plugins/utils/CommandUtils.java | 92 +++++++++ .../external/plugins/utils/RequestUtils.java | 6 +- .../src/main/resources/editor/chat.json | 16 +- .../src/main/resources/editor/root.json | 7 +- .../src/main/resources/editor/vision.json | 127 +++++++++++++ .../external/plugins/AnthropicPluginTest.java | 39 ---- .../com/external/plugins/ChatCommandTest.java | 4 +- ...050MoveAnthropicLegacyModelsInQueries.java | 101 ++++++++++ 17 files changed, 766 insertions(+), 141 deletions(-) create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/VisionCommand.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/CompletionDTO.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/Message.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/MessageDTO.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/CommandUtils.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/vision.json create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration050MoveAnthropicLegacyModelsInQueries.java diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/AnthropicPlugin.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/AnthropicPlugin.java index c256ee336a..7d8559d931 100644 --- a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/AnthropicPlugin.java +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/AnthropicPlugin.java @@ -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 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> getDataToMap(List 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()); } } } diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/ChatCommand.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/ChatCommand.java index 5349d68f1a..915a8095cd 100644 --- a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/ChatCommand.java +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/ChatCommand.java @@ -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 formData) { - StringBuilder stringBuilder = new StringBuilder(); - if (formData.containsKey(MESSAGES)) { - List> messageMaps = getMessages((Map) formData.get(MESSAGES)); - if (messageMaps == null) { - throw new AppsmithPluginException( - AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, - "messages are not provided in the configuration correctly"); - } - for (Map 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 createMessages(Map 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> getMessages(Map messages) { - Type listType = new TypeToken>>() {}.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>) messages.get(DATA); - } - - private int getMaxTokenFromFormData(Map 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> messageMaps = getMessages((Map) 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 messages = new ArrayList<>(); + for (Map 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 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; } } diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/VisionCommand.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/VisionCommand.java new file mode 100644 index 0000000000..91200f408d --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/VisionCommand.java @@ -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 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 createMessages(Map formData) { + if (!formData.containsKey(MESSAGES)) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, + "messages are not provided in the configuration"); + } + List> messageMaps = getMessages((Map) formData.get(MESSAGES)); + if (messageMaps == null) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, + "messages are not provided in the configuration correctly"); + } + List messages = new ArrayList<>(); + for (Map 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 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 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]; + } +} diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/constants/AnthropicConstants.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/constants/AnthropicConstants.java index c8f8ec4ca7..f8fb392b09 100644 --- a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/constants/AnthropicConstants.java +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/constants/AnthropicConstants.java @@ -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 ANTHROPIC_MODELS = - List.of("claude-2.1", "claude-2", "claude-instant-1.2", "claude-instant-1"); + public static final Map> 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)) diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/AnthropicRequestDTO.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/AnthropicRequestDTO.java index 876fa20387..0ea3e4760b 100644 --- a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/AnthropicRequestDTO.java +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/AnthropicRequestDTO.java @@ -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 messages; Float temperature; } diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/CompletionDTO.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/CompletionDTO.java new file mode 100644 index 0000000000..634f21af24 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/CompletionDTO.java @@ -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; +} diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/Message.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/Message.java new file mode 100644 index 0000000000..9cb58a8c92 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/Message.java @@ -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; + + @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; + } +} diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/MessageDTO.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/MessageDTO.java new file mode 100644 index 0000000000..4443ab076e --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/MessageDTO.java @@ -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 content; + private String id; + private String model; + private String role; + private String stopReason; + private Integer stopSequence; + private String type; + private Map 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(); + } +} diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/AnthropicMethodStrategy.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/AnthropicMethodStrategy.java index 70fad45639..df204780b7 100644 --- a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/AnthropicMethodStrategy.java +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/AnthropicMethodStrategy.java @@ -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)); }; diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/CommandUtils.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/CommandUtils.java new file mode 100644 index 0000000000..1fba40679f --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/CommandUtils.java @@ -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> getMessages(Map messages) { + Type listType = new TypeToken>>() {}.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>) messages.get(DATA); + } + + public static int getMaxTokenFromFormData(Map 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 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; + }; + } +} diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java index 0822862e07..772673790e 100644 --- a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java @@ -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 { diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/chat.json b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/chat.json index 9fb894dc33..f182d8a125 100644 --- a/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/chat.json +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/chat.json @@ -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", diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/root.json b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/root.json index 6a2a8fb00a..a5c1b0abaf 100644 --- a/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/root.json +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/root.json @@ -15,6 +15,10 @@ { "label": "Chat", "value": "CHAT" + }, + { + "label": "Vision", + "value": "VISION" } ] } @@ -22,6 +26,7 @@ } ], "files": [ - "chat.json" + "chat.json", + "vision.json" ] } diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/vision.json b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/vision.json new file mode 100644 index 0000000000..fcb9f75af2 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/vision.json @@ -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" + } + } + ] +} diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/AnthropicPluginTest.java b/app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/AnthropicPluginTest.java index b29f2b4b37..3e5caa79c5 100644 --- a/app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/AnthropicPluginTest.java +++ b/app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/AnthropicPluginTest.java @@ -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 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> getDataToMap(List data) { - return data.stream().sorted().map(x -> Map.of(LABEL, x, VALUE, x)).collect(Collectors.toList()); - } } diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/ChatCommandTest.java b/app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/ChatCommandTest.java index c39e4bc8df..f0c97bd396 100644 --- a/app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/ChatCommandTest.java +++ b/app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/ChatCommandTest.java @@ -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 diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration050MoveAnthropicLegacyModelsInQueries.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration050MoveAnthropicLegacyModelsInQueries.java new file mode 100644 index 0000000000..bcbcfa7466 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration050MoveAnthropicLegacyModelsInQueries.java @@ -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 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 formData = actionConfiguration.getFormData(); + if (formData == null || formData.isEmpty()) { + return actionConfiguration; + } + if (formData.containsKey(CHAT_MODEL)) { + Map chatModelData = (Map) 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; + } +}