diff --git a/API/src/main/java/me/neurodock/api/controllers/Message.java b/API/src/main/java/me/neurodock/api/controllers/Message.java index f6e2f7c..bd0d9d3 100644 --- a/API/src/main/java/me/neurodock/api/controllers/Message.java +++ b/API/src/main/java/me/neurodock/api/controllers/Message.java @@ -28,7 +28,7 @@ public class Message { } Thread t = new Thread(() -> { apiApplication.getCore().getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, query)); - apiApplication.getCore().handleResponce(apiApplication.getCore().qurryOllama()); + apiApplication.getCore().handleResponse(apiApplication.getCore().qurryOllama()); }); t.start(); return ResponseEntity.ok("Query received"); diff --git a/Core/src/main/java/me/neurodock/core/Core.java b/Core/src/main/java/me/neurodock/core/Core.java index dafcc09..d3ef67b 100644 --- a/Core/src/main/java/me/neurodock/core/Core.java +++ b/Core/src/main/java/me/neurodock/core/Core.java @@ -18,7 +18,6 @@ import java.nio.file.FileSystems; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -639,69 +638,142 @@ public class Core { } /** - * Handles the response from Ollama - * By Processing the response, handles Function Calls, Logs relevant information, Appends information to the OllamaObject, Prints messages to the User. - * @param responce The response from Ollama + * Handles the response from Ollama. + * + * Processes tool calls, logs information, appends messages to the OllamaObject, + * and prints output to the user. + * + * @param response The response from Ollama */ - public void handleResponce(JSONObject responce) - { - //System.out.println("Responce: "+responce); - if(responce != null) { - writeLog("Raw responce: "+responce.toString()); - JSONObject message = responce.getJSONObject("message"); - if(message.has("tool_calls")) - { - ollamaObject.addMessage(new OllamaMessageToolCall(OllamaMessageRole.fromRole(message.optString("role")), message.getString("content"), message.getJSONArray("tool_calls"))); - JSONArray calls = message.getJSONArray("tool_calls"); - for(Object call : calls) - { - if(call instanceof JSONObject jsonObject) - { - if(jsonObject.has("function")) - { - JSONObject function = jsonObject.getJSONObject("function"); - List> functions = funtionTools.stream() - .filter(func -> ( - func.getKey().name()+func.getValue()).equalsIgnoreCase(function.getString("name"))).toList(); + public void handleResponse(JSONObject response) { + if(response == null) return; - ArrayList argumentArrayList = new ArrayList<>(); + writeLog("Raw response: " + response.toString()); + JSONObject message = response.getJSONObject("message"); - JSONObject arguments = function.getJSONObject("arguments"); + if(!message.has("tool_calls")) { + checkIfResponceMessage(response); + return; + } - for (String key : arguments.keySet()) { - argumentArrayList.add(new OllamaFunctionArgument(key, arguments.get(key))); - } + ollamaObject.addMessage(new OllamaMessageToolCall( + OllamaMessageRole.fromRole(message.optString("role")), + message.getString("content"), + message.getJSONArray("tool_calls") + )); - if(functions.isEmpty()) { - OllamaToolError ollamaToolError = new OllamaToolError("Function '" + function.getString("name") + "' does not exist"); - ollamaObject.addMessage(ollamaToolError); + // Process each tool call + for(Object call : message.getJSONArray("tool_calls")) { + processToolCall(call); + } - } - else { + checkIfResponceMessage(response); + handleResponse(qurryOllama()); + } - OllamaFunctionTool func = functions.getFirst().getKey(); + /** + * Processes a single tool call from the Ollama response. + * + * Validates the call, finds the corresponding tool, renders the invocation, + * and executes it. + * + * @param call a JSONObject representing the tool call + */ + private void processToolCall(Object call) { + if(!(call instanceof JSONObject jsonObject)) return; + if(!jsonObject.has("function")) return; - // TODO: Check so all arguments is the correct type, else error out in a safe mannet. + JSONObject function = jsonObject.getJSONObject("function"); + OllamaFunctionTool func = findTool(function); - try { - OllamaToolResponse function1 = func.function(argumentArrayList.toArray(new OllamaFunctionArgument[0])); - ollamaObject.addMessage(function1); - printMessageHandler.printMessage(function1); - writeLog("Successfully function call " + func.name() + " output: " + function1.getResponse()); - } catch (OllamaToolErrorException e) { - OllamaToolError ollamaToolError = new OllamaToolError(e.getMessage()); - ollamaObject.addMessage(ollamaToolError); - printMessageHandler.printErrorMessage(ollamaToolError); - writeLog("ERROR: "+e.getMessage()); - } - } - } - } - } - checkIfResponceMessage(responce); - handleResponce(qurryOllama()); + if(func == null) { + reportToolNotFound(function); + return; + } + + JSONObject arguments = function.getJSONObject("arguments"); + renderToolCalling(func.renderCalling(function), function, arguments); + executeToolCall(func, arguments); + } + + /** + * Finds a registered tool by name. + * + * @param function the function JSON containing the tool name + * @return the OllamaFunctionTool if found, {@code null} otherwise + */ + private OllamaFunctionTool findTool(JSONObject function) { + return funtionTools.stream() + .filter(f -> (f.getKey().name() + f.getValue()) + .equalsIgnoreCase(function.getString("name"))) + .map(Pair::getKey) + .findFirst() + .orElse(null); + } + + /** + * Reports when a tool call references a function that doesn't exist. + * + * @param function the function JSON representing a hallucinated or removed function + */ + private void reportToolNotFound(JSONObject function) { + OllamaToolError error = new OllamaToolError( + "Function '" + function.getString("name") + "' does not exist" + ); + ollamaObject.addMessage(error); + printMessageHandler.printToolCalling(error.getError()); + } + + /** + * Renders a tool calling based on the tool's rendering preference. + * + *

Tools may suppress rendering entirely, use a default JSON representation, + * or provide a custom string representation via {@link ToolCallingRender}. + * + * @param render the {@link ToolCallingRender} returned by the tool + * @param function the raw JSON of the function being called + * @param arguments the raw JSON arguments for the function + */ + private void renderToolCalling(ToolCallingRender render, JSONObject function, JSONObject arguments) { + switch(render) { + case ToolCallingRender.Suppress() -> {} + case ToolCallingRender.Default() -> { + JSONObject obj = new JSONObject(); + obj.put("name", function.getString("name")); + obj.put("args", arguments); + printMessageHandler.printToolCalling(obj.toString()); } - else checkIfResponceMessage(responce); + case ToolCallingRender.Custom(String representation) -> + printMessageHandler.printToolCalling(representation); + } + } + + /** + * Executes a tool call and handles the response. + * + *

TODO: Refactor to support + * + * Ollama parallel tool calling. + * + * @param func the {@link OllamaFunctionTool} to execute + * @param arguments the raw JSON arguments for the tool + */ + private void executeToolCall(OllamaFunctionTool func, JSONObject arguments) { + ArrayList args = new ArrayList<>(); + for(String key : arguments.keySet()) { + args.add(new OllamaFunctionArgument(key, arguments.get(key))); + } + + try { + OllamaToolResponse response = func.function(args.toArray(new OllamaFunctionArgument[0])); + ollamaObject.addMessage(response); + printMessageHandler.printMessage(response); + writeLog("Successfully function call " + func.name() + " output: " + response.getResponse()); + } catch(OllamaToolErrorException e) { + OllamaToolError error = new OllamaToolError(e.getMessage()); + ollamaObject.addMessage(error); + printMessageHandler.printErrorMessage(error); + writeLog("ERROR: " + e.getMessage()); } } diff --git a/Core/src/main/java/me/neurodock/core/PrintAdvanceMessageHandler.java b/Core/src/main/java/me/neurodock/core/PrintAdvanceMessageHandler.java index 52f2ce2..01c347f 100644 --- a/Core/src/main/java/me/neurodock/core/PrintAdvanceMessageHandler.java +++ b/Core/src/main/java/me/neurodock/core/PrintAdvanceMessageHandler.java @@ -22,4 +22,10 @@ public interface PrintAdvanceMessageHandler { * If color is not supported, it will print the message without color. */ void printErrorMessage(OllamaMessage errorMessage); + + /** + * Used when a tool is to have it's calling rendered + * @param representation the string to be printed + */ + void printToolCalling(String representation); } diff --git a/Core/src/main/java/me/neurodock/core/PrintMessageHandler.java b/Core/src/main/java/me/neurodock/core/PrintMessageHandler.java index 8963555..48e34c5 100644 --- a/Core/src/main/java/me/neurodock/core/PrintMessageHandler.java +++ b/Core/src/main/java/me/neurodock/core/PrintMessageHandler.java @@ -65,4 +65,16 @@ public interface PrintMessageHandler extends PrintAdvanceMessageHandler { default void printErrorMessage(OllamaMessage errorMessage) { printError(errorMessage.getContent()); } + + @Override + default void printToolCalling(String representation) { + if(color()) + { + printMessage("> \u001B[34m"+representation+"\u001B[0m"); // Blue color for tool calling representations + } + else + { + printMessage("> "+representation); + } + } } diff --git a/Core/src/main/java/me/neurodock/core/ToolCallingRender.java b/Core/src/main/java/me/neurodock/core/ToolCallingRender.java new file mode 100644 index 0000000..e103a75 --- /dev/null +++ b/Core/src/main/java/me/neurodock/core/ToolCallingRender.java @@ -0,0 +1,7 @@ +package me.neurodock.core; + +public sealed interface ToolCallingRender { + record Suppress() implements ToolCallingRender {} + record Default() implements ToolCallingRender {} + record Custom(String representation) implements ToolCallingRender {} +} \ No newline at end of file diff --git a/Core/src/main/java/me/neurodock/core/files/FileHandler.java b/Core/src/main/java/me/neurodock/core/files/FileHandler.java index 2b1d2a3..615abaf 100644 --- a/Core/src/main/java/me/neurodock/core/files/FileHandler.java +++ b/Core/src/main/java/me/neurodock/core/files/FileHandler.java @@ -3,6 +3,7 @@ package me.neurodock.core.files; import me.neurodock.core.Core; import me.neurodock.core.Pair; import me.neurodock.core.files.tools.ReadFileTool; +import me.neurodock.core.files.tools.WriteFileTool; import me.neurodock.ollama.OllamaTool; import org.intellij.lang.annotations.MagicConstant; @@ -52,11 +53,12 @@ public class FileHandler { * @return */ public static ArrayList> getTools() { - ArrayList> tools = new ArrayList<>(); + ArrayList> fileTools = new ArrayList<>(); - tools.add(new Pair<>(new ReadFileTool(), Core.Source.CORE)); + fileTools.add(new Pair<>(new ReadFileTool(), Core.Source.CORE)); + fileTools.add(new Pair<>(new WriteFileTool(), Core.Source.CORE)); - return tools; + return fileTools; } public Path resolve(String relative) throws IOException{ diff --git a/Core/src/main/java/me/neurodock/core/files/tools/WriteFileTool.java b/Core/src/main/java/me/neurodock/core/files/tools/WriteFileTool.java new file mode 100644 index 0000000..e54791b --- /dev/null +++ b/Core/src/main/java/me/neurodock/core/files/tools/WriteFileTool.java @@ -0,0 +1,93 @@ +package me.neurodock.core.files.tools; + +import me.neurodock.core.files.FileHandler; +import me.neurodock.ollama.OllamaFunctionArgument; +import me.neurodock.ollama.OllamaFunctionTool; +import me.neurodock.ollama.OllamaPerameter; +import me.neurodock.ollama.OllamaToolResponse; +import me.neurodock.ollama.OllamaPerameter.OllamaPerameterBuilder.Type; +import me.neurodock.ollama.exceptions.OllamaToolErrorException; +import org.jetbrains.annotations.NotNull; + +import java.io.*; +import java.nio.file.Path; + +public class WriteFileTool extends OllamaFunctionTool { + FileHandler fs = FileHandler.getInstance(); + + @Override + public @NotNull String name() { + return "write_file"; + } + + @Override + public @NotNull OllamaPerameter parameters() { + return OllamaPerameter.builder() + .addProperty("file_path", Type.STRING, "The path to the file to write to", true) + .addProperty("file_content", Type.STRING, "The content to write to the file", true) + .addProperty("overwrite", Type.BOOLEAN, "Overwrite the file, defaults to false", false) + .build(); + } + + @Override + public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { + String path = null; + String content = null; + boolean overwrite = false; + + for(OllamaFunctionArgument arg : args) + { + switch (arg.argument()) + { + case "file_path" -> { + path = (String) arg.value(); + } + case "file_content" -> { + content = (String) arg.value(); + } + case "overwrite" -> { + overwrite = (boolean) arg.value(); + } + } + } + + if(path == null || content == null || path.isBlank() || content.isBlank()) + { + throw new OllamaToolErrorException(name(), "file_content or file_path is empty or null"); + } + + Path filePath = null; + + try{ + filePath = fs.resolve(path); + } + catch (IOException ex) { + throw new OllamaToolErrorException(this.name(), ex); + } + + File file = filePath.toFile(); + if(file.exists() && !overwrite) { + throw new OllamaToolErrorException(this.name(), "File already exists, and instructed to not be overwritten: " + fs.path(file)); + } + else if(file.exists() && overwrite) + { + file.delete(); + } + + try { + file.createNewFile(); + } catch (IOException e) { + throw new OllamaToolErrorException(name(), e); + } + + try(BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)))) + { + bw.write(content); + return new OllamaToolResponse(name(), "Successfully wrote data to "+fs.path(file)); + } + catch (IOException ex) + { + throw new OllamaToolErrorException(name(), ex); + } + } +} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaFunctionTool.java b/Core/src/main/java/me/neurodock/ollama/OllamaFunctionTool.java index 03ad0a2..776e0bc 100644 --- a/Core/src/main/java/me/neurodock/ollama/OllamaFunctionTool.java +++ b/Core/src/main/java/me/neurodock/ollama/OllamaFunctionTool.java @@ -1,10 +1,13 @@ package me.neurodock.ollama; import me.neurodock.core.Core; +import me.neurodock.core.ToolCallingRender; import me.neurodock.ollama.exceptions.OllamaToolErrorException; import org.jetbrains.annotations.NotNull; import org.json.JSONObject; +import java.util.Optional; + /** * Represents a tool that Ollama can call. */ @@ -12,7 +15,7 @@ public abstract class OllamaFunctionTool implements OllamaTool { /** * This field is set via Reflection injection from {@link OllamaObject#addTool(OllamaTool, String)}. - * As is, it will be over written. Do not set it yourself. + * As is, it will be overwritten. Do not set it yourself. */ protected String source = ""; @@ -33,6 +36,18 @@ public abstract class OllamaFunctionTool implements OllamaTool { return ret.toString(); } + + /** + * Generates a string representation of this tool calling for rendering. + * + * @param calling the raw JSON sent from Ollama describing the complete function call + * @return an {@link Optional} containing the formatted representation if this calling + * should be rendered, or {@link Optional#empty()} otherwise + */ + public ToolCallingRender renderCalling(JSONObject calling) + { + return new ToolCallingRender.Suppress(); + } /** * The name of the tool @@ -64,7 +79,7 @@ public abstract class OllamaFunctionTool implements OllamaTool { /** * The function of the tool.
* This is used by Ollama to call the tool.
- * Throw {@link OllamaToolErrorException} if the tool encounters an error instead of normal exceptions. The {@link OllamaToolErrorException} gets handled more gracefully by {@link Core#handleResponce(JSONObject)} + * Throw {@link OllamaToolErrorException} if the tool encounters an error instead of normal exceptions. The {@link OllamaToolErrorException} gets handled more gracefully by {@link Core#handleResponse(JSONObject)} * @param args The arguments to pass to the tool, if any * @return The response from the tool, if null return {@link OllamaToolResponse} * @throws OllamaToolErrorException If the tool encounters an error diff --git a/Core/src/main/java/me/neurodock/ollama/exceptions/OllamaToolErrorException.java b/Core/src/main/java/me/neurodock/ollama/exceptions/OllamaToolErrorException.java index 11fc5eb..0074529 100644 --- a/Core/src/main/java/me/neurodock/ollama/exceptions/OllamaToolErrorException.java +++ b/Core/src/main/java/me/neurodock/ollama/exceptions/OllamaToolErrorException.java @@ -5,7 +5,7 @@ import org.json.JSONObject; /** * Represents an error from a tool.
- * This is used internally by tools instead of {@link Exception}, to then be handled gracefully by {@link Core#handleResponce(JSONObject)} + * This is used internally by tools instead of {@link Exception}, to then be handled gracefully by {@link Core#handleResponse(JSONObject)} */ public class OllamaToolErrorException extends RuntimeException { /** diff --git a/Core/src/test/java/plugin/Test.java b/Core/src/test/java/plugin/Test.java index 28eafba..027250e 100644 --- a/Core/src/test/java/plugin/Test.java +++ b/Core/src/test/java/plugin/Test.java @@ -1,6 +1,12 @@ package plugin; import me.neurodock.plugin.Plugin; +import me.neurodock.plugin.PluginMetadata; +import org.jetbrains.annotations.NotNull; public class Test extends Plugin { + @Override + public @NotNull PluginMetadata getMetadata() { + return null; + } } diff --git a/Core/src/test/java/plugin/Tool.java b/Core/src/test/java/plugin/Tool.java index be6a017..7cc9397 100644 --- a/Core/src/test/java/plugin/Tool.java +++ b/Core/src/test/java/plugin/Tool.java @@ -6,13 +6,13 @@ import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.ollama.OllamaPerameter; import me.neurodock.ollama.OllamaToolResponse; -import me.zacharias.chat.plugin.annotation.OllamaTool; -import me.zacharias.chat.plugin.annotation.injectons.InjectPlugin; +//import me.zacharias.chat.plugin.annotation.OllamaTool; +//import me.zacharias.chat.plugin.annotation.injectons.InjectPlugin; import org.jetbrains.annotations.NotNull; -@OllamaTool +//@OllamaTool public class Tool extends OllamaFunctionTool { - @InjectPlugin(classType = Test.class) + //@InjectPlugin(classType = Test.class) Test core; @Override diff --git a/Display/src/main/java/me/neurodock/display/Display.java b/Display/src/main/java/me/neurodock/display/Display.java index a4e5780..fcdf9f3 100644 --- a/Display/src/main/java/me/neurodock/display/Display.java +++ b/Display/src/main/java/me/neurodock/display/Display.java @@ -5,11 +5,8 @@ import me.neurodock.core.Pair; import me.neurodock.core.PrintMessageHandler; import me.neurodock.core.files.FileHandlerLocation; import me.neurodock.core.memory.CoreMemory; -import me.neurodock.mal.api.MALAPITool; import me.neurodock.ollama.*; import me.neurodock.ollama.utils.SystemMessage; -import me.neurodock.genius.GeniusTools; -import me.neurodock.wikipedia.WikipediaTool; import org.json.JSONObject; import java.io.*; @@ -172,7 +169,7 @@ public class Display { writeLog("User: " + message); core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, message.toString())); //System.out.println(ollamaObject.toString()); - core.handleResponce(core.qurryOllama()); + core.handleResponse(core.qurryOllama()); } } } catch (Exception e) { diff --git a/Display/src/main/java/me/neurodock/display/PythonRunner.java b/Display/src/main/java/me/neurodock/display/PythonRunner.java index f0b8859..8fb6ed3 100644 --- a/Display/src/main/java/me/neurodock/display/PythonRunner.java +++ b/Display/src/main/java/me/neurodock/display/PythonRunner.java @@ -22,6 +22,7 @@ import org.jspecify.annotations.NonNull; import java.io.*; import java.net.ServerSocket; import java.net.Socket; +import java.net.SocketException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -194,11 +195,27 @@ public class PythonRunner extends OllamaFunctionTool { return; } } + catch (RuntimeException re) + { + if(re.getCause() instanceof SocketException) { + writeLog("Failed to ping docker: " + re.getMessage()); + System.out.println("Failed to ping docker. Docker component is disabled."); + dockerClient = null; + return; + } + else{ + writeLog("Failed to ping docker: " + re.getMessage()); + re.printStackTrace(); + System.out.println("Failed to ping docker. Docker component is disabled."); + dockerClient = null; + return; + } + } catch (Exception e) { writeLog("Failed to ping docker: " + e.getMessage()); e.printStackTrace(); - System.out.println("Failed to ping docker. Docker components is disabled."); + System.out.println("Failed to ping docker. Docker component is disabled."); dockerClient = null; return; } @@ -445,7 +462,7 @@ public class PythonRunner extends OllamaFunctionTool { dockerClient.removeContainerCmd(containerId).exec(); - return new OllamaToolResponse(name(), output.isEmpty() ? "Code ran successfully with no output" : output); + return new OllamaToolResponse(name(), output.isEmpty() ? "Python script names "+name+" ran successfully with no output" : "Python script names "+name+" output: "+output); } catch (Exception e) { throw new OllamaToolErrorException(name(), "Docker unavailable"); diff --git a/Display/src/main/resources/external_tool_base.py b/Display/src/main/resources/external_tool_base.py index ca8959c..d633054 100644 --- a/Display/src/main/resources/external_tool_base.py +++ b/Display/src/main/resources/external_tool_base.py @@ -10,8 +10,8 @@ def connect(data): s.connect((HOST, PORT)) data = data + "\n" s.sendall(data.encode("utf-8")) - responce = s.recv(4096) - return responce.decode("utf-8") + response = s.recv(4096) + return response.decode("utf-8") def print_output(text): data = {"function": "print_output", "container_id": CONTAINER_ID, "text": text} diff --git a/build.gradle b/build.gradle index af706c4..5e723ba 100644 --- a/build.gradle +++ b/build.gradle @@ -25,10 +25,17 @@ subprojects { implementation("org.jetbrains:annotations:23.1.0") testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } - test { + tasks.named('test', Test) { useJUnitPlatform() + + maxHeapSize = '1G' + + testLogging { + events "passed" + } } task sourcesJar(type: Jar, dependsOn: classes) { diff --git a/examples/external_tools_example.py b/examples/external_tools_example.py index 351284a..7055723 100644 --- a/examples/external_tools_example.py +++ b/examples/external_tools_example.py @@ -12,8 +12,8 @@ def connect(data): s.connect((HOST, PORT)) data = data + "\n" s.sendall(data.encode("utf-8")) - responce = s.recv(4096) - return responce.decode("utf-8") + response = s.recv(4096) + return response.decode("utf-8") def print_output(text): data = {"function": "print_output", "container_id": CONTAINER_ID, "text": text}