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: