diff --git a/pom.xml b/pom.xml index 6b786e3f..55ee47fb 100644 --- a/pom.xml +++ b/pom.xml @@ -209,15 +209,11 @@ - com.theokanning.openai-gpt3-java - client - 0.14.0 - - - com.theokanning.openai-gpt3-java - service - 0.14.0 + org.json + json + 20230227 + diff --git a/src/main/java/org/scijava/ui/swing/script/ClaudeApiClient.java b/src/main/java/org/scijava/ui/swing/script/ClaudeApiClient.java new file mode 100644 index 00000000..f7f13b7c --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/ClaudeApiClient.java @@ -0,0 +1,84 @@ +package org.scijava.ui.swing.script; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +public class ClaudeApiClient { + + public static String prompt(String prompt, String model, String apikey, String apiurl) throws IOException { + + if (apiurl == null) { + apiurl = "https://api.anthropic.com/v1/messages"; + } + if (apikey == null) { + apikey = System.getenv("ANTHROPIC_API_KEY"); + } + + System.out.println("API KEY:" + apikey); + + URL url = new URL(apiurl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("x-api-key", apikey); + connection.setRequestProperty("anthropic-version", "2023-06-01"); + connection.setRequestProperty("content-type", "application/json"); + connection.setDoOutput(true); + + JSONObject requestBody = new JSONObject(); + requestBody.put("model", model); + requestBody.put("max_tokens", 8192); + requestBody.put("messages", new JSONObject[]{ + new JSONObject().put("role", "user").put("content", prompt) + }); + + + // Send request + try (OutputStream os = connection.getOutputStream()) { + byte[] input = requestBody.toString().getBytes("utf-8"); + os.write(input, 0, input.length); + } + + int responseCode = connection.getResponseCode(); + + StringBuilder response = new StringBuilder(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader( + responseCode >= 400 ? connection.getErrorStream() : connection.getInputStream(), + StandardCharsets.UTF_8 + ) + )) { + String responseLine; + while ((responseLine = br.readLine()) != null) { + response.append(responseLine.trim()); + } + } + + //System.out.println("Response Code: " + responseCode); + //System.out.println("Full response: " + response.toString()); + + if (responseCode >= 400) { + return "Error: " + response.toString(); + } + + try { + JSONObject jsonObject = new JSONObject(response.toString()); + String content = jsonObject.getJSONArray("content").getJSONObject(0).getString("text"); + return content; + } catch (Exception e) { + System.out.println("Error parsing JSON: " + e.getMessage()); + return null; + } + } + + public static void main(String[] args) throws IOException { + String input = "Hello, Claude! How are you today?"; + String response = prompt(input, "claude-3-5-sonnet-20240620", null, null); + System.out.println("Claude's response: " + response); + } +} \ No newline at end of file diff --git a/src/main/java/org/scijava/ui/swing/script/OpenAIOptions.java b/src/main/java/org/scijava/ui/swing/script/LLMServicesOptions.java similarity index 60% rename from src/main/java/org/scijava/ui/swing/script/OpenAIOptions.java rename to src/main/java/org/scijava/ui/swing/script/LLMServicesOptions.java index d7da558f..60734128 100644 --- a/src/main/java/org/scijava/ui/swing/script/OpenAIOptions.java +++ b/src/main/java/org/scijava/ui/swing/script/LLMServicesOptions.java @@ -38,28 +38,43 @@ * * @author Curtis Rueden */ -@Plugin(type = OptionsPlugin.class, menuPath = "Edit>Options>OpenAI...") -public class OpenAIOptions extends OptionsPlugin { +@Plugin(type = OptionsPlugin.class, menuPath = "Edit>Options>LLM Service Options...") +public class LLMServicesOptions extends OptionsPlugin { - @Parameter(label = "OpenAI key", required = false) - private String openAIKey; + @Parameter(label = "OpenAI API key", required = false) + private String openAIAPIKey; + + @Parameter(label = "Anthropic API key", required = false) + private String anthropicAPIKey; @Parameter(label = "OpenAI Model name") - private String modelName = "gpt-3.5-turbo-0613"; + private String openAIModelName = "gpt-4o-2024-08-06"; + + @Parameter(label = "Anthropic Model name") + private String anthropicModelName = "claude-3-5-sonnet-20240620"; @Parameter(label = "Prompt prefix (added before custom prompt)", style = "text area") - private String promptPrefix = "Write code in {programming_language}.\n" + + private String promptPrefix = + "You are an extremely talented Bio-image Analyst and programmer.\n" + + "You write code in {programming_language}.\n" + "Write concise and high quality code for ImageJ/Fiji.\n" + "Put minimal comments explaining what the code does.\n" + - "The code should do the following:\n" + + "Your task is the following:\n" + "{custom_prompt}"; - public String getOpenAIKey() { - return openAIKey; + public String getOpenAIAPIKey() { + return openAIAPIKey; } - public void setOpenAIKey(final String openAIKey) { - this.openAIKey = openAIKey; + public String getAnthropicAPIKey() { + return anthropicAPIKey; + } + + public void setOpenAIAPIKey(final String openAIAPIKey) { + this.openAIAPIKey = openAIAPIKey; + } + public void setAnthropicAPIKey(final String anthropicAPIKey) { + this.anthropicAPIKey = anthropicAPIKey; } public String getPromptPrefix() { @@ -70,12 +85,20 @@ public void setPromptPrefix(final String promptPrefix) { this.promptPrefix = promptPrefix; } - public String getModelName() { - return modelName; + public String getOpenAIModelName() { + return openAIModelName; + } + + public void setOpenAIModelName(final String openAIModelName) { + this.openAIModelName = openAIModelName; + } + + public String getAnthropidModelName() { + return anthropicModelName; } - public void setModelName(final String modelName) { - this.modelName = modelName; + public void setAnthropidModelName(final String anthropicModelName) { + this.anthropicModelName = anthropicModelName; } diff --git a/src/main/java/org/scijava/ui/swing/script/OpenAIClient.java b/src/main/java/org/scijava/ui/swing/script/OpenAIClient.java new file mode 100644 index 00000000..472fc4b1 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/OpenAIClient.java @@ -0,0 +1,68 @@ +package org.scijava.ui.swing.script; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +public class OpenAIClient { + + public static void main(String[] args) { + String prompt = "Hello, GPT-4!"; + try { + String response = prompt(prompt, "gpt-4o-2024-08-06", null, null); + System.out.println("GPT-4 Response: " + response); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static String prompt(String prompt, String model, String apikey, String apiurl) throws Exception { + + if (apiurl == null) { + apiurl = "https://api.openai.com/v1/chat/completions"; + } + if (apikey == null) { + apikey = System.getenv("OPENAI_API_KEY"); + } + + URL url = new URL(apiurl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Authorization", "Bearer " + apikey); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + + // Create JSON request body + JSONObject requestBody = new JSONObject(); + requestBody.put("model", model); + requestBody.put("messages", new JSONObject[]{ + new JSONObject().put("role", "user").put("content", prompt) + }); + + System.out.println(connection); + System.out.println(requestBody); + + // Send request + try (OutputStream os = connection.getOutputStream()) { + byte[] input = requestBody.toString().getBytes("utf-8"); + os.write(input, 0, input.length); + } + + // Read response + StringBuilder response = new StringBuilder(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader(connection.getInputStream(), "utf-8"))) { + String responseLine; + while ((responseLine = br.readLine()) != null) { + response.append(responseLine.trim()); + } + } + + // Parse JSON response + JSONObject jsonResponse = new JSONObject(response.toString()); + return jsonResponse.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content"); + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 6b6232ce..a2cff1a1 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -107,7 +107,6 @@ import javax.script.ScriptEngine; import javax.script.ScriptException; import javax.swing.*; -import javax.swing.border.BevelBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; @@ -118,10 +117,6 @@ import javax.swing.text.Position; import javax.swing.tree.TreePath; -import com.theokanning.openai.completion.chat.ChatCompletionRequest; -import com.theokanning.openai.completion.chat.ChatMessage; -import com.theokanning.openai.completion.chat.ChatMessageRole; -import com.theokanning.openai.service.OpenAiService; import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.Theme; @@ -220,7 +215,7 @@ public class TextEditor extends JFrame implements ActionListener, removeTrailingWhitespace, findNext, findPrevious, openHelp, addImport, nextError, previousError, openHelpWithoutFrames, nextTab, previousTab, runSelection, extractSourceJar, askChatGPTtoGenerateCode, - openSourceForClass, + askClaudetoGenerateCode, openSourceForClass, //openSourceForMenuItem, // this never had an actionListener!?? openMacroFunctions, decreaseFontSize, increaseFontSize, chooseFontSize, chooseTabSize, gitGrep, replaceTabsWithSpaces, @@ -518,8 +513,9 @@ public TextEditor(final Context context) { //openSourceForMenuItem = addToMenu(toolsMenu, "Open Java File for Menu Item...", 0, 0); //openSourceForMenuItem.setMnemonic(KeyEvent.VK_M); - GuiUtils.addMenubarSeparator(toolsMenu, "chatGPT"); - askChatGPTtoGenerateCode = addToMenu(toolsMenu, "Ask chatGPT...", 0, 0); + GuiUtils.addMenubarSeparator(toolsMenu, "AI assistance"); + askChatGPTtoGenerateCode = addToMenu(toolsMenu, "Ask OpenAI's chatGPT...", 0, 0); + askClaudetoGenerateCode = addToMenu(toolsMenu, "Ask Anthropic's Claude...", 0, 0 ); addScritpEditorMacroCommands(toolsMenu); @@ -1627,6 +1623,7 @@ else if (source == chooseTabSize) { else if (source == openClassOrPackageHelp) openClassOrPackageHelp(null); else if (source == extractSourceJar) extractSourceJar(); else if (source == askChatGPTtoGenerateCode) askChatGPTtoGenerateCode(); + else if (source == askClaudetoGenerateCode) askClaudetoGenerateCode(); else if (source == openSourceForClass) { final String className = getSelectedClassNameOrAsk("Class (fully qualified name):", "Which Class?"); if (className != null) { @@ -3244,17 +3241,41 @@ public void extractSourceJar() { } public void askChatGPTtoGenerateCode() { + askLLMServiceProviderToGenerateCode("openai"); + } + public void askClaudetoGenerateCode() { + askLLMServiceProviderToGenerateCode("anthropic"); + } + + public void askLLMServiceProviderToGenerateCode(String service_provider) { SwingUtilities.invokeLater(() -> { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + String text = getTextArea().getSelectedText(); + if (text == null) { + text = getTextArea().getText(); + } + // setup default prompt String prompt = promptPrefix() .replace("{programming_language}", getCurrentLanguage().getLanguageName() ) - .replace("{custom_prompt}", getTextArea().getSelectedText()); + .replace("{custom_prompt}", text); - String answer = askChatGPT(prompt); + String answer = null; + + try { + if (service_provider.equals("openai")) { + answer = new OpenAIClient().prompt(prompt, openAIModelName(), openaiApiKey(), null); + } else if (service_provider.equals("anthropic")) { + answer = new ClaudeApiClient().prompt(prompt, anthropicModelName(), anthropicApiKey(), null); + } else { + throw new RuntimeException("LLM Service provider " + service_provider + " is not implemented."); + } + } catch (Exception e) { + throw new RuntimeException(e); + } if (answer.contains("```")) { // clean answer by removing blabla outside the code block @@ -3264,6 +3285,8 @@ public void askChatGPTtoGenerateCode() { answer = answer.replace("```jython", "```"); answer = answer.replace("```macro", "```"); answer = answer.replace("```groovy", "```"); + answer = answer.replace("```ijm", "```"); + answer = answer.replace("```imagej", "```"); String[] temp = answer.split("```"); answer = temp[1]; @@ -3275,47 +3298,48 @@ public void askChatGPTtoGenerateCode() { }); } - private String askChatGPT(String text) { - // Modified from: https://github.com/TheoKanning/openai-java/blob/main/example/src/main/java/example/OpenAiApiFunctionsExample.java - String token = apiKey(); - - OpenAiService service = new OpenAiService(token); - - List messages = new ArrayList<>(); - ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), text); - messages.add(userMessage); - - ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest - .builder() - .model(modelName()) - .messages(messages).build(); - - ChatMessage responseMessage = service.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage(); - messages.add(responseMessage); - - String result = responseMessage.getContent(); - System.out.println(result); - return result; + private String anthropicApiKey() { + if (optionsService != null) { + final LLMServicesOptions llmOptions = + optionsService.getOptions(LLMServicesOptions.class); + if (llmOptions != null) { + final String key = llmOptions.getAnthropicAPIKey(); + if (key != null && !key.isEmpty()) return key; + } + } + return System.getenv("ANTHROPIC_API_KEY"); } - private String apiKey() { + private String openaiApiKey() { if (optionsService != null) { - final OpenAIOptions openAIOptions = - optionsService.getOptions(OpenAIOptions.class); - if (openAIOptions != null) { - final String key = openAIOptions.getOpenAIKey(); + final LLMServicesOptions llmOptions = + optionsService.getOptions(LLMServicesOptions.class); + if (llmOptions != null) { + final String key = llmOptions.getOpenAIAPIKey(); if (key != null && !key.isEmpty()) return key; } } return System.getenv("OPENAI_API_KEY"); } - private String modelName() { + private String openAIModelName() { if (optionsService != null) { - final OpenAIOptions openAIOptions = - optionsService.getOptions(OpenAIOptions.class); - if (openAIOptions != null) { - final String key = openAIOptions.getModelName(); + final LLMServicesOptions llmOptions = + optionsService.getOptions(LLMServicesOptions.class); + if (llmOptions != null) { + final String key = llmOptions.getOpenAIModelName(); + if (key != null && !key.isEmpty()) return key; + } + } + return null; + } + + private String anthropicModelName() { + if (optionsService != null) { + final LLMServicesOptions llmOptions = + optionsService.getOptions(LLMServicesOptions.class); + if (llmOptions != null) { + final String key = llmOptions.getAnthropidModelName(); if (key != null && !key.isEmpty()) return key; } } @@ -3324,8 +3348,8 @@ private String modelName() { private String promptPrefix() { if (optionsService != null) { - final OpenAIOptions openAIOptions = - optionsService.getOptions(OpenAIOptions.class); + final LLMServicesOptions openAIOptions = + optionsService.getOptions(LLMServicesOptions.class); if (openAIOptions != null) { final String promptPrefix = openAIOptions.getPromptPrefix(); if (promptPrefix != null && !promptPrefix.isEmpty()) return promptPrefix;