Streamline rule integration in stateful prompt

The rules within the stateful review prompt have been atomized, and
their compilation into the prompt is now automated. This approach
reduces duplication of prompt components and enhances overall prompt
management.

Change-Id: I01e202e9b21977311714895d30b3fa05d3465b15
Signed-off-by: Patrizio <patrizio.gelosi@amarulasolutions.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/prompt/ChatGptPrompt.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/prompt/ChatGptPrompt.java
index 571b635..8553a6b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/prompt/ChatGptPrompt.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/common/client/prompt/ChatGptPrompt.java
@@ -38,7 +38,8 @@
     // Prompt constants loaded from JSON file
     public static String DEFAULT_GPT_SYSTEM_PROMPT;
     public static String DEFAULT_GPT_REVIEW_PROMPT_DIRECTIVES;
-    public static String DEFAULT_GPT_REPLIES_PROMPT;
+    public static String DEFAULT_GPT_PROMPT_FORCE_JSON_FORMAT;
+    public static String DEFAULT_GPT_REPLIES_PROMPT_SPECS;
     public static String DEFAULT_GPT_REPLIES_PROMPT_INLINE;
     public static String DEFAULT_GPT_REPLIES_PROMPT_ENFORCE_RESPONSE_CHECK;
     public static String DEFAULT_GPT_REQUEST_PROMPT_DIFF;
@@ -67,7 +68,8 @@
     }
 
     public static String getCommentRequestUserPrompt(int commentPropertiesSize) {
-        return buildFieldSpecifications(REQUEST_REPLY_ATTRIBUTES) + SPACE +
+        return DEFAULT_GPT_PROMPT_FORCE_JSON_FORMAT + SPACE +
+                buildFieldSpecifications(REQUEST_REPLY_ATTRIBUTES) + SPACE +
                 DEFAULT_GPT_REPLIES_PROMPT_INLINE + SPACE +
                 String.format(DEFAULT_GPT_REPLIES_PROMPT_ENFORCE_RESPONSE_CHECK, commentPropertiesSize);
     }
@@ -112,7 +114,7 @@
                 .map(entry -> entry.getKey() + SPACE + entry.getValue())
                 .collect(Collectors.toList());
 
-        return String.format(DEFAULT_GPT_REPLIES_PROMPT,
+        return String.format(DEFAULT_GPT_REPLIES_PROMPT_SPECS,
                 joinWithComma(attributes.keySet()),
                 joinWithSemicolon(fieldDescription)
         );
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/prompt/ChatGptPromptStateful.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/prompt/ChatGptPromptStateful.java
index 40fec9b..2cf6d2b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/prompt/ChatGptPromptStateful.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/prompt/ChatGptPromptStateful.java
@@ -9,14 +9,20 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import static com.googlesource.gerrit.plugins.chatgpt.utils.TextUtils.*;
+
 
 @Slf4j
 public class ChatGptPromptStateful extends ChatGptPrompt {
+    private static final String RULE_NUMBER_PREFIX = "RULE #";
+
     public static String DEFAULT_GPT_ASSISTANT_NAME;
     public static String DEFAULT_GPT_ASSISTANT_DESCRIPTION;
     public static String DEFAULT_GPT_ASSISTANT_INSTRUCTIONS;
     public static String DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_REVIEW;
     public static String DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_REQUESTS;
+    public static String DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_DONT_GUESS_CODE;
+    public static String DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_HISTORY;
     public static String DEFAULT_GPT_MESSAGE_REVIEW;
 
     private final ChangeSetData changeSetData;
@@ -50,7 +56,7 @@
         }
         else {
             instructions.addAll(List.of(
-                    DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_REVIEW,
+                    getGptAssistantInstructionsReview(),
                     getPatchSetReviewUserPrompt()
             ));
             if (config.getGptReviewCommitMessages()) {
@@ -75,4 +81,15 @@
         if (changeSetData == null || !isCommentEvent) return null;
         return changeSetData.getGptRequestUserPrompt();
     }
+
+    private String getGptAssistantInstructionsReview() {
+        return String.format(DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_REVIEW, joinWithNewLine(getNumberedList(
+                new ArrayList<>(List.of(
+                        DEFAULT_GPT_PROMPT_FORCE_JSON_FORMAT,
+                        DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_DONT_GUESS_CODE,
+                        DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_HISTORY
+                )),
+                RULE_NUMBER_PREFIX, COLON
+        )));
+    }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/prompt/ChatGptPromptStateless.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/prompt/ChatGptPromptStateless.java
index 50dc7a9..b63256e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/prompt/ChatGptPromptStateless.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateless/client/prompt/ChatGptPromptStateless.java
@@ -74,7 +74,7 @@
             }
             if (!changeSetData.getDirectives().isEmpty()) {
                 prompt.add(DEFAULT_GPT_REVIEW_PROMPT_DIRECTIVES);
-                prompt.add(getNumberedListString(new ArrayList<>(changeSetData.getDirectives())));
+                prompt.add(getNumberedListString(new ArrayList<>(changeSetData.getDirectives()), null, null));
             }
         }
         return joinWithNewLine(prompt);
@@ -89,7 +89,11 @@
 
     private List<String> getReviewSteps() {
         List<String> steps = new ArrayList<>(){};
-        steps.add(ChatGptPromptStateless.DEFAULT_GPT_REVIEW_PROMPT_REVIEW + SPACE + getPatchSetReviewUserPrompt());
+        steps.add(
+                DEFAULT_GPT_REVIEW_PROMPT_REVIEW + SPACE +
+                DEFAULT_GPT_PROMPT_FORCE_JSON_FORMAT + SPACE +
+                getPatchSetReviewUserPrompt()
+        );
         if (config.getGptReviewCommitMessages()) {
             steps.add(getReviewPromptCommitMessages());
         }
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 b64c5a4..a78470f 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
@@ -14,6 +14,7 @@
     public static final String CODE_DELIMITER = "```";
     public static final String CODE_DELIMITER_BEGIN ="\n\n" + CODE_DELIMITER + "\n";
     public static final String CODE_DELIMITER_END ="\n" + CODE_DELIMITER + "\n";
+    public static final String COLON = ": ";
 
     private static final String COMMA = ", ";
     private static final String SEMICOLON = "; ";
@@ -55,14 +56,18 @@
         return String.join(SEMICOLON, components);
     }
 
-    public static List<String> getNumberedList(List<String> components) {
+    public static List<String> getNumberedList(List<String> components, String prefix, String postfix) {
         return IntStream.range(0, components.size())
-                .mapToObj(i -> (i + 1) + ". " + components.get(i))
+                .mapToObj(i -> Optional.ofNullable(prefix).orElse("") +
+                        (i + 1) +
+                        Optional.ofNullable(postfix).orElse(". ") +
+                        components.get(i)
+                )
                 .collect(Collectors.toList());
     }
 
-    public static String getNumberedListString(List<String> components) {
-        return joinWithSemicolon(getNumberedList(components));
+    public static String getNumberedListString(List<String> components, String prefix, String postfix) {
+        return joinWithSemicolon(getNumberedList(components, prefix, postfix));
     }
 
     public static String prettyStringifyObject(Object object) {
diff --git a/src/main/resources/config/prompts.json b/src/main/resources/config/prompts.json
index 340d8ed..aa976f1 100644
--- a/src/main/resources/config/prompts.json
+++ b/src/main/resources/config/prompts.json
@@ -4,7 +4,8 @@
   "DEFAULT_GPT_REVIEW_PROMPT_COMMIT_MESSAGES": "You MUST review the commit message of the PatchSet and provide your feedback in an additional reply. The commit message is provided in %s. Ensure that the commit message accurately and succinctly describes the changes made, and verify if it matches the nature and scope of the changes in the PatchSet. If your feedback on the commit message is negative, you are required to supply an example of commit message that meets these criteria. For instance, if your comment is \"The commit message lacks detail\", you should follow up with \"A clearer commit message would be '...'\".",
   "DEFAULT_GPT_REQUEST_PROMPT_DIFF": "I have some requests about the following PatchSet Diff:",
   "DEFAULT_GPT_REQUEST_PROMPT_REQUESTS": "My requests are given in a JSON-formatted array, where each element includes the compulsory field `request`, the field `history` with any prior exchanged messages, and, for inline code comments, the fields `filename`, `lineNumber`, and `codeSnippet`:",
-  "DEFAULT_GPT_REPLIES_PROMPT": "You MUST provide your entire response as a JSON object; no other formats, such as plain text lists of suggestions, will be considered acceptable. Each reply must be formatted as an individual answer object within an array in the key `replies` of the response object, as defined in the tools function named `format_replies`. The answer object includes the string attributes %s, with the following specifications: %s.",
+  "DEFAULT_GPT_PROMPT_FORCE_JSON_FORMAT": "You MUST provide your entire response as a JSON object; no other formats, such as plain text lists of suggestions, will be considered acceptable. Each reply must be formatted as an individual answer object within an array in the key `replies` of the response object, as defined in the tools function named `format_replies`.",
+  "DEFAULT_GPT_REPLIES_PROMPT_SPECS": "The answer object includes the string attributes %s, with the following specifications: %s.",
   "DEFAULT_GPT_REPLIES_PROMPT_INLINE": "For replies that are specific to a certain part of the code, the object must additionally include the keys `filename`, `lineNumber`, and `codeSnippet` to precisely identify the relevant code section.",
   "DEFAULT_GPT_REPLIES_PROMPT_ENFORCE_RESPONSE_CHECK": "Make sure that the array in `replies` contains exactly %d element(s), one for each request.",
   "DEFAULT_GPT_REPLIES_ATTRIBUTES": {
diff --git a/src/main/resources/config/promptsStateful.json b/src/main/resources/config/promptsStateful.json
index 7b43efc..7f0e584 100644
--- a/src/main/resources/config/promptsStateful.json
+++ b/src/main/resources/config/promptsStateful.json
@@ -2,8 +2,10 @@
   "DEFAULT_GPT_ASSISTANT_NAME": "PatchSet Reviewer",
   "DEFAULT_GPT_ASSISTANT_DESCRIPTION": "PatchSet Reviewer for project %s.",
   "DEFAULT_GPT_ASSISTANT_INSTRUCTIONS": "The project file uploaded as JSON object includes the source files for the `%s` project. The JSON object structure uses the file paths (from the project's root) as keys, and the corresponding file contents (stored as arrays of lines) as their values. This arrangement ensures that the line number for any given line of content is equal to its array index plus one.",
-  "DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_REVIEW": "You will receive a patch in the standard git format-patch format. Your tasks include: 1. applying this patch to the corresponding existing files, and 2. conducting a review of the patch. While reviewing the patch, you MUST strictly adhere to each of the following rules; failure to do so will make your response invalid.\nRULE #1: You MUST provide your entire response as a JSON object; no other formats, such as plain text lists of suggestions, will be considered acceptable. Each reply must be formatted as an individual answer object within an array in the key `replies` of the response object, as defined in the tools function named `format_replies`.\nRULE #2: NEVER attempt to speculate about code that isn't explicitly included in the patch itself. You must locate all referenced code within the project's codebase. If certain code cannot be found, it indicates a potential error. For example, if a patch modifies a function call without changing the function's signature, you should verify compatibility with the existing signature in the codebase. If you cannot find the function's signature in the codebase, you must conclude that the function is not defined and raise a warning accordingly.\nRULE #3: You MUST take into account of the messages previously exchanged in the thread for your review. For instance, if you uncover new information relevant to the review that was not identified in your initial assessment, you must incorporate this information to update your review. Here are other guidelines for reviewing the patch: A. Identify any potential problems and offer suggestions for enhancements, presenting each point as a separate reply; B. Focus solely on identifying and suggesting solutions for issues; refrain from highlighting any positive aspects; C. Only evaluate the code that has been modified in the patch; refrain from reviewing any other parts of the project's code that were not changed.",
+  "DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_REVIEW": "You will receive a patch in the standard git format-patch format. Your tasks include: 1. applying this patch to the corresponding existing files, and 2. conducting a review of the patch. While reviewing the patch, you MUST strictly adhere to each of the following rules; failure to do so will make your response invalid.\n%s\nHere are other guidelines for reviewing the patch: A. Identify any potential problems and offer suggestions for enhancements, presenting each point as a separate reply; B. Focus solely on identifying and suggesting solutions for issues; refrain from highlighting any positive aspects; C. Only evaluate the code that has been modified in the patch; refrain from reviewing any other parts of the project's code that were not changed.",
   "DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_REQUESTS": "You will receive a prompt request regarding the codebase files and/or one or more patches applied to these files. You are required to respond to the prompt, which may involve providing information, completing a task, answering a query, or making specified modifications. If you need or are requested to access any file from codebase, you will extract it from the project file uploaded. Additionally, you MUST take into account of the messages previously exchanged in the thread in your responses. For example, if you discover something in your previous answers that is relevant to the current response but was not initially identified, you must use this information in your answer.",
+  "DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_DONT_GUESS_CODE": "NEVER attempt to speculate about code that isn't explicitly included in the patch itself. You must locate all referenced code within the project's codebase. If certain code cannot be found, it indicates a potential error. For example, if a patch modifies a function call without changing the function's signature, you should verify compatibility with the existing signature in the codebase. If you cannot find the function's signature in the codebase, you must conclude that the function is not defined and raise a warning accordingly.",
+  "DEFAULT_GPT_ASSISTANT_INSTRUCTIONS_HISTORY": "You MUST take into account of the messages previously exchanged in the thread for your review. For instance, if you uncover new information relevant to the review that was not identified in your initial assessment, you must incorporate this information to update your review.",
   "DEFAULT_GPT_MESSAGE_REVIEW": "Review the following Patch Set: ```%s```",
   "DEFAULT_GPT_HOW_TO_FIND_COMMIT_MESSAGE": "the \"Subject:\" entry of the Patch Set"
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java
index b28bb7e..101eb61 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatelessTest.java
@@ -109,6 +109,7 @@
         return joinWithNewLine(Arrays.asList(
                 ChatGptPromptStateless.DEFAULT_GPT_REVIEW_PROMPT,
                 ChatGptPromptStateless.DEFAULT_GPT_REVIEW_PROMPT_REVIEW + " " +
+                        ChatGptPromptStateless.DEFAULT_GPT_PROMPT_FORCE_JSON_FORMAT + " " +
                         chatGptPromptStateless.getPatchSetReviewUserPrompt(),
                 ChatGptPromptStateless.getReviewPromptCommitMessages(),
                 ChatGptPromptStateless.DEFAULT_GPT_REVIEW_PROMPT_DIFF,