Skip to content

Commit

Permalink
Improved error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
chedim committed Mar 12, 2024
1 parent dcb074c commit 4011214
Show file tree
Hide file tree
Showing 15 changed files with 331 additions and 54 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies {
testImplementation("org.mockito:mockito-core:5.2.0")
testImplementation("org.mockito:mockito-inline:5.2.0")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1")
testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.8.1")

implementation("com.didalgo:gpt3-tokenizer:0.1.5")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@
import java.util.Base64;

public class CapellaApiMethods {
// DEV:
//public static final String CAPELLA_DOMAIN = "https://api.dev.nonprod-project-avengers.com";
// PROD:
public static final String CAPELLA_DOMAIN = "https://api.cloud.couchbase.com";
private static final String AUTH_URL = CAPELLA_DOMAIN + "/sessions";
public static final String ORGANIZATIONS_URL = CAPELLA_DOMAIN + "/v2/organizations";
private static String CAPELLA_DOMAIN;
private static String AUTH_URL;
private static String ORGANIZATIONS_URL;

static {
// DEV:
// setCapellaDomain(System.getenv().getOrDefault("CAPELLA_DOMAIN", "https://api.dev.nonprod-project-avengers.com"));
// PROD:
setCapellaDomain(System.getenv().getOrDefault("CAPELLA_DOMAIN", "https://api.cloud.couchbase.com"));
}


public static CapellaAuth login(String username, String password) throws IOException, IllegalStateException {
Expand Down Expand Up @@ -69,4 +73,14 @@ public static CapellaOrganizationList loadOrganizations(CapellaAuth auth) throws
CapellaOrganizationList resultList = objectMapper.readValue(result, CapellaOrganizationList.class);
return resultList;
}

public static String getCapellaDomain() {
return CAPELLA_DOMAIN;
}

public static void setCapellaDomain(String domain) {
CAPELLA_DOMAIN = domain;
AUTH_URL = domain + "/sessions";
ORGANIZATIONS_URL = domain + "/v2/organizations";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@
import java.io.InputStream;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Collectors;

public class IQWindowContent extends JPanel implements LoginPanel.Listener, ChatPanel.LogoutListener, ChatPanel.OrganizationListener {
private static final String IQ_URL = CapellaApiMethods.CAPELLA_DOMAIN + "/v2/organizations/%s/integrations/iq/";
private static final Supplier<String> IQ_URL = () -> CapellaApiMethods.getCapellaDomain() + "/v2/organizations/%s/integrations/iq/";
private final Project project;
private IQCredentials credentials = new IQCredentials();
private CapellaOrganizationList organizationList;
Expand Down Expand Up @@ -173,7 +174,7 @@ public void onOrgSelected(CapellaOrganization organization) {
this.updateUI();
SwingUtilities.invokeLater(() -> {
IQStorage.getInstance().getState().setActiveOrganization(organization.getId());
final String iqUrl = String.format(IQ_URL, organization.getId());
final String iqUrl = String.format(IQ_URL.get(), organization.getId());
iqGptConfig = new OpenAISettingsState.OpenAIConfig();
OpenAISettingsState.getInstance().setGpt4Config(iqGptConfig);
OpenAISettingsState.getInstance().setEnableInitialMessage(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public Consumer<ChatCompletionResult> onNext() {

public Consumer<Throwable> onError() {
return cause -> {
listener.exchangeFailed(event.failed(cause));
listener.exchangeFailed(event == null ? null : event.failed(cause));
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.couchbase.intellij.tree.iq.spi.iq;

import com.couchbase.intellij.tree.iq.CapellaApiMethods;
import com.couchbase.intellij.tree.iq.settings.OpenAISettingsState;
import com.couchbase.intellij.tree.iq.spi.OpenAiServiceProvider;
import com.couchbase.intellij.workbench.Log;
Expand All @@ -25,7 +26,8 @@ public boolean supportsEndpoint(String url) {
try {
URI uri = new URI(url);
return uri.getHost().endsWith("couchbase.com")
|| uri.getHost().endsWith("project-avengers.com");
|| uri.getHost().endsWith("project-avengers.com")
|| url.toLowerCase().contains(CapellaApiMethods.getCapellaDomain().toLowerCase());
} catch (URISyntaxException e) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.couchbase.intellij.tree.iq.spi.iq;

import com.couchbase.client.java.json.JsonObject;
import com.couchbase.intellij.tree.iq.settings.OpenAISettingsState;
import com.theokanning.openai.OpenAiError;
import com.theokanning.openai.OpenAiHttpException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import retrofit2.HttpException;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class IQAuthenticationInterceptor implements Interceptor {

Expand All @@ -15,11 +20,20 @@ public IQAuthenticationInterceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
String token = OpenAISettingsState.getInstance().getGpt4Config().getApiKey();
Request request = chain.request()
Request request = chain.withReadTimeout(10, TimeUnit.SECONDS)
.request()
.newBuilder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.build();
return chain.proceed(request);
Response response = chain.proceed(request);
if (response.code() > 399) {
JsonObject body = JsonObject.fromJson(response.body().string());
if (!body.containsKey("error") && body.containsKey("message")) {
OpenAiError error = new OpenAiError(new OpenAiError.OpenAiErrorDetails(body.getString("message"), "", "", String.valueOf(body.getInt("code"))));
throw new OpenAiHttpException(error, null, body.getInt("code"));
}
}
return response;
}
}
45 changes: 37 additions & 8 deletions src/main/java/com/couchbase/intellij/tree/iq/ui/ChatPanel.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.couchbase.intellij.tree.iq.ui;

import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.intellij.tree.iq.CapellaOrganization;
import com.couchbase.intellij.tree.iq.CapellaOrganizationList;
Expand Down Expand Up @@ -37,6 +38,8 @@
import javax.swing.text.AbstractDocument;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

Expand All @@ -45,6 +48,7 @@

public class ChatPanel extends OnePixelSplitter implements ChatMessageListener {

public static final String FAKE_CONFUSION = "Sorry, I couldn't quite get that. Could you restate the request?";
private final ExpandableTextFieldExt searchTextField;
private final JButton button;
private final JButton stopGenerating;
Expand Down Expand Up @@ -259,16 +263,41 @@ public void responseCompleted(ChatMessageEvent.ResponseArrived event) {
List<ChatMessage> response = event.getResponseChoices();
response.forEach(message -> Log.info(String.format("IQ response message: %s", message.toString())));
if (response.size() == 1 && response.get(0).getContent().startsWith("{")) {
IntentProcessor intentProcessor = ApplicationManager.getApplication().getService(IntentProcessor.class);
JsonObject intents = JsonObject.fromJson(response.get(0).getContent());
getQuestion().addIntentResponse(intents);
intentProcessor.process(this, event.getUserMessage(), intents);
} else {
setContent(event.getResponseChoices());
SwingUtilities.invokeLater(() -> {
aroundRequest(false);
});
if (isEmptyResponse(intents) || containsNoneIntent(intents)) {
response = Arrays.asList(new ChatMessage("assistant", FAKE_CONFUSION) );
} else {
IntentProcessor intentProcessor = ApplicationManager.getApplication().getService(IntentProcessor.class);
getQuestion().addIntentResponse(intents);
intentProcessor.process(this, event.getUserMessage(), intents);
return;
}
}

setContent(response);
SwingUtilities.invokeLater(() -> {
aroundRequest(false);
});
}

private boolean containsNoneIntent(JsonObject intents) {
JsonArray actions = intents.getArray("actions");
for (int i = 0; i < actions.size(); i++) {
JsonObject action = actions.getObject(i);

if (!action.containsKey("action") || action.getString("action").equalsIgnoreCase("none")) {
return true;
}
}

return false;
}

private boolean isEmptyResponse(JsonObject intents) {
return intents.size() == 0
|| !intents.containsKey("actions")
|| !(intents.get("actions") instanceof JsonArray)
|| intents.getArray("actions").size() == 0;
}

public void setContent(List<ChatMessage> content) {
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/com/couchbase/intellij/workbench/Log.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ public static ConsoleView getView() {
return console;
}

public static void info(String message) {
public static void info(String message, Object... arguments) {
if (logLevel >= 2) {
if (arguments.length > 0) {
message = String.format(message, arguments);
}
getLogger().print("\n" + message, ConsoleViewContentType.LOG_INFO_OUTPUT);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.couchbase.intellij.tree.iq;

import com.couchbase.intellij.tree.iq.chat.ChatLinkService;
import com.couchbase.intellij.tree.iq.chat.ChatLinkState;
import com.couchbase.intellij.tree.iq.chat.ConfigurationPage;
import com.couchbase.intellij.tree.iq.core.IQCredentials;
import com.couchbase.intellij.tree.iq.settings.OpenAISettingsState;
import com.couchbase.intellij.workbench.Log;

public abstract class AbstractCapellaTest extends AbstractIQTest {
@Override
protected void setUp() throws Exception {
super.setUp();
assertTrue("Please set Capella domain using `CAPELLA_DOMAIN` envvar", System.getenv().containsKey("CAPELLA_DOMAIN"));
CapellaApiMethods.setCapellaDomain(System.getenv("CAPELLA_DOMAIN"));
IQCredentials credentials = new IQCredentials(System.getenv("IQ_ORG_LOGIN"), System.getenv("IQ_ORG_PASSWD"));
assertTrue("Please set IQ credentials using `IQ_ORG_ID`, `IQ_ORG_LOGIN`, and `IQ_ORG_PASSWD` envvars", credentials.doLogin());
String orgId = System.getenv("IQ_ORG_ID");
assertTrue("Please set IQ org id using `IQ_ORG_ID` envvar", orgId != null && orgId.length() > 0);
final String iqUrl = String.format(IQ_URL, orgId);
OpenAISettingsState.OpenAIConfig iqGptConfig = new OpenAISettingsState.OpenAIConfig();
OpenAISettingsState.getInstance().setGpt4Config(iqGptConfig);
OpenAISettingsState.getInstance().setEnableInitialMessage(false);
iqGptConfig.setApiKey(credentials.getAuth().getJwt());
iqGptConfig.setEnableStreamResponse(false);
iqGptConfig.setModelName("gpt-4");
iqGptConfig.setApiEndpointUrl(iqUrl);
iqGptConfig.setEnableCustomApiEndpointUrl(true);
ConfigurationPage cp = iqGptConfig.withSystemPrompt(IQWindowContent::systemPrompt);
Log.setLevel(3);
Log.setPrinter(new Log.StdoutPrinter());

link = new ChatLinkService(getProject(), null, cp);
ctx = new ChatLinkState(cp);
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.couchbase.intellij.tree.iq.intents;
package com.couchbase.intellij.tree.iq;

import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.intellij.tree.iq.IQWindowContent;
import com.couchbase.intellij.tree.iq.chat.ChatExchangeAbortException;
import com.couchbase.intellij.tree.iq.chat.ChatGptHandler;
import com.couchbase.intellij.tree.iq.chat.ChatLink;
Expand All @@ -23,41 +22,21 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;

public abstract class AbstractIQTest extends BasePlatformTestCase {
private static final String IQ_URL = System.getenv("CAPELLA_DOMAIN") + "/v2/organizations/%s/integrations/iq/";
protected static final String IQ_URL = System.getenv("CAPELLA_DOMAIN") + "/v2/organizations/%s/integrations/iq/";
private static final ChatGptHandler handler = new ChatGptHandler();
private static ConversationContext ctx;
private static ChatLink link;

@Override
protected void setUp() throws Exception {
super.setUp();
IQCredentials credentials = new IQCredentials(System.getenv("IQ_ORG_LOGIN"), System.getenv("IQ_ORG_PASSWD"));
assertTrue("Please set capella domain and IQ credentials using `CAPELLA_DOMAIN`, `IQ_ORG_ID`, `IQ_ORG_LOGIN`, and `IQ_ORG_PASSWD` envvars", credentials.doLogin());
String orgId = System.getenv("IQ_ORG_ID");
final String iqUrl = String.format(IQ_URL, orgId);
OpenAISettingsState.OpenAIConfig iqGptConfig = new OpenAISettingsState.OpenAIConfig();
OpenAISettingsState.getInstance().setGpt4Config(iqGptConfig);
OpenAISettingsState.getInstance().setEnableInitialMessage(false);
iqGptConfig.setApiKey(credentials.getAuth().getJwt());
iqGptConfig.setEnableStreamResponse(false);
iqGptConfig.setModelName("gpt-4");
iqGptConfig.setApiEndpointUrl(iqUrl);
iqGptConfig.setEnableCustomApiEndpointUrl(true);
ConfigurationPage cp = iqGptConfig.withSystemPrompt(IQWindowContent::systemPrompt);
Log.setLevel(3);
Log.setPrinter(new Log.StdoutPrinter());

link = new ChatLinkService(getProject(), null, cp);
ctx = new ChatLinkState(cp);
}
protected static ConversationContext ctx;
protected static ChatLink link;

protected void send(String message, Consumer<ChatMessageEvent.ResponseArrived> listener) {
send(message, false, listener);
}
protected void send(String message, boolean isSystem, Consumer<ChatMessageEvent.ResponseArrived> listener) {
assertNotNull(ctx);
assertNotNull(link);
ChatMessage chatMessage = new ChatMessage(
isSystem ? ChatMessageRole.SYSTEM.value() : ChatMessageRole.USER.value(),
message
Expand Down Expand Up @@ -99,7 +78,7 @@ public void responseCompleted(ChatMessageEvent.ResponseArrived event) {

@Override
public void exchangeFailed(ChatMessageEvent.Failed event) {
throw new RuntimeException("IQ Exchange failed", event.getCause());
throw new RuntimeException("IQ Exchange failed", event == null ? null : event.getCause());
}

@Override
Expand Down Expand Up @@ -142,4 +121,23 @@ protected List<JsonObject> getIntents(ChatMessageEvent.ResponseArrived response,
}
return results;
}

public void assertCauseMessage(Throwable e, String msg) {
while (e != null && e.getCause() != e) {
if (e.getMessage().equals(msg)) {
return;
}
if (e.getCause() == e) {
break;
}
e = e.getCause();
}
throw new AssertionError("Exception was not caused by an error with message '" + msg + "'");
}

protected void assertResponseTextEquals(ChatMessageEvent.ResponseArrived responseArrived, String fakeConfusion) {
assertTrue(responseArrived.getResponseChoices().stream()
.filter(Objects::nonNull)
.anyMatch(choice -> choice.getContent() != null && choice.getContent().equals(fakeConfusion)));
}
}
Loading

0 comments on commit 4011214

Please sign in to comment.