feat: Google AI integration (#29620)

## Description
Here's PR for adding Google AI Gemini model as a data source
integration.
Features:
1. Text generation based on text inputs

Fixes https://github.com/appsmithorg/appsmith/issues/29621

#### Type of change
- New feature (non-breaking change which adds functionality)

## Testing

#### How Has This Been Tested?
- [x] Manual
- [x] JUnit

## 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

## Summary by CodeRabbit

- **New Features**
	- Integrated Google AI plugin for advanced AI functionality.
	- Added Google AI plugin to the available plugins.
	- Implemented new commands and utilities for Google AI services.

- **Enhancements**
	- Expanded plugin constants to include Google AI references.
	- Developed a method strategy for Google AI plugin execution.

- **Documentation**
	- Updated plugin properties to include Google AI plugin details.

- **Database Changes**
- Performed a database migration to add the Google AI plugin to existing
workspaces.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Nirmal Sarswat 2023-12-14 23:26:46 +05:30 committed by GitHub
parent c1487a6125
commit b4441969d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1183 additions and 0 deletions

View File

@ -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 {

View 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>googleAiPlugin</artifactId>
<version>1.0-SNAPSHOT</version>
<name>googleAiPlugin</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>

View File

@ -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<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));
}
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<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);
// 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<TriggerResultDTO> trigger(
APIConnection connection, DatasourceConfiguration datasourceConfiguration, TriggerRequestDTO request) {
return Mono.just(new TriggerResultDTO(getDataToMap(GoogleAIConstants.GOOGLE_AI_MODELS)));
}
@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());
}
}
}

View File

@ -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<String, String>) 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<String, Object> formData = actionConfiguration.getFormData();
GoogleAIRequestDTO googleAIRequestDTO = new GoogleAIRequestDTO();
List<Map<String, String>> messages = getMessages((Map<String, Object>) 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<GoogleAIRequestDTO.Part> userQueryParts = new ArrayList<>();
for (Map<String, String> 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<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));
}
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<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);
}
}

View File

@ -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);
}

View File

@ -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<String> 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";
}

View File

@ -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";
}

View File

@ -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<Content> contents;
@Data
@AllArgsConstructor
public static class Content {
Role role;
List<Part> parts;
}
@Data
@AllArgsConstructor
public static class Part {
String text;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<String, Object> 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));
};
}
}

View File

@ -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<String, Object> formData, String key) {
return (String) ((Map<String, Object>) formData.get(key)).get(GoogleAIConstants.DATA);
}
public static String extractValueFromFormData(Map<String, Object> 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<ResponseEntity<byte[]>> makeRequest(
HttpMethod httpMethod, URI uri, ApiKeyAuth apiKeyAuth, BodyInserter<?, ? super ClientHttpRequest> 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<String> validateApiKeyAuthDatasource(DatasourceConfiguration datasourceConfiguration) {
Set<String> invalids = new HashSet<>();
final ApiKeyAuth apiKeyAuth = (ApiKeyAuth) datasourceConfiguration.getAuthentication();
if (apiKeyAuth == null || !StringUtils.hasText(apiKeyAuth.getValue())) {
invalids.add(GoogleAIErrorMessages.EMPTY_API_KEY);
}
return invalids;
}
}

View File

@ -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 }}"
}
]
}
]
}

View File

@ -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"
]
}

View File

@ -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"]
}

View File

@ -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=

View File

@ -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"
}
]
}
]
}

View File

@ -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<String, Object> formData = new HashMap<>();
Map<String, Object> 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));
}
}

View File

@ -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<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(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<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 = "[\"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<TriggerResultDTO> 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<Map<String, String>> getDataToMap(List<String> data) {
return data.stream().sorted().map(x -> Map.of(LABEL, x, VALUE, x)).collect(Collectors.toList());
}
}

View File

@ -62,6 +62,7 @@
<module>openAiPlugin</module>
<module>anthropicPlugin</module>
<module>googleAiPlugin</module>
</modules>

View File

@ -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());
}
}