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 7d337ffc79..8d5aebfead 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 @@ -14,6 +14,7 @@ public interface PluginConstants { String GRAPH_QL_PLUGIN = "graphql-plugin"; String OPEN_AI_PLUGIN = "openai-plugin"; String ANTHROPIC_PLUGIN = "anthropic-plugin"; + String GOOGLE_AI_PLUGIN = "googleai-plugin"; } public static final String DEFAULT_REST_DATASOURCE = "DEFAULT_REST_DATASOURCE"; @@ -39,6 +40,7 @@ public interface PluginConstants { public static final String OPEN_AI_PLUGIN_NAME = "Open AI"; public static final String ANTHROPIC_PLUGIN_NAME = "Anthropic"; + public static final String GOOGLE_AI_PLUGIN_NAME = "Google AI"; } interface HostName { diff --git a/app/server/appsmith-plugins/googleAiPlugin/pom.xml b/app/server/appsmith-plugins/googleAiPlugin/pom.xml new file mode 100644 index 0000000000..9c8ba07e5c --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + com.appsmith + appsmith-plugins + 1.0-SNAPSHOT + + + com.external.plugins + googleAiPlugin + 1.0-SNAPSHOT + googleAiPlugin + 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/googleAiPlugin/src/main/java/com/external/plugins/GoogleAiPlugin.java b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/GoogleAiPlugin.java new file mode 100644 index 0000000000..534fce99f7 --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/GoogleAiPlugin.java @@ -0,0 +1,209 @@ +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.GoogleAICommand; +import com.external.plugins.constants.GoogleAIConstants; +import com.external.plugins.models.GoogleAIRequestDTO; +import com.external.plugins.utils.GoogleAIMethodStrategy; +import com.external.plugins.utils.RequestUtils; +import com.google.gson.Gson; +import lombok.extern.slf4j.Slf4j; +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 org.springframework.web.util.UriComponentsBuilder; +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.stream.Collectors; + +import static com.external.plugins.constants.GoogleAIConstants.BODY; +import static com.external.plugins.constants.GoogleAIConstants.GOOGLE_AI_API_ENDPOINT; +import static com.external.plugins.constants.GoogleAIConstants.LABEL; +import static com.external.plugins.constants.GoogleAIConstants.MODELS; +import static com.external.plugins.constants.GoogleAIConstants.VALUE; +import static com.external.plugins.constants.GoogleAIErrorMessages.EMPTY_API_KEY; +import static com.external.plugins.constants.GoogleAIErrorMessages.INVALID_API_KEY; +import static com.external.plugins.constants.GoogleAIErrorMessages.QUERY_FAILED_TO_EXECUTE; + +@Slf4j +public class GoogleAiPlugin extends BasePlugin { + public GoogleAiPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + public static class GoogleAiPluginExecutor extends BaseRestApiPluginExecutor { + private static final Gson gson = new Gson(); + + protected GoogleAiPluginExecutor(SharedConfig sharedConfig) { + super(sharedConfig); + } + + /** + * Tries to fetch the models list from GoogleAI API and if request succeed, then datasource configuration is valid + */ + @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)); + } + + URI uri = UriComponentsBuilder.fromUriString(GOOGLE_AI_API_ENDPOINT) + .path(MODELS) + .build() + .toUri(); + HttpMethod httpMethod = HttpMethod.GET; + + return RequestUtils.makeRequest(httpMethod, uri, apiKeyAuth, BodyInserters.empty()) + .map(responseEntity -> { + if (responseEntity.getStatusCode().is2xxSuccessful()) { + // valid credentials + return new DatasourceTestResult(); + } + return new DatasourceTestResult(INVALID_API_KEY); + }) + .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); + + // Initializing object for error condition + ActionExecutionResult errorResult = new ActionExecutionResult(); + initUtils.initializeResponseWithError(errorResult); + + GoogleAICommand googleAICommand = GoogleAIMethodStrategy.selectExecutionMethod(actionConfiguration, gson); + googleAICommand.validateRequest(actionConfiguration); + GoogleAIRequestDTO googleAIRequestDTO = googleAICommand.makeRequestBody(actionConfiguration); + + URI uri = googleAICommand.createExecutionUri(actionConfiguration); + HttpMethod httpMethod = googleAICommand.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(googleAIRequestDTO)) + .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.error( + "An error has occurred while trying to run the Google AI API query command with error {}", + error.getMessage()); + 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) { + return Mono.just(new TriggerResultDTO(getDataToMap(GoogleAIConstants.GOOGLE_AI_MODELS))); + } + + @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/googleAiPlugin/src/main/java/com/external/plugins/commands/GenerateContentCommand.java b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/commands/GenerateContentCommand.java new file mode 100644 index 0000000000..5850609648 --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/commands/GenerateContentCommand.java @@ -0,0 +1,152 @@ +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.GoogleAIConstants; +import com.external.plugins.models.GoogleAIRequestDTO; +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 java.lang.reflect.Type; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.external.plugins.constants.GoogleAIConstants.COMPONENT; +import static com.external.plugins.constants.GoogleAIConstants.COMPONENT_DATA; +import static com.external.plugins.constants.GoogleAIConstants.CONTENT; +import static com.external.plugins.constants.GoogleAIConstants.DATA; +import static com.external.plugins.constants.GoogleAIConstants.GENERATE_CONTENT_MODEL; +import static com.external.plugins.constants.GoogleAIConstants.JSON; +import static com.external.plugins.constants.GoogleAIConstants.MESSAGES; +import static com.external.plugins.constants.GoogleAIConstants.ROLE; +import static com.external.plugins.constants.GoogleAIConstants.TYPE; +import static com.external.plugins.constants.GoogleAIConstants.VIEW_TYPE; +import static com.external.plugins.constants.GoogleAIErrorMessages.EXECUTION_FAILURE; +import static com.external.plugins.constants.GoogleAIErrorMessages.INCORRECT_MESSAGE_FORMAT; +import static com.external.plugins.constants.GoogleAIErrorMessages.MODEL_NOT_SELECTED; +import static com.external.plugins.constants.GoogleAIErrorMessages.QUERY_NOT_CONFIGURED; +import static com.external.plugins.constants.GoogleAIErrorMessages.STRING_APPENDER; + +public class GenerateContentCommand implements GoogleAICommand { + private final Gson gson = new Gson(); + + @Override + public HttpMethod getTriggerHTTPMethod() { + return HttpMethod.GET; + } + + @Override + public HttpMethod getExecutionMethod() { + return HttpMethod.POST; + } + + /** + * This will be implemented in later stage once we integrate all the functions provided by Google AI + */ + @Override + public URI createTriggerUri() { + return URI.create(""); + } + + @Override + public URI createExecutionUri(ActionConfiguration actionConfiguration) { + return RequestUtils.createUriFromCommand(GoogleAIConstants.GENERATE_CONTENT, selectModel(actionConfiguration)); + } + + private String selectModel(ActionConfiguration actionConfiguration) { + if (actionConfiguration != null + && actionConfiguration.getFormData() != null + && actionConfiguration.getFormData().containsKey(GENERATE_CONTENT_MODEL)) { + return ((Map) actionConfiguration.getFormData().get(GENERATE_CONTENT_MODEL)).get(DATA); + } + // throw error if no model selected + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + "No generate content model is selected in the configuration"); + } + + @Override + public GoogleAIRequestDTO makeRequestBody(ActionConfiguration actionConfiguration) { + Map formData = actionConfiguration.getFormData(); + GoogleAIRequestDTO googleAIRequestDTO = new GoogleAIRequestDTO(); + List> messages = getMessages((Map) formData.get(MESSAGES)); + if (messages == null || messages.isEmpty()) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + String.format(STRING_APPENDER, EXECUTION_FAILURE, INCORRECT_MESSAGE_FORMAT)); + } + // as of today, we are going to support only text input to text output, so we will condense user messages in + // a single content parts of request body + List userQueryParts = new ArrayList<>(); + for (Map message : messages) { + if (message.containsKey(ROLE) && message.containsKey(TYPE) && message.containsKey(CONTENT)) { + String role = message.get(ROLE); + String type = message.get(TYPE); + String content = message.get(CONTENT); + if (content.isEmpty()) { + continue; + } + + if (Role.USER.getValue().equals(role) + && com.external.plugins.models.Type.TEXT.toString().equals(type)) { + userQueryParts.add(new GoogleAIRequestDTO.Part(content)); + } + } + } + // no content is configured completely + if (userQueryParts.isEmpty()) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + String.format(STRING_APPENDER, EXECUTION_FAILURE, INCORRECT_MESSAGE_FORMAT)); + } + googleAIRequestDTO.setContents(List.of(new GoogleAIRequestDTO.Content(Role.USER, userQueryParts))); + return googleAIRequestDTO; + } + + /** + * Place all necessary validation checks here + */ + @Override + public void validateRequest(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)); + } + + String model = RequestUtils.extractDataFromFormData(formData, GENERATE_CONTENT_MODEL); + if (!StringUtils.hasText(model)) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + String.format(STRING_APPENDER, EXECUTION_FAILURE, MODEL_NOT_SELECTED)); + } + if (!formData.containsKey(MESSAGES) || formData.get(MESSAGES) == null) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + String.format(STRING_APPENDER, EXECUTION_FAILURE, INCORRECT_MESSAGE_FORMAT)); + } + } + + 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); + } +} diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/commands/GoogleAICommand.java b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/commands/GoogleAICommand.java new file mode 100644 index 0000000000..ebbf24c41b --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/commands/GoogleAICommand.java @@ -0,0 +1,21 @@ +package com.external.plugins.commands; + +import com.appsmith.external.models.ActionConfiguration; +import com.external.plugins.models.GoogleAIRequestDTO; +import org.springframework.http.HttpMethod; + +import java.net.URI; + +public interface GoogleAICommand { + HttpMethod getTriggerHTTPMethod(); + + HttpMethod getExecutionMethod(); + + URI createTriggerUri(); + + URI createExecutionUri(ActionConfiguration actionConfiguration); + + GoogleAIRequestDTO makeRequestBody(ActionConfiguration actionConfiguration); + + void validateRequest(ActionConfiguration actionConfiguration); +} diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/constants/GoogleAIConstants.java b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/constants/GoogleAIConstants.java new file mode 100644 index 0000000000..b2f6bd2caf --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/constants/GoogleAIConstants.java @@ -0,0 +1,32 @@ +package com.external.plugins.constants; + +import org.springframework.web.reactive.function.client.ExchangeStrategies; + +import java.util.List; + +public class GoogleAIConstants { + public static final String GOOGLE_AI_API_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta"; + public static final String MODELS = "/models"; + public static final String GENERATE_CONTENT_MODELS = "GENERATE_CONTENT_MODELS"; + public static final String GENERATE_CONTENT = "GENERATE_CONTENT"; + public static final String GENERATE_CONTENT_MODEL = "generateContentModel"; + public static final String GENERATE_CONTENT_ACTION = ":generateContent"; + 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 TYPE = "type"; + public static final String CONTENT = "content"; + public static final String KEY = "key"; + public static final String MESSAGES = "messages"; + public static final String LABEL = "label"; + public static final String VALUE = "value"; + public static final List GOOGLE_AI_MODELS = List.of("gemini-pro"); + 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/googleAiPlugin/src/main/java/com/external/plugins/constants/GoogleAIErrorMessages.java b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/constants/GoogleAIErrorMessages.java new file mode 100644 index 0000000000..b569a4d7ad --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/constants/GoogleAIErrorMessages.java @@ -0,0 +1,14 @@ +package com.external.plugins.constants; + +public class GoogleAIErrorMessages { + 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 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/googleAiPlugin/src/main/java/com/external/plugins/models/GoogleAIRequestDTO.java b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/models/GoogleAIRequestDTO.java new file mode 100644 index 0000000000..9307abdb5d --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/models/GoogleAIRequestDTO.java @@ -0,0 +1,30 @@ +package com.external.plugins.models; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class GoogleAIRequestDTO { + List contents; + + @Data + @AllArgsConstructor + public static class Content { + Role role; + List parts; + } + + @Data + @AllArgsConstructor + public static class Part { + String text; + } +} diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/models/Role.java b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/models/Role.java new file mode 100644 index 0000000000..ee2f1fb821 --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/models/Role.java @@ -0,0 +1,17 @@ +package com.external.plugins.models; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum Role { + USER("user"); + + private final String value; + + @Override + public String toString() { + return value; + } +} diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/models/Type.java b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/models/Type.java new file mode 100644 index 0000000000..e5516fc6ce --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/models/Type.java @@ -0,0 +1,16 @@ +package com.external.plugins.models; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum Type { + TEXT("text"); + private final String value; + + @Override + public String toString() { + return this.value; + } +} diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/utils/GoogleAIMethodStrategy.java b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/utils/GoogleAIMethodStrategy.java new file mode 100644 index 0000000000..5e9c4d3e0b --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/utils/GoogleAIMethodStrategy.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.GenerateContentCommand; +import com.external.plugins.commands.GoogleAICommand; +import com.external.plugins.constants.GoogleAIConstants; +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 GoogleAIMethodStrategy { + public static GoogleAICommand selectTriggerMethod(TriggerRequestDTO triggerRequestDTO, Gson gson) { + String requestType = triggerRequestDTO.getRequestType(); + + return switch (requestType) { + case GoogleAIConstants.GENERATE_CONTENT_MODELS -> new GenerateContentCommand(); + default -> throw Exceptions.propagate( + new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR)); + }; + } + + public static GoogleAICommand 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, GoogleAIConstants.COMMAND); + + return selectExecutionMethod(command); + } + + public static GoogleAICommand selectExecutionMethod(String command) { + return switch (command) { + case GoogleAIConstants.GENERATE_CONTENT -> new GenerateContentCommand(); + default -> throw Exceptions.propagate(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Unsupported command: " + command)); + }; + } +} diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java new file mode 100644 index 0000000000..048ca6c185 --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java @@ -0,0 +1,107 @@ +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.GoogleAIConstants; +import com.external.plugins.constants.GoogleAIErrorMessages; +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 org.springframework.web.util.UriComponentsBuilder; +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.GoogleAIConstants.GENERATE_CONTENT_ACTION; +import static com.external.plugins.constants.GoogleAIConstants.GOOGLE_AI_API_ENDPOINT; +import static com.external.plugins.constants.GoogleAIConstants.KEY; +import static com.external.plugins.constants.GoogleAIConstants.MODELS; + +public class RequestUtils { + + private static final WebClient webClient = createWebClient(); + + public static String extractDataFromFormData(Map formData, String key) { + return (String) ((Map) formData.get(key)).get(GoogleAIConstants.DATA); + } + + public static String extractValueFromFormData(Map formData, String key) { + return (String) formData.get(key); + } + + public static URI createUriFromCommand(String command, String model) { + if (GoogleAIConstants.GENERATE_CONTENT.equals(command)) { + return URI.create(GOOGLE_AI_API_ENDPOINT + MODELS + "/" + model + GENERATE_CONTENT_ACTION); + } else { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Unsupported command: " + command); + } + } + + public static Mono> makeRequest( + HttpMethod httpMethod, URI uri, ApiKeyAuth apiKeyAuth, BodyInserter body) { + + if (!StringUtils.hasText(apiKeyAuth.getValue())) { + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, GoogleAIErrorMessages.EMPTY_API_KEY); + } + + return webClient + .method(httpMethod) + .uri(appendKeyInUri(apiKeyAuth.getValue(), uri)) + .contentType(MediaType.APPLICATION_JSON) + .body(body) + .exchangeToMono(clientResponse -> clientResponse.toEntity(byte[].class)); + } + + /** + * Add key query params in requests + * @param apiKey - Google AI API Key + * @param uri - Actual request URI + */ + private static URI appendKeyInUri(String apiKey, URI uri) { + return UriComponentsBuilder.fromUri(uri).queryParam(KEY, apiKey).build().toUri(); + } + + private static WebClient createWebClient() { + // Initializing webClient to be used for http call + WebClient.Builder webClientBuilder = WebClient.builder(); + return webClientBuilder + .exchangeStrategies(GoogleAIConstants.EXCHANGE_STRATEGIES) + .clientConnector(new ReactorClientHttpConnector(HttpClient.create(connectionProvider()))) + .build(); + } + + private static ConnectionProvider connectionProvider() { + return ConnectionProvider.builder("googleAi") + .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(GoogleAIErrorMessages.EMPTY_API_KEY); + } + + return invalids; + } +} diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/editor/generate.json b/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/editor/generate.json new file mode 100644 index 0000000000..fc7442f2ea --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/editor/generate.json @@ -0,0 +1,79 @@ +{ + "identifier": "CHAT", + "controlType": "SECTION", + "conditionals": { + "show": "{{actionConfiguration.formData.command.data === 'GENERATE_CONTENT'}}" + }, + "children": [ + { + "label": "Models", + "tooltipText": "Select the model for content generation", + "subtitle": "ID of the model to use.", + "isRequired": true, + "propertyName": "generate_content_model_id", + "configProperty": "actionConfiguration.formData.generateContentModel.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 === 'GENERATE_CONTENT'}}", + "config": { + "params": { + "requestType": "GENERATE_CONTENT_MODELS", + "displayType": "DROP_DOWN" + } + } + } + } + }, + { + "label": "Messages", + "tooltipText": "Ask a question", + "subtitle": "A list of messages to generate the content", + "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": "user", + "options": [ + { + "label": "User", + "value": "user" + } + ] + }, + { + "label": "Type", + "key": "type", + "controlType": "DROP_DOWN", + "initialValue": "text", + "options": [ + { + "label": "Text", + "value": "text" + } + ] + }, + { + "label": "Content", + "key": "content", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "placeholderText": "{{ UserInput.text }}" + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/editor/root.json b/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/editor/root.json new file mode 100644 index 0000000000..60d41f8747 --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/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": "GENERATE_CONTENT", + "options": [ + { + "label": "Generate Content", + "value": "GENERATE_CONTENT" + } + ] + } + ] + } + ], + "files": [ + "generate.json" + ] +} diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/form.json new file mode 100644 index 0000000000..9f992ffe25 --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/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", + "setFirstOptionAsDefault": true, + "options": [ + { + "label": "API Key", + "value": "apiKey" + } + ], + "isRequired": 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://generativelanguage.googleapis.com/", + "isRequired": true, + "hidden": true + } + ] + } + ], + "formButton" : ["TEST", "CANCEL", "SAVE"] +} diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/plugin.properties b/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/plugin.properties new file mode 100644 index 0000000000..6231319a90 --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=googleai-plugin +plugin.class=com.external.plugins.GoogleAiPlugin +plugin.version=1.0-SNAPSHOT +plugin.provider=tech@appsmith.com +plugin.dependencies= diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/setting.json b/app/server/appsmith-plugins/googleAiPlugin/src/main/resources/setting.json new file mode 100644 index 0000000000..4dd710a3bc --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/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/googleAiPlugin/src/test/java/com/external/plugins/GenerateContentCommandTest.java b/app/server/appsmith-plugins/googleAiPlugin/src/test/java/com/external/plugins/GenerateContentCommandTest.java new file mode 100644 index 0000000000..a7178c683b --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/test/java/com/external/plugins/GenerateContentCommandTest.java @@ -0,0 +1,57 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.external.plugins.commands.GenerateContentCommand; +import com.external.plugins.models.GoogleAIRequestDTO; +import com.external.plugins.models.Role; +import com.external.plugins.models.Type; +import com.google.gson.Gson; +import org.junit.jupiter.api.Assertions; +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.GoogleAIConstants.CONTENT; +import static com.external.plugins.constants.GoogleAIConstants.DATA; +import static com.external.plugins.constants.GoogleAIConstants.GENERATE_CONTENT_MODEL; +import static com.external.plugins.constants.GoogleAIConstants.MESSAGES; +import static com.external.plugins.constants.GoogleAIConstants.ROLE; +import static com.external.plugins.constants.GoogleAIConstants.TYPE; + +public class GenerateContentCommandTest { + GenerateContentCommand generateContentCommand = new GenerateContentCommand(); + + @Test + public void testCreateTriggerUri() { + Assertions.assertEquals(URI.create(""), generateContentCommand.createTriggerUri()); + } + + @Test + public void testCreateExecutionUri() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setFormData(Map.of(GENERATE_CONTENT_MODEL, Map.of(DATA, "gemini-pro"))); + URI uri = generateContentCommand.createExecutionUri(actionConfiguration); + Assertions.assertEquals( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent", uri.toString()); + } + + @Test + public void testMakeRequestBody_withValidData() { + // Test with valid form data + ActionConfiguration actionConfiguration = new ActionConfiguration(); + Map formData = new HashMap<>(); + Map message = new HashMap<>(); + message.put(ROLE, Role.USER.toString()); + message.put(TYPE, Type.TEXT.toString()); + message.put(CONTENT, "Hello Gemini"); + formData.put(MESSAGES, Map.of(DATA, List.of(message))); + actionConfiguration.setFormData(formData); + GoogleAIRequestDTO googleAIRequestDTO = generateContentCommand.makeRequestBody(actionConfiguration); + Assertions.assertEquals( + "{\"contents\":[{\"role\":\"USER\",\"parts\":[{\"text\":\"Hello Gemini\"}]}]}", + new Gson().toJson(googleAIRequestDTO)); + } +} diff --git a/app/server/appsmith-plugins/googleAiPlugin/src/test/java/com/external/plugins/GoogleAiPluginTest.java b/app/server/appsmith-plugins/googleAiPlugin/src/test/java/com/external/plugins/GoogleAiPluginTest.java new file mode 100644 index 0000000000..dd8b0b757f --- /dev/null +++ b/app/server/appsmith-plugins/googleAiPlugin/src/test/java/com/external/plugins/GoogleAiPluginTest.java @@ -0,0 +1,134 @@ +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.GoogleAIConstants; +import com.external.plugins.constants.GoogleAIErrorMessages; +import mockwebserver3.MockResponse; +import mockwebserver3.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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.GoogleAIConstants.LABEL; +import static com.external.plugins.constants.GoogleAIConstants.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 GoogleAiPluginTest { + 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 ""; + } + } + + GoogleAiPlugin.GoogleAiPluginExecutor pluginExecutor = + new GoogleAiPlugin.GoogleAiPluginExecutor(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(GoogleAIErrorMessages.EMPTY_API_KEY)); + } + + @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 = "[\"gemini-pro\"]"; + MockResponse mockResponse = new MockResponse().setBody(responseBody); + mockResponse.setResponseCode(200); + mockEndpoint.enqueue(mockResponse); + + TriggerRequestDTO request = new TriggerRequestDTO(); + request.setRequestType(GoogleAIConstants.GENERATE_CONTENT_MODEL); + Mono datasourceTriggerResultMono = + pluginExecutor.trigger(null, datasourceConfiguration, request); + + StepVerifier.create(datasourceTriggerResultMono) + .assertNext(result -> { + assertTrue(result.getTrigger() instanceof List); + assertEquals(((List) result.getTrigger()).size(), 1); + assertEquals(result.getTrigger(), getDataToMap(List.of("gemini-pro"))); + }) + .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/pom.xml b/app/server/appsmith-plugins/pom.xml index 7abd23a8b6..2d2a6b8b9c 100644 --- a/app/server/appsmith-plugins/pom.xml +++ b/app/server/appsmith-plugins/pom.xml @@ -62,6 +62,7 @@ openAiPlugin anthropicPlugin + googleAiPlugin diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration038AddGoogleAIPlugin.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration038AddGoogleAIPlugin.java new file mode 100644 index 0000000000..a0ebb648d1 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration038AddGoogleAIPlugin.java @@ -0,0 +1,52 @@ +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 = "038", id = "add-google-ai-plugin", author = " ") +public class Migration038AddGoogleAIPlugin { + private final MongoTemplate mongoTemplate; + + public Migration038AddGoogleAIPlugin(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + @RollbackExecution + public void rollbackExecution() {} + + @Execution + public void addPluginToDbAndWorkspace() { + Plugin plugin = new Plugin(); + plugin.setName(PluginConstants.PluginName.GOOGLE_AI_PLUGIN_NAME); + plugin.setType(PluginType.AI); + plugin.setPluginName(PluginConstants.PluginName.GOOGLE_AI_PLUGIN_NAME); + plugin.setPackageName(PluginConstants.PackageName.GOOGLE_AI_PLUGIN); + plugin.setUiComponent("UQIDbEditorForm"); + plugin.setDatasourceComponent("DbEditorForm"); + plugin.setResponseType(Plugin.ResponseType.JSON); + plugin.setIconLocation("https://assets.appsmith.com/google-ai.svg"); + plugin.setDocumentationLink("https://docs.appsmith.com/connect-data/reference/google-ai"); + plugin.setDefaultInstall(true); + try { + mongoTemplate.insert(plugin); + } catch (DuplicateKeyException e) { + log.warn(plugin.getPackageName() + " already present in database."); + } + + if (plugin.getId() == null) { + log.error("Failed to insert the Google AI plugin into the database."); + } + + installPluginToAllWorkspaces(mongoTemplate, plugin.getId()); + } +}