Create assistant for Gerrit project
When a patchset or comment event triggers and no assistant has been
established yet for a specific Gerrit project in stateful mode, the
following steps are taken:
1. The related Git repository is transformed into a JSON file and
uploaded to ChatGPT.
2. A new assistant associated with the JSON file is created.
Change-Id: I49c3f1580979eb49655f22aeca3cd564e9d90dfe
Signed-off-by: Patrizio <patrizio.gelosi@amarulasolutions.com>
diff --git a/.gitignore b/.gitignore
index 2f0efc9..c81f16a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+dependency-reduced-pom.xml
.idea/
/target
/.classpath
diff --git a/pom.xml b/pom.xml
index cdfc0e4..6f05725 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,6 +63,66 @@
<version>2.22.2</version>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>3.4.1</version>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <createDependencyReducedPom>true</createDependencyReducedPom>
+ <artifactSet>
+ <includes>
+ <include>com.squareup.*</include>
+ <include>org.jetbrains*</include>
+ </includes>
+ </artifactSet>
+ <relocations>
+ <relocation>
+ <pattern>okhttp3</pattern>
+ <shadedPattern>com.googlesource.gerrit.plugins.chatgpt.okhttp3</shadedPattern>
+ </relocation>
+ <relocation>
+ <pattern>okio</pattern>
+ <shadedPattern>com.googlesource.gerrit.plugins.chatgpt.okio</shadedPattern>
+ </relocation>
+ </relocations>
+ <filters>
+ <filter>
+ <!-- Do not include files that should not be shaded -->
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*.SF</exclude>
+ <exclude>META-INF/*.DSA</exclude>
+ <exclude>META-INF/*.RSA</exclude>
+ <exclude>META-INF/*.MF</exclude>
+ <exclude>META-INF/license/*</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ <transformers>
+ <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+ <manifestEntries>
+ <Gerrit-PluginName>chatgpt-code-review-gerrit-plugin</Gerrit-PluginName>
+ <Gerrit-Module>com.googlesource.gerrit.plugins.chatgpt.Module</Gerrit-Module>
+ <Implementation-Vendor>Amarula</Implementation-Vendor>
+ <Implementation-URL>https://github.com/amarula/chatgpt-code-review-gerrit-plugin</Implementation-URL>
+ <Implementation-Title>ChatGPT Code Review Gerrit Plugin</Implementation-Title>
+ <Implementation-Version>${project.version}</Implementation-Version>
+ <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
+ <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
+ </manifestEntries>
+ </transformer>
+ </transformers>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
</plugins>
</build>
@@ -108,6 +168,11 @@
<version>2.27.2</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp</artifactId>
+ <version>4.1.0</version>
+ </dependency>
</dependencies>
</project>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/ProjectDataHandler.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/ProjectDataHandler.java
new file mode 100644
index 0000000..9df9944
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/ProjectDataHandler.java
@@ -0,0 +1,30 @@
+package com.googlesource.gerrit.plugins.chatgpt.data;
+
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class ProjectDataHandler {
+ private static PluginDataHandler pluginDataHandler;
+
+ public static void createNewInstance(PluginDataHandler loadedPluginDataHandler) {
+ pluginDataHandler = loadedPluginDataHandler;
+ }
+
+ public static synchronized void setValue(GerritChange change, String key, String value) {
+ pluginDataHandler.setValue(getProjectKey(change, key), value);
+ }
+
+ public static String getValue(GerritChange change, String key) {
+ return pluginDataHandler.getValue(getProjectKey(change, key));
+ }
+
+ public static synchronized void removeValue(GerritChange change, String key) {
+ pluginDataHandler.removeValue(getProjectKey(change, key));
+ }
+
+ private static String getProjectKey(GerritChange change, String key) {
+ return change.getProjectName() + "." + key;
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java
index eca28cc..c9849e4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java
@@ -11,9 +11,12 @@
import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
import com.googlesource.gerrit.plugins.chatgpt.data.ChangeSetDataHandler;
import com.googlesource.gerrit.plugins.chatgpt.data.PluginDataHandler;
+import com.googlesource.gerrit.plugins.chatgpt.data.ProjectDataHandler;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClient;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.data.ChangeSetData;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFilesHandler;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@@ -53,16 +56,23 @@
addShutdownHoot();
}
- public void initialize(GerritChange change) {
+ public void initialize(GerritChange change, GitRepoFiles gitRepoFiles, PluginDataHandler pluginDataHandler) {
gerritClient.initialize(config, change);
Integer gptAccountId = gerritClient.getNotNullAccountId(change, config.getGerritUserName());
changeSetData = ChangeSetDataHandler.getNewInstance(config, change, gptAccountId);
+ GitRepoFilesHandler.createNewInstance(gitRepoFiles);
+ ProjectDataHandler.createNewInstance(pluginDataHandler);
}
- public void handleEvent(Configuration config, Event event, PluginDataHandler pluginDataHandler) {
+ public void handleEvent(
+ Configuration config,
+ Event event,
+ GitRepoFiles gitRepoFiles,
+ PluginDataHandler pluginDataHandler
+ ) {
this.config = config;
GerritChange change = new GerritChange(event);
- initialize(change);
+ initialize(change, gitRepoFiles, pluginDataHandler);
if (!preProcessEvent(change)) {
destroy(change);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/GerritListener.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/GerritListener.java
index 1142806..15bcffb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/GerritListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/GerritListener.java
@@ -11,19 +11,26 @@
import com.googlesource.gerrit.plugins.chatgpt.config.ConfigCreator;
import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
import com.googlesource.gerrit.plugins.chatgpt.data.PluginDataHandler;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class GerritListener implements EventListener {
private final ConfigCreator configCreator;
private final EventListenerHandler eventListenerHandler;
+ private final GitRepoFiles gitRepoFiles;
private final PluginDataHandler pluginDataHandler;
@Inject
- public GerritListener(ConfigCreator configCreator, EventListenerHandler eventListenerHandler, PluginDataHandler
- pluginDataHandler) {
+ public GerritListener(
+ ConfigCreator configCreator,
+ EventListenerHandler eventListenerHandler,
+ GitRepoFiles gitRepoFiles,
+ PluginDataHandler pluginDataHandler
+ ) {
this.configCreator = configCreator;
this.eventListenerHandler = eventListenerHandler;
+ this.gitRepoFiles = gitRepoFiles;
this.pluginDataHandler = pluginDataHandler;
}
@@ -40,7 +47,7 @@
try {
Configuration config = configCreator.createConfig(projectNameKey);
- eventListenerHandler.handleEvent(config, patchSetEvent, pluginDataHandler);
+ eventListenerHandler.handleEvent(config, patchSetEvent, gitRepoFiles, pluginDataHandler);
} catch (NoSuchProjectException e) {
log.error("Project not found: {}", projectNameKey, e);
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritChange.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritChange.java
index 2d15939..d33b3ed 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritChange.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritChange.java
@@ -25,8 +25,10 @@
private BranchNameKey branchNameKey;
private Change.Key changeKey;
private String fullChangeId;
+ // "Boolean" is used instead of "boolean" to have "getIsCommentEvent" instead of "isCommentEvent" as getter method
+ // (due to Lombok's magic naming convention)
@Setter
- private Boolean isCommentEvent;
+ private Boolean isCommentEvent = false;
public GerritChange(Project.NameKey projectNameKey, BranchNameKey branchNameKey, Change.Key changeKey) {
this.projectNameKey = projectNameKey;
@@ -60,6 +62,10 @@
}
}
+ public String getProjectName() {
+ return getProjectNameKey().toString();
+ }
+
private void buildFullChangeId() {
fullChangeId = String.join("~", URLEncoder.encode(projectNameKey.get(), StandardCharsets.UTF_8),
branchNameKey.shortName(), changeKey.get());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritClientFacade.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritClientFacade.java
index aebf0f0..5e872d1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritClientFacade.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/gerrit/GerritClientFacade.java
@@ -6,6 +6,7 @@
import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.gerrit.GerritPermittedVotingRange;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.data.GerritClientData;
import com.googlesource.gerrit.plugins.chatgpt.mode.stateless.client.api.gerrit.GerritClientPatchSetStateless;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.gerrit.GerritClientPatchSetStateful;
import com.googlesource.gerrit.plugins.chatgpt.mode.interfaces.client.api.gerrit.IGerritClientPatchSet;
import lombok.extern.slf4j.Slf4j;
@@ -23,7 +24,7 @@
gerritClientDetail = new GerritClientDetail(config);
gerritClientPatchSet = (IGerritClientPatchSet) ModeClassLoader.getInstance(
"client.api.gerrit.GerritClientPatchSet", config, config);
- registerDynamicClasses(GerritClientPatchSetStateless.class);
+ registerDynamicClasses(GerritClientPatchSetStateless.class, GerritClientPatchSetStateful.class);
gerritClientComments = new GerritClientComments(config);
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/http/HttpClient.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/http/HttpClient.java
new file mode 100644
index 0000000..cf7b88b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/http/HttpClient.java
@@ -0,0 +1,62 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.common.client.http;
+
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+
+import java.io.IOException;
+import java.util.Map;
+
+import static com.googlesource.gerrit.plugins.chatgpt.utils.GsonUtils.getGson;
+
+@Slf4j
+public class HttpClient {
+ private final OkHttpClient client = new OkHttpClient();
+
+ public String execute(Request request) {
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+ log.debug("HttpClient Response body: {}", response.body());
+ if (response.body() != null) {
+ return response.body().string();
+ }
+ else {
+ log.error("Request {} returned an empty string", request);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return null;
+ }
+
+ public Request createRequest(String uri, String bearer, RequestBody body, Map<String, String> additionalHeaders) {
+ Request.Builder builder = new Request.Builder()
+ .url(uri)
+ .header("Authorization", "Bearer " + bearer)
+ .post(body);
+
+ if (additionalHeaders != null) {
+ for (Map.Entry<String, String> header : additionalHeaders.entrySet()) {
+ builder.header(header.getKey(), header.getValue());
+ }
+ }
+ return builder.build();
+ }
+
+ public Request createRequest(String uri, String bearer, RequestBody body) {
+ return createRequest(uri, bearer, body, null);
+ }
+
+ public Request createRequestFromJson(String uri, String bearer, Object requestObject,
+ Map<String, String> additionalHeaders) {
+ String bodyJson = getGson().toJson(requestObject);
+ log.info("ChatGPT request body: {}", bodyJson);
+ RequestBody body = RequestBody.create(bodyJson, MediaType.get("application/json"));
+
+ return createRequest(uri, bearer, body, additionalHeaders);
+ }
+
+ public Request createRequestFromJson(String uri, String bearer, Object requestObject) {
+ return createRequestFromJson(uri, bearer, requestObject, null);
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/prompt/ChatGptPrompt.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/prompt/ChatGptPrompt.java
index c130996..6af9e39 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/prompt/ChatGptPrompt.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/prompt/ChatGptPrompt.java
@@ -113,4 +113,36 @@
);
}
+ public String getPatchSetReviewUserPrompt() {
+ List<String> attributes = new ArrayList<>(PATCH_SET_REVIEW_REPLY_ATTRIBUTES);
+ if (config.isVotingEnabled() || config.getFilterNegativeComments()) {
+ updateScoreDescription();
+ }
+ else {
+ attributes.remove(ATTRIBUTE_SCORE);
+ }
+ updateRelevanceDescription();
+ return buildFieldSpecifications(attributes) + SPACE +
+ DEFAULT_GPT_REPLIES_PROMPT_INLINE;
+ }
+
+
+ private void updateScoreDescription() {
+ String scoreDescription = DEFAULT_GPT_REPLIES_ATTRIBUTES.get(ATTRIBUTE_SCORE);
+ if (scoreDescription.contains("%d")) {
+ scoreDescription = String.format(scoreDescription, config.getVotingMinScore(), config.getVotingMaxScore());
+ DEFAULT_GPT_REPLIES_ATTRIBUTES.put(ATTRIBUTE_SCORE, scoreDescription);
+ }
+ }
+
+ private void updateRelevanceDescription() {
+ String relevanceDescription = DEFAULT_GPT_REPLIES_ATTRIBUTES.get(ATTRIBUTE_RELEVANCE);
+ if (relevanceDescription.contains("%s")) {
+ String defaultGptRelevanceRules = config.getString(Configuration.KEY_GPT_RELEVANCE_RULES,
+ DEFAULT_GPT_RELEVANCE_RULES);
+ relevanceDescription = String.format(relevanceDescription, defaultGptRelevanceRules);
+ DEFAULT_GPT_REPLIES_ATTRIBUTES.put(ATTRIBUTE_RELEVANCE, relevanceDescription);
+ }
+ }
+
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptTool.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptTool.java
index 94f0845..2b501b3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptTool.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptTool.java
@@ -1,11 +1,15 @@
package com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt;
import lombok.Data;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
import java.util.List;
+@RequiredArgsConstructor
@Data
public class ChatGptTool {
+ @NonNull
private String type;
private Function function;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/UriResourceLocatorStateful.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/UriResourceLocatorStateful.java
new file mode 100644
index 0000000..87fc0f3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/UriResourceLocatorStateful.java
@@ -0,0 +1,14 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api;
+
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.UriResourceLocator;
+
+public class UriResourceLocatorStateful extends UriResourceLocator {
+ public static String chatCreateFilesUri() {
+ return "/v1/files";
+ }
+
+ public static String chatCreateAssistantsUri() {
+ return "/v1/assistants";
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistant.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistant.java
new file mode 100644
index 0000000..d8cc4de
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistant.java
@@ -0,0 +1,99 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt;
+
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.data.ProjectDataHandler;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.ClientBase;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.chatgpt.ChatGptTools;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.http.HttpClient;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptTool;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.UriResourceLocatorStateful;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFilesHandler;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.prompt.ChatGptPromptStateful;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt.ChatGptCreateAssistantResponse;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt.ChatGptFilesResponse;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt.ChatGptCreateAssistantRequestBody;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateless.client.api.chatgpt.ChatGptParameters;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.Request;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.util.Map;
+
+import static com.googlesource.gerrit.plugins.chatgpt.utils.FileUtils.createTempFileWithContent;
+import static com.googlesource.gerrit.plugins.chatgpt.utils.GsonUtils.getGson;
+
+@Slf4j
+public class ChatGptAssistant extends ClientBase {
+ public static final String KEY_FILE_ID = "fileId";
+ public static final String KEY_ASSISTANT_ID = "assistantId";
+ public static final String CODE_INTERPRETER_TOOL_TYPE = "code_interpreter";
+
+ private final HttpClient httpClient = new HttpClient();
+ private final GerritChange change;
+
+ public ChatGptAssistant(Configuration config, GerritChange change) {
+ super(config);
+ this.change = change;
+ }
+
+ public void setupAssistant() {
+ String assistantId = ProjectDataHandler.getValue(change, KEY_ASSISTANT_ID);
+ if (assistantId == null) {
+ String fileId = uploadRepoFiles();
+ ProjectDataHandler.setValue(change, KEY_FILE_ID, fileId);
+ assistantId = createAssistant(fileId);
+ ProjectDataHandler.setValue(change, KEY_ASSISTANT_ID, assistantId);
+ log.info("Project assistant created with ID: {}", assistantId);
+ }
+ else {
+ log.info("Project assistant found for the project. Assistant ID: {}", assistantId);
+ }
+ }
+
+ private String uploadRepoFiles() {
+ String repoFiles = GitRepoFilesHandler.getInstance().getGitRepoFiles(change);
+ Path repoPath = createTempFileWithContent(change.getProjectName(), ".json", repoFiles);
+ ChatGptFiles chatGptFiles = new ChatGptFiles(config);
+ ChatGptFilesResponse chatGptFilesResponse = chatGptFiles.uploadFiles(repoPath);
+
+ return chatGptFilesResponse.getId();
+ }
+
+ private String createAssistant(String fileId) {
+ Request request = createRequest(fileId);
+ log.debug("ChatGPT Create Assistant request: {}", request);
+
+ ChatGptCreateAssistantResponse assistantResponse = getGson().fromJson(httpClient.execute(request),
+ ChatGptCreateAssistantResponse.class);
+
+ log.debug("Assistant created: {}", assistantResponse);
+
+ return assistantResponse.getId();
+ }
+
+ private Request createRequest(String fileId) {
+ URI uri = URI.create(config.getGptDomain() + UriResourceLocatorStateful.chatCreateAssistantsUri());
+ log.debug("ChatGPT Create Assistant request URI: {}", uri);
+ Map<String, String> additionalHeaders = Map.of("OpenAI-Beta", "assistants=v1");
+ ChatGptPromptStateful chatGptPromptStateful = new ChatGptPromptStateful(config, change);
+ ChatGptParameters chatGptParameters = new ChatGptParameters(config, change.getIsCommentEvent());
+ ChatGptTool[] tools = new ChatGptTool[] {
+ new ChatGptTool(CODE_INTERPRETER_TOOL_TYPE),
+ ChatGptTools.retrieveFormatRepliesTool()
+ };
+ ChatGptCreateAssistantRequestBody requestBody = ChatGptCreateAssistantRequestBody.builder()
+ .name(ChatGptPromptStateful.DEFAULT_GPT_ASSISTANT_NAME)
+ .description(chatGptPromptStateful.getDefaultGptAssistantDescription())
+ .instructions(chatGptPromptStateful.getDefaultGptAssistantInstructions())
+ .model(config.getGptModel())
+ .temperature(chatGptParameters.getGptTemperature())
+ .fileIds(new String[]{fileId})
+ .tools(tools)
+ .build();
+
+ return httpClient.createRequestFromJson(uri.toString(), config.getGptToken(), requestBody, additionalHeaders);
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptClientStateful.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptClientStateful.java
new file mode 100644
index 0000000..9e72e98
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptClientStateful.java
@@ -0,0 +1,30 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt;
+
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import com.googlesource.gerrit.plugins.chatgpt.mode.interfaces.client.api.chatgpt.IChatGptClient;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Singleton
+public class ChatGptClientStateful implements IChatGptClient {
+ @Override
+ public String ask(Configuration config, String changeId, String patchSet) throws Exception {
+ // Placeholder implementation, change to actual logic later.
+ throw new UnsupportedOperationException("Method not implemented yet.");
+ }
+
+ @Override
+ public String ask(Configuration config, GerritChange change, String patchSet) throws Exception {
+ // Placeholder implementation, change to actual logic later.
+ throw new UnsupportedOperationException("Method not implemented yet.");
+ }
+
+ @Override
+ public String getRequestBody() {
+ // Placeholder implementation, change to actual logic later.
+ return "Method not implemented yet.";
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptFiles.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptFiles.java
new file mode 100644
index 0000000..2cbde38
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptFiles.java
@@ -0,0 +1,49 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt;
+
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.ClientBase;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.http.HttpClient;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.UriResourceLocatorStateful;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt.ChatGptFilesResponse;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.file.Path;
+
+import static com.googlesource.gerrit.plugins.chatgpt.utils.GsonUtils.getGson;
+
+@Slf4j
+public class ChatGptFiles extends ClientBase {
+ private final HttpClient httpClient = new HttpClient();
+
+ public ChatGptFiles(Configuration config) {
+ super(config);
+ }
+
+ public ChatGptFilesResponse uploadFiles(Path repoPath) {
+ Request request = createUploadFileRequest(repoPath);
+ log.debug("ChatGPT Upload Files request: {}", request);
+
+ String response = httpClient.execute(request);
+ log.debug("ChatGPT Upload Files response: {}", response);
+
+ return getGson().fromJson(response, ChatGptFilesResponse.class);
+ }
+
+ private Request createUploadFileRequest(Path repoPath) {
+ URI uri = URI.create(config.getGptDomain() + UriResourceLocatorStateful.chatCreateFilesUri());
+ log.debug("ChatGPT Upload Files request URI: {}", uri);
+ File file = repoPath.toFile();
+ RequestBody requestBody = new MultipartBody.Builder()
+ .setType(MultipartBody.FORM)
+ .addFormDataPart("purpose", "assistants")
+ .addFormDataPart("file", file.getName(),
+ RequestBody.create(file, MediaType.parse("application/json")))
+ .build();
+
+ return httpClient.createRequest(uri.toString(), config.getGptToken(), requestBody);
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/gerrit/GerritClientPatchSetStateful.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/gerrit/GerritClientPatchSetStateful.java
new file mode 100644
index 0000000..1336685
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/gerrit/GerritClientPatchSetStateful.java
@@ -0,0 +1,24 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.gerrit;
+
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt.ChatGptAssistant;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClientPatchSet;
+import com.googlesource.gerrit.plugins.chatgpt.mode.interfaces.client.api.gerrit.IGerritClientPatchSet;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class GerritClientPatchSetStateful extends GerritClientPatchSet implements IGerritClientPatchSet {
+
+ public GerritClientPatchSetStateful(Configuration config) {
+ super(config);
+ }
+
+ public String getPatchSet(GerritChange change) {
+ ChatGptAssistant chatGptAssistant = new ChatGptAssistant(config, change);
+ chatGptAssistant.setupAssistant();
+
+ return "";
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/git/GitRepoFiles.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/git/GitRepoFiles.java
new file mode 100644
index 0000000..9ae6979
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/git/GitRepoFiles.java
@@ -0,0 +1,71 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git;
+
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.*;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.googlesource.gerrit.plugins.chatgpt.utils.GsonUtils.getGson;
+
+@Slf4j
+public class GitRepoFiles {
+ public static final String REPO_PATTERN = "git/%s.git";
+
+ public String getGitRepoFiles(GerritChange change) {
+ String repoPath = String.format(REPO_PATTERN, change.getProjectNameKey().toString());
+ try {
+ Repository repository = openRepository(repoPath);
+ Map<String, String> filesWithContent = listFilesWithContent(repository);
+
+ return getGson().toJson(filesWithContent);
+ } catch (IOException | GitAPIException e) {
+ throw new RuntimeException("Failed to retrieve files in master branch: ", e);
+ }
+ }
+
+ public Map<String, String> listFilesWithContent(Repository repository) throws IOException, GitAPIException {
+ Map<String, String> filesWithContent = new HashMap<>();
+ try (ObjectReader reader = repository.newObjectReader();
+ RevWalk revWalk = new RevWalk(repository)) {
+ ObjectId lastCommitId = repository.resolve(Constants.R_HEADS + "master");
+ RevCommit commit = revWalk.parseCommit(lastCommitId);
+ RevTree tree = commit.getTree();
+
+ try (TreeWalk treeWalk = new TreeWalk(repository)) {
+ treeWalk.addTree(tree);
+ treeWalk.setRecursive(true);
+ treeWalk.setFilter(TreeFilter.ANY_DIFF);
+
+ while (treeWalk.next()) {
+ String path = treeWalk.getPathString();
+ ObjectId objectId = treeWalk.getObjectId(0);
+ byte[] bytes = reader.open(objectId).getBytes();
+ String content = new String(bytes, StandardCharsets.UTF_8); // Assumes text files with UTF-8 encoding
+ filesWithContent.put(path, content);
+ }
+ }
+ }
+ return filesWithContent;
+ }
+
+ public Repository openRepository(String path) throws IOException {
+ FileRepositoryBuilder builder = new FileRepositoryBuilder();
+ return builder.setGitDir(new File(path))
+ .readEnvironment()
+ .findGitDir()
+ .setMustExist(true)
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/git/GitRepoFilesHandler.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/git/GitRepoFilesHandler.java
new file mode 100644
index 0000000..7e734ff
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/git/GitRepoFilesHandler.java
@@ -0,0 +1,14 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git;
+
+public class GitRepoFilesHandler {
+ private static GitRepoFiles gitRepoFiles;
+
+ public static synchronized GitRepoFiles getInstance() {
+ return gitRepoFiles;
+ }
+
+ public static synchronized void createNewInstance(GitRepoFiles gitRepoFiles) {
+ GitRepoFilesHandler.gitRepoFiles = gitRepoFiles;
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/prompt/ChatGptPromptStateful.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/prompt/ChatGptPromptStateful.java
new file mode 100644
index 0000000..d54a12d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/prompt/ChatGptPromptStateful.java
@@ -0,0 +1,37 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.prompt;
+
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.prompt.ChatGptPrompt;
+import lombok.extern.slf4j.Slf4j;
+
+
+@Slf4j
+public class ChatGptPromptStateful extends ChatGptPrompt {
+ public static String DEFAULT_GPT_ASSISTANT_NAME;
+ public static String DEFAULT_GPT_ASSISTANT_DESCRIPTION;
+ public static String DEFAULT_GPT_ASSISTANT_INSTRUCTIONS;
+
+ private final GerritChange change;
+
+ public ChatGptPromptStateful(Configuration config, GerritChange change) {
+ super(config);
+ this.change = change;
+ this.isCommentEvent = change.getIsCommentEvent();
+ // Avoid repeated loading of prompt constants
+ if (DEFAULT_GPT_ASSISTANT_NAME == null) {
+ loadPrompts("promptsStateful");
+ }
+ }
+
+ public String getDefaultGptAssistantDescription() {
+ return String.format(DEFAULT_GPT_ASSISTANT_DESCRIPTION, change.getProjectName());
+ }
+
+ public String getDefaultGptAssistantInstructions() {
+ return DEFAULT_GPT_SYSTEM_PROMPT + DOT +
+ String.format(DEFAULT_GPT_ASSISTANT_INSTRUCTIONS, change.getProjectName()) +
+ getPatchSetReviewUserPrompt();
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/api/chatgpt/ChatGptCreateAssistantRequestBody.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/api/chatgpt/ChatGptCreateAssistantRequestBody.java
new file mode 100644
index 0000000..2875a8f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/api/chatgpt/ChatGptCreateAssistantRequestBody.java
@@ -0,0 +1,19 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt;
+
+import com.google.gson.annotations.SerializedName;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptTool;
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class ChatGptCreateAssistantRequestBody {
+ String name;
+ String description;
+ String instructions;
+ String model;
+ Double temperature;
+ @SerializedName("file_ids")
+ String[] fileIds;
+ ChatGptTool[] tools;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/api/chatgpt/ChatGptCreateAssistantResponse.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/api/chatgpt/ChatGptCreateAssistantResponse.java
new file mode 100644
index 0000000..743b3d8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/api/chatgpt/ChatGptCreateAssistantResponse.java
@@ -0,0 +1,9 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt;
+
+import lombok.Data;
+
+@Data
+public class ChatGptCreateAssistantResponse {
+ String id;
+ String object;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/api/chatgpt/ChatGptFilesResponse.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/api/chatgpt/ChatGptFilesResponse.java
new file mode 100644
index 0000000..5329bcb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/api/chatgpt/ChatGptFilesResponse.java
@@ -0,0 +1,10 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt;
+
+import lombok.Data;
+
+@Data
+public class ChatGptFilesResponse {
+ String id;
+ String filename;
+ String status;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/data/ProjectData.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/data/ProjectData.java
new file mode 100644
index 0000000..46b8d43
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/data/ProjectData.java
@@ -0,0 +1,8 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.data;
+
+import lombok.Data;
+
+@Data
+public class ProjectData {
+ String assistantId;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/prompt/ChatGptPromptStateless.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/prompt/ChatGptPromptStateless.java
index 25ca0db..3aa4621 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/prompt/ChatGptPromptStateless.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/prompt/ChatGptPromptStateless.java
@@ -39,19 +39,6 @@
DEFAULT_GPT_SYSTEM_PROMPT_INPUT_DESCRIPTION_REVIEW;
}
- public String getPatchSetReviewUserPrompt() {
- List<String> attributes = new ArrayList<>(PATCH_SET_REVIEW_REPLY_ATTRIBUTES);
- if (config.isVotingEnabled() || config.getFilterNegativeComments()) {
- updateScoreDescription();
- }
- else {
- attributes.remove(ATTRIBUTE_SCORE);
- }
- updateRelevanceDescription();
- return buildFieldSpecifications(attributes) + SPACE +
- DEFAULT_GPT_REPLIES_PROMPT_INLINE;
- }
-
public String getGptSystemPrompt() {
List<String> prompt = new ArrayList<>(Arrays.asList(
config.getString(Configuration.KEY_GPT_SYSTEM_PROMPT, DEFAULT_GPT_SYSTEM_PROMPT), DOT,
@@ -111,22 +98,4 @@
return steps;
}
- private void updateScoreDescription() {
- String scoreDescription = DEFAULT_GPT_REPLIES_ATTRIBUTES.get(ATTRIBUTE_SCORE);
- if (scoreDescription.contains("%d")) {
- scoreDescription = String.format(scoreDescription, config.getVotingMinScore(), config.getVotingMaxScore());
- DEFAULT_GPT_REPLIES_ATTRIBUTES.put(ATTRIBUTE_SCORE, scoreDescription);
- }
- }
-
- private void updateRelevanceDescription() {
- String relevanceDescription = DEFAULT_GPT_REPLIES_ATTRIBUTES.get(ATTRIBUTE_RELEVANCE);
- if (relevanceDescription.contains("%s")) {
- String defaultGptRelevanceRules = config.getString(Configuration.KEY_GPT_RELEVANCE_RULES,
- DEFAULT_GPT_RELEVANCE_RULES);
- relevanceDescription = String.format(relevanceDescription, defaultGptRelevanceRules);
- DEFAULT_GPT_REPLIES_ATTRIBUTES.put(ATTRIBUTE_RELEVANCE, relevanceDescription);
- }
- }
-
}
diff --git a/src/main/resources/Config/prompts.json b/src/main/resources/Config/prompts.json
index 5e3371b..e128262 100644
--- a/src/main/resources/Config/prompts.json
+++ b/src/main/resources/Config/prompts.json
@@ -4,7 +4,7 @@
"DEFAULT_GPT_REVIEW_PROMPT_COMMIT_MESSAGES": "Review the commit message of the PatchSet and provide your feedback in an additional reply. The commit message is provided in the \"content\" field of \"/COMMIT_MSG\" in the same way as the file changes. Ensure that the commit message accurately and succinctly describes the changes made, and verify if it matches the nature and scope of the changes in the PatchSet. If your feedback on the commit message is negative, you are required to supply an example of commit message that meets these criteria. For instance, if your comment is \"The commit message lacks detail\", you should follow up with \"A clearer commit message would be '...'\".",
"DEFAULT_GPT_REQUEST_PROMPT_DIFF": "I have some requests about the following PatchSet Diff:",
"DEFAULT_GPT_REQUEST_PROMPT_REQUESTS": "My requests are given in a JSON-formatted array, where each element includes the compulsory field `request`, the field `history` with any prior exchanged messages, and, for inline code comments, the fields `filename`, `lineNumber`, and `codeSnippet`:",
- "DEFAULT_GPT_REPLIES_PROMPT": "Each reply must be formatted as an individual object within an array in the key `replies`. The object includes the string attributes %s, with the following specifications: %s.",
+ "DEFAULT_GPT_REPLIES_PROMPT": "Each reply must be formatted as an individual object within an array in the key `replies`, as defined in the `format_replies` tools function. The object includes the string attributes %s, with the following specifications: %s.",
"DEFAULT_GPT_REPLIES_PROMPT_INLINE": "For replies that are specific to a certain part of the code, the object must additionally include the keys `filename`, `lineNumber`, and `codeSnippet` to precisely identify the relevant code section.",
"DEFAULT_GPT_REPLIES_PROMPT_ENFORCE_RESPONSE_CHECK": "Make sure that the array in `replies` contains exactly %d element(s), one for each request.",
"DEFAULT_GPT_REPLIES_ATTRIBUTES": {
diff --git a/src/main/resources/Config/promptsStateful.json b/src/main/resources/Config/promptsStateful.json
new file mode 100644
index 0000000..c34f909
--- /dev/null
+++ b/src/main/resources/Config/promptsStateful.json
@@ -0,0 +1,5 @@
+{
+ "DEFAULT_GPT_ASSISTANT_NAME": "PatchSet Reviewer",
+ "DEFAULT_GPT_ASSISTANT_DESCRIPTION": "PatchSet Reviewer for project %s.",
+ "DEFAULT_GPT_ASSISTANT_INSTRUCTIONS": "The JSON project file uploaded includes the source files for the `%s` project. The structure uses the file paths from the project's root as keys, and arrays of lines as their values. This arrangement ensures that the line number for any given line corresponds to its index in the array plus one. You will receive a patch in the standard git format-patch format. Your tasks include: 1. applying this patch to the corresponding existing files, and 2. Conducting a review of the patch. Here are the guidelines for reviewing the patch: A. identify any potential problems and offer suggestions for enhancements, presenting each point as a separate reply; B. Focus solely on identifying and suggesting solutions for issues; refrain from highlighting any positive aspects; C. Only evaluate the code that has been modified in the patch; refrain from reviewing any other parts of the project's code that were not changed."
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
new file mode 100644
index 0000000..001f0e8
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
@@ -0,0 +1,106 @@
+package com.googlesource.gerrit.plugins.chatgpt;
+
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.google.common.net.HttpHeaders;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.UriResourceLocatorStateful;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.prompt.ChatGptPromptStateful;
+import com.googlesource.gerrit.plugins.chatgpt.settings.Settings.MODES;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.entity.ContentType;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import static com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt.ChatGptAssistant.*;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@Slf4j
+@RunWith(MockitoJUnitRunner.class)
+public class ChatGptReviewStatefulTest extends ChatGptReviewTestBase {
+ private static final String CHAT_GPT_FILE_ID = "file-TEST_FILE_ID";
+ private static final String CHAT_GPT_ASSISTANT_ID = "asst_TEST_ASSISTANT_ID";
+
+ private ChatGptPromptStateful chatGptPromptStateful;
+
+ public ChatGptReviewStatefulTest() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ protected void initGlobalAndProjectConfig() {
+ super.initGlobalAndProjectConfig();
+
+ // Mock the Global Config values that differ from the ones provided by Default
+ when(globalConfig.getString(Mockito.eq("gptMode"), Mockito.anyString()))
+ .thenReturn(MODES.stateful.name());
+ }
+
+ protected void initConfig() {
+ super.initConfig();
+
+ // Load the prompts
+ chatGptPromptStateful = new ChatGptPromptStateful(config, getGerritChange());
+ }
+
+ protected void setupMockRequests() {
+ super.setupMockRequests();
+
+ // Mock the behavior of the ChatGPT create file request
+ WireMock.stubFor(WireMock.post(WireMock.urlEqualTo(URI.create(config.getGptDomain()
+ + UriResourceLocatorStateful.chatCreateFilesUri()).getPath()))
+ .willReturn(WireMock.aResponse()
+ .withStatus(HTTP_OK)
+ .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
+ .withBody("{\"id\": " + CHAT_GPT_FILE_ID + "}")));
+
+ // Mock the behavior of the ChatGPT create assistant request
+ WireMock.stubFor(WireMock.post(WireMock.urlEqualTo(URI.create(config.getGptDomain()
+ + UriResourceLocatorStateful.chatCreateAssistantsUri()).getPath()))
+ .willReturn(WireMock.aResponse()
+ .withStatus(HTTP_OK)
+ .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
+ .withBody("{\"id\": " + CHAT_GPT_ASSISTANT_ID + "}")));
+ }
+
+ protected void initComparisonContent() {
+ super.initComparisonContent();
+ }
+
+ private void setupAssistantCreatedTest(Map<String, String> mockedPluginDataHandler) {
+ // Mock the behavior of the Git Repository Manager
+ String repoJson = readTestFile("__files/gitProjectFiles.json");
+ when(gitRepoFiles.getGitRepoFiles(any())).thenReturn(repoJson);
+
+ // Mock the behavior of the `setValue` method of the Plugin Data Handler
+ doAnswer(invocation -> {
+ String key = invocation.getArgument(0);
+ String value = invocation.getArgument(1);
+ mockedPluginDataHandler.put(key, value);
+ return null; // since setValue is void
+ }).when(pluginDataHandler).setValue(anyString(), anyString());
+ }
+
+ @Test
+ public void assistantCreated() throws InterruptedException, ExecutionException {
+ Map<String, String> mockedPluginDataHandler = new HashMap<>();
+ setupAssistantCreatedTest(mockedPluginDataHandler);
+ CompletableFuture<Void> future = handleEventBasedOnType(false);
+ future.get();
+
+ String projectFileIdKey = PROJECT_NAME + "." + KEY_FILE_ID;
+ Assert.assertEquals(mockedPluginDataHandler.get(projectFileIdKey), CHAT_GPT_FILE_ID);
+ String projectAssistantKey = PROJECT_NAME + "." + KEY_ASSISTANT_ID;
+ Assert.assertEquals(mockedPluginDataHandler.get(projectAssistantKey), CHAT_GPT_ASSISTANT_ID);
+ }
+
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java
index 67a6aef..b8c7665 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java
@@ -34,6 +34,13 @@
private ChatGptPromptStateless chatGptPromptStateless;
protected void initConfig() {
+ super.initGlobalAndProjectConfig();
+
+ when(globalConfig.getBoolean(Mockito.eq("gptStreamOutput"), Mockito.anyBoolean()))
+ .thenReturn(GPT_STREAM_OUTPUT);
+ when(globalConfig.getBoolean(Mockito.eq("gptReviewCommitMessages"), Mockito.anyBoolean()))
+ .thenReturn(true);
+
super.initConfig();
// Load the prompts
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java
index d4669ee..17c9b96 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java
@@ -23,9 +23,10 @@
import com.googlesource.gerrit.plugins.chatgpt.data.PluginDataHandler;
import com.googlesource.gerrit.plugins.chatgpt.listener.EventListenerHandler;
import com.googlesource.gerrit.plugins.chatgpt.listener.GerritListener;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.UriResourceLocator;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClient;
-import com.googlesource.gerrit.plugins.chatgpt.mode.stateless.client.api.UriResourceLocatorStateless;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
import lombok.NonNull;
import org.apache.http.entity.ContentType;
import org.junit.Assert;
@@ -74,6 +75,9 @@
public WireMockRule wireMockRule = new WireMockRule(9527);
@Mock
+ protected GitRepoFiles gitRepoFiles;
+
+ @Mock
protected PluginDataHandler pluginDataHandler;
protected PluginConfig globalConfig;
@@ -87,6 +91,7 @@
@Before
public void before() throws NoSuchProjectException {
+ initGlobalAndProjectConfig();
initConfig();
setupMockRequests();
initComparisonContent();
@@ -97,7 +102,7 @@
return new GerritChange(PROJECT_NAME, BRANCH_NAME, CHANGE_ID);
}
- protected void initConfig() {
+ protected void initGlobalAndProjectConfig() {
globalConfig = mock(PluginConfig.class);
Answer<Object> returnDefaultArgument = invocation -> {
// Return the second argument (i.e., the Default value) passed to the method
@@ -118,16 +123,14 @@
// Mock the Global Config values that differ from the ones provided by Default
when(globalConfig.getString(Mockito.eq("gptDomain"), Mockito.anyString()))
.thenReturn(GPT_DOMAIN);
- when(globalConfig.getBoolean(Mockito.eq("gptStreamOutput"), Mockito.anyBoolean()))
- .thenReturn(GPT_STREAM_OUTPUT);
- when(globalConfig.getBoolean(Mockito.eq("gptReviewCommitMessages"), Mockito.anyBoolean()))
- .thenReturn(true);
projectConfig = mock(PluginConfig.class);
// Mock the Project Config values
when(projectConfig.getBoolean(Mockito.eq("isEnabled"), Mockito.anyBoolean())).thenReturn(true);
+ }
+ protected void initConfig() {
config = new Configuration(globalConfig, projectConfig);
// Mock the config instance values
@@ -152,7 +155,7 @@
.withBody("[{\"_account_id\": " + GERRIT_USER_ACCOUNT_ID + "}]")));
// Mock the behavior of the gerritAccountGroups request
- WireMock.stubFor(WireMock.get(UriResourceLocatorStateless.gerritAccountsUri() +
+ WireMock.stubFor(WireMock.get(UriResourceLocator.gerritAccountsUri() +
gerritGroupPostfixUri(GERRIT_USER_ACCOUNT_ID))
.willReturn(WireMock.aResponse()
.withStatus(HTTP_OK)
@@ -203,7 +206,8 @@
typeSpecificSetup.accept(event);
EventListenerHandler eventListenerHandler = new EventListenerHandler(patchSetReviewer, gerritClient);
- GerritListener gerritListener = new GerritListener(mockConfigCreator, eventListenerHandler, pluginDataHandler);
+ GerritListener gerritListener = new GerritListener(mockConfigCreator, eventListenerHandler, gitRepoFiles,
+ pluginDataHandler);
gerritListener.onEvent(event);
return eventListenerHandler.getLatestFuture();
diff --git a/src/test/resources/__files/gitProjectFiles.json b/src/test/resources/__files/gitProjectFiles.json
new file mode 100644
index 0000000..df94a55
--- /dev/null
+++ b/src/test/resources/__files/gitProjectFiles.json
@@ -0,0 +1,4 @@
+{
+ "test_file_1.py": "from typing import Any, Callable, Type, Union\nimport importlib\n\n__all__ = [\"importclass\", \"preprocess_classes\", \"TypeClassOrPath\"]\n\nTypeClassOrPath = Union[Type, str]\n\n\ndef importclass(\n module_name: str,\n class_name: Union[str, None] = None\n) -> Type:\n \"\"\"\n Dynamically import a class from a specified module.\n\n :param module_name: The name of the module to import.\n :param class_name: The name of the class in the module to import. Defaults to None.\n :return: The dynamically imported class.\n \"\"\"\n if not class_name:\n module_name, class_name = module_name.rsplit('.', 1)\n loaded_module = importclass(module_name, fromlist=[class_name])\n return getattr(loaded_module, class_name)\n\n\ndef preprocess_classes(func: Callable) -> Callable:\n \"\"\"Decorator to convert dot-notated class paths into strings from positional arguments.\"\"\"\n def __preprocess_classes_wrapper(*all_classes: TypeClassOrPath, **kwargs: Any) -> Any:\n \"\"\"\n Dynamically import classes if they are passed as strings.\n\n :param all_classes: A variable number of class paths (strings or actual types).\n :param kwargs: Any keyword arguments to pass to the decorated function.\n :return: The result of the decorated function.\n \"\"\"\n classes_processed = (\n class_id if isinstance(class_id, type)\n else importclass(class_id)\n for class_id in all_classes\n )\n return func(*classes_processed, *kwargs)\n return __preprocess_classes_wrapper\n",
+ "test_file_2.py": "from typing import Any\n\n__all__ = [\"SingletonMeta\"]\n\n\nclass SingletonMeta(type):\n \"\"\"A metaclass to manage Singleton classes.\"\"\"\n\n _instances: Any = {}\n\n def __new__(cls, name, bases, dct) -> type:\n \"\"\"Create the Singleton class and add the class method `destroy_singleton` to it.\"\"\"\n def __destroy_singleton(_):\n cls.destroy(name)\n\n instance = super().__new__(cls, name, bases, dct)\n setattr(instance, 'destroy_singleton', __destroy_singleton)\n return instance\n\n def __call__(cls, *args, **kwargs) -> type:\n \"\"\"\n Return the Singleton class instance, creating a new instance if one does not already exist.\n\n :param args: Arguments to pass to the class constructor.\n :param kwargs: Keyword arguments to pass to the class constructor.\n :return: Singleton class instance.\n \"\"\"\n if cls not in cls._instances:\n instance = super(SingletonMeta, cls).__call__(*args, **kwargs)\n cls._instances[cls] = instance\n return cls._instances[cls]\n\n @classmethod\n def destroy(cls, *class_names: str):\n \"\"\"\n Delete instances of Singleton classes based on provided class names.\n\n :param class_names: Names of the Singleton class instances to destroy.\n If no class name is provided, instances of all Singleton classes are deleted.\n \"\"\"\n if class_names:\n cls._instances = {k: v for k, v in cls._instances.items() if k.__name__ not in class_names}\n else:\n cls._instances = {}\n"
+}
\ No newline at end of file