Copilot commented on code in PR #816:
URL: 
https://github.com/apache/rocketmq-dashboard/pull/816#discussion_r3702368140


##########
server/src/main/java/org/apache/rocketmq/studio/ops/ai/OpenAiCompatibleLlmClient.java:
##########
@@ -208,19 +217,40 @@ private HttpRequest request(LlmConfigVO config, String 
accept, Map<String, Objec
         return builder.build();
     }
 
-    private Map<String, Object> requestBody(LlmConfigVO config, String prompt, 
String modelOverride,
+    private Map<String, Object> requestBody(LlmConfigVO config, 
List<LlmChatMessage> messages, String modelOverride,
                                             boolean stream) {
         Map<String, Object> body = new LinkedHashMap<>();
         body.put("model", StringUtils.hasText(modelOverride) ? 
modelOverride.trim() : config.getModel().trim());
-        body.put("messages", List.of(Map.of(
-                "role", "user",
-                "content", StringUtils.hasText(prompt) ? prompt.trim() : "")));
+        body.put("messages", normalizeMessages(messages));
         body.put("temperature", config.getTemperature());
         body.put("max_tokens", config.getMaxTokens());
         body.put("stream", stream);
         return body;
     }
 
+    private List<Map<String, String>> normalizeMessages(List<LlmChatMessage> 
messages) {
+        List<LlmChatMessage> normalizedMessages = messages == null || 
messages.isEmpty()
+                ? List.of(LlmChatMessage.user(""))
+                : messages;
+        return normalizedMessages.stream()
+                .map(message -> Map.of(
+                        "role", normalizeRole(message),
+                        "content", normalizeMessage(message == null ? null : 
message.content())))
+                .toList();
+    }
+
+    private String normalizeRole(LlmChatMessage message) {
+        if (message == null || !StringUtils.hasText(message.role())) {
+            return "user";
+        }
+        String role = message.role().trim();
+        return "assistant".equals(role) ? "assistant" : "user";
+    }

Review Comment:
   `normalizeRole` currently collapses any non-"assistant" role to "user". For 
an OpenAI-compatible chat-completions payload, this breaks support for standard 
roles like "system" (and potentially future roles), and can silently change 
semantics if callers ever pass such messages via the new `List<LlmChatMessage>` 
API. It’s safer to preserve known roles (at least `system`, `user`, 
`assistant`) and only default unknown/blank roles to `user`.



##########
server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConversationMemory.java:
##########
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai;
+
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class LlmConversationMemory {
+
+    private static final int MAX_CONVERSATIONS = 200;
+    private static final int MAX_MESSAGES_PER_CONVERSATION = 12;
+    private static final int MAX_MESSAGE_CHARS = 8_000;
+
+    private final Map<String, Deque<LlmChatMessage>> conversations =
+            new LinkedHashMap<>(16, 0.75f, true);
+
+    public synchronized List<LlmChatMessage> appendUserAndSnapshot(String 
conversationId, String message) {
+        LlmChatMessage userMessage = 
LlmChatMessage.user(normalizeContent(message));
+        String id = normalizeConversationId(conversationId);
+        if (!StringUtils.hasText(id)) {
+            return List.of(userMessage);
+        }
+        Deque<LlmChatMessage> messages = conversations.computeIfAbsent(id, 
ignored -> new ArrayDeque<>());

Review Comment:
   `LlmConversationMemory` is a singleton Spring `@Component` and the storage 
key is only the client-provided `conversationId`. This makes it possible for 
different users/sessions to collide or intentionally spoof the same 
`conversationId`, causing cross-user conversation context to be injected into 
the provider prompt (privacy / data-leak risk). Consider scoping the key by 
authenticated user/session (e.g., `${principalId}:${conversationId}`) or 
generating server-side conversation IDs instead of trusting a caller-supplied 
ID.



##########
server/src/main/java/org/apache/rocketmq/studio/ops/ai/OpenAiCompatibleLlmGateway.java:
##########
@@ -52,8 +54,11 @@ public SseEmitter chat(ChatDTO request) {
             return errorEmitter(unsupportedProviderException());
         }
 
+        String conversationId = request == null ? null : 
request.getConversationId();
+        String message = request == null ? null : request.getMessage();
+        List<LlmChatMessage> messages = 
conversationMemory.appendUserAndSnapshot(conversationId, message);
         SseEmitter emitter = new SseEmitter(60_000L);
-        executor.execute(() -> streamChat(request, config, emitter));
+        executor.execute(() -> streamChat(request, config, emitter, messages));

Review Comment:
   Conversation state is mutated before the provider call 
(`appendUserAndSnapshot`), and the assistant turn is appended only after 
streaming completes. If two requests for the same `conversationId` are 
in-flight concurrently, the second user turn can be appended before the first 
assistant turn, resulting in mis-ordered history (assistant answer attached 
after a later user question). Also, failed streams leave an unmatched user turn 
in memory, so a retry may duplicate the user message in context. Consider 
enforcing single in-flight request per conversationId or appending/committing 
turns atomically (e.g., buffer per-request and only commit on success).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to