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 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Diljit VJ <diljit@appsmith.com>
This commit is contained in:
parent
c1a7afe688
commit
b03c51e05d
|
|
@ -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<Action>;
|
||||
setFirstOptionAsDefault?: boolean;
|
||||
}
|
||||
|
||||
interface ReduxDispatchProps {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<PrimaryBox>
|
||||
{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) {
|
|||
/>
|
||||
);
|
||||
})}
|
||||
<CenteredIcon
|
||||
isIconButton
|
||||
<CenteredIconButton
|
||||
alignSelf={"start"}
|
||||
data-testid={`t--where-clause-delete-[${index}]`}
|
||||
kind="tertiary"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
props.fields.remove(index);
|
||||
}}
|
||||
size="sm"
|
||||
startIcon="delete"
|
||||
size="md"
|
||||
startIcon="close"
|
||||
top={index === 0 ? "20px" : ""}
|
||||
/>
|
||||
</SecondaryBox>
|
||||
);
|
||||
})}
|
||||
<AddMoreAction onClick={() => props.fields.push({})}>
|
||||
{/*Hardcoded label to be removed */}
|
||||
<Text type={TextType.H5}>{props.addMoreButtonLabel}</Text>
|
||||
<AddMoreAction>
|
||||
<Button
|
||||
className={`t--where-add-condition[${props?.currentNestingLevel}]`}
|
||||
kind="tertiary"
|
||||
onClick={addMore}
|
||||
size="md"
|
||||
startIcon="add-more"
|
||||
>
|
||||
{props.addMoreButtonLabel}
|
||||
</Button>
|
||||
</AddMoreAction>
|
||||
</PrimaryBox>
|
||||
);
|
||||
|
|
@ -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) {
|
|||
<FieldArray
|
||||
component={NestedComponents}
|
||||
name={configProperty}
|
||||
props={{ formName, schema, addMoreButtonLabel }}
|
||||
props={{
|
||||
formName,
|
||||
schema,
|
||||
addMoreButtonLabel,
|
||||
configProperty,
|
||||
customStyles,
|
||||
}}
|
||||
rerenderOnEveryChange={false}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -238,10 +238,10 @@ function QueryResponseView({
|
|||
(actionResponse.pluginErrorDetails ? (
|
||||
<>
|
||||
<div data-testid="t--query-error">
|
||||
{
|
||||
{actionResponse.pluginErrorDetails
|
||||
.downstreamErrorMessage ||
|
||||
actionResponse.pluginErrorDetails
|
||||
.downstreamErrorMessage
|
||||
}
|
||||
.appsmithErrorMessage}
|
||||
</div>
|
||||
{actionResponse.pluginErrorDetails
|
||||
.downstreamErrorCode && (
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
108
app/server/appsmith-plugins/anthropicPlugin/pom.xml
Normal file
108
app/server/appsmith-plugins/anthropicPlugin/pom.xml
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.appsmith</groupId>
|
||||
<artifactId>appsmith-plugins</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.external.plugins</groupId>
|
||||
<artifactId>anthropicPlugin</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<name>anthropicPlugin</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webflux</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson-bom.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jdk8</artifactId>
|
||||
<version>${jackson-bom.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<version>${jackson-bom.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>32.0.1-jre</version>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
Ideally this dependency should have been added with 'compile' scope here. But that is causing 'java.lang
|
||||
.NoClassDefFoundError'. After trying to fix it right way many times, I decided to move the dependency to
|
||||
the main server module's pom.xml file and keep the dependency here with 'provided' scope. This seems to
|
||||
fix the problem for now.
|
||||
-->
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.netty</groupId>
|
||||
<artifactId>reactor-netty-http</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
274
app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/AnthropicPlugin.java
vendored
Normal file
274
app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/AnthropicPlugin.java
vendored
Normal file
|
|
@ -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<String, TriggerResultDTO> triggerResponseCache =
|
||||
CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build();
|
||||
|
||||
protected AnthropicPluginExecutor(SharedConfig sharedConfig) {
|
||||
super(sharedConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<DatasourceTestResult> 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<ActionExecutionResult> executeParameterized(
|
||||
APIConnection connection,
|
||||
ExecuteActionDTO executeActionDTO,
|
||||
DatasourceConfiguration datasourceConfiguration,
|
||||
ActionConfiguration actionConfiguration) {
|
||||
// Get prompt from action configuration
|
||||
List<Map.Entry<String, String>> 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<TriggerResultDTO> 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<String> 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<String> validateDatasource(DatasourceConfiguration datasourceConfiguration) {
|
||||
return RequestUtils.validateApiKeyAuthDatasource(datasourceConfiguration);
|
||||
}
|
||||
|
||||
private List<Map<String, String>> getDataToMap(List<String> data) {
|
||||
return data.stream().sorted().map(x -> Map.of(LABEL, x, VALUE, x)).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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<String, Object> formData = actionConfiguration.getFormData();
|
||||
if (CollectionUtils.isEmpty(formData)) {
|
||||
throw new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
String.format(STRING_APPENDER, EXECUTION_FAILURE, QUERY_NOT_CONFIGURED));
|
||||
}
|
||||
|
||||
AnthropicRequestDTO anthropicRequestDTO = new AnthropicRequestDTO();
|
||||
String model = RequestUtils.extractDataFromFormData(formData, 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<String, Object> formData) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
if (formData.containsKey(MESSAGES)) {
|
||||
List<Map<String, String>> messageMaps = getMessages((Map<String, Object>) formData.get(MESSAGES));
|
||||
if (messageMaps == null) {
|
||||
throw new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
"messages are not provided in the configuration correctly");
|
||||
}
|
||||
for (Map<String, String> messageMap : messageMaps) {
|
||||
if (messageMap != null && messageMap.containsKey(ROLE) && messageMap.containsKey(CONTENT)) {
|
||||
stringBuilder
|
||||
.append("\n\n")
|
||||
.append(messageMap.get(ROLE))
|
||||
.append(": ")
|
||||
.append(messageMap.get(CONTENT));
|
||||
}
|
||||
}
|
||||
return stringBuilder.append("\n").append(Role.Assistant).append(":").toString();
|
||||
} else {
|
||||
throw new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
"messages are not provided in the configuration");
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<String, String>> getMessages(Map<String, Object> messages) {
|
||||
Type listType = new TypeToken<List<Map<String, String>>>() {}.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<Map<String, String>>) messages.get(COMPONENT_DATA);
|
||||
}
|
||||
}
|
||||
// return object stored in data key
|
||||
return (List<Map<String, String>>) 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<String, Object> messages) {
|
||||
if (messages.containsKey(VIEW_TYPE) && "json".equals(messages.get(VIEW_TYPE))) {
|
||||
return COMPONENT_DATA;
|
||||
}
|
||||
return DATA;
|
||||
}
|
||||
|
||||
private int getMaxTokenFromFormData(Map<String, Object> formData) {
|
||||
String maxTokenAsString = RequestUtils.extractValueFromFormData(formData, MAX_TOKENS);
|
||||
|
||||
if (!StringUtils.hasText(maxTokenAsString)) {
|
||||
return DEFAULT_MAX_TOKEN;
|
||||
}
|
||||
|
||||
try {
|
||||
return Integer.parseInt(maxTokenAsString);
|
||||
} catch (IllegalArgumentException illegalArgumentException) {
|
||||
return DEFAULT_MAX_TOKEN;
|
||||
} catch (Exception exception) {
|
||||
throw new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
String.format(STRING_APPENDER, EXECUTION_FAILURE, BAD_MAX_TOKEN_CONFIGURATION));
|
||||
}
|
||||
}
|
||||
|
||||
private Float getTemperatureFromFormData(Map<String, Object> formData) {
|
||||
String temperatureString = RequestUtils.extractValueFromFormData(formData, TEMPERATURE);
|
||||
|
||||
if (!StringUtils.hasText(temperatureString)) {
|
||||
return DEFAULT_TEMPERATURE;
|
||||
}
|
||||
|
||||
try {
|
||||
return Float.parseFloat(temperatureString);
|
||||
} catch (IllegalArgumentException illegalArgumentException) {
|
||||
return DEFAULT_TEMPERATURE;
|
||||
} catch (Exception exception) {
|
||||
throw new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
String.format(STRING_APPENDER, EXECUTION_FAILURE, BAD_TEMPERATURE_CONFIGURATION));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> 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";
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.external.plugins.models;
|
||||
|
||||
public enum Role {
|
||||
Assistant,
|
||||
Human
|
||||
}
|
||||
|
|
@ -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<String, Object> 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));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Object> formData, String key) {
|
||||
return (String) ((Map<String, Object>) formData.get(key)).get(AnthropicConstants.DATA);
|
||||
}
|
||||
|
||||
public static String extractValueFromFormData(Map<String, Object> 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<ResponseEntity<byte[]>> makeRequest(
|
||||
HttpMethod httpMethod, URI uri, ApiKeyAuth apiKeyAuth, BodyInserter<?, ? super ClientHttpRequest> 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<String> validateApiKeyAuthDatasource(DatasourceConfiguration datasourceConfiguration) {
|
||||
Set<String> invalids = new HashSet<>();
|
||||
final ApiKeyAuth apiKeyAuth = (ApiKeyAuth) datasourceConfiguration.getAuthentication();
|
||||
|
||||
if (apiKeyAuth == null || !StringUtils.hasText(apiKeyAuth.getValue())) {
|
||||
invalids.add(AnthropicErrorMessages.EMPTY_API_KEY);
|
||||
}
|
||||
|
||||
return invalids;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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=
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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<String> invalids = pluginExecutor.validateDatasource(datasourceConfiguration);
|
||||
assertEquals(invalids.size(), 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateDatasourceGivesInvalids() {
|
||||
Set<String> 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<DatasourceTestResult> 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<DatasourceTestResult> 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<TriggerResultDTO> datasourceTriggerResultMono =
|
||||
pluginExecutor.trigger(null, datasourceConfiguration, request);
|
||||
|
||||
StepVerifier.create(datasourceTriggerResultMono)
|
||||
.assertNext(result -> {
|
||||
assertTrue(result.getTrigger() instanceof List<?>);
|
||||
assertEquals(((List) result.getTrigger()).size(), 4);
|
||||
assertEquals(
|
||||
result.getTrigger(),
|
||||
getDataToMap(List.of("claude-2.1", "claude-2", "claude-instant-1.2", "claude-instant-1")));
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
private List<Map<String, String>> getDataToMap(List<String> data) {
|
||||
return data.stream().sorted().map(x -> Map.of(LABEL, x, VALUE, x)).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Object> 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -114,6 +114,11 @@
|
|||
<artifactId>reactor-netty-http</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>32.0.1-jre</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String, JSONObject> 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<String> 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<JSONObject> 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<Map<String, String>> triggerModelList = new ArrayList<>();
|
||||
List<String> compatibleModels = new ArrayList<>();
|
||||
Map<String, JSONObject> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, Object> formData, String key) {
|
||||
return (String) ((Map<String, Object>) formData.get(key)).get(DATA);
|
||||
|
|
@ -75,15 +80,11 @@ public class RequestUtils {
|
|||
BearerTokenAuth bearerTokenAuth,
|
||||
BodyInserter<?, ? super ClientHttpRequest> 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<String> validateBearerTokenDatasource(DatasourceConfiguration datasourceConfiguration) {
|
||||
Set<String> invalids = new HashSet<>();
|
||||
final BearerTokenAuth bearerTokenAuth = (BearerTokenAuth) datasourceConfiguration.getAuthentication();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
<module>smtpPlugin</module>
|
||||
|
||||
<module>openAiPlugin</module>
|
||||
<module>anthropicPlugin</module>
|
||||
|
||||
</modules>
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user