Add context locator for Python function definitions
- Structured ON_DEMAND context locator to provide ChatGPT with function
definitions based on context requests.
- Added `codeContextOnDemandBasePath` config setting to specify the
project files' base path.
- Implemented Python-specific context locator to handle function
definition requests in Python projects.
Change-Id: Iffadbe564447f4cb180340f39ffcc3ea6afb34ca
Signed-off-by: Patrizio <patrizio.gelosi@amarulasolutions.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/Configuration.java
index a8dbb57..1f792d6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/config/Configuration.java
@@ -29,6 +29,7 @@
private static final boolean DEFAULT_REVIEW_COMMIT_MESSAGES = true;
private static final boolean DEFAULT_FULL_FILE_REVIEW = true;
private static final String DEFAULT_CODE_CONTEXT_POLICY = "UPLOAD_ALL";
+ private static final String DEFAULT_CODE_CONTEXT_ON_DEMAND_BASE_PATH = "";
private static final boolean DEFAULT_STREAM_OUTPUT = false;
private static final boolean DEFAULT_GLOBAL_ENABLE = false;
private static final String DEFAULT_DISABLED_USERS = "";
@@ -118,6 +119,7 @@
private static final String KEY_REVIEW_PATCH_SET = "gptReviewPatchSet";
private static final String KEY_FULL_FILE_REVIEW = "gptFullFileReview";
private static final String KEY_CODE_CONTEXT_POLICY = "codeContextPolicy";
+ private static final String KEY_CODE_CONTEXT_ON_DEMAND_BASE_PATH = "codeContextOnDemandBasePath";
private static final String KEY_PROJECT_ENABLE = "isEnabled";
private static final String KEY_GLOBAL_ENABLE = "globalEnable";
private static final String KEY_DISABLED_USERS = "disabledUsers";
@@ -208,6 +210,10 @@
return getEnum(KEY_CODE_CONTEXT_POLICY, DEFAULT_CODE_CONTEXT_POLICY, CodeContextPolicies.class);
}
+ public String getCodeContextOnDemandBasePath() {
+ return getString(KEY_CODE_CONTEXT_ON_DEMAND_BASE_PATH, DEFAULT_CODE_CONTEXT_ON_DEMAND_BASE_PATH);
+ }
+
public boolean getGptStreamOutput() {
return getBoolean(KEY_STREAM_OUTPUT, DEFAULT_STREAM_OUTPUT);
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/errors/exceptions/CodeContextOnDemandLocatorException.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/errors/exceptions/CodeContextOnDemandLocatorException.java
new file mode 100644
index 0000000..c2e4d89
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/errors/exceptions/CodeContextOnDemandLocatorException.java
@@ -0,0 +1,15 @@
+package com.googlesource.gerrit.plugins.chatgpt.errors.exceptions;
+
+public class CodeContextOnDemandLocatorException extends Exception {
+ public CodeContextOnDemandLocatorException() {
+ super("Failed retrieve Code Context Locator for On-Demand policy");
+ }
+
+ public CodeContextOnDemandLocatorException(String message) {
+ super(message);
+ }
+
+ public CodeContextOnDemandLocatorException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/interfaces/mode/stateful/client/code/context/ondemand/IEntityLocator.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/interfaces/mode/stateful/client/code/context/ondemand/IEntityLocator.java
new file mode 100644
index 0000000..ac632ff
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/interfaces/mode/stateful/client/code/context/ondemand/IEntityLocator.java
@@ -0,0 +1,7 @@
+package com.googlesource.gerrit.plugins.chatgpt.interfaces.mode.stateful.client.code.context.ondemand;
+
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptGetContextItem;
+
+public interface IEntityLocator {
+ String findDefinition(ChatGptGetContextItem chatGptGetContextItem);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptClient.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptClient.java
index d6e693a..a79c165 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptClient.java
@@ -1,7 +1,6 @@
package com.googlesource.gerrit.plugins.chatgpt.mode.common.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.model.api.chatgpt.*;
import lombok.Getter;
@@ -15,7 +14,7 @@
import static com.googlesource.gerrit.plugins.chatgpt.utils.GsonUtils.jsonToClass;
@Slf4j
-abstract public class ChatGptClient extends ClientBase {
+abstract public class ChatGptClient extends ChatGptClientBase {
protected boolean isCommentEvent = false;
@Getter
protected String requestBody;
@@ -82,22 +81,6 @@
return Optional.ofNullable(content);
}
- private ChatGptResponseContent convertResponseContentFromJson(String content) {
- return jsonToClass(content, ChatGptResponseContent.class);
- }
-
- private ChatGptToolCall.Function getFunction(List<ChatGptToolCall> toolCalls, int ind) {
- return toolCalls.get(ind).getFunction();
- }
-
- private String getArgumentAsString(List<ChatGptToolCall> toolCalls, int ind) {
- return getFunction(toolCalls, ind).getArguments();
- }
-
- private ChatGptResponseContent getArgumentAsResponse(List<ChatGptToolCall> toolCalls, int ind) {
- return convertResponseContentFromJson(getArgumentAsString(toolCalls, ind));
- }
-
private ChatGptResponseContent mergeToolCalls(List<ChatGptToolCall> toolCalls) {
log.debug("Merging responses from multiple tool calls.");
ChatGptResponseContent responseContent = getArgumentAsResponse(toolCalls, 0);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptClientBase.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptClientBase.java
new file mode 100644
index 0000000..25be5c2
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/api/chatgpt/ChatGptClientBase.java
@@ -0,0 +1,37 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.common.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.model.api.chatgpt.ChatGptResponseContent;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptToolCall;
+
+import java.util.List;
+
+import static com.googlesource.gerrit.plugins.chatgpt.utils.GsonUtils.jsonToClass;
+
+public abstract class ChatGptClientBase extends ClientBase {
+
+ public ChatGptClientBase(Configuration config) {
+ super(config);
+ }
+
+ protected ChatGptResponseContent convertResponseContentFromJson(String content) {
+ return jsonToClass(content, ChatGptResponseContent.class);
+ }
+
+ protected ChatGptToolCall.Function getFunction(List<ChatGptToolCall> toolCalls, int ind) {
+ return toolCalls.get(ind).getFunction();
+ }
+
+ protected String getArgumentAsString(List<ChatGptToolCall> toolCalls, int ind) {
+ return getFunction(toolCalls, ind).getArguments();
+ }
+
+ protected ChatGptResponseContent getArgumentAsResponse(List<ChatGptToolCall> toolCalls, int ind) {
+ return convertResponseContentFromJson(getArgumentAsString(toolCalls, ind));
+ }
+
+ protected <T> T getArgumentAsType(List<ChatGptToolCall> toolCalls, int ind, Class<T> clazz) {
+ return jsonToClass(getArgumentAsString(toolCalls, ind), clazz);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptDialogueItem.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptDialogueItem.java
index e4e75fe..9cdead3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptDialogueItem.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptDialogueItem.java
@@ -1,8 +1,13 @@
package com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt;
+import lombok.AccessLevel;
import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
@Data
+@SuperBuilder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class ChatGptDialogueItem {
protected Integer id;
protected String filename;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptGetContextContent.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptGetContextContent.java
new file mode 100644
index 0000000..b2f81bd
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptGetContextContent.java
@@ -0,0 +1,12 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt;
+
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+
+import java.util.List;
+
+@Data
+@RequiredArgsConstructor
+public class ChatGptGetContextContent {
+ private List<ChatGptGetContextItem> replies;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptGetContextItem.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptGetContextItem.java
new file mode 100644
index 0000000..870fd38
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptGetContextItem.java
@@ -0,0 +1,17 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import lombok.experimental.SuperBuilder;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@SuperBuilder
+@ToString(callSuper = true)
+public class ChatGptGetContextItem extends ChatGptDialogueItem {
+ private String requestType;
+ private String otherDescription;
+ private String entityCategory;
+ private String contextRequiredEntity;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptMessageItem.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptMessageItem.java
index 2749880..0215488 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptMessageItem.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptMessageItem.java
@@ -2,11 +2,15 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
+@NoArgsConstructor
+@SuperBuilder
public class ChatGptMessageItem extends ChatGptDialogueItem {
private String request;
private List<ChatGptRequestMessage> history;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptReplyItem.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptReplyItem.java
index bb91382..bc30a14 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptReplyItem.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/model/api/chatgpt/ChatGptReplyItem.java
@@ -2,9 +2,11 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
+import lombok.experimental.SuperBuilder;
@EqualsAndHashCode(callSuper = true)
@Data
+@SuperBuilder
public class ChatGptReplyItem extends ChatGptDialogueItem {
private String reply;
private Integer score;
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 2b501b3..b22a306 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,5 +1,6 @@
package com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt;
+import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@@ -53,6 +54,10 @@
private Field filename;
private Field lineNumber;
private Field codeSnippet;
+ private Field requestType;
+ private Field otherDescription;
+ private Field entityCategory;
+ private Field contextRequiredEntity;
}
}
}
@@ -60,6 +65,9 @@
@Data
public static class Field {
private String type;
+ private String description;
+ @SerializedName("enum")
+ private List<String> enumeration;
}
}
}
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
index e8d1b48..1491d52 100644
--- 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
@@ -19,7 +19,6 @@
import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt.ChatGptThreadMessageResponse;
import lombok.extern.slf4j.Slf4j;
-import static com.googlesource.gerrit.plugins.chatgpt.utils.GsonUtils.jsonToClass;
import static com.googlesource.gerrit.plugins.chatgpt.utils.JsonTextUtils.isJsonObjectAsString;
import static com.googlesource.gerrit.plugins.chatgpt.utils.JsonTextUtils.unwrapJsonCode;
@@ -166,6 +165,6 @@
private ChatGptResponseContent extractResponseContent(String responseText) {
log.debug("Extracting response content from JSON.");
- return jsonToClass(unwrapJsonCode(responseText), ChatGptResponseContent.class);
+ return convertResponseContentFromJson(unwrapJsonCode(responseText));
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRunActionHandler.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRunActionHandler.java
index fc5cc59..8013542 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRunActionHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRunActionHandler.java
@@ -3,8 +3,10 @@
import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
import com.googlesource.gerrit.plugins.chatgpt.errors.exceptions.OpenAiConnectionFailException;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.ClientBase;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptToolCall;
import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt.endpoint.ChatGptRun;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt.ChatGptRunResponse;
import lombok.extern.slf4j.Slf4j;
@@ -14,12 +16,21 @@
public class ChatGptRunActionHandler extends ClientBase {
private static final int MAX_ACTION_REQUIRED_RETRIES = 1;
+ private final GerritChange change;
+ private final GitRepoFiles gitRepoFiles;
private final ChatGptRun chatGptRun;
private int actionRequiredRetries;
- public ChatGptRunActionHandler(Configuration config, ChatGptRun chatGptRun) {
+ public ChatGptRunActionHandler(
+ Configuration config,
+ GerritChange change,
+ GitRepoFiles gitRepoFiles,
+ ChatGptRun chatGptRun
+ ) {
super(config);
+ this.change = change;
+ this.gitRepoFiles = gitRepoFiles;
this.chatGptRun = chatGptRun;
actionRequiredRetries = 0;
log.debug("ChatGptRunActionHandler initialized");
@@ -33,6 +44,8 @@
log.debug("Action required for response: {}", runResponse);
ChatGptRunToolOutputHandler chatGptRunToolOutputHandler = new ChatGptRunToolOutputHandler(
config,
+ change,
+ gitRepoFiles,
chatGptRun
);
chatGptRunToolOutputHandler.submitToolOutput(getRunToolCalls(runResponse));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRunToolOutputHandler.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRunToolOutputHandler.java
index 7d931cf..4e9ab60 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRunToolOutputHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRunToolOutputHandler.java
@@ -2,9 +2,13 @@
import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
import com.googlesource.gerrit.plugins.chatgpt.errors.exceptions.OpenAiConnectionFailException;
-import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.ClientBase;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.chatgpt.ChatGptClientBase;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptGetContextContent;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptToolCall;
import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt.endpoint.ChatGptRun;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.code.context.ondemand.CodeContextBuilder;
import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt.ChatGptToolOutput;
import lombok.extern.slf4j.Slf4j;
@@ -12,38 +16,51 @@
import java.util.List;
@Slf4j
-public class ChatGptRunToolOutputHandler extends ClientBase {
+public class ChatGptRunToolOutputHandler extends ChatGptClientBase {
// ChatGPT may occasionally return the fixed string "multi_tool_use" as the function name when multiple tools are
// utilized.
private static final List<String> ON_DEMAND_FUNCTION_NAMES = List.of("get_context", "multi_tool_use");
private final ChatGptRun chatGptRun;
+ private final CodeContextBuilder codeContextBuilder;
+
+ private List<ChatGptToolCall> chatGptToolCalls;
public ChatGptRunToolOutputHandler(
Configuration config,
+ GerritChange change,
+ GitRepoFiles gitRepoFiles,
ChatGptRun chatGptRun
) {
super(config);
this.chatGptRun = chatGptRun;
+ codeContextBuilder = new CodeContextBuilder(config, change, gitRepoFiles);
}
public void submitToolOutput(List<ChatGptToolCall> chatGptToolCalls) throws OpenAiConnectionFailException {
+ this.chatGptToolCalls = chatGptToolCalls;
List<ChatGptToolOutput> toolOutputs = new ArrayList<>();
log.debug("ChatGpt Tool Calls: {}", chatGptToolCalls);
- for (ChatGptToolCall chatGptToolCall : chatGptToolCalls) {
+ for (int i = 0; i < chatGptToolCalls.size(); i++) {
toolOutputs.add(ChatGptToolOutput.builder()
- .toolCallId(chatGptToolCall.getId())
- .output(getOutput(chatGptToolCall))
+ .toolCallId(chatGptToolCalls.get(i).getId())
+ .output(getOutput(i))
.build());
}
log.debug("ChatGpt Tool Outputs: {}", toolOutputs);
chatGptRun.submitToolOutputs(toolOutputs);
}
- private String getOutput(ChatGptToolCall chatGptToolCall) {
- if (ON_DEMAND_FUNCTION_NAMES.contains(chatGptToolCall.getFunction().getName())) {
- // Placeholder string, will be replaced by logic to calculate code context based on ChatGPT request
- return "CONTEXT PROVIDED";
+ private String getOutput(int i) {
+ ChatGptToolCall.Function function = getFunction(chatGptToolCalls, i);
+ if (ON_DEMAND_FUNCTION_NAMES.contains(function.getName())) {
+ ChatGptGetContextContent getContextContent = getArgumentAsType(
+ chatGptToolCalls,
+ i,
+ ChatGptGetContextContent.class
+ );
+ log.debug("ChatGpt `get_context` Response Content: {}", getContextContent);
+ return codeContextBuilder.buildCodeContext(getContextContent);
}
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
index d115a4f..43aabfc 100644
--- 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
@@ -6,13 +6,14 @@
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.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
@@ -27,17 +28,13 @@
private GitFileChunkBuilder gitFileChunkBuilder;
private List<String> enabledFileExtensions;
+ private long fileSize;
public List<String> getGitRepoFiles(Configuration config, GerritChange change) {
gitFileChunkBuilder = new GitFileChunkBuilder(config);
enabledFileExtensions = config.getEnabledFileExtensions();
- log.debug("Open Repo from {}", change.getProjectNameKey());
- String repoPath = String.format(REPO_PATTERN, change.getProjectNameKey().toString());
- try {
- Repository repository = openRepository(repoPath);
- log.debug("Open Repo path: {}", repoPath);
+ try (Repository repository = openRepository(change)) {
List<Map<String, String>> chunkedFileContent = listFilesWithContent(repository);
-
return chunkedFileContent.stream()
.map(chunk -> getGson().toJson(chunk))
.collect(Collectors.toList());
@@ -46,14 +43,26 @@
}
}
+ public String getFileContent(GerritChange change, String path) throws IOException {
+ try (Repository repository = openRepository(change);
+ ObjectReader reader = repository.newObjectReader()) {
+ RevTree tree = getMasterRevTree(repository);
+ String content = readFileContent(reader, tree, path);
+ if (content != null) {
+ return content;
+ } else {
+ throw new FileNotFoundException("Error retrieving file at " + path);
+ }
+ } catch (IOException e) {
+ throw new FileNotFoundException("File not found: " + path);
+ }
+ }
+
private List<Map<String, String>> listFilesWithContent(Repository repository) throws IOException, GitAPIException {
Map<String, List<FileEntry>> dirFilesMap = new LinkedHashMap<>();
- 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 (ObjectReader reader = repository.newObjectReader()) {
+ RevTree tree = getMasterRevTree(repository);
try (TreeWalk treeWalk = new TreeWalk(repository)) {
treeWalk.addTree(tree);
@@ -65,10 +74,7 @@
if (!matchesExtensionList(path, enabledFileExtensions)) continue;
int lastSlashIndex = path.lastIndexOf('/');
String dirPath = (lastSlashIndex != -1) ? path.substring(0, lastSlashIndex) : "";
- ObjectId objectId = treeWalk.getObjectId(0);
- byte[] bytes = reader.open(objectId).getBytes();
- long fileSize = bytes.length;
- String content = new String(bytes, StandardCharsets.UTF_8); // Assumes text files with UTF-8 encoding
+ String content = getContent(reader, treeWalk);
dirFilesMap.computeIfAbsent(dirPath, k -> new ArrayList<>())
.add(new FileEntry(path, content, fileSize));
@@ -86,12 +92,38 @@
return gitFileChunkBuilder.getChunks();
}
- private Repository openRepository(String path) throws IOException {
+ private Repository openRepository(GerritChange change) throws IOException {
+ String repoPath = String.format(REPO_PATTERN, change.getProjectNameKey().toString());
+ log.debug("Opening repository at path: {}", repoPath);
FileRepositoryBuilder builder = new FileRepositoryBuilder();
- return builder.setGitDir(new File(path))
+ return builder.setGitDir(new File(repoPath))
.readEnvironment()
.findGitDir()
.setMustExist(true)
.build();
}
+
+ private RevTree getMasterRevTree(Repository repository) throws IOException {
+ ObjectId lastCommitId = repository.resolve(Constants.R_HEADS + "master");
+ try (RevWalk revWalk = new RevWalk(repository)) {
+ return revWalk.parseCommit(lastCommitId).getTree();
+ }
+ }
+
+ private String readFileContent(ObjectReader reader, RevTree tree, String path) throws IOException {
+ try (TreeWalk treeWalk = TreeWalk.forPath(reader, path, tree)) {
+ if (treeWalk != null) {
+ return getContent(reader, treeWalk);
+ }
+ return null;
+ }
+ }
+
+ private String getContent(ObjectReader reader, TreeWalk treeWalk) throws IOException {
+ ObjectId objectId = treeWalk.getObjectId(0);
+ byte[] bytes = reader.open(objectId).getBytes();
+ fileSize = bytes.length;
+
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/CodeContextPolicyOnDemand.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/CodeContextPolicyOnDemand.java
index e499a3f..0f31032 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/CodeContextPolicyOnDemand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/CodeContextPolicyOnDemand.java
@@ -6,8 +6,10 @@
import com.googlesource.gerrit.plugins.chatgpt.errors.exceptions.OpenAiConnectionFailException;
import com.googlesource.gerrit.plugins.chatgpt.interfaces.mode.common.client.code.context.ICodeContextPolicy;
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.stateful.client.api.chatgpt.ChatGptRunActionHandler;
import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt.endpoint.ChatGptRun;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt.ChatGptAssistantTools;
import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt.ChatGptRunResponse;
import lombok.extern.slf4j.Slf4j;
@@ -18,17 +20,22 @@
@Slf4j
public class CodeContextPolicyOnDemand extends CodeContextPolicyBase implements ICodeContextPolicy {
+ private final GerritChange change;
+ private final GitRepoFiles gitRepoFiles;
+
private ChatGptRunActionHandler chatGptRunActionHandler;
@VisibleForTesting
@Inject
- public CodeContextPolicyOnDemand(Configuration config) {
+ public CodeContextPolicyOnDemand(Configuration config, GerritChange change, GitRepoFiles gitRepoFiles) {
super(config);
+ this.change = change;
+ this.gitRepoFiles = gitRepoFiles;
log.debug("CodeContextPolicyOnDemand initialized");
}
public void setupRunAction(ChatGptRun chatGptRun) {
- chatGptRunActionHandler = new ChatGptRunActionHandler(config, chatGptRun);
+ chatGptRunActionHandler = new ChatGptRunActionHandler(config, change, gitRepoFiles, chatGptRun);
log.debug("Run Action setup with On-Demand code context policy");
}
@@ -42,7 +49,7 @@
public void updateAssistantTools(ChatGptAssistantTools chatGptAssistantTools, String vectorStoreId) {
ChatGptTools chatGptGetContextTools = new ChatGptTools(ChatGptTools.Functions.getContext);
chatGptAssistantTools.getTools().add(chatGptGetContextTools.retrieveFunctionTool());
- log.debug("Updated Assistant Tools for On-Demand code context policy");
+ log.debug("Updated Assistant Tools for On-Demand code context policy: {}", chatGptAssistantTools);
}
@Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/CodeContextBuilder.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/CodeContextBuilder.java
new file mode 100644
index 0000000..6e9b201
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/CodeContextBuilder.java
@@ -0,0 +1,59 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.code.context.ondemand;
+
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.errors.exceptions.CodeContextOnDemandLocatorException;
+import com.googlesource.gerrit.plugins.chatgpt.interfaces.mode.stateful.client.code.context.ondemand.IEntityLocator;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.ClientBase;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptGetContextContent;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptGetContextItem;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.code.context.ondemand.GetContextOutputItem;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.code.context.ondemand.locator.CodeLocatorFactory.getEntityLocator;
+import static com.googlesource.gerrit.plugins.chatgpt.utils.GsonUtils.getGson;
+
+@Slf4j
+public class CodeContextBuilder extends ClientBase {
+ private static final String CONTEXT_NOT_PROVIDED = "CONTEXT NOT PROVIDED";
+
+ private final GerritChange change;
+ private final GitRepoFiles gitRepoFiles;
+
+ public CodeContextBuilder(Configuration config, GerritChange change, GitRepoFiles gitRepoFiles) {
+ super(config);
+ this.change = change;
+ this.gitRepoFiles = gitRepoFiles;
+ }
+
+ public String buildCodeContext(ChatGptGetContextContent getContextContent) {
+ log.debug("Building code context for {}", getContextContent);
+ List<GetContextOutputItem> getContextOutput = new ArrayList<>();
+ for (ChatGptGetContextItem chatGptGetContextItem : getContextContent.getReplies()) {
+ IEntityLocator entityLocator;
+ try {
+ entityLocator = getEntityLocator(chatGptGetContextItem, config, change, gitRepoFiles);
+ } catch (CodeContextOnDemandLocatorException e) {
+ continue;
+ }
+ String definition = entityLocator.findDefinition(chatGptGetContextItem);
+ if (definition == null) {
+ log.warn("Unable to find definition for `{}`", chatGptGetContextItem.getContextRequiredEntity());
+ definition = CONTEXT_NOT_PROVIDED;
+ }
+ GetContextOutputItem getContextOutputItem = GetContextOutputItem.builder()
+ .requestType(chatGptGetContextItem.getRequestType())
+ .entityCategory(chatGptGetContextItem.getEntityCategory())
+ .contextRequiredEntity(chatGptGetContextItem.getContextRequiredEntity())
+ .definition(definition)
+ .build();
+ log.debug("Added code context: {}", getContextOutputItem);
+ getContextOutput.add(getContextOutputItem);
+ }
+ return getGson().toJson(getContextOutput);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/CodeFileFetcher.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/CodeFileFetcher.java
new file mode 100644
index 0000000..9bf0b86
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/CodeFileFetcher.java
@@ -0,0 +1,33 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.code.context.ondemand;
+
+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.api.gerrit.GerritChange;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+
+@Slf4j
+public class CodeFileFetcher extends ClientBase {
+ private final GerritChange change;
+ private final GitRepoFiles gitRepoFiles;
+
+ private String basePathRegEx = "";
+
+ public CodeFileFetcher(Configuration config, GerritChange change, GitRepoFiles gitRepoFiles) {
+ super(config);
+ this.change = change;
+ this.gitRepoFiles = gitRepoFiles;
+ if (!config.getCodeContextOnDemandBasePath().isEmpty()) {
+ basePathRegEx = "^" + config.getCodeContextOnDemandBasePath() + "/";
+ }
+ }
+
+ public String getFileContent(String filename) throws IOException {
+ if (!basePathRegEx.isEmpty()) {
+ filename = filename.replaceAll(basePathRegEx, "");
+ }
+ return gitRepoFiles.getFileContent(change, filename);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/CallableLocatorBase.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/CallableLocatorBase.java
new file mode 100644
index 0000000..637b3e8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/CallableLocatorBase.java
@@ -0,0 +1,106 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.code.context.ondemand.locator;
+
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.interfaces.mode.stateful.client.code.context.ondemand.IEntityLocator;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.ClientBase;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptGetContextItem;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.code.context.ondemand.CodeFileFetcher;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import static com.googlesource.gerrit.plugins.chatgpt.utils.ModuleUtils.convertDotNotationToPath;
+import static com.googlesource.gerrit.plugins.chatgpt.utils.ModuleUtils.getSimpleName;
+import static com.googlesource.gerrit.plugins.chatgpt.utils.FileUtils.getDirName;
+import static com.googlesource.gerrit.plugins.chatgpt.utils.StringUtils.cutString;
+
+@Slf4j
+public abstract class CallableLocatorBase extends ClientBase implements IEntityLocator {
+ protected static final String DOT_NOTATION_REGEX = "[\\w.]+";
+ private static final int LOG_MAX_CONTENT_SIZE = 256;
+
+ protected final List<String> importModules = new ArrayList<>();
+ protected final CodeFileFetcher codeFileFetcher;
+
+ protected String languageModuleExtension;
+ protected String rootFileDir;
+
+ private Set<String> visitedFiles;
+
+ public CallableLocatorBase(Configuration config, GerritChange change, GitRepoFiles gitRepoFiles) {
+ super(config);
+ log.debug("Initializing FunctionLocatorBase");
+ codeFileFetcher = new CodeFileFetcher(config, change, gitRepoFiles);
+ }
+
+ public String findDefinition(ChatGptGetContextItem chatGptGetContextItem) {
+ log.debug("Finding function definition for {}", chatGptGetContextItem);
+ visitedFiles = new HashSet<>();
+ String filename = chatGptGetContextItem.getFilename();
+ String functionName = getSimpleName(chatGptGetContextItem.getContextRequiredEntity());
+ rootFileDir = getDirName(filename);
+ log.debug("Root file dir: {}", rootFileDir);
+
+ return findFunctionInFile(filename, functionName);
+ }
+
+ protected abstract String getFunctionRegex(String functionName);
+
+ protected abstract String findImportedFunctionDefinition(String functionName, String content);
+
+ protected String getFunctionFromModule(String functionName, String module) {
+ String modulePath = convertDotNotationToPath(module) + languageModuleExtension;
+ modulePath = modulePath.replaceAll("^(?=/)", rootFileDir);
+ log.debug("Module path: {}", modulePath);
+
+ return findFunctionInFile(modulePath, functionName);
+ }
+
+ protected Stream<String> getGroupStream(String group) {
+ return Arrays.stream(group.split(","))
+ .map(String::trim)
+ .filter(entity -> !entity.isEmpty());
+ }
+
+ protected String findInModules(String functionName) {
+ for (String module : importModules) {
+ log.debug("Searching for function `{}` in module: {}", functionName, module);
+ String result = getFunctionFromModule(functionName, module);
+ if (result != null) return result;
+ }
+ return null;
+ }
+
+ private String findFunctionInFile(String filename, String functionName) {
+ log.debug("Finding function {} in file {}", functionName, filename);
+ if (visitedFiles.contains(filename)) {
+ return null;
+ }
+ visitedFiles.add(filename);
+
+ String content;
+ try {
+ content = codeFileFetcher.getFileContent(filename);
+ } catch (IOException e) {
+ log.debug("File `{}` not found in the git repository", filename);
+ return null;
+ }
+ log.debug("File content retrieved for file `{}`:\n{}", filename, cutString(content, LOG_MAX_CONTENT_SIZE));
+
+ // Search the file for the function definition
+ Pattern functionPattern = Pattern.compile(getFunctionRegex(functionName), Pattern.MULTILINE);
+ Matcher functionMatcher = functionPattern.matcher(content);
+ if (functionMatcher.find()) {
+ String functionDefinition = functionMatcher.group(0).trim();
+ log.debug("Found function definition: {}", functionDefinition);
+ return functionDefinition;
+ }
+ return findImportedFunctionDefinition(functionName, content);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/CodeLocatorFactory.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/CodeLocatorFactory.java
new file mode 100644
index 0000000..53b3778
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/CodeLocatorFactory.java
@@ -0,0 +1,104 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.code.context.ondemand.locator;
+
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.errors.exceptions.CodeContextOnDemandLocatorException;
+import com.googlesource.gerrit.plugins.chatgpt.interfaces.mode.stateful.client.code.context.ondemand.IEntityLocator;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptGetContextItem;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static com.googlesource.gerrit.plugins.chatgpt.utils.FileUtils.getExtension;
+import static com.googlesource.gerrit.plugins.chatgpt.utils.ModuleUtils.joinComponents;
+import static com.googlesource.gerrit.plugins.chatgpt.utils.StringUtils.convertSnakeToPascalCase;
+
+@Slf4j
+public class CodeLocatorFactory {
+ public static final String LANGUAGE_PACKAGE = "language";
+
+ public enum EntityCategory {
+ Callable, Data, Type
+ }
+
+ private static final Map<String, String> MAP_EXTENSION = Map.of(
+ "py", "python",
+ "java", "java",
+ "c", "c",
+ "h", "c"
+ );
+
+ private static List<String> classNameComponents;
+
+ public static IEntityLocator getEntityLocator(
+ ChatGptGetContextItem chatGptGetContextItem,
+ Configuration config,
+ GerritChange change,
+ GitRepoFiles gitRepoFiles
+ ) throws CodeContextOnDemandLocatorException {
+ log.debug("Getting Entity Locator for context item {}", chatGptGetContextItem);
+ IEntityLocator entityLocator;
+ classNameComponents = new ArrayList<>(List.of(
+ CodeLocatorFactory.class.getPackage().getName(),
+ LANGUAGE_PACKAGE
+ ));
+ addProgrammingLanguage(chatGptGetContextItem);
+ addLocatorTypeClass(chatGptGetContextItem);
+ String className = joinComponents(classNameComponents);
+ log.debug("Entity locator class name found: {}", className);
+ try {
+ log.debug("Getting Instance of Entity Locator");
+ @SuppressWarnings("unchecked")
+ Class<IEntityLocator> clazz = (Class<IEntityLocator>) Class.forName(className);
+ log.debug("Class found: {}", clazz);
+ entityLocator = clazz
+ .getDeclaredConstructor(Configuration.class, GerritChange.class, GitRepoFiles.class)
+ .newInstance(config, change, gitRepoFiles);
+ } catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException |
+ NoSuchMethodException e) {
+ log.warn("Entity locator `{}` class not found for Get-Context Item: {}", className, chatGptGetContextItem);
+ throw new CodeContextOnDemandLocatorException(e);
+ }
+ return entityLocator;
+ }
+
+
+ private static void addProgrammingLanguage(ChatGptGetContextItem chatGptGetContextItem)
+ throws CodeContextOnDemandLocatorException {
+ String language = getCodeLocatorLanguage(chatGptGetContextItem);
+ if (language == null) {
+ log.warn("No language supported for file {}", chatGptGetContextItem.getFilename());
+ throw new CodeContextOnDemandLocatorException();
+ }
+ classNameComponents.add(language);
+ }
+
+ private static void addLocatorTypeClass(ChatGptGetContextItem chatGptGetContextItem)
+ throws CodeContextOnDemandLocatorException {
+ EntityCategory entityCategory;
+ try {
+ entityCategory = EntityCategory.valueOf(
+ convertSnakeToPascalCase(chatGptGetContextItem.getEntityCategory())
+ );
+ } catch (IllegalArgumentException e) {
+ log.warn("Invalid entity category: {}", chatGptGetContextItem.getEntityCategory());
+ throw new CodeContextOnDemandLocatorException();
+ }
+ classNameComponents.add(entityCategory + "Locator");
+ }
+
+ private static String getCodeLocatorLanguage(ChatGptGetContextItem chatGptGetContextItem)
+ throws CodeContextOnDemandLocatorException {
+ String fileExtension;
+ try {
+ fileExtension = getExtension(chatGptGetContextItem.getFilename());
+ } catch (Exception e) {
+ throw new CodeContextOnDemandLocatorException();
+ }
+ return MAP_EXTENSION.get(fileExtension);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/language/python/CallableLocator.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/language/python/CallableLocator.java
new file mode 100644
index 0000000..6766deb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/code/context/ondemand/locator/language/python/CallableLocator.java
@@ -0,0 +1,87 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.code.context.ondemand.locator.language.python;
+
+import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.interfaces.mode.stateful.client.code.context.ondemand.IEntityLocator;
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.code.context.ondemand.locator.CallableLocatorBase;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.googlesource.gerrit.plugins.chatgpt.utils.TextUtils.ITEM_COMMA_DELIMITED_REGEX;
+
+@Slf4j
+public class CallableLocator extends CallableLocatorBase implements IEntityLocator {
+ private static final String PYTHON_MODULE_EXTENSION = ".py";
+ private static final Pattern IMPORT_PATTERN = Pattern.compile(
+ String.format(
+ "^(?:from\\s+(%1$s)\\s+import\\s+(\\*|\\w+(?:%2$s\\w+)*)|import\\s+(%1$s(?:%2$s%1$s))*)",
+ DOT_NOTATION_REGEX,
+ ITEM_COMMA_DELIMITED_REGEX
+ ),
+ Pattern.MULTILINE
+ );
+
+ private final Map<String, String> fromModuleMap = new HashMap<>();
+
+ public CallableLocator(Configuration config, GerritChange change, GitRepoFiles gitRepoFiles) {
+ super(config, change, gitRepoFiles);
+ log.debug("Initializing CallableLocator for Python projects");
+ languageModuleExtension = PYTHON_MODULE_EXTENSION;
+ }
+
+ @Override
+ protected String getFunctionRegex(String functionName) {
+ return "^\\s*(?:async\\s+)?def\\s+" + Pattern.quote(functionName) + "\\s*" +
+ "(?:\\[[^]]+\\]\\s*)?" + // Type Parameter List
+ "(?:\\([^)]*\\)\\s*)?" + // Arguments
+ "(?:->[^:]+\\s*)?:"; // Return type
+ }
+
+ @Override
+ protected String findImportedFunctionDefinition(String functionName, String content) {
+ parseImportStatements(content);
+
+ return findInImportModules(functionName);
+ }
+
+ private void parseImportStatements(String content) {
+ log.debug("Parsing import statements");
+ Matcher importMatcher = IMPORT_PATTERN.matcher(content);
+ while (importMatcher.find()) {
+ String fromModuleGroup = importMatcher.group(1);
+ String fromEntitiesGroup = importMatcher.group(2);
+ String importModulesGroup = importMatcher.group(3);
+
+ if (fromModuleGroup != null) {
+ log.debug("Parsing FROM module: `{}` / IMPORT entities: `{}`", fromModuleGroup, fromEntitiesGroup);
+ // The module is included in both `fromModuleMap` and `importModules`. The second case handles
+ // `from MODULE import *` statements and component class invocations that use dot notation.
+ getGroupStream(fromEntitiesGroup).forEach(entity -> fromModuleMap.put(entity, fromModuleGroup));
+ importModules.add(fromModuleGroup);
+ }
+ if (importModulesGroup != null) {
+ log.debug("Parsing IMPORT modules: `{}`", importModulesGroup);
+ getGroupStream(importModulesGroup).forEach(importModules::add);
+ }
+ }
+ log.debug("Found fromModuleMap: {}", fromModuleMap);
+ log.debug("Found importModules: {}", importModules);
+ }
+
+ private String findInImportModules(String functionName) {
+ // Check if the function is directly imported via `from MODULE import ENTITY`
+ if (fromModuleMap.containsKey(functionName)) {
+ String module = fromModuleMap.get(functionName);
+ log.debug("Function {} is directly imported from module: {}", functionName, module);
+ String result = getFunctionFromModule(functionName, module);
+ if (result != null) return result;
+ }
+ // If not found, proceed to check modules imported via `import MODULE`
+ return findInModules(functionName);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/code/context/ondemand/GetContextOutputItem.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/code/context/ondemand/GetContextOutputItem.java
new file mode 100644
index 0000000..1c0af1c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/model/code/context/ondemand/GetContextOutputItem.java
@@ -0,0 +1,16 @@
+package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.code.context.ondemand;
+
+import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptGetContextItem;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import lombok.experimental.SuperBuilder;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@SuperBuilder
+@ToString(callSuper = true)
+public class GetContextOutputItem extends ChatGptGetContextItem {
+ private String definition;
+ private String body;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/FileUtils.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/FileUtils.java
index 0cfb456..994f247 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/FileUtils.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/FileUtils.java
@@ -2,6 +2,7 @@
import lombok.extern.slf4j.Slf4j;
+import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
@@ -9,6 +10,7 @@
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
@Slf4j
public class FileUtils {
@@ -58,4 +60,8 @@
}
return filename.substring(lastDotIndex + 1);
}
+
+ public static String getDirName(String filename) {
+ return Optional.ofNullable(new File(filename).getParent()).orElse("");
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/ModuleUtils.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/ModuleUtils.java
new file mode 100644
index 0000000..906d945
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/ModuleUtils.java
@@ -0,0 +1,23 @@
+package com.googlesource.gerrit.plugins.chatgpt.utils;
+
+import java.util.List;
+
+public class ModuleUtils {
+ public static final String MODULE_SEPARATOR = ".";
+
+ public static String convertDotNotationToPath(String dotNotated) {
+ return dotNotated.replace(MODULE_SEPARATOR.charAt(0), '/');
+ }
+
+ public static String getSimpleName(String moduleName) {
+ int lastDotIndex = moduleName.lastIndexOf(MODULE_SEPARATOR);
+ if (lastDotIndex <= 0 || lastDotIndex == moduleName.length() - 1) {
+ return moduleName;
+ }
+ return moduleName.substring(lastDotIndex + 1);
+ }
+
+ public static String joinComponents(List<String> components) {
+ return String.join(MODULE_SEPARATOR, components);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/StringUtils.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/StringUtils.java
index 770922e..c998ec7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/StringUtils.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/StringUtils.java
@@ -3,6 +3,8 @@
import lombok.extern.slf4j.Slf4j;
import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
@Slf4j
public class StringUtils {
@@ -52,4 +54,26 @@
return camelCase.replaceAll("(?<!^)([A-Z])", "_$1").toLowerCase();
}
+ public static String convertSnakeToPascalCase(String snakeCase) {
+ if (snakeCase == null || snakeCase.isEmpty()) {
+ return snakeCase;
+ }
+ snakeCase = snakeCase.toLowerCase();
+ Pattern pattern = Pattern.compile("(?:^|_)(.)");
+ Matcher matcher = pattern.matcher(snakeCase);
+ StringBuilder result = new StringBuilder();
+ while (matcher.find()) {
+ matcher.appendReplacement(result, matcher.group(1).toUpperCase());
+ }
+ matcher.appendTail(result);
+
+ return result.toString();
+ }
+
+ public static String cutString(String str, int length) {
+ if (str == null) {
+ return "";
+ }
+ return str.length() <= length ? str : str.substring(0, length) + "...";
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/TextUtils.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/TextUtils.java
index 5010763..1dc4f71 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/TextUtils.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/TextUtils.java
@@ -21,7 +21,7 @@
public static final String COMMA_SPACE = ", ";
public static final String COLON_SPACE = ": ";
public static final String SEMICOLON_SPACE = "; ";
- public static final String ITEM_COMMA_DELIMITED = "\\s*,\\s*";
+ public static final String ITEM_COMMA_DELIMITED_REGEX = "\\s*,\\s*";
public static final String QUOTED_ENTIRE_ITEM = "^" + DOUBLE_QUOTES + "(.*)" + DOUBLE_QUOTES + "$";
@@ -112,7 +112,7 @@
}
public static List<String> splitString(String value) {
- return splitString(value, ITEM_COMMA_DELIMITED);
+ return splitString(value, ITEM_COMMA_DELIMITED_REGEX);
}
public static List<String> splitString(String value, String delimiter) {
diff --git a/src/main/resources/config/getContextTool.json b/src/main/resources/config/getContextTool.json
index 05b0ce8..3c81991 100644
--- a/src/main/resources/config/getContextTool.json
+++ b/src/main/resources/config/getContextTool.json
@@ -8,9 +8,28 @@
"properties": {
"replies": {
"type": "array",
+ "description": "Each item in `replies` represents a context request for a single code entity.",
"items": {
"type": "object",
"properties": {
+ "requestType": {
+ "type": "string",
+ "enum": ["definition", "body", "other"],
+ "description": "The type of context request: `definition` requests the entity's definition (e.g., the function signature); `body` requests the full content of the entity (e.g., the function body); `other` refers to any request that doesn't fit the previous categories."
+ },
+ "otherDescription": {
+ "type": "string",
+ "description": "If `requestType` is set to `other`, this field contains a description of the specific type of context being requested."
+ },
+ "entityCategory": {
+ "type": "string",
+ "enum": ["callable", "data", "type"],
+ "description": "The category of the entity: `callable` includes constructs that can be invoked or executed (e.g., functions, methods, ...); `data` includes elements that can hold, represent, or assume values during program execution (e.g., variables, constants, objects, arrays, ...); `type` refers to constructs that define the structure, blueprint, or schema of data types (e.g., classes, structs, enums, interfaces, ...)."
+ },
+ "contextRequiredEntity": {
+ "type": "string",
+ "description": "The entity for which the context is being requested."
+ },
"filename": {
"type": "string"
},
@@ -18,10 +37,15 @@
"type": "integer"
},
"codeSnippet": {
- "type": "string"
+ "type": "string",
+ "description": "The line of code containing the entity, from the first line character to the line break."
}
},
"required": [
+ "requestType",
+ "entityCategory",
+ "contextRequiredEntity",
+ "filename",
"codeSnippet"
]
}
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 940937c..78fbd4f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewTestBase.java
@@ -352,7 +352,7 @@
protected ICodeContextPolicy getCodeContextPolicy() {
return switch (config.getCodeContextPolicy()){
case NONE -> new CodeContextPolicyNone(config);
- case ON_DEMAND -> new CodeContextPolicyOnDemand(config);
+ case ON_DEMAND -> new CodeContextPolicyOnDemand(config, getGerritChange(), gitRepoFiles);
case UPLOAD_ALL -> new CodeContextPolicyUploadAll(config, getGerritChange(), gitRepoFiles, pluginDataHandlerProvider);
};
}
diff --git a/src/test/resources/__files/commands/dumpConfig.txt b/src/test/resources/__files/commands/dumpConfig.txt
index 16193c7..cb7b849 100644
--- a/src/test/resources/__files/commands/dumpConfig.txt
+++ b/src/test/resources/__files/commands/dumpConfig.txt
@@ -3,6 +3,7 @@
```
CONFIGURATION SETTINGS
+codeContextOnDemandBasePath:
codeContextPolicy: UPLOAD_ALL
directive:
disabledGroups: