1.1.3 - Improve error handling, add block caret

This commit is contained in:
Carl-Robert Linnupuu 2023-02-28 18:43:17 +00:00
parent f8fdb2cbab
commit 04c2ee1f09
17 changed files with 193 additions and 105 deletions

View file

@ -4,7 +4,7 @@ plugins {
}
group = "ee.carlrobert"
version = "1.1.2"
version = "1.1.3"
repositories {
mavenCentral()

View file

@ -1,6 +0,0 @@
package ee.carlrobert.chatgpt;
@FunctionalInterface
public interface EmptyCallback {
void call();
}

View file

@ -2,7 +2,6 @@ package ee.carlrobert.chatgpt.client;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ee.carlrobert.chatgpt.EmptyCallback;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@ -24,12 +23,9 @@ public abstract class Client {
protected abstract BodySubscriber<Void> subscribe(
ResponseInfo responseInfo,
Consumer<String> onMessageReceived,
EmptyCallback onComplete);
Runnable onComplete);
public void getCompletionsAsync(
String prompt,
Consumer<String> onMessageReceived,
EmptyCallback onComplete) {
public void getCompletionsAsync(String prompt, Consumer<String> onMessageReceived, Runnable onComplete) {
this.userPrompt = prompt;
this.client.sendAsync(
buildHttpRequest(prompt),

View file

@ -2,34 +2,25 @@ package ee.carlrobert.chatgpt.client;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.net.http.HttpResponse.BodySubscriber;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Flow.Subscription;
import java.util.function.Consumer;
import java.util.regex.Pattern;
public abstract class Subscriber<T extends ApiResponse> implements BodySubscriber<Void> {
protected final CompletableFuture<Void> future = new CompletableFuture<>();
private final Consumer<T> responseConsumer;
private volatile Subscription subscription;
private volatile String deferredText;
protected abstract T deserializePayload(String jsonPayload) throws JsonProcessingException;
protected abstract void onRequestComplete();
// Overridden from concrete class
protected void processRegularResponse(String response) {
}
protected abstract void onErrorOccurred();
public Subscriber(Consumer<T> responseConsumer) {
this.responseConsumer = responseConsumer;
}
protected abstract void send(String jsonPayload, String token);
public CompletionStage<Void> getBody() {
return this.future;
@ -52,20 +43,12 @@ public abstract class Subscriber<T extends ApiResponse> implements BodySubscribe
for (var buffer : buffers) {
var decodedText = deferredText + UTF_8.decode(buffer);
var tokens = decodedText.split("\n\n", -1);
if (tokens.length == 1) {
processRegularResponse(decodedText);
}
for (var i = 0; i < tokens.length - 1; i++) {
var responsePayload = extractPayload(tokens[i].split("\n"));
for (String token : tokens) {
var responsePayload = extractPayload(token.split("\n"));
if ("[DONE]".equals(responsePayload)) {
future.complete(null);
} else {
try {
this.responseConsumer.accept(deserializePayload(responsePayload));
} catch (JsonProcessingException e) {
throw new RuntimeException("Couldn't deserialize the payload", e);
}
send(responsePayload, token);
}
}
deferredText = tokens[tokens.length - 1];
@ -80,7 +63,11 @@ public abstract class Subscriber<T extends ApiResponse> implements BodySubscribe
}
public void onError(Throwable e) {
this.future.completeExceptionally(e);
try {
onErrorOccurred();
} finally {
this.future.completeExceptionally(e);
}
}
public void onComplete() {
@ -93,7 +80,7 @@ public abstract class Subscriber<T extends ApiResponse> implements BodySubscribe
}
private String extractPayload(String[] payload) {
Pattern dataLinePattern = Pattern.compile("^data: ?(.*)$");
Pattern dataLinePattern = Pattern.compile("^data:\\s*(\\{.*})\\s*$");
var responseBuilder = new StringBuilder();
for (var line : payload) {
var matcher = dataLinePattern.matcher(line);

View file

@ -4,39 +4,64 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ee.carlrobert.chatgpt.client.Subscriber;
import ee.carlrobert.chatgpt.client.chatgpt.response.ChatGPTResponse;
import ee.carlrobert.chatgpt.client.chatgpt.response.ChatGPTResponseDetail;
import ee.carlrobert.chatgpt.client.chatgpt.response.ChatGPTResponseError;
import java.util.function.Consumer;
public class ChatGPTBodySubscriber extends Subscriber<ChatGPTResponse> {
private final Consumer<String> responseConsumer;
private final Consumer<ChatGPTResponse> onCompleteCallback;
private final Consumer<ChatGPTResponseError> onErrorCallback;
private final ObjectMapper objectMapper = new ObjectMapper();
private ChatGPTResponse lastReceivedResponse;
public ChatGPTBodySubscriber(
Consumer<ChatGPTResponse> responseConsumer,
Consumer<ChatGPTResponse> onCompleteCallback,
Consumer<ChatGPTResponseError> onErrorCallback) {
super(responseConsumer);
Consumer<String> responseConsumer,
Consumer<ChatGPTResponse> onCompleteCallback) {
this.responseConsumer = responseConsumer;
this.onCompleteCallback = onCompleteCallback;
this.onErrorCallback = onErrorCallback;
}
protected ChatGPTResponse deserializePayload(String jsonPayload) throws JsonProcessingException {
lastReceivedResponse = objectMapper.readValue(jsonPayload, ChatGPTResponse.class);
return lastReceivedResponse;
}
protected void onRequestComplete() {
onCompleteCallback.accept(lastReceivedResponse);
}
protected void onErrorOccurred() {
responseConsumer.accept("Something went wrong. Please try again later.");
}
protected void processRegularResponse(String jsonPayload) {
protected void send(String responsePayload, String token) {
if (!responsePayload.isEmpty()) {
try {
var response = objectMapper.readValue(responsePayload, ChatGPTResponse.class);
var author = response.getMessage().getAuthor();
if (author != null && "assistant".equals(author.getRole())) {
var message = response.getFullMessage();
if (lastReceivedResponse != null) {
message = message.replace(lastReceivedResponse.getFullMessage(), "");
}
lastReceivedResponse = response;
this.responseConsumer.accept(message);
}
} catch (JsonProcessingException e) {
throw new RuntimeException("Unable to deserialize the payload", e);
}
} else {
try {
var response = objectMapper.readValue(token, ChatGPTResponseDetail.class);
this.responseConsumer.accept(response.getDetail());
} catch (JsonProcessingException e) {
tryProcessingErrorResponse(token);
}
}
}
private void tryProcessingErrorResponse(String jsonPayload) {
try {
onErrorCallback.accept(objectMapper.readValue(jsonPayload, ChatGPTResponseError.class));
var error = objectMapper.readValue(jsonPayload, ChatGPTResponseError.class);
if ("invalid_api_key".equals(error.getDetail().getCode())) {
responseConsumer.accept(error.getDetail().getMessage());
}
future.complete(null);
} catch (JsonProcessingException e) {
future.completeExceptionally(e);

View file

@ -1,11 +1,11 @@
package ee.carlrobert.chatgpt.client.chatgpt;
import ee.carlrobert.chatgpt.EmptyCallback;
import ee.carlrobert.chatgpt.client.ApiRequestDetails;
import ee.carlrobert.chatgpt.client.Client;
import ee.carlrobert.chatgpt.client.chatgpt.response.ChatGPTResponse;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodySubscriber;
import java.net.http.HttpResponse.ResponseInfo;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -17,7 +17,6 @@ public class ChatGPTClient extends Client {
private static ChatGPTClient instance;
private static ChatGPTResponse lastReceivedResponse;
private ChatGPTClient() {
}
@ -61,24 +60,20 @@ public class ChatGPTClient extends Client {
settings.accessToken);
}
protected HttpResponse.BodySubscriber<Void> subscribe(
HttpResponse.ResponseInfo responseInfo,
protected BodySubscriber<Void> subscribe(
ResponseInfo responseInfo,
Consumer<String> onMessageReceived,
EmptyCallback onComplete) {
Runnable onComplete) {
if (responseInfo.statusCode() == 200) {
return new ChatGPTBodySubscriber((
response -> onMessageReceived.accept(String.join("", response.getMessage().getContent().getParts()))),
return new ChatGPTBodySubscriber(
onMessageReceived,
response -> {
lastReceivedResponse = response;
onComplete.call();
},
error -> {
if ("invalid_api_key".equals(error.getDetail().getCode())) {
onMessageReceived.accept(error.getDetail().getMessage());
}
onComplete.run();
});
} else {
onMessageReceived.accept("Something went wrong. Please try again later.");
onComplete.run();
throw new RuntimeException();
}
}

View file

@ -26,4 +26,8 @@ public class ChatGPTResponse implements ApiResponse {
public void setConversationId(String conversationId) {
this.conversationId = conversationId;
}
public String getFullMessage() {
return String.join("", message.getContent().getParts());
}
}

View file

@ -0,0 +1,16 @@
package ee.carlrobert.chatgpt.client.chatgpt.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatGPTResponseDetail {
private String detail;
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
}

View file

@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
public class ChatGPTResponseMessage {
private String id;
private ChatGPTResponseMessageAuthor author;
private ChatGPTResponseMessageContent content;
public String getId() {
@ -16,6 +17,14 @@ public class ChatGPTResponseMessage {
this.id = id;
}
public ChatGPTResponseMessageAuthor getAuthor() {
return author;
}
public void setAuthor(ChatGPTResponseMessageAuthor author) {
this.author = author;
}
public ChatGPTResponseMessageContent getContent() {
return content;
}

View file

@ -0,0 +1,17 @@
package ee.carlrobert.chatgpt.client.chatgpt.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatGPTResponseMessageAuthor {
private String role;
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}

View file

@ -10,21 +10,33 @@ public class GPTBodySubscriber extends Subscriber<GPTResponse> {
private final Consumer<String> onCompleteCallback;
private final StringBuilder messageBuilder = new StringBuilder();
private final Consumer<String> responseConsumer;
public GPTBodySubscriber(
Consumer<GPTResponse> responseConsumer,
Consumer<String> responseConsumer,
Consumer<String> onCompleteCallback) {
super(responseConsumer);
this.responseConsumer = responseConsumer;
this.onCompleteCallback = onCompleteCallback;
}
protected GPTResponse deserializePayload(String jsonPayload) throws JsonProcessingException {
var response = new ObjectMapper().readValue(jsonPayload, GPTResponse.class);
messageBuilder.append(response.getChoices().get(0).getText());
return response;
}
protected void onRequestComplete() {
onCompleteCallback.accept(messageBuilder.toString());
}
protected void onErrorOccurred() {
responseConsumer.accept("Something went wrong. Please try again later.");
}
protected void send(String responsePayload, String token) {
try {
if (!responsePayload.isEmpty()) {
var response = new ObjectMapper().readValue(responsePayload, GPTResponse.class);
var message = response.getChoices().get(0).getText();
messageBuilder.append(message);
this.responseConsumer.accept(message);
}
} catch (JsonProcessingException e) {
future.completeExceptionally(e);
}
}
}

View file

@ -2,13 +2,12 @@ package ee.carlrobert.chatgpt.client.gpt;
import static java.lang.String.format;
import ee.carlrobert.chatgpt.EmptyCallback;
import ee.carlrobert.chatgpt.client.ApiRequestDetails;
import ee.carlrobert.chatgpt.client.BaseModel;
import ee.carlrobert.chatgpt.client.Client;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodySubscriber;
import java.net.http.HttpResponse.ResponseInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -51,26 +50,40 @@ public class GPTClient extends Client {
}
protected BodySubscriber<Void> subscribe(
HttpResponse.ResponseInfo responseInfo,
ResponseInfo responseInfo,
Consumer<String> onMessageReceived,
EmptyCallback onComplete) {
Runnable onComplete) {
if (responseInfo.statusCode() == 200) {
return new GPTBodySubscriber(
response -> onMessageReceived.accept(response.getChoices().get(0).getText()),
onMessageReceived,
finalMsg -> {
queries.add(Map.entry(super.userPrompt, finalMsg));
onComplete.call();
onComplete.run();
});
} else if (responseInfo.statusCode() == 401) {
onMessageReceived.accept("Incorrect API key provided.\n" +
"You can find your API key at https://platform.openai.com/account/api-keys.");
throw new IllegalArgumentException();
} else if (responseInfo.statusCode() == 429) {
onMessageReceived.accept("You exceeded your current quota, please check your plan and billing details.");
throw new RuntimeException("Insufficient quota");
} else {
onMessageReceived.accept("Something went wrong. Please try again later.");
throw new RuntimeException();
handleError(responseInfo, onMessageReceived, onComplete);
return null;
}
}
private void handleError(
ResponseInfo responseInfo,
Consumer<String> onMessageReceived,
Runnable onComplete) {
try {
if (responseInfo.statusCode() == 401) {
onMessageReceived.accept("Incorrect API key provided.\n" +
"You can find your API key at https://platform.openai.com/account/api-keys.");
throw new IllegalArgumentException();
} else if (responseInfo.statusCode() == 429) {
onMessageReceived.accept("You exceeded your current quota, please check your plan and billing details.");
throw new RuntimeException("Insufficient quota");
} else {
onMessageReceived.accept("Something went wrong. Please try again later.");
throw new RuntimeException();
}
} finally {
onComplete.run();
}
}

View file

@ -115,7 +115,7 @@ public class SettingsComponent {
.addComponent(createFirstSelectionForm())
.addVerticalGap(8)
.addComponent(UI.PanelFactory.panel(useChatGPTRadioButton)
.withComment("Slow and free, more suitable for conversational tasks")
.withComment("Slow and free, more suitable for conversational tasks, rate-limited")
.createPanel())
.addComponent(createSecondSelectionForm())
.getPanel();

View file

@ -12,9 +12,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowManager;
import ee.carlrobert.chatgpt.EmptyCallback;
import ee.carlrobert.chatgpt.client.ClientFactory;
import ee.carlrobert.chatgpt.client.chatgpt.ChatGPTClient;
import ee.carlrobert.chatgpt.ide.settings.SettingsConfigurable;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import ee.carlrobert.chatgpt.ide.toolwindow.components.SyntaxTextArea;
@ -35,9 +33,9 @@ import org.jetbrains.annotations.NotNull;
public class ToolWindowService implements LafManagerListener {
private ScrollablePanel scrollablePanel;
private boolean isLandingViewVisible;
private static final List<SyntaxTextArea> textAreas = new ArrayList<>();
private boolean isLandingViewVisible;
private ScrollablePanel scrollablePanel;
public void setScrollablePanel(ScrollablePanel scrollablePanel) {
this.scrollablePanel = scrollablePanel;
@ -56,7 +54,7 @@ public class ToolWindowService implements LafManagerListener {
scrollablePanel.add(createTextArea(userMessage, true));
}
public void sendMessage(String prompt, Project project, @Nullable EmptyCallback scrollToBottom) {
public void sendMessage(String prompt, Project project, @Nullable Runnable scrollToBottom) {
addSpacing(16);
addIconLabel(Icons.DefaultImageIcon, "ChatGPT:");
addSpacing(8);
@ -73,16 +71,13 @@ public class ToolWindowService implements LafManagerListener {
var client = new ClientFactory().getClient();
client.getCompletionsAsync(prompt, message -> {
if (client instanceof ChatGPTClient) {
textArea.setText(message);
} else {
textArea.append(message);
}
textArea.append(message);
if (scrollToBottom != null) {
scrollToBottom.call();
scrollToBottom.run();
}
}, () -> {
textArea.getCaret().setVisible(false);
textArea.displayCopyButton();
textArea.enableSelection();
});

View file

@ -10,15 +10,19 @@ import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.io.IOException;
import javax.swing.JButton;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rsyntaxtextarea.Theme;
import org.fife.ui.rtextarea.CaretStyle;
public class SyntaxTextArea extends RSyntaxTextArea {
public SyntaxTextArea() {
super("");
setStyles();
addDocumentListener();
}
public void displayCopyButton() {
@ -50,6 +54,8 @@ public class SyntaxTextArea extends RSyntaxTextArea {
setPaintTabLines(false);
setHighlightCurrentLine(false);
setLineWrap(true);
setCaretStyle(0, CaretStyle.BLOCK_STYLE);
getCaret().setVisible(true);
setWrapStyleWord(true);
setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
changeStyleViaThemeXml();
@ -69,4 +75,23 @@ public class SyntaxTextArea extends RSyntaxTextArea {
});
return button;
}
private void addDocumentListener() {
getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
setCaretPosition(getText().length());
}
@Override
public void removeUpdate(DocumentEvent e) {
setCaretPosition(getText().length());
}
@Override
public void changedUpdate(DocumentEvent e) {
setCaretPosition(getText().length());
}
});
}
}

View file

@ -4,7 +4,6 @@ import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.createIconButt
import com.intellij.ui.JBColor;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.chatgpt.EmptyCallback;
import icons.Icons;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@ -18,7 +17,7 @@ import javax.swing.KeyStroke;
public class TextArea extends JTextArea {
public TextArea(EmptyCallback onSubmit, JScrollPane textAreaScrollPane) {
public TextArea(Runnable onSubmit, JScrollPane textAreaScrollPane) {
setForeground(JBColor.GRAY);
setMargin(JBUI.insets(5));
addFocusListener(getFocusListener());
@ -33,13 +32,13 @@ public class TextArea extends JTextArea {
var actions = getActionMap();
actions.put("text-submit", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
onSubmit.call();
onSubmit.run();
}
});
}
private void addSubmitButton(EmptyCallback onSubmit, JScrollPane textAreaScrollPane) {
var button = createSubmitButton(e -> onSubmit.call());
private void addSubmitButton(Runnable onSubmit, JScrollPane textAreaScrollPane) {
var button = createSubmitButton(e -> onSubmit.run());
ComponentBorder cb = new ComponentBorder(button);
cb.setAdjustInsets(true);
cb.install(textAreaScrollPane);

View file

@ -21,6 +21,7 @@
<change-notes>
<![CDATA[
<ul>
<li><b>1.1.3</b> Improve error handling, add block caret</li>
<li><b>1.1.2</b> Add option to select other language models</li>
<li><b>1.1.1</b> Remove startup notification</li>
<li><b>1.1.0</b> Add reverse proxy support, fix text selection and copy functionality</li>