Handle "message_creation" responses in JSON format

Occasionally, ChatGPT's "message_creation" response may include a
JSON-structured output embedded within a plain text message. This update
ensures these cases are properly managed.

Change-Id: I16ef37448cd65bbf10ba779636b74754ba2317f8
Signed-off-by: Patrizio <patrizio.gelosi@amarulasolutions.com>
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 f0eb5f8..3d7b0b7 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
@@ -14,6 +14,10 @@
 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.getGson;
+import static com.googlesource.gerrit.plugins.chatgpt.utils.JsonTextUtils.isJsonString;
+import static com.googlesource.gerrit.plugins.chatgpt.utils.JsonTextUtils.unwrapJsonCode;
+
 @Slf4j
 @Singleton
 public class ChatGptClientStateful extends ChatGptClient implements IChatGptClient {
@@ -87,6 +91,17 @@
         ChatGptThreadMessageResponse threadMessageResponse = chatGptThreadMessage.retrieveMessage(
                 chatGptRun.getFirstStepDetails().getMessageCreation().getMessageId()
         );
-        return new ChatGptResponseContent(threadMessageResponse.getContent().get(0).getText().getValue());
+        String responseText = threadMessageResponse.getContent().get(0).getText().getValue();
+        if (responseText == null) {
+            throw new RuntimeException("ChatGPT thread message response content is null");
+        }
+        if (isJsonString(responseText)) {
+            return extractResponseContent(responseText);
+        }
+        return new ChatGptResponseContent(responseText);
+    }
+
+    private ChatGptResponseContent extractResponseContent(String responseText) {
+        return getGson().fromJson(unwrapJsonCode(responseText), ChatGptResponseContent.class);
     }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/JsonTextUtils.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/JsonTextUtils.java
new file mode 100644
index 0000000..e03ca7b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/JsonTextUtils.java
@@ -0,0 +1,20 @@
+package com.googlesource.gerrit.plugins.chatgpt.utils;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.regex.Pattern;
+
+@Slf4j
+public class JsonTextUtils extends TextUtils {
+    private static final Pattern JSON_DELIMITED = Pattern.compile("^.*?" + CODE_DELIMITER + "json\\s*(.*)\\s*" +
+                    CODE_DELIMITER + ".*$", Pattern.DOTALL);
+    private static final Pattern JSON_OBJECT = Pattern.compile("^\\{.*\\}$", Pattern.DOTALL);
+
+    public static String unwrapJsonCode(String text) {
+        return JSON_DELIMITED.matcher(text).replaceAll("$1");
+    }
+
+    public static boolean isJsonString(String text) {
+        return JSON_OBJECT.matcher(text).matches() || JSON_DELIMITED.matcher(text).matches();
+    }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
index 75be9c3..5b07882 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
@@ -204,7 +204,7 @@
     }
 
     @Test
-    public void gptMentionedInCommentMessageResponse() throws RestApiException {
+    public void gptMentionedInCommentMessageResponseText() throws RestApiException {
         String reviewMessageCommitMessage = getReviewMessage("__files/chatGptResponseRequestStateful.json", 0);
 
         chatGptPromptStateful.setCommentEvent(true);
@@ -220,7 +220,33 @@
                 .willReturn(WireMock.aResponse()
                         .withStatus(HTTP_OK)
                         .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
-                        .withBodyFile("chatGptResponseThreadMessage.json")));
+                        .withBodyFile("chatGptResponseThreadMessageText.json")));
+
+        handleEventBasedOnType(true);
+
+        ArgumentCaptor<ReviewInput> captor = testRequestSent();
+        Assert.assertEquals(promptTagComments, requestContent);
+        Assert.assertEquals(reviewMessageCommitMessage, getCapturedMessage(captor, GERRIT_PATCH_SET_FILENAME));
+    }
+
+    @Test
+    public void gptMentionedInCommentMessageResponseJson() throws RestApiException {
+        String reviewMessageCommitMessage = getReviewMessage("__files/chatGptResponseRequestStateful.json", 0);
+
+        chatGptPromptStateful.setCommentEvent(true);
+        // Mock the behavior of the ChatGPT retrieve-run-steps request
+        WireMock.stubFor(WireMock.get(WireMock.urlEqualTo(URI.create(config.getGptDomain()
+                        + UriResourceLocatorStateful.runStepsUri(CHAT_GPT_THREAD_ID, CHAT_GPT_RUN_ID)).getPath()))
+                .willReturn(WireMock.aResponse()
+                        .withStatus(HTTP_OK)
+                        .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
+                        .withBodyFile("chatGptResponseRequestMessageStateful.json")));
+        WireMock.stubFor(WireMock.get(WireMock.urlEqualTo(URI.create(config.getGptDomain()
+                        + UriResourceLocatorStateful.threadMessageRetrieveUri(CHAT_GPT_THREAD_ID, CHAT_GPT_MESSAGE_ID)).getPath()))
+                .willReturn(WireMock.aResponse()
+                        .withStatus(HTTP_OK)
+                        .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
+                        .withBodyFile("chatGptResponseThreadMessageJson.json")));
 
         handleEventBasedOnType(true);
 
diff --git a/src/test/resources/__files/chatGptResponseThreadMessageJson.json b/src/test/resources/__files/chatGptResponseThreadMessageJson.json
new file mode 100644
index 0000000..0cabd66
--- /dev/null
+++ b/src/test/resources/__files/chatGptResponseThreadMessageJson.json
@@ -0,0 +1,12 @@
+{
+  "object": "thread.message",
+  "role": "assistant",
+  "content": [
+    {
+      "type": "text",
+      "text": {
+        "value": "```json\n{\n  \"replies\": [\n    {\n      \"id\": 0,\n      \"reply\": \"The commit message 'Corrected Indentation in Module-Class Retrieval Line' accurately represents the change made in the code.\"\n    }\n  ]\n}\n```"
+      }
+    }
+  ]
+}
diff --git a/src/test/resources/__files/chatGptResponseThreadMessage.json b/src/test/resources/__files/chatGptResponseThreadMessageText.json
similarity index 100%
rename from src/test/resources/__files/chatGptResponseThreadMessage.json
rename to src/test/resources/__files/chatGptResponseThreadMessageText.json