Skip to content

Commit 690f6ed

Browse files
authored
Chat Partial Result (#36)
* Log partial result * Reaching partial result handler * Rendering partial responses successfully * Add comments * Cleaned up code and added comments * Update comment * Merged with latest changes * Move ignore null serialization to ChatResult record * Fix checkstyle errors * Fix checkstyle after merge
1 parent c8713d6 commit 690f6ed

File tree

9 files changed

+304
-21
lines changed

9 files changed

+304
-21
lines changed

plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManager.java

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package software.aws.toolkits.eclipse.amazonq.chat;
44

55
import java.util.concurrent.CompletableFuture;
6+
import java.util.UUID;
67

78
import org.eclipse.swt.browser.Browser;
89

@@ -15,23 +16,40 @@
1516
import software.aws.toolkits.eclipse.amazonq.util.PluginLogger;
1617
import software.aws.toolkits.eclipse.amazonq.views.model.Command;
1718

19+
/**
20+
* ChatCommunicationManager is a central component of the Amazon Q Eclipse Plugin that
21+
* acts as a bridge between the plugin's UI and the LSP server. It is also responsible
22+
* for managing communication between the plugin and the webview used for displaying
23+
* chat conversations. It is implemented as a singleton to centralize control of all
24+
* communication in the plugin.
25+
*/
1826
public final class ChatCommunicationManager {
27+
private static ChatCommunicationManager instance;
1928

2029
private final JsonHandler jsonHandler;
2130
private final CompletableFuture<ChatMessageProvider> chatMessageProvider;
31+
private final ChatPartialResultMap chatPartialResultMap;
2232

23-
public ChatCommunicationManager() {
33+
private ChatCommunicationManager() {
2434
this.jsonHandler = new JsonHandler();
2535
this.chatMessageProvider = ChatMessageProvider.createAsync();
36+
this.chatPartialResultMap = new ChatPartialResultMap();
2637
}
2738

28-
public CompletableFuture<ChatResult> sendMessageToChatServer(final Command command, final Object params) {
39+
public static synchronized ChatCommunicationManager getInstance() {
40+
if (instance == null) {
41+
instance = new ChatCommunicationManager();
42+
}
43+
return instance;
44+
}
45+
46+
public CompletableFuture<ChatResult> sendMessageToChatServer(final Browser browser, final Command command, final Object params) {
2947
return chatMessageProvider.thenCompose(chatMessageProvider -> {
3048
try {
3149
switch (command) {
3250
case CHAT_SEND_PROMPT:
3351
ChatRequestParams chatRequestParams = jsonHandler.convertObject(params, ChatRequestParams.class);
34-
return chatMessageProvider.sendChatPrompt(chatRequestParams);
52+
return chatMessageProvider.sendChatPrompt(browser, chatRequestParams);
3553
case CHAT_READY:
3654
chatMessageProvider.sendChatReady();
3755
return CompletableFuture.completedFuture(null);
@@ -50,11 +68,39 @@ public CompletableFuture<ChatResult> sendMessageToChatServer(final Command comma
5068
}
5169

5270
public void sendMessageToChatUI(final Browser browser, final ChatUIInboundCommand command) {
53-
String message = this.jsonHandler.serialize(command);
71+
String message = jsonHandler.serialize(command);
72+
5473
String script = "window.postMessage(" + message + ");";
5574
browser.getDisplay().asyncExec(() -> {
5675
browser.evaluate(script);
5776
});
5877
}
5978

79+
/*
80+
* Gets the partial chat message using the provided token.
81+
*/
82+
public ChatMessage getPartialChatMessage(final String partialResultToken) {
83+
return chatPartialResultMap.getValue(partialResultToken);
84+
}
85+
86+
/*
87+
* Adds an entry to the partialResultToken to ChatMessage map.
88+
*/
89+
public String addPartialChatMessage(final ChatMessage chatMessage) {
90+
String partialResultToken = UUID.randomUUID().toString();
91+
92+
// Indicator for the server to send partial result notifications
93+
chatMessage.getChatRequestParams().setPartialResultToken(partialResultToken);
94+
95+
chatPartialResultMap.setEntry(partialResultToken, chatMessage);
96+
return partialResultToken;
97+
}
98+
99+
/*
100+
* Removes an entry from the partialResultToken to ChatMessage map.
101+
*/
102+
public void removePartialChatMessage(final String partialResultToken) {
103+
chatPartialResultMap.removeEntry(partialResultToken);
104+
}
60105
}
106+
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
package software.aws.toolkits.eclipse.amazonq.chat;
4+
5+
import java.util.concurrent.CompletableFuture;
6+
7+
import org.eclipse.swt.browser.Browser;
8+
9+
import software.aws.toolkits.eclipse.amazonq.chat.models.ChatRequestParams;
10+
import software.aws.toolkits.eclipse.amazonq.chat.models.ChatResult;
11+
import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer;
12+
13+
public final class ChatMessage {
14+
private final Browser browser;
15+
private final ChatRequestParams chatRequestParams;
16+
private final AmazonQLspServer amazonQLspServer;
17+
private final ChatCommunicationManager chatCommunicationManager;
18+
19+
public ChatMessage(final AmazonQLspServer amazonQLspServer, final Browser browser, final ChatRequestParams chatRequestParams) {
20+
this.amazonQLspServer = amazonQLspServer;
21+
this.browser = browser;
22+
this.chatRequestParams = chatRequestParams;
23+
this.chatCommunicationManager = ChatCommunicationManager.getInstance();
24+
}
25+
26+
public Browser getBrowser() {
27+
return browser;
28+
}
29+
30+
public ChatRequestParams getChatRequestParams() {
31+
return chatRequestParams;
32+
}
33+
34+
public String getPartialResultToken() {
35+
return chatRequestParams.getPartialResultToken();
36+
}
37+
38+
public CompletableFuture<ChatResult> sendChatMessageWithProgress() {
39+
// Retrieving the chat result is expected to be a long-running process with intermittent progress notifications being sent
40+
// from the LSP server. The progress notifications provide a token and a partial result Object - we are utilizing a token to
41+
// ChatMessage mapping to acquire the associated ChatMessage so we can formulate a message for the UI.
42+
String partialResultToken = chatCommunicationManager.addPartialChatMessage(this);
43+
44+
CompletableFuture<ChatResult> chatResult = amazonQLspServer.sendChatPrompt(chatRequestParams)
45+
.thenApply(result -> {
46+
// The mapping entry no longer needs to be maintained once the final result is retrieved.
47+
chatCommunicationManager.removePartialChatMessage(partialResultToken);
48+
return result;
49+
});
50+
51+
return chatResult;
52+
}
53+
}

plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatMessageProvider.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import java.util.concurrent.CompletableFuture;
66

7+
import org.eclipse.swt.browser.Browser;
8+
79
import software.aws.toolkits.eclipse.amazonq.chat.models.ChatRequestParams;
810
import software.aws.toolkits.eclipse.amazonq.chat.models.ChatResult;
911
import software.aws.toolkits.eclipse.amazonq.chat.models.GenericTabParams;
@@ -26,14 +28,9 @@ private ChatMessageProvider(final AmazonQLspServer amazonQLspServer) {
2628
this.amazonQLspServer = amazonQLspServer;
2729
}
2830

29-
public CompletableFuture<ChatResult> sendChatPrompt(final ChatRequestParams chatRequestParams) {
30-
try {
31-
PluginLogger.info("Sending " + Command.CHAT_SEND_PROMPT + " message to Amazon Q LSP server");
32-
return amazonQLspServer.sendChatPrompt(chatRequestParams);
33-
} catch (Exception e) {
34-
PluginLogger.error("Error occurred while sending " + Command.CHAT_SEND_PROMPT + " message to Amazon Q LSP server", e);
35-
return CompletableFuture.failedFuture(new AmazonQPluginException(e));
36-
}
31+
public CompletableFuture<ChatResult> sendChatPrompt(final Browser browser, final ChatRequestParams chatRequestParams) {
32+
ChatMessage chatMessage = new ChatMessage(amazonQLspServer, browser, chatRequestParams);
33+
return chatMessage.sendChatMessageWithProgress();
3734
}
3835

3936
public void sendChatReady() {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
package software.aws.toolkits.eclipse.amazonq.chat;
4+
5+
import java.util.Map;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
8+
9+
/**
10+
* ChatPartialResultMap is a utility class responsible for managing the mapping between
11+
* partial result tokens and their corresponding ChatMessage objects in the Amazon Q plugin for Eclipse.
12+
*
13+
* The Language Server Protocol (LSP) server sends progress notifications during long-running operations,
14+
* such as processing chat requests. These notifications include a token that identifies the specific operation
15+
* and a partial result object containing the progress information.
16+
*
17+
* This class maintains a concurrent map (tokenToChatMessageMap) that associates each token with
18+
* its respective ChatMessage object. This mapping is crucial for correctly updating the chat UI
19+
* with the latest progress information as it becomes available from the LSP server.
20+
*
21+
* The progress notifications are handled by the {@link AmazonQLspClientImpl#notifyProgress(ProgressParams)}
22+
* method, which retrieves the corresponding ChatMessage object from the tokenToChatMessageMap using
23+
* the token provided in the ProgressParams. The ChatMessage can then be updated with the partial result.
24+
*/
25+
public final class ChatPartialResultMap {
26+
27+
private final Map<String, ChatMessage> tokenToChatMessageMap;
28+
29+
public ChatPartialResultMap() {
30+
tokenToChatMessageMap = new ConcurrentHashMap<String, ChatMessage>();
31+
}
32+
33+
public void setEntry(final String token, final ChatMessage chatMessage) {
34+
tokenToChatMessageMap.put(token, chatMessage);
35+
}
36+
37+
public void removeEntry(final String token) {
38+
tokenToChatMessageMap.remove(token);
39+
}
40+
41+
public ChatMessage getValue(final String token) {
42+
return tokenToChatMessageMap.getOrDefault(token, null);
43+
}
44+
45+
public Boolean hasKey(final String token) {
46+
return tokenToChatMessageMap.containsKey(token);
47+
}
48+
}

plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatRequestParams.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,33 @@
44

55
import com.fasterxml.jackson.annotation.JsonProperty;
66

7-
public record ChatRequestParams(
8-
@JsonProperty("tabId") String tabId,
9-
@JsonProperty("prompt") ChatPrompt prompt
10-
) { }
7+
public final class ChatRequestParams {
8+
private final String tabId;
9+
private final ChatPrompt prompt;
10+
private String partialResultToken;
11+
12+
public ChatRequestParams(
13+
@JsonProperty("tabId") final String tabId,
14+
@JsonProperty("prompt") final ChatPrompt prompt
15+
) {
16+
this.tabId = tabId;
17+
this.prompt = prompt;
18+
}
19+
20+
public String getTabId() {
21+
return tabId;
22+
}
23+
24+
public ChatPrompt getPrompt() {
25+
return prompt;
26+
}
27+
28+
public String getPartialResultToken() {
29+
return partialResultToken;
30+
}
31+
32+
public void setPartialResultToken(final String partialResultToken) {
33+
this.partialResultToken = partialResultToken;
34+
}
35+
}
1136

plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatResult.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
package software.aws.toolkits.eclipse.amazonq.chat.models;
44

5+
import com.fasterxml.jackson.annotation.JsonInclude;
56
import com.fasterxml.jackson.annotation.JsonProperty;
67

8+
// Mynah-ui will not render the partial result if null values are included. Must ignore nulls values.
9+
@JsonInclude(JsonInclude.Include.NON_NULL)
710
public record ChatResult(
811
@JsonProperty("body") String body,
912
@JsonProperty("messageId") String messageId,

plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClientImpl.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@
1111

1212
import org.eclipse.lsp4e.LanguageClientImpl;
1313
import org.eclipse.lsp4j.ConfigurationParams;
14+
import org.eclipse.lsp4j.ProgressParams;
1415

1516
import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore;
1617
import software.aws.toolkits.eclipse.amazonq.lsp.model.ConnectionMetadata;
1718
import software.aws.toolkits.eclipse.amazonq.lsp.model.SsoProfileData;
1819
import software.aws.toolkits.eclipse.amazonq.util.Constants;
20+
import software.aws.toolkits.eclipse.amazonq.util.PluginLogger;
21+
import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils;
22+
import software.aws.toolkits.eclipse.amazonq.views.AmazonQChatViewActionHandler;
1923

2024
@SuppressWarnings("restriction")
2125
public class AmazonQLspClientImpl extends LanguageClientImpl implements AmazonQLspClient {
@@ -30,6 +34,11 @@ public final CompletableFuture<ConnectionMetadata> getConnectionMetadata() {
3034
return CompletableFuture.completedFuture(metadata);
3135
}
3236

37+
/*
38+
* Handles the progress notifications received from the LSP server.
39+
* - Process partial results for Chat messages if provided token is maintained by ChatCommunicationManager
40+
* - Other notifications are ignored at this time.
41+
*/
3342
@Override
3443
public final CompletableFuture<List<Object>> configuration(final ConfigurationParams configurationParams) {
3544
if (configurationParams.getItems().size() == 0) {
@@ -49,4 +58,16 @@ public final CompletableFuture<List<Object>> configuration(final ConfigurationPa
4958
return CompletableFuture.completedFuture(output);
5059
}
5160

61+
@Override
62+
public final void notifyProgress(final ProgressParams params) {
63+
AmazonQChatViewActionHandler chatActionHandler = new AmazonQChatViewActionHandler();
64+
65+
ThreadingUtils.executeAsyncTask(() -> {
66+
try {
67+
chatActionHandler.handlePartialResultProgressNotification(params);
68+
} catch (Exception e) {
69+
PluginLogger.error("Error processing partial result progress notification", e);
70+
}
71+
});
72+
}
5273
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
package software.aws.toolkits.eclipse.amazonq.util;
4+
5+
import org.eclipse.lsp4j.ProgressParams;
6+
7+
import com.google.gson.Gson;
8+
import com.google.gson.JsonElement;
9+
10+
11+
public final class ProgressNotficationUtils {
12+
private ProgressNotficationUtils() {
13+
// Prevent instantiation
14+
}
15+
16+
/*
17+
* Get the token from the ProgressParams value
18+
* @return The token as a String
19+
*/
20+
public static String getToken(final ProgressParams params) {
21+
String token;
22+
23+
if (params.getToken().isLeft()) {
24+
token = params.getToken().getLeft();
25+
} else {
26+
token = params.getToken().getRight().toString();
27+
}
28+
29+
return token;
30+
}
31+
32+
/*
33+
* Get the object from the ProgressParams value
34+
* @param cls The class of the object to be deserialized
35+
* @return The deserialized object, or null if the value is not a JsonElement
36+
*/
37+
public static <T> T getObject(final ProgressParams params, final Class<T> cls) {
38+
Object val = params.getValue().getRight();
39+
40+
if (!(val instanceof JsonElement)) {
41+
return null;
42+
}
43+
44+
Gson gson = new Gson();
45+
JsonElement element = (JsonElement) val;
46+
T obj = gson.fromJson(element, cls);
47+
48+
return obj;
49+
}
50+
}

0 commit comments

Comments
 (0)