From b03c51e05d050ec902575aab3d975bdd8d3dabad Mon Sep 17 00:00:00 2001 From: Nirmal Sarswat <25587962+vivonk@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:03:27 +0530 Subject: [PATCH] feat: Anthropic AI Plugin (#29095) ## Description Anthropic AI plugin - provides chat completion API support as a datasource plugin #### PR fixes following issue(s) Fixes https://github.com/appsmithorg/appsmith/issues/29036 #### Type of change - New feature (non-breaking change which adds functionality) ## Testing #### How Has This Been Tested? - [x] Manual - [x] JUnit - [ ] Jest - [ ] Cypress ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed ## Summary by CodeRabbit - **New Features** - Introduced a new Anthropic plugin with capabilities for testing data sources, executing actions, and validating data source configurations. - Added a search bar component to enhance user navigation and interaction within the application. - **Enhancements** - Improved form control elements, including dropdown and field array controls, for better user experience and interface consistency. - Optimized HTTP request handling in the OpenAI plugin for increased efficiency and performance. - **Bug Fixes** - Addressed issues with form control properties to ensure correct behavior and data handling. - **Documentation** - Updated plugin properties and test documentation to reflect new features and changes. - **Refactor** - Refactored various components and utilities for code clarity and maintainability. - **Tests** - Added comprehensive tests for the new Anthropic plugin to ensure reliability and functionality. --------- Co-authored-by: Diljit VJ --- .../formControls/DropDownControl.tsx | 12 +- .../formControls/FieldArrayControl.tsx | 85 +++--- .../Editor/QueryEditor/QueryResponseView.tsx | 6 +- .../external/constants/PluginConstants.java | 2 + .../appsmith-plugins/anthropicPlugin/pom.xml | 108 +++++++ .../com/external/plugins/AnthropicPlugin.java | 274 ++++++++++++++++++ .../plugins/commands/AnthropicCommand.java | 19 ++ .../plugins/commands/ChatCommand.java | 193 ++++++++++++ .../plugins/constants/AnthropicConstants.java | 44 +++ .../constants/AnthropicErrorMessages.java | 19 ++ .../plugins/models/AnthropicRequestDTO.java | 16 + .../com/external/plugins/models/Role.java | 6 + .../utils/AnthropicMethodStrategy.java | 47 +++ .../external/plugins/utils/RequestUtils.java | 99 +++++++ .../src/main/resources/editor/chat.json | 101 +++++++ .../src/main/resources/editor/root.json | 27 ++ .../src/main/resources/form.json | 43 +++ .../src/main/resources/plugin.properties | 5 + .../src/main/resources/setting.json | 30 ++ .../external/plugins/AnthropicPluginTest.java | 160 ++++++++++ .../com/external/plugins/ChatCommandTest.java | 61 ++++ .../appsmith-plugins/openAiPlugin/pom.xml | 5 + .../com/external/plugins/OpenAiPlugin.java | 83 ++++-- .../external/plugins/utils/RequestUtils.java | 32 +- .../src/main/resources/editor/chat.json | 29 +- .../src/main/resources/editor/embeddings.json | 3 +- .../src/main/resources/editor/vision.json | 40 ++- app/server/appsmith-plugins/pom.xml | 1 + .../db/ce/Migration035AddAnthropicPlugin.java | 50 ++++ 29 files changed, 1511 insertions(+), 89 deletions(-) create mode 100644 app/server/appsmith-plugins/anthropicPlugin/pom.xml create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/AnthropicPlugin.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/AnthropicCommand.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/ChatCommand.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/constants/AnthropicConstants.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/constants/AnthropicErrorMessages.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/AnthropicRequestDTO.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/Role.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/AnthropicMethodStrategy.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/chat.json create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/root.json create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/resources/form.json create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/resources/plugin.properties create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/main/resources/setting.json create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/AnthropicPluginTest.java create mode 100644 app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/ChatCommandTest.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration035AddAnthropicPlugin.java diff --git a/app/client/src/components/formControls/DropDownControl.tsx b/app/client/src/components/formControls/DropDownControl.tsx index c32ca384d3..e46ae75182 100644 --- a/app/client/src/components/formControls/DropDownControl.tsx +++ b/app/client/src/components/formControls/DropDownControl.tsx @@ -3,7 +3,7 @@ import type { ControlProps } from "./BaseControl"; import BaseControl from "./BaseControl"; import styled from "styled-components"; import type { ControlType } from "constants/PropertyControlConstants"; -import { get, isNil } from "lodash"; +import { get, isEmpty, isNil } from "lodash"; import type { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form"; import { Field } from "redux-form"; import { connect } from "react-redux"; @@ -119,13 +119,18 @@ function renderDropdown( } & DropDownControlProps, ): JSX.Element { let selectedValue: string | string[]; - if (isNil(props.input?.value)) { + if (isEmpty(props.input?.value)) { if (props.isMultiSelect) selectedValue = props?.initialValue ? (props.initialValue as string) : []; - else + else { selectedValue = props?.initialValue ? (props.initialValue as string[]) : ""; + if (props.setFirstOptionAsDefault && props.options.length > 0) { + selectedValue = props.options[0].value as string; + props.input?.onChange(selectedValue); + } + } } else { selectedValue = props.input?.value; if (props.isMultiSelect) { @@ -267,6 +272,7 @@ export interface DropDownControlProps extends ControlProps { fetchOptionsConditionally?: boolean; isLoading: boolean; formValues: Partial; + setFirstOptionAsDefault?: boolean; } interface ReduxDispatchProps { diff --git a/app/client/src/components/formControls/FieldArrayControl.tsx b/app/client/src/components/formControls/FieldArrayControl.tsx index 6495d03b33..85b5bfd0de 100644 --- a/app/client/src/components/formControls/FieldArrayControl.tsx +++ b/app/client/src/components/formControls/FieldArrayControl.tsx @@ -1,25 +1,28 @@ -import React, { useEffect } from "react"; +import React, { useCallback } from "react"; import FormControl from "pages/Editor/FormControl"; -import { Classes, Text, TextType } from "design-system-old"; import styled from "styled-components"; import { FieldArray } from "redux-form"; import type { ControlProps } from "./BaseControl"; import { Button } from "design-system"; -const CenteredIcon = styled(Button)` - margin-top: 26px; - &.hide { - opacity: 0; - pointer-events: none; - } +const CenteredIconButton = styled(Button)<{ + alignSelf?: string; + top?: string; +}>` + position: relative; + align-self: ${(props) => (props.alignSelf ? props.alignSelf : "center")}; + top: ${(props) => (props.top ? props.top : "0px")}; `; const PrimaryBox = styled.div` display: flex; width: min-content; flex-direction: column; - border: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg}; - padding: 10px; + padding: 10px 0px 0px 0px; + + > div:not(:first-child) .form-config-top { + display: none; + } `; const SecondaryBox = styled.div` @@ -28,11 +31,10 @@ const SecondaryBox = styled.div` width: min-content; align-items: center; justify-content: space-between; - padding: 5px; & > div { margin-right: 8px; - height: 60px; + margin-bottom: 8px; } & > .t--form-control-QUERY_DYNAMIC_INPUT_TEXT > div { @@ -47,21 +49,22 @@ const SecondaryBox = styled.div` `; const AddMoreAction = styled.div` - width: fit-content; cursor: pointer; - display: flex; - margin-top: 16px; - .${Classes.TEXT} { - margin-left: 8px; - color: #03b365; - } + width: max-content; `; + function NestedComponents(props: any) { - useEffect(() => { - if (props.fields.length < 1) { - props.fields.push({}); - } - }, [props.fields.length]); + const addMore = useCallback(() => { + const { schema = {} } = props; + const newObject: any = {}; + + schema.forEach((s: any) => { + newObject[s.key] = s.initialValue || ""; + }); + + props.fields.push(newObject); + }, [props.fields]); + return ( {props.fields && @@ -75,7 +78,7 @@ function NestedComponents(props: any) { configProperty: `${field}.${sch.key}`, customStyles: { width: "20vw", - ...props.customStyles, + ...(props.customStyles ?? {}), }, }; return ( @@ -86,22 +89,31 @@ function NestedComponents(props: any) { /> ); })} - { e.stopPropagation(); props.fields.remove(index); }} - size="sm" - startIcon="delete" + size="md" + startIcon="close" + top={index === 0 ? "20px" : ""} /> ); })} - props.fields.push({})}> - {/*Hardcoded label to be removed */} - {props.addMoreButtonLabel} + + ); @@ -111,6 +123,7 @@ export default function FieldArrayControl(props: FieldArrayControlProps) { const { addMoreButtonLabel = "+ Add Condition (And)", configProperty, + customStyles = {}, formName, schema, } = props; @@ -118,7 +131,13 @@ export default function FieldArrayControl(props: FieldArrayControlProps) { ); diff --git a/app/client/src/pages/Editor/QueryEditor/QueryResponseView.tsx b/app/client/src/pages/Editor/QueryEditor/QueryResponseView.tsx index bf4f687f71..f7403f204d 100644 --- a/app/client/src/pages/Editor/QueryEditor/QueryResponseView.tsx +++ b/app/client/src/pages/Editor/QueryEditor/QueryResponseView.tsx @@ -238,10 +238,10 @@ function QueryResponseView({ (actionResponse.pluginErrorDetails ? ( <>
- { + {actionResponse.pluginErrorDetails + .downstreamErrorMessage || actionResponse.pluginErrorDetails - .downstreamErrorMessage - } + .appsmithErrorMessage}
{actionResponse.pluginErrorDetails .downstreamErrorCode && ( diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java index ce819d88d4..7d337ffc79 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java @@ -13,6 +13,7 @@ public interface PluginConstants { String REST_API_PLUGIN = "restapi-plugin"; String GRAPH_QL_PLUGIN = "graphql-plugin"; String OPEN_AI_PLUGIN = "openai-plugin"; + String ANTHROPIC_PLUGIN = "anthropic-plugin"; } public static final String DEFAULT_REST_DATASOURCE = "DEFAULT_REST_DATASOURCE"; @@ -37,6 +38,7 @@ public interface PluginConstants { public static final String SNOWFLAKE_PLUGIN_NAME = "Snowflake"; public static final String OPEN_AI_PLUGIN_NAME = "Open AI"; + public static final String ANTHROPIC_PLUGIN_NAME = "Anthropic"; } interface HostName { diff --git a/app/server/appsmith-plugins/anthropicPlugin/pom.xml b/app/server/appsmith-plugins/anthropicPlugin/pom.xml new file mode 100644 index 0000000000..5a863b84f7 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + com.appsmith + appsmith-plugins + 1.0-SNAPSHOT + + + com.external.plugins + anthropicPlugin + 1.0-SNAPSHOT + anthropicPlugin + http://maven.apache.org + + + + + org.springframework + spring-core + provided + + + + org.springframework + spring-web + provided + + + + org.springframework + spring-webflux + + + io.projectreactor + reactor-core + + + org.springframework + spring-core + + + org.springframework + spring-web + + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-bom.version} + provided + + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson-bom.version} + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson-bom.version} + + + com.google.guava + guava + 32.0.1-jre + + + + + + + org.assertj + assertj-core + 3.13.2 + test + + + + org.springframework.boot + spring-boot-starter-webflux + ${spring-boot.version} + test + + + org.springframework + spring-test + test + + + io.projectreactor.netty + reactor-netty-http + provided + + + + + 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 new file mode 100644 index 0000000000..c256ee336a --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/AnthropicPlugin.java @@ -0,0 +1,274 @@ +package com.external.plugins; + +import com.appsmith.external.dtos.ExecuteActionDTO; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.helpers.restApiUtils.connections.APIConnection; +import com.appsmith.external.helpers.restApiUtils.helpers.RequestCaptureFilter; +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionRequest; +import com.appsmith.external.models.ActionExecutionResult; +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.plugins.BasePlugin; +import com.appsmith.external.plugins.BaseRestApiPluginExecutor; +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.utils.AnthropicMethodStrategy; +import com.external.plugins.utils.RequestUtils; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.gson.Gson; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONArray; +import org.json.JSONObject; +import org.pf4j.PluginWrapper; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.BodyInserters; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +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.LABEL; +import static com.external.plugins.constants.AnthropicConstants.TEST_MODEL; +import static com.external.plugins.constants.AnthropicConstants.TEST_PROMPT; +import static com.external.plugins.constants.AnthropicConstants.VALUE; +import static com.external.plugins.constants.AnthropicErrorMessages.EMPTY_API_KEY; +import static com.external.plugins.constants.AnthropicErrorMessages.INVALID_API_KEY; +import static com.external.plugins.constants.AnthropicErrorMessages.QUERY_FAILED_TO_EXECUTE; + +@Slf4j +public class AnthropicPlugin extends BasePlugin { + public AnthropicPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + public static class AnthropicPluginExecutor extends BaseRestApiPluginExecutor { + private static final Gson gson = new Gson(); + private static final Cache triggerResponseCache = + CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build(); + + protected AnthropicPluginExecutor(SharedConfig sharedConfig) { + super(sharedConfig); + } + + @Override + public Mono testDatasource(DatasourceConfiguration datasourceConfiguration) { + final ApiKeyAuth apiKeyAuth = (ApiKeyAuth) datasourceConfiguration.getAuthentication(); + if (!StringUtils.hasText(apiKeyAuth.getValue())) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, EMPTY_API_KEY)); + } + + AnthropicCommand anthropicCommand = AnthropicMethodStrategy.selectExecutionMethod(AnthropicConstants.CHAT); + URI uri = anthropicCommand.createExecutionUri(); + HttpMethod httpMethod = anthropicCommand.getExecutionMethod(); + + AnthropicRequestDTO anthropicRequestDTO = new AnthropicRequestDTO(); + anthropicRequestDTO.setPrompt(TEST_PROMPT); + anthropicRequestDTO.setModel(TEST_MODEL); + anthropicRequestDTO.setMaxTokensToSample(1); + anthropicRequestDTO.setTemperature(0f); + + return RequestUtils.makeRequest(httpMethod, uri, apiKeyAuth, BodyInserters.fromValue(anthropicRequestDTO)) + .map(responseEntity -> { + HttpStatusCode statusCode = responseEntity.getStatusCode(); + if (HttpStatusCode.valueOf(401).isSameCodeAs(statusCode)) { + // invalid credentials + return new DatasourceTestResult(INVALID_API_KEY); + } + + return new DatasourceTestResult(); + }) + .onErrorResume(error -> Mono.just(new DatasourceTestResult( + "Error while trying to test the datasource configurations" + error.getMessage()))); + } + + @Override + public Mono executeParameterized( + APIConnection connection, + ExecuteActionDTO executeActionDTO, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + // Get prompt from action configuration + List> parameters = new ArrayList<>(); + + prepareConfigurationsForExecution(executeActionDTO, actionConfiguration, datasourceConfiguration); + // Filter out any empty headers + headerUtils.removeEmptyHeaders(actionConfiguration); + headerUtils.setHeaderFromAutoGeneratedHeaders(actionConfiguration); + + // Initializing object for error condition + ActionExecutionResult errorResult = new ActionExecutionResult(); + initUtils.initializeResponseWithError(errorResult); + + AnthropicCommand anthropicCommand = + AnthropicMethodStrategy.selectExecutionMethod(actionConfiguration, gson); + AnthropicRequestDTO anthropicRequestDTO = anthropicCommand.makeRequestBody(actionConfiguration); + + URI uri = anthropicCommand.createExecutionUri(); + HttpMethod httpMethod = anthropicCommand.getExecutionMethod(); + ActionExecutionRequest actionExecutionRequest = + RequestCaptureFilter.populateRequestFields(actionConfiguration, uri, parameters, objectMapper); + + final ApiKeyAuth apiKeyAuth = (ApiKeyAuth) datasourceConfiguration.getAuthentication(); + + if (!StringUtils.hasText(apiKeyAuth.getValue())) { + ActionExecutionResult apiKeyNotPresentErrorResult = new ActionExecutionResult(); + apiKeyNotPresentErrorResult.setIsExecutionSuccess(false); + apiKeyNotPresentErrorResult.setErrorInfo(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, EMPTY_API_KEY)); + return Mono.just(apiKeyNotPresentErrorResult); + } + + return RequestUtils.makeRequest(httpMethod, uri, apiKeyAuth, BodyInserters.fromValue(anthropicRequestDTO)) + .flatMap(responseEntity -> { + HttpStatusCode statusCode = responseEntity.getStatusCode(); + + ActionExecutionResult actionExecutionResult = new ActionExecutionResult(); + actionExecutionResult.setRequest(actionExecutionRequest); + actionExecutionResult.setStatusCode(statusCode.toString()); + + if (HttpStatusCode.valueOf(401).isSameCodeAs(statusCode)) { + actionExecutionResult.setIsExecutionSuccess(false); + String errorMessage = ""; + if (responseEntity.getBody() != null && responseEntity.getBody().length > 0) { + errorMessage = new String(responseEntity.getBody()); + } + actionExecutionResult.setErrorInfo(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_AUTHENTICATION_ERROR, errorMessage)); + return Mono.just(actionExecutionResult); + } + + if (statusCode.is4xxClientError()) { + actionExecutionResult.setIsExecutionSuccess(false); + String errorMessage = ""; + if (responseEntity.getBody() != null && responseEntity.getBody().length > 0) { + errorMessage = new String(responseEntity.getBody()); + } + actionExecutionResult.setErrorInfo(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_DATASOURCE_ERROR, errorMessage)); + + return Mono.just(actionExecutionResult); + } + + Object body; + try { + body = objectMapper.readValue(responseEntity.getBody(), Object.class); + actionExecutionResult.setBody(body); + } catch (IOException ex) { + actionExecutionResult.setIsExecutionSuccess(false); + actionExecutionResult.setErrorInfo(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_JSON_PARSE_ERROR, BODY, ex.getMessage())); + return Mono.just(actionExecutionResult); + } + + if (!statusCode.is2xxSuccessful()) { + actionExecutionResult.setIsExecutionSuccess(false); + actionExecutionResult.setErrorInfo(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, QUERY_FAILED_TO_EXECUTE, body)); + return Mono.just(actionExecutionResult); + } + + actionExecutionResult.setIsExecutionSuccess(true); + + return Mono.just(actionExecutionResult); + }) + .onErrorResume(error -> { + errorResult.setIsExecutionSuccess(false); + log.debug( + "An error has occurred while trying to run the anthropic API query command with error {}", + error.getStackTrace()); + if (!(error instanceof AppsmithPluginException)) { + error = new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, error.getMessage(), error); + } + errorResult.setErrorInfo(error); + return Mono.just(errorResult); + }); + } + + @Override + public Mono trigger( + APIConnection connection, DatasourceConfiguration datasourceConfiguration, TriggerRequestDTO request) { + final ApiKeyAuth apiKeyAuth = (ApiKeyAuth) datasourceConfiguration.getAuthentication(); + if (!StringUtils.hasText(apiKeyAuth.getValue())) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, EMPTY_API_KEY)); + } + if (!StringUtils.hasText(request.getRequestType())) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "request type is missing"); + } + String requestType = request.getRequestType(); + + AnthropicCommand anthropicCommand = AnthropicMethodStrategy.selectTriggerMethod(request, gson); + HttpMethod httpMethod = anthropicCommand.getTriggerHTTPMethod(); + URI uri = anthropicCommand.createTriggerUri(); + + TriggerResultDTO triggerResultDTO = triggerResponseCache.getIfPresent(requestType); + if (triggerResultDTO != null) { + return Mono.just(triggerResultDTO); + } + return RequestUtils.makeRequest(httpMethod, uri, apiKeyAuth, BodyInserters.empty()) + .flatMap(responseEntity -> { + if (responseEntity.getStatusCode().is4xxClientError()) { + return Mono.error( + new AppsmithPluginException(AppsmithPluginError.PLUGIN_AUTHENTICATION_ERROR)); + } + + if (!responseEntity.getStatusCode().is2xxSuccessful()) { + return Mono.error( + new AppsmithPluginException(AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR)); + } + + // link to get response data https://platform.openai.com/docs/api-reference/models/list + return Mono.just(new JSONObject(new String(responseEntity.getBody()))); + }) + .map(jsonObject -> { + JSONArray jsonArray = jsonObject.getJSONArray("data"); + int len = jsonArray.length(); + List models = new ArrayList<>(); + for (int i = 0; i < len; i++) { + models.add(jsonArray.getString(i)); + } + return getDataToMap(models); + }) + .onErrorResume(error -> { + log.debug("Error while fetching Anthropic models list", error); + return Mono.just(getDataToMap(ANTHROPIC_MODELS)); + }) + .map(trigger -> { + TriggerResultDTO triggerResult = new TriggerResultDTO(trigger); + // saving response on request type + triggerResponseCache.put(requestType, triggerResult); + return triggerResult; + }); + } + + @Override + public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) { + return RequestUtils.validateApiKeyAuthDatasource(datasourceConfiguration); + } + + 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/main/java/com/external/plugins/commands/AnthropicCommand.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/AnthropicCommand.java new file mode 100644 index 0000000000..a3d10accf1 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/AnthropicCommand.java @@ -0,0 +1,19 @@ +package com.external.plugins.commands; + +import com.appsmith.external.models.ActionConfiguration; +import com.external.plugins.models.AnthropicRequestDTO; +import org.springframework.http.HttpMethod; + +import java.net.URI; + +public interface AnthropicCommand { + HttpMethod getTriggerHTTPMethod(); + + HttpMethod getExecutionMethod(); + + URI createTriggerUri(); + + URI createExecutionUri(); + + AnthropicRequestDTO makeRequestBody(ActionConfiguration actionConfiguration); +} 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 new file mode 100644 index 0000000000..8b78c1a96d --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/commands/ChatCommand.java @@ -0,0 +1,193 @@ +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.constants.AnthropicConstants; +import com.external.plugins.models.AnthropicRequestDTO; +import com.external.plugins.models.Role; +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.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.CLOUD_SERVICES; +import static com.external.plugins.constants.AnthropicConstants.COMMAND; +import static com.external.plugins.constants.AnthropicConstants.COMPONENT; +import static com.external.plugins.constants.AnthropicConstants.COMPONENT_DATA; +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.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; + +public class ChatCommand 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, CHAT.toLowerCase()) + .build() + .toUri(); + } + + @Override + public URI createExecutionUri() { + return RequestUtils.createUriFromCommand(AnthropicConstants.CHAT); + } + + @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, CHAT_MODEL_SELECTOR); + + if (!StringUtils.hasText(model)) { + throw new AppsmithPluginException( + 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)); + 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 { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, + "messages are not provided in the configuration"); + } + } + + private List> getMessages(Map messages) { + Type listType = new TypeToken>>() {}.getType(); + if (messages.containsKey(VIEW_TYPE)) { + if (JSON.equals(messages.get(VIEW_TYPE))) { + // data is present in data key as String + return gson.fromJson((String) messages.get(DATA), listType); + } else if (COMPONENT.equals(messages.get(VIEW_TYPE))) { + return (List>) messages.get(COMPONENT_DATA); + } + } + // return object stored in data key + return (List>) messages.get(DATA); + } + + /** + * Finds right data key from formData.messages. If viewType is present and it's json, then use `componentData`key + * else use `data` key to find right messages. + */ + private String findDataKey(Map messages) { + if (messages.containsKey(VIEW_TYPE) && "json".equals(messages.get(VIEW_TYPE))) { + return COMPONENT_DATA; + } + return 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) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + String.format(STRING_APPENDER, EXECUTION_FAILURE, BAD_MAX_TOKEN_CONFIGURATION)); + } + } + + private 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)); + } + } +} 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 new file mode 100644 index 0000000000..c8f8ec4ca7 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/constants/AnthropicConstants.java @@ -0,0 +1,44 @@ +package com.external.plugins.constants; + +import org.springframework.web.reactive.function.client.ExchangeStrategies; + +import java.util.List; + +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 CHAT_MODELS = "CHAT_MODELS"; + public static final String CHAT = "CHAT"; + public static final String COMMAND = "command"; + public static final String DATA = "data"; + 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 CONTENT = "content"; + public static final String MODEL = "model"; + public static final String CHAT_MODEL_SELECTOR = "chatModel"; + public static final String MESSAGES = "messages"; + public static final String TEMPERATURE = "temperature"; + public static final String MAX_TOKENS = "maxTokens"; + public static final Integer DEFAULT_MAX_TOKEN = 256; + public static final Float DEFAULT_TEMPERATURE = 1.0f; + public static final String LABEL = "label"; + public static final String VALUE = "value"; + 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 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_PROMPT = "Human:Hey Assistant:"; + public static final ExchangeStrategies EXCHANGE_STRATEGIES = ExchangeStrategies.builder() + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(/* 10MB */ 10 * 1024 * 1024)) + .build(); + public static final String JSON = "json"; + public static final String COMPONENT = "component"; +} diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/constants/AnthropicErrorMessages.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/constants/AnthropicErrorMessages.java new file mode 100644 index 0000000000..4216107b77 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/constants/AnthropicErrorMessages.java @@ -0,0 +1,19 @@ +package com.external.plugins.constants; + +public class AnthropicErrorMessages { + public static final String STRING_APPENDER = "%s %s"; + public static final String EXECUTION_FAILURE = "Query failed to execute because"; + public static final String QUERY_FAILED_TO_EXECUTE = "Your query failed to execute"; + public static final String MODEL_NOT_SELECTED = "model hasn't been selected. Please select a model"; + public static final String QUERY_NOT_CONFIGURED = "query is not configured."; + public static final String BAD_TEMPERATURE_CONFIGURATION = + "temperature value doesn't conform to the contract. Please enter a real number between 0 and 2"; + public static final String BAD_MAX_TOKEN_CONFIGURATION = "max token value is not an integer number"; + public static final String INCORRECT_ROLE_VALUE = + "role value is incorrect. Please choose a role value which conforms to the OpenAI contract"; + public static final String INCORRECT_MESSAGE_FORMAT = + "messages object is not correctly configured. Please provide a list of messages"; + public static final String EMPTY_API_KEY = "API Key should not be empty, Please add an API Key"; + public static final String INVALID_API_KEY = + "Invalid authentication credentials provided in datasource configurations"; +} 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 new file mode 100644 index 0000000000..876fa20387 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/AnthropicRequestDTO.java @@ -0,0 +1,16 @@ +package com.external.plugins.models; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class AnthropicRequestDTO { + String model; + String prompt; + Integer maxTokensToSample; + Float temperature; +} diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/Role.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/Role.java new file mode 100644 index 0000000000..2c5aa9beb2 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/models/Role.java @@ -0,0 +1,6 @@ +package com.external.plugins.models; + +public enum Role { + Assistant, + Human +} 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 new file mode 100644 index 0000000000..70fad45639 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/AnthropicMethodStrategy.java @@ -0,0 +1,47 @@ +package com.external.plugins.utils; + +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +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.constants.AnthropicConstants; +import com.google.gson.Gson; +import org.springframework.util.CollectionUtils; +import reactor.core.Exceptions; + +import java.util.Map; + +import static com.external.plugins.utils.RequestUtils.extractDataFromFormData; + +public class AnthropicMethodStrategy { + public static AnthropicCommand selectTriggerMethod(TriggerRequestDTO triggerRequestDTO, Gson gson) { + String requestType = triggerRequestDTO.getRequestType(); + + return switch (requestType) { + case AnthropicConstants.CHAT_MODELS -> new ChatCommand(); + default -> throw Exceptions.propagate( + new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR)); + }; + } + + public static AnthropicCommand selectExecutionMethod(ActionConfiguration actionConfiguration, Gson gson) { + Map formData = actionConfiguration.getFormData(); + if (CollectionUtils.isEmpty(formData)) { + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR); + } + + String command = extractDataFromFormData(formData, AnthropicConstants.COMMAND); + + return selectExecutionMethod(command); + } + + public static AnthropicCommand selectExecutionMethod(String command) { + return switch (command) { + case AnthropicConstants.CHAT -> new ChatCommand(); + 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/RequestUtils.java b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java new file mode 100644 index 0000000000..0822862e07 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java @@ -0,0 +1,99 @@ +package com.external.plugins.utils; + +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.models.ApiKeyAuth; +import com.appsmith.external.models.DatasourceConfiguration; +import com.external.plugins.constants.AnthropicConstants; +import com.external.plugins.constants.AnthropicErrorMessages; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; + +import java.net.URI; +import java.time.Duration; +import java.util.HashSet; +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; + +public class RequestUtils { + + private static final WebClient webClient = createWebClient(); + + public static String extractDataFromFormData(Map formData, String key) { + return (String) ((Map) formData.get(key)).get(AnthropicConstants.DATA); + } + + public static String extractValueFromFormData(Map formData, String key) { + return (String) formData.get(key); + } + + public static URI createUriFromCommand(String command) { + if (AnthropicConstants.CHAT.equals(command)) { + return URI.create(ANTHROPIC_API_ENDPOINT + COMPLETION_API); + } else if (AnthropicConstants.MODEL.equals(command)) { + return URI.create(""); + } else { + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR); + } + } + + public static Mono> makeRequest( + HttpMethod httpMethod, URI uri, ApiKeyAuth apiKeyAuth, BodyInserter body) { + + // Authentication will already be valid at this point + assert (apiKeyAuth.getValue() != null); + + return webClient + .method(httpMethod) + .uri(uri) + .contentType(MediaType.APPLICATION_JSON) + .body(body) + .headers(headers -> { + headers.set(AnthropicConstants.API_KEY_HEADER, apiKeyAuth.getValue()); + headers.set(AnthropicConstants.ANTHROPIC_VERSION_HEADER, AnthropicConstants.ANTHROPIC_VERSION); + }) + .exchangeToMono(clientResponse -> clientResponse.toEntity(byte[].class)); + } + + private static WebClient createWebClient() { + // Initializing webClient to be used for http call + WebClient.Builder webClientBuilder = WebClient.builder(); + return webClientBuilder + .exchangeStrategies(AnthropicConstants.EXCHANGE_STRATEGIES) + .clientConnector(new ReactorClientHttpConnector(HttpClient.create(connectionProvider()))) + .build(); + } + + private static ConnectionProvider connectionProvider() { + return ConnectionProvider.builder("anthropic") + .maxConnections(100) + .maxIdleTime(Duration.ofSeconds(60)) + .maxLifeTime(Duration.ofSeconds(60)) + .pendingAcquireTimeout(Duration.ofSeconds(30)) + .evictInBackground(Duration.ofSeconds(120)) + .build(); + } + + public static Set validateApiKeyAuthDatasource(DatasourceConfiguration datasourceConfiguration) { + Set invalids = new HashSet<>(); + final ApiKeyAuth apiKeyAuth = (ApiKeyAuth) datasourceConfiguration.getAuthentication(); + + if (apiKeyAuth == null || !StringUtils.hasText(apiKeyAuth.getValue())) { + invalids.add(AnthropicErrorMessages.EMPTY_API_KEY); + } + + return invalids; + } +} 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 new file mode 100644 index 0000000000..9fb894dc33 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/chat.json @@ -0,0 +1,101 @@ +{ + "identifier": "CHAT", + "controlType": "SECTION", + "conditionals": { + "show": "{{actionConfiguration.formData.command.data === 'CHAT'}}" + }, + "children": [ + { + "label": "Models", + "tooltipText": "Select the model for response generation", + "subtitle": "ID of the model to use.", + "isRequired": true, + "propertyName": "chat_model_id", + "configProperty": "actionConfiguration.formData.chatModel.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 === 'CHAT'}}", + "config": { + "params": { + "requestType": "CHAT_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": "Messages", + "tooltipText": "Ask a question", + "subtitle": "A list of messages comprising the conversation so far.", + "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": "Content", + "key": "content", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "placeholderText": "{{ UserInput.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/main/resources/editor/root.json b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/root.json new file mode 100644 index 0000000000..e7ca5d5674 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/editor/root.json @@ -0,0 +1,27 @@ +{ + "editor": [ + { + "controlType": "SECTION", + "identifier": "SELECTOR", + "children": [ + { + "label": "Commands", + "description": "Choose the method you would like to use", + "configProperty": "actionConfiguration.formData.command.data", + "controlType": "DROP_DOWN", + "isRequired": true, + "initialValue": "CHAT", + "options": [ + { + "label": "Chat", + "value": "CHAT" + } + ] + } + ] + } + ], + "files": [ + "chat.json" + ] +} diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/form.json new file mode 100644 index 0000000000..a577a83d22 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/form.json @@ -0,0 +1,43 @@ +{ + "form": [ + { + "sectionName": "Details", + "id": 1, + "children": [ + { + "label": "Authentication type", + "description": "Select the authentication type to use", + "configProperty": "datasourceConfiguration.authentication.authenticationType", + "controlType": "DROP_DOWN", + "initialValue" : "apiKey", + "options": [ + { + "label": "API Key", + "value": "apiKey" + } + ], + "isRequired": true, + "hidden": true + }, + { + "label": "API Key", + "configProperty": "datasourceConfiguration.authentication.value", + "controlType": "INPUT_TEXT", + "dataType": "PASSWORD", + "initialValue": "", + "isRequired": true, + "encrypted": true + }, + { + "label": "Endpoint URL (with or without protocol and port no)", + "configProperty": "datasourceConfiguration.url", + "controlType": "INPUT_TEXT", + "initialValue": "https://api.anthropic.com", + "isRequired": true, + "hidden": true + } + ] + } + ], + "formButton" : ["TEST", "CANCEL", "SAVE"] +} diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/plugin.properties b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/plugin.properties new file mode 100644 index 0000000000..7705ad97c7 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=anthropic-plugin +plugin.class=com.external.plugins.AnthropicPlugin +plugin.version=1.0-SNAPSHOT +plugin.provider=tech@appsmith.com +plugin.dependencies= diff --git a/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/setting.json b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/setting.json new file mode 100644 index 0000000000..4dd710a3bc --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/main/resources/setting.json @@ -0,0 +1,30 @@ +{ + "setting": [ + { + "sectionName": "", + "id": 1, + "children": [ + { + "label": "Run query on page load", + "configProperty": "executeOnLoad", + "controlType": "SWITCH", + "subtitle": "Will refresh data each time the page is loaded" + }, + { + "label": "Request confirmation before running query", + "configProperty": "confirmBeforeExecute", + "controlType": "SWITCH", + "subtitle": "Ask confirmation from the user each time before refreshing data" + }, + { + "label": "Query timeout (in milliseconds)", + "subtitle": "Maximum time after which the query will return", + "configProperty": "actionConfiguration.timeoutInMillisecond", + "controlType": "INPUT_TEXT", + "initialValue": 60000, + "dataType": "NUMBER" + } + ] + } + ] +} 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 new file mode 100644 index 0000000000..b29f2b4b37 --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/AnthropicPluginTest.java @@ -0,0 +1,160 @@ +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; +import mockwebserver3.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +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; + +public class AnthropicPluginTest { + private static MockWebServer mockEndpoint; + + public static class MockSharedConfig implements SharedConfig { + + @Override + public int getCodecSize() { + return 10 * 1024 * 1024; + } + + @Override + public int getMaxResponseSize() { + return 10000; + } + + @Override + public String getRemoteExecutionUrl() { + return ""; + } + } + + AnthropicPlugin.AnthropicPluginExecutor pluginExecutor = + new AnthropicPlugin.AnthropicPluginExecutor(new MockSharedConfig()); + + @BeforeEach + public void setUp() throws IOException { + mockEndpoint = new MockWebServer(); + mockEndpoint.start(); + } + + @AfterEach + public void tearDown() throws IOException { + mockEndpoint.shutdown(); + } + + @Test + public void testValidateDatasourceGivesNoInvalidsWhenConfiguredWithString() { + ApiKeyAuth apiKeyAuth = new ApiKeyAuth(); + apiKeyAuth.setValue("apiKey"); + + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasourceConfiguration.setAuthentication(apiKeyAuth); + + Set invalids = pluginExecutor.validateDatasource(datasourceConfiguration); + assertEquals(invalids.size(), 0); + } + + @Test + public void testValidateDatasourceGivesInvalids() { + Set invalids = pluginExecutor.validateDatasource(new DatasourceConfiguration()); + assertEquals(invalids.size(), 1); + assertEquals(invalids, Set.of(AnthropicErrorMessages.EMPTY_API_KEY)); + } + + @Test + @Disabled + public void verifyTestDatasourceReturns() { + ApiKeyAuth apiKeyAuth = new ApiKeyAuth(); + apiKeyAuth.setValue("apiKey"); + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasourceConfiguration.setAuthentication(apiKeyAuth); + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(200); + mockEndpoint.enqueue(mockResponse); + + Mono datasourceTestResultMono = pluginExecutor.testDatasource(datasourceConfiguration); + + StepVerifier.create(datasourceTestResultMono) + .assertNext(datasourceTestResult -> { + assertEquals(datasourceTestResult.getInvalids().size(), 0); + assertEquals(datasourceTestResult.getMessages().size(), 0); + assertTrue(datasourceTestResult.isSuccess()); + }) + .verifyComplete(); + } + + @Test + public void verifyTestDatasourceReturnsFalse() { + ApiKeyAuth apiKeyAuth = new ApiKeyAuth(); + apiKeyAuth.setValue("apiKey"); + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasourceConfiguration.setAuthentication(apiKeyAuth); + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(401); + mockEndpoint.enqueue(mockResponse); + + Mono datasourceTestResultMono = pluginExecutor.testDatasource(datasourceConfiguration); + + StepVerifier.create(datasourceTestResultMono) + .assertNext(datasourceTestResult -> { + assertEquals(datasourceTestResult.getInvalids().size(), 1); + assertFalse(datasourceTestResult.isSuccess()); + }) + .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 new file mode 100644 index 0000000000..c39e4bc8df --- /dev/null +++ b/app/server/appsmith-plugins/anthropicPlugin/src/test/java/com/external/plugins/ChatCommandTest.java @@ -0,0 +1,61 @@ +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.Test; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.external.plugins.constants.AnthropicConstants.CHAT_MODEL_SELECTOR; +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.TEST_MODEL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ChatCommandTest { + + @Test + public void testCreateTriggerUri() { + ChatCommand command = new ChatCommand(); + URI uri = command.createTriggerUri(); + assertEquals("/api/v1/ai/models", uri.getPath()); + } + + @Test + public void testCreateExecutionUri() { + ChatCommand command = new ChatCommand(); + URI uri = command.createExecutionUri(); + + assertEquals("/v1/complete", uri.getPath()); + } + + @Test + public void testMakeRequestBody_withValidData() { + // Test with valid form data + + ChatCommand command = new ChatCommand(); + + Map formData = new HashMap<>(); + formData.put(CHAT_MODEL_SELECTOR, Map.of(DATA, TEST_MODEL)); + + Object messages = + List.of(Map.of("role", "Human", "content", "Hey"), Map.of("role", "Assistant", "content", "Hi there!")); + formData.put("messages", Map.of(DATA, messages)); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setFormData(formData); + + AnthropicRequestDTO request = command.makeRequestBody(actionConfiguration); + + assertEquals(TEST_MODEL, request.getModel()); + assertNotNull(request.getPrompt()); + assertEquals(DEFAULT_TEMPERATURE, request.getTemperature()); + assertEquals(DEFAULT_MAX_TOKEN, request.getMaxTokensToSample()); + assertEquals("\n\nHuman: Hey\n\nAssistant: Hi there!\nAssistant:", request.getPrompt()); + } +} diff --git a/app/server/appsmith-plugins/openAiPlugin/pom.xml b/app/server/appsmith-plugins/openAiPlugin/pom.xml index 1211b43892..f19ad0b6bf 100644 --- a/app/server/appsmith-plugins/openAiPlugin/pom.xml +++ b/app/server/appsmith-plugins/openAiPlugin/pom.xml @@ -114,6 +114,11 @@ reactor-netty-http provided + + com.google.guava + guava + 32.0.1-jre + diff --git a/app/server/appsmith-plugins/openAiPlugin/src/main/java/com/external/plugins/OpenAiPlugin.java b/app/server/appsmith-plugins/openAiPlugin/src/main/java/com/external/plugins/OpenAiPlugin.java index 45ae694308..967b6d1a36 100644 --- a/app/server/appsmith-plugins/openAiPlugin/src/main/java/com/external/plugins/OpenAiPlugin.java +++ b/app/server/appsmith-plugins/openAiPlugin/src/main/java/com/external/plugins/OpenAiPlugin.java @@ -20,6 +20,8 @@ import com.external.plugins.commands.OpenAICommand; import com.external.plugins.models.OpenAIRequestDTO; import com.external.plugins.utils.OpenAIMethodStrategy; import com.external.plugins.utils.RequestUtils; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.google.gson.Gson; import lombok.extern.slf4j.Slf4j; import org.json.JSONArray; @@ -33,10 +35,15 @@ import reactor.core.publisher.Mono; import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static com.external.plugins.constants.OpenAIConstants.BODY; import static com.external.plugins.constants.OpenAIConstants.DATA; @@ -54,6 +61,8 @@ public class OpenAiPlugin extends BasePlugin { public static class OpenAiPluginExecutor extends BaseRestApiPluginExecutor { private static final Gson gson = new Gson(); + private static final Cache modelResponseCache = + CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build(); public OpenAiPluginExecutor(SharedConfig config) { super(config); @@ -167,6 +176,28 @@ public class OpenAiPlugin extends BasePlugin { }); } + private String cacheKey(String bearerToken) { + return sha256(bearerToken); + } + + private String sha256(String base) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(base.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + + return hexString.toString(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + @Override public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) { return RequestUtils.validateBearerTokenDatasource(datasourceConfiguration); @@ -179,26 +210,36 @@ public class OpenAiPlugin extends BasePlugin { // Authentication will already be valid at this point final BearerTokenAuth bearerTokenAuth = (BearerTokenAuth) datasourceConfiguration.getAuthentication(); assert (bearerTokenAuth.getBearerToken() != null); - OpenAICommand openAICommand = OpenAIMethodStrategy.selectTriggerMethod(request, gson); HttpMethod httpMethod = openAICommand.getTriggerHTTPMethod(); URI uri = openAICommand.createTriggerUri(); - return RequestUtils.makeRequest(httpMethod, uri, bearerTokenAuth, BodyInserters.empty()) - .flatMap(responseEntity -> { - if (responseEntity.getStatusCode().is4xxClientError()) { - return Mono.error(new AppsmithPluginException( - AppsmithPluginError.PLUGIN_DATASOURCE_AUTHENTICATION_ERROR)); - } + String cacheKey = cacheKey(bearerTokenAuth.getBearerToken()); + JSONObject modelsResponse = modelResponseCache.getIfPresent(cacheKey); - if (!responseEntity.getStatusCode().is2xxSuccessful()) { - return Mono.error( - new AppsmithPluginException(AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR)); - } + Mono responseMono; + if (modelsResponse != null) { + responseMono = Mono.just(modelsResponse); + } else { + responseMono = RequestUtils.makeRequest(httpMethod, uri, bearerTokenAuth, BodyInserters.empty()) + .flatMap(responseEntity -> { + if (responseEntity.getStatusCode().is4xxClientError()) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_DATASOURCE_AUTHENTICATION_ERROR)); + } - // link to get response data https://platform.openai.com/docs/api-reference/models/list - return Mono.just(new JSONObject(new String(responseEntity.getBody()))); - }) + if (!responseEntity.getStatusCode().is2xxSuccessful()) { + return Mono.error( + new AppsmithPluginException(AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR)); + } + JSONObject responseObject = new JSONObject(new String(responseEntity.getBody())); + modelResponseCache.put(cacheKey, responseObject); + // link to get response data https://platform.openai.com/docs/api-reference/models/list + return Mono.just(responseObject); + }); + } + + return responseMono .map(jsonObject -> { if (!jsonObject.has(DATA) && jsonObject.get(DATA) instanceof JSONArray) { // let's throw some error. @@ -206,7 +247,8 @@ public class OpenAiPlugin extends BasePlugin { new AppsmithPluginException(AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR)); } - List> triggerModelList = new ArrayList<>(); + List compatibleModels = new ArrayList<>(); + Map modelsMap = new HashMap<>(); JSONArray modelList = jsonObject.getJSONArray(DATA); int modelListIndex = 0; while (modelListIndex < modelList.length()) { @@ -216,13 +258,18 @@ public class OpenAiPlugin extends BasePlugin { } if (openAICommand.isModelCompatible(model)) { - triggerModelList.add(openAICommand.getModelMap(model)); + String id = model.getString(ID); + compatibleModels.add(id); + modelsMap.put(id, model); } modelListIndex += 1; } - - return triggerModelList; + // sort models alphabetically + return compatibleModels.stream() + .sorted() + .map(model -> openAICommand.getModelMap(modelsMap.get(model))) + .collect(Collectors.toList()); }) .map(TriggerResultDTO::new); } diff --git a/app/server/appsmith-plugins/openAiPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java b/app/server/appsmith-plugins/openAiPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java index 2ee7b25953..39fa48e91b 100644 --- a/app/server/appsmith-plugins/openAiPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java +++ b/app/server/appsmith-plugins/openAiPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java @@ -10,13 +10,17 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; import java.net.URI; +import java.time.Duration; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -36,6 +40,7 @@ import static com.external.plugins.constants.OpenAIConstants.VISION_ENDPOINT; import static com.external.plugins.constants.OpenAIErrorMessages.EMPTY_BEARER_TOKEN; public class RequestUtils { + private static final WebClient webClient = createWebClient(); public static String extractDataFromFormData(Map formData, String key) { return (String) ((Map) formData.get(key)).get(DATA); @@ -75,15 +80,11 @@ public class RequestUtils { BearerTokenAuth bearerTokenAuth, BodyInserter body) { - // Initializing webClient to be used for http call - WebClient.Builder webClientBuilder = WebClient.builder(); - WebClient client = - webClientBuilder.exchangeStrategies(EXCHANGE_STRATEGIES).build(); - // Authentication will already be valid at this point assert (bearerTokenAuth.getBearerToken() != null); - return client.method(httpMethod) + return webClient + .method(httpMethod) .uri(uri) .contentType(MediaType.APPLICATION_JSON) .body(body) @@ -92,6 +93,25 @@ public class RequestUtils { .exchangeToMono(clientResponse -> clientResponse.toEntity(byte[].class)); } + private static WebClient createWebClient() { + // Initializing webClient to be used for http call + WebClient.Builder webClientBuilder = WebClient.builder(); + return webClientBuilder + .exchangeStrategies(EXCHANGE_STRATEGIES) + .clientConnector(new ReactorClientHttpConnector(HttpClient.create(connectionProvider()))) + .build(); + } + + private static ConnectionProvider connectionProvider() { + return ConnectionProvider.builder("openai") + .maxConnections(100) + .maxIdleTime(Duration.ofSeconds(60)) + .maxLifeTime(Duration.ofSeconds(60)) + .pendingAcquireTimeout(Duration.ofSeconds(30)) + .evictInBackground(Duration.ofSeconds(120)) + .build(); + } + public static Set validateBearerTokenDatasource(DatasourceConfiguration datasourceConfiguration) { Set invalids = new HashSet<>(); final BearerTokenAuth bearerTokenAuth = (BearerTokenAuth) datasourceConfiguration.getAuthentication(); diff --git a/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/chat.json b/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/chat.json index 0766c4dd2b..280434e921 100644 --- a/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/chat.json +++ b/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/chat.json @@ -17,11 +17,12 @@ "options": [], "placeholderText": "All models will be fetched.", "fetchOptionsConditionally": true, + "setFirstOptionAsDefault": true, "alternateViewTypes": ["json"], "conditionals": { "enable": "{{true}}", "fetchDynamicValues": { - "condition": "{{true}}", + "condition": "{{actionConfiguration.formData.command.data === 'CHAT'}}", "config": { "params": { "requestType": "CHAT_MODELS", @@ -31,17 +32,6 @@ } } }, - { - "label": "Temperature", - "tooltipText": "Put a value between 0 and 2", - "Description": "Put a value between 0 and 2", - "subtitle": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.", - "configProperty": "actionConfiguration.formData.temperature", - "controlType": "INPUT_TEXT", - "dataType": "NUMBER", - "initialValue": "0", - "isRequired": false - }, { "label": "Messages", "tooltipText": "Ask a question", @@ -66,6 +56,21 @@ "placeholderText": "{{ UserInput.text }}" } ] + }, + { + "label": "Temperature", + "tooltipText": "Put a value between 0 and 2", + "Description": "Put a value between 0 and 2", + "subtitle": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.", + "configProperty": "actionConfiguration.formData.temperature", + "controlType": "INPUT_TEXT", + "dataType": "NUMBER", + "initialValue": "0", + "isRequired": false, + "customStyles": { + "width": "270px", + "minWidth": "270px" + } } ] } diff --git a/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/embeddings.json b/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/embeddings.json index 2abdcdf53a..1831e114ac 100644 --- a/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/embeddings.json +++ b/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/embeddings.json @@ -17,11 +17,12 @@ "options": [], "placeholderText": "All models will be fetched.", "fetchOptionsConditionally": true, + "setFirstOptionAsDefault": true, "alternateViewTypes": ["json"], "conditionals": { "enable": "{{true}}", "fetchDynamicValues": { - "condition": "{{true}}", + "condition": "{{actionConfiguration.formData.command.data === 'EMBEDDINGS'}}", "config": { "params": { "requestType": "EMBEDDING_MODELS", diff --git a/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/vision.json b/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/vision.json index bd7ace5d24..7888cf460f 100644 --- a/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/vision.json +++ b/app/server/appsmith-plugins/openAiPlugin/src/main/resources/editor/vision.json @@ -17,11 +17,12 @@ "options": [], "placeholderText": "All models will be fetched.", "fetchOptionsConditionally": true, + "setFirstOptionAsDefault": true, "alternateViewTypes": ["json"], "conditionals": { "enable": "{{true}}", "fetchDynamicValues": { - "condition": "{{true}}", + "condition": "{{actionConfiguration.formData.command.data === 'VISION'}}", "config": { "params": { "requestType": "VISION_MODELS", @@ -40,18 +41,11 @@ "controlType": "INPUT_TEXT", "initialValue": "16", "isRequired": true, - "dataType": "NUMBER" - }, - { - "label": "Temperature", - "tooltipText": "Put a value between 0 and 2", - "Description": "Put a value between 0 and 2", - "subtitle": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.", - "configProperty": "actionConfiguration.formData.temperature", - "controlType": "INPUT_TEXT", "dataType": "NUMBER", - "initialValue": "0", - "isRequired": false + "customStyles": { + "width": "270px", + "minWidth": "270px" + } }, { "label": "System Messages", @@ -62,12 +56,16 @@ "configProperty": "actionConfiguration.formData.systemMessages", "controlType": "ARRAY_FIELD", "addMoreButtonLabel": "Add System Message", + "customStyles": { + "width": "40vw" + }, "schema": [ { "label": "Content", "key": "content", "controlType": "QUERY_DYNAMIC_INPUT_TEXT", - "placeholderText": "{{ UserInput.text }}" + "placeholderText": "{{ UserInput.text }}", + "initialValue": "As an OCR expert your skills are unparalleled. Respond with just the text in the image" } ] }, @@ -85,6 +83,7 @@ "label": "Type", "key": "type", "controlType": "DROP_DOWN", + "initialValue": "text", "options": [ { "label": "Text", @@ -103,6 +102,21 @@ "placeholderText": "{{Img1.image}} or {{Input1.text}}" } ] + }, + { + "label": "Temperature", + "tooltipText": "Put a value between 0 and 2", + "Description": "Put a value between 0 and 2", + "subtitle": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.", + "configProperty": "actionConfiguration.formData.temperature", + "controlType": "INPUT_TEXT", + "dataType": "NUMBER", + "initialValue": "0", + "isRequired": false, + "customStyles": { + "width": "270px", + "minWidth": "270px" + } } ] } diff --git a/app/server/appsmith-plugins/pom.xml b/app/server/appsmith-plugins/pom.xml index 36cd1d63fc..7abd23a8b6 100644 --- a/app/server/appsmith-plugins/pom.xml +++ b/app/server/appsmith-plugins/pom.xml @@ -61,6 +61,7 @@ smtpPlugin openAiPlugin + anthropicPlugin diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration035AddAnthropicPlugin.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration035AddAnthropicPlugin.java new file mode 100644 index 0000000000..4328ad6eb3 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration035AddAnthropicPlugin.java @@ -0,0 +1,50 @@ +package com.appsmith.server.migrations.db.ce; + +import com.appsmith.external.constants.PluginConstants; +import com.appsmith.external.models.PluginType; +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.dao.DuplicateKeyException; +import org.springframework.data.mongodb.core.MongoTemplate; + +import static com.appsmith.server.migrations.DatabaseChangelog1.installPluginToAllWorkspaces; + +@Slf4j +@ChangeUnit(order = "035", id = "add-anthropic-plugin", author = " ") +public class Migration035AddAnthropicPlugin { + private final MongoTemplate mongoTemplate; + + public Migration035AddAnthropicPlugin(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + @RollbackExecution + public void rollbackExecution() {} + + @Execution + public void addPluginToDbAndWorkspace() { + Plugin plugin = new Plugin(); + plugin.setName(PluginConstants.PluginName.ANTHROPIC_PLUGIN_NAME); + plugin.setType(PluginType.AI); + plugin.setPluginName(PluginConstants.PluginName.ANTHROPIC_PLUGIN_NAME); + plugin.setPackageName(PluginConstants.PackageName.ANTHROPIC_PLUGIN); + plugin.setUiComponent("UQIDbEditorForm"); + plugin.setDatasourceComponent("DbEditorForm"); + plugin.setResponseType(Plugin.ResponseType.JSON); + plugin.setIconLocation("https://assets.appsmith.com/logo/anthropic.svg"); + plugin.setDocumentationLink("https://docs.appsmith.com/connect-data/reference/anthropic"); + plugin.setDefaultInstall(true); + try { + mongoTemplate.insert(plugin); + } catch (DuplicateKeyException e) { + log.warn(plugin.getPackageName() + " already present in database."); + } + + assert plugin.getId() != null; + + installPluginToAllWorkspaces(mongoTemplate, plugin.getId()); + } +}