Optimize assistant reuse by properties

This commit implements a new approach to store ChatGPT assistant IDs,
achieving several goals:
- Fixes the issue where existing assistants retain old settings after a
configuration change.
- Addresses a similar issue where dynamically changed settings via the
`/configure` command were not updated in existing assistants.
- Reduces the creation of new assistants by reusing those with identical
properties.

A hash key is now calculated based on the assistant's configurable
properties, and this key is used to store and retrieve the ChatGPT
assistant ID.

Change-Id: I226e28b7c24da12c05b8d8554c04bc0a79d99ae8
Signed-off-by: Patrizio <patrizio.gelosi@amarulasolutions.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandler.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandler.java
index 44c1817..cf390e4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandler.java
@@ -55,6 +55,14 @@
         }
     }
 
+    public synchronized void destroy() {
+        try {
+            Files.deleteIfExists(configFile);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to delete the config file: " + configFile, e);
+        }
+    }
+
     private void storeProperties() {
         try (var output = Files.newOutputStream(configFile)) {
             configProperties.store(output, null);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandlerProvider.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandlerProvider.java
index b178743..a21c74b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandlerProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/data/PluginDataHandlerProvider.java
@@ -11,8 +11,11 @@
 
 @Singleton
 public class PluginDataHandlerProvider extends PluginDataHandlerBaseProvider implements Provider<PluginDataHandler> {
+    private static final String PATH_ASSISTANTS = ".assistants";
+
     private final String projectName;
     private final String changeKey;
+    private final String assistantsWorkspace;
 
     @Inject
     public PluginDataHandlerProvider(
@@ -22,6 +25,7 @@
         super(defaultPluginDataPath);
         projectName = sanitizeFilename(change.getProjectName());
         changeKey = change.getChangeKey().toString();
+        assistantsWorkspace = projectName + PATH_ASSISTANTS;
     }
 
     public PluginDataHandler getGlobalScope() {
@@ -35,4 +39,8 @@
     public PluginDataHandler getChangeScope() {
         return super.get(changeKey);
     }
+
+    public PluginDataHandler getAssistantsWorkspace() {
+        return super.get(assistantsWorkspace);
+    }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventHandlerTypeChangeMerged.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventHandlerTypeChangeMerged.java
index 6d033ba..8b82e56 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventHandlerTypeChangeMerged.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventHandlerTypeChangeMerged.java
@@ -5,8 +5,7 @@
 import com.googlesource.gerrit.plugins.chatgpt.interfaces.listener.IEventHandlerType;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.data.ChangeSetData;
-import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt.ChatGptAssistantBase;
-import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt.ChatGptAssistantReview;
+import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt.ChatGptAssistant;
 import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
 import lombok.extern.slf4j.Slf4j;
 
@@ -39,7 +38,7 @@
 
     @Override
     public void processEvent() {
-        ChatGptAssistantBase chatGptAssistant = new ChatGptAssistantBase(
+        ChatGptAssistant chatGptAssistant = new ChatGptAssistant(
                 config,
                 changeSetData,
                 change,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistantBase.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistant.java
similarity index 78%
rename from src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistantBase.java
rename to src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistant.java
index ba4f1c8..1092a8b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistantBase.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistant.java
@@ -13,12 +13,14 @@
 import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
 import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.prompt.ChatGptPromptStateful;
 import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.model.api.chatgpt.*;
-import lombok.Getter;
+import com.googlesource.gerrit.plugins.chatgpt.utils.HashUtils;
 import lombok.extern.slf4j.Slf4j;
 import okhttp3.Request;
 
 import java.net.URI;
 import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
 
 import static com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt.ChatGptVectorStore.KEY_VECTOR_STORE_ID;
 import static com.googlesource.gerrit.plugins.chatgpt.utils.FileUtils.createTempFileWithContent;
@@ -26,20 +28,20 @@
 import static com.googlesource.gerrit.plugins.chatgpt.utils.GsonUtils.getGson;
 
 @Slf4j
-public class ChatGptAssistantBase extends ClientBase {
-    protected static final String KEY_REVIEW_ASSISTANT_ID = "reviewAssistantId";
-    protected static final String KEY_REQUESTS_ASSISTANT_ID = "requestsAssistantId";
-
-    @Getter
-    protected String keyAssistantId;
-
+public class ChatGptAssistant extends ClientBase {
     private final ChatGptHttpClient httpClient = new ChatGptHttpClient();
     private final ChangeSetData changeSetData;
     private final GerritChange change;
     private final GitRepoFiles gitRepoFiles;
     private final PluginDataHandler projectDataHandler;
+    private final PluginDataHandler assistantsDataHandler;
 
-    public ChatGptAssistantBase(
+    private String description;
+    private String instructions;
+    private String model;
+    private Double temperature;
+
+    public ChatGptAssistant(
             Configuration config,
             ChangeSetData changeSetData,
             GerritChange change,
@@ -51,20 +53,25 @@
         this.change = change;
         this.gitRepoFiles = gitRepoFiles;
         this.projectDataHandler = pluginDataHandlerProvider.getProjectScope();
+        this.assistantsDataHandler = pluginDataHandlerProvider.getAssistantsWorkspace();
     }
 
-    public void setupAssistant() {
-        String assistantId = projectDataHandler.getValue(keyAssistantId);
+    public String setupAssistant() {
+        setupAssistantParameters();
+        String assistantIdHashKey = calculateAssistantIdHashKey();
+        log.info("Calculated assistant id hash key: {}", assistantIdHashKey);
+        String assistantId = assistantsDataHandler.getValue(assistantIdHashKey);
         if (assistantId == null || config.getForceCreateAssistant()) {
             log.debug("Setup Assistant for project {}", change.getProjectNameKey());
             String vectorStoreId = createVectorStore();
             assistantId = createAssistant(vectorStoreId);
-            projectDataHandler.setValue(keyAssistantId, assistantId);
+            assistantsDataHandler.setValue(assistantIdHashKey, assistantId);
             log.info("Project assistant created with ID: {}", assistantId);
         }
         else {
             log.info("Project assistant found for the project. Assistant ID: {}", assistantId);
         }
+        return assistantId;
     }
 
     public String createVectorStore() {
@@ -85,8 +92,7 @@
 
     public void flushAssistantIds() {
         projectDataHandler.removeValue(KEY_VECTOR_STORE_ID);
-        projectDataHandler.removeValue(KEY_REVIEW_ASSISTANT_ID);
-        projectDataHandler.removeValue(KEY_REQUESTS_ASSISTANT_ID);
+        assistantsDataHandler.destroy();
     }
 
     private String uploadRepoFiles() {
@@ -111,8 +117,6 @@
     private Request createRequest(String vectorStoreId) {
         URI uri = URI.create(config.getGptDomain() + UriResourceLocatorStateful.assistantCreateUri());
         log.debug("ChatGPT Create Assistant request URI: {}", uri);
-        ChatGptPromptStateful chatGptPromptStateful = new ChatGptPromptStateful(config, changeSetData, change);
-        ChatGptParameters chatGptParameters = new ChatGptParameters(config, change.getIsCommentEvent());
         ChatGptTool[] tools = new ChatGptTool[] {
                 new ChatGptTool("file_search"),
                 ChatGptTools.retrieveFormatRepliesTool()
@@ -124,10 +128,10 @@
         );
         ChatGptCreateAssistantRequestBody requestBody = ChatGptCreateAssistantRequestBody.builder()
                 .name(ChatGptPromptStateful.DEFAULT_GPT_ASSISTANT_NAME)
-                .description(chatGptPromptStateful.getDefaultGptAssistantDescription())
-                .instructions(chatGptPromptStateful.getDefaultGptAssistantInstructions())
-                .model(config.getGptModel())
-                .temperature(chatGptParameters.getGptTemperature())
+                .description(description)
+                .instructions(instructions)
+                .model(model)
+                .temperature(temperature)
                 .tools(tools)
                 .toolResources(toolResources)
                 .build();
@@ -135,4 +139,23 @@
 
         return httpClient.createRequestFromJson(uri.toString(), config.getGptToken(), requestBody);
     }
+
+    private void setupAssistantParameters() {
+        ChatGptPromptStateful chatGptPromptStateful = new ChatGptPromptStateful(config, changeSetData, change);
+        ChatGptParameters chatGptParameters = new ChatGptParameters(config, change.getIsCommentEvent());
+
+        description = chatGptPromptStateful.getDefaultGptAssistantDescription();
+        instructions = chatGptPromptStateful.getDefaultGptAssistantInstructions();
+        model = config.getGptModel();
+        temperature = chatGptParameters.getGptTemperature();
+    }
+
+    private String calculateAssistantIdHashKey() {
+        return HashUtils.hashData(new ArrayList<>(List.of(
+                description,
+                instructions,
+                model,
+                temperature.toString()
+        )));
+    }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistantRequests.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistantRequests.java
deleted file mode 100644
index f2f0b5a..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistantRequests.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt;
-
-import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
-import com.googlesource.gerrit.plugins.chatgpt.data.PluginDataHandlerProvider;
-import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
-import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.data.ChangeSetData;
-import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
-import lombok.extern.slf4j.Slf4j;
-
-@Slf4j
-public class ChatGptAssistantRequests extends ChatGptAssistantBase {
-    public ChatGptAssistantRequests(
-            Configuration config,
-            ChangeSetData changeSetData,
-            GerritChange change,
-            GitRepoFiles gitRepoFiles,
-            PluginDataHandlerProvider pluginDataHandlerProvider
-    ) {
-        super(config, changeSetData, change, gitRepoFiles, pluginDataHandlerProvider);
-        keyAssistantId = KEY_REQUESTS_ASSISTANT_ID;
-    }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistantReview.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistantReview.java
deleted file mode 100644
index abc8ac9..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptAssistantReview.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt;
-
-import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
-import com.googlesource.gerrit.plugins.chatgpt.data.PluginDataHandlerProvider;
-import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
-import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.data.ChangeSetData;
-import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles;
-import lombok.extern.slf4j.Slf4j;
-
-@Slf4j
-public class ChatGptAssistantReview extends ChatGptAssistantBase {
-    public ChatGptAssistantReview(
-            Configuration config,
-            ChangeSetData changeSetData,
-            GerritChange change,
-            GitRepoFiles gitRepoFiles,
-            PluginDataHandlerProvider pluginDataHandlerProvider
-    ) {
-        super(config, changeSetData, change, gitRepoFiles, pluginDataHandlerProvider);
-        keyAssistantId = KEY_REVIEW_ASSISTANT_ID;
-    }
-}
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 eee376b..e38af1c 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
@@ -62,8 +62,7 @@
                 changeSetData,
                 change,
                 gitRepoFiles,
-                pluginDataHandlerProvider,
-                isCommentEvent
+                pluginDataHandlerProvider
         );
         chatGptRun.createRun();
         chatGptRun.pollRunStep();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRun.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRun.java
index 7e5ed34..02c9b99 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRun.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/mode/stateful/client/api/chatgpt/ChatGptRun.java
@@ -1,7 +1,6 @@
 package com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt;
 
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
-import com.googlesource.gerrit.plugins.chatgpt.data.PluginDataHandler;
 import com.googlesource.gerrit.plugins.chatgpt.data.PluginDataHandlerProvider;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.ClientBase;
 import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
@@ -39,11 +38,10 @@
     private final String threadId;
     private final GitRepoFiles gitRepoFiles;
     private final PluginDataHandlerProvider pluginDataHandlerProvider;
-    private final boolean isCommentEvent;
 
     private ChatGptResponse runResponse;
     private ChatGptListResponse stepResponse;
-    private String keyAssistantId;
+    private String assistantId;
 
     public ChatGptRun(
             String threadId,
@@ -51,8 +49,7 @@
             ChangeSetData changeSetData,
             GerritChange change,
             GitRepoFiles gitRepoFiles,
-            PluginDataHandlerProvider pluginDataHandlerProvider,
-            boolean isCommentEvent
+            PluginDataHandlerProvider pluginDataHandlerProvider
     ) {
         super(config);
         this.changeSetData = changeSetData;
@@ -60,16 +57,17 @@
         this.threadId = threadId;
         this.gitRepoFiles = gitRepoFiles;
         this.pluginDataHandlerProvider = pluginDataHandlerProvider;
-        this.isCommentEvent = isCommentEvent;
     }
 
     public void createRun() {
-        ChatGptAssistantBase chatGptAssistant = isCommentEvent ?
-             new ChatGptAssistantRequests(config, changeSetData, change, gitRepoFiles, pluginDataHandlerProvider) :
-             new ChatGptAssistantReview(config, changeSetData, change, gitRepoFiles, pluginDataHandlerProvider);
-
-        chatGptAssistant.setupAssistant();
-        keyAssistantId = chatGptAssistant.getKeyAssistantId();
+        ChatGptAssistant chatGptAssistant = new ChatGptAssistant(
+                config,
+                changeSetData,
+                change,
+                gitRepoFiles,
+                pluginDataHandlerProvider
+        );
+        assistantId = chatGptAssistant.setupAssistant();
 
         Request request = runCreateRequest();
         log.info("ChatGPT Create Run request: {}", request);
@@ -145,10 +143,8 @@
     private Request runCreateRequest() {
         URI uri = URI.create(config.getGptDomain() + UriResourceLocatorStateful.runsUri(threadId));
         log.debug("ChatGPT Create Run request URI: {}", uri);
-
-        PluginDataHandler projectDataHandler = pluginDataHandlerProvider.getProjectScope();
         ChatGptCreateRunRequest requestBody = ChatGptCreateRunRequest.builder()
-                .assistantId(projectDataHandler.getValue(keyAssistantId))
+                .assistantId(assistantId)
                 .build();
 
         return httpClient.createRequestFromJson(uri.toString(), config.getGptToken(), requestBody);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/HashUtils.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/HashUtils.java
new file mode 100644
index 0000000..7e9840c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/HashUtils.java
@@ -0,0 +1,39 @@
+package com.googlesource.gerrit.plugins.chatgpt.utils;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+
+public class HashUtils {
+
+    public static String hashData(List<String> dataItems) {
+        StringBuilder concatenatedData = new StringBuilder();
+        for (String item : dataItems) {
+            concatenatedData.append(item);
+        }
+        return sha1(concatenatedData.toString());
+    }
+
+    private static String sha1(String data) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-1");
+            byte[] hashBytes = digest.digest(data.getBytes(StandardCharsets.UTF_8));
+            return bytesToHex(hashBytes);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("SHA-1 algorithm not found", e);
+        }
+    }
+
+    private static String bytesToHex(byte[] hashBytes) {
+        StringBuilder hexString = new StringBuilder(2 * hashBytes.length);
+        for (byte b : hashBytes) {
+            String hex = Integer.toHexString(0xff & b);
+            if (hex.length() == 1) {
+                hexString.append('0');
+            }
+            hexString.append(hex);
+        }
+        return hexString.toString();
+    }
+}
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 239f84b..6c74d1d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/ChatGptReviewStatefulTest.java
@@ -69,6 +69,8 @@
         projectHandler = provider.getProjectScope();
         // Mock the pluginDataHandlerProvider to return the mocked project pluginDataHandler
         when(pluginDataHandlerProvider.getProjectScope()).thenReturn(projectHandler);
+        // Mock the pluginDataHandlerProvider to return the mocked assistant pluginDataHandler
+        when(pluginDataHandlerProvider.getAssistantsWorkspace()).thenReturn(projectHandler);
     }
 
     protected void initTest() {