refactor(Core): decompose handleResponse and add ToolCallingRender system
build / build (push) Has been cancelled

- Extract handleResponse into focused helpers: processToolCall, findTool,
  reportToolNotFound, renderToolCalling, executeToolCall
- Introduce ToolCallingRender sealed interface (Suppress/Default/Custom)
  allowing tools to control how their invocations are rendered
- Add printToolCalling to PrintAdvanceMessageHandler contract
- Implement default tool calling rendering in PrintMessageHandler (blue text)
- Add WriteFileTool for file writing capabilities
- Fix "responce" → "response" typo across Java and Python files
- Improve PythonRunner docker error handling and output messages
- Update Gradle test configuration (maxHeapSize, test logging)

Signed-off-by: zacharias <alienfromdia@proton.me>
This commit is contained in:
2026-06-15 14:55:09 +02:00
committed by zacharias
parent ff0496eb61
commit d016ad9a48
16 changed files with 310 additions and 76 deletions
@@ -28,7 +28,7 @@ public class Message {
} }
Thread t = new Thread(() -> { Thread t = new Thread(() -> {
apiApplication.getCore().getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, query)); apiApplication.getCore().getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, query));
apiApplication.getCore().handleResponce(apiApplication.getCore().qurryOllama()); apiApplication.getCore().handleResponse(apiApplication.getCore().qurryOllama());
}); });
t.start(); t.start();
return ResponseEntity.ok("Query received"); return ResponseEntity.ok("Query received");
+125 -53
View File
@@ -18,7 +18,6 @@ import java.nio.file.FileSystems;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -639,71 +638,144 @@ public class Core {
} }
/** /**
* Handles the response from Ollama * 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 * 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) public void handleResponse(JSONObject response) {
{ if(response == null) return;
//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<Pair<OllamaFunctionTool, String>> functions = funtionTools.stream()
.filter(func -> (
func.getKey().name()+func.getValue()).equalsIgnoreCase(function.getString("name"))).toList();
ArrayList<OllamaFunctionArgument> argumentArrayList = new ArrayList<>(); writeLog("Raw response: " + response.toString());
JSONObject message = response.getJSONObject("message");
if(!message.has("tool_calls")) {
checkIfResponceMessage(response);
return;
}
ollamaObject.addMessage(new OllamaMessageToolCall(
OllamaMessageRole.fromRole(message.optString("role")),
message.getString("content"),
message.getJSONArray("tool_calls")
));
// Process each tool call
for(Object call : message.getJSONArray("tool_calls")) {
processToolCall(call);
}
checkIfResponceMessage(response);
handleResponse(qurryOllama());
}
/**
* 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;
JSONObject function = jsonObject.getJSONObject("function");
OllamaFunctionTool func = findTool(function);
if(func == null) {
reportToolNotFound(function);
return;
}
JSONObject arguments = function.getJSONObject("arguments"); 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.
*
* <p>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());
}
case ToolCallingRender.Custom(String representation) ->
printMessageHandler.printToolCalling(representation);
}
}
/**
* Executes a tool call and handles the response.
*
* <p><strong>TODO:</strong> Refactor to support
* <a href="https://docs.ollama.com/capabilities/tool-calling#parallel-tool-calling">
* Ollama parallel tool calling</a>.
*
* @param func the {@link OllamaFunctionTool} to execute
* @param arguments the raw JSON arguments for the tool
*/
private void executeToolCall(OllamaFunctionTool func, JSONObject arguments) {
ArrayList<OllamaFunctionArgument> args = new ArrayList<>();
for(String key : arguments.keySet()) { for(String key : arguments.keySet()) {
argumentArrayList.add(new OllamaFunctionArgument(key, arguments.get(key))); args.add(new OllamaFunctionArgument(key, arguments.get(key)));
} }
if(functions.isEmpty()) {
OllamaToolError ollamaToolError = new OllamaToolError("Function '" + function.getString("name") + "' does not exist");
ollamaObject.addMessage(ollamaToolError);
}
else {
OllamaFunctionTool func = functions.getFirst().getKey();
// TODO: Check so all arguments is the correct type, else error out in a safe mannet.
try { try {
OllamaToolResponse function1 = func.function(argumentArrayList.toArray(new OllamaFunctionArgument[0])); OllamaToolResponse response = func.function(args.toArray(new OllamaFunctionArgument[0]));
ollamaObject.addMessage(function1); ollamaObject.addMessage(response);
printMessageHandler.printMessage(function1); printMessageHandler.printMessage(response);
writeLog("Successfully function call " + func.name() + " output: " + function1.getResponse()); writeLog("Successfully function call " + func.name() + " output: " + response.getResponse());
} catch(OllamaToolErrorException e) { } catch(OllamaToolErrorException e) {
OllamaToolError ollamaToolError = new OllamaToolError(e.getMessage()); OllamaToolError error = new OllamaToolError(e.getMessage());
ollamaObject.addMessage(ollamaToolError); ollamaObject.addMessage(error);
printMessageHandler.printErrorMessage(ollamaToolError); printMessageHandler.printErrorMessage(error);
writeLog("ERROR: " + e.getMessage()); writeLog("ERROR: " + e.getMessage());
} }
} }
}
}
}
checkIfResponceMessage(responce);
handleResponce(qurryOllama());
}
else checkIfResponceMessage(responce);
}
}
/** /**
* Checks if the response contains a message and if so, prints it to the user * Checks if the response contains a message and if so, prints it to the user
@@ -22,4 +22,10 @@ public interface PrintAdvanceMessageHandler {
* If color is not supported, it will print the message without color. * If color is not supported, it will print the message without color.
*/ */
void printErrorMessage(OllamaMessage errorMessage); 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);
} }
@@ -65,4 +65,16 @@ public interface PrintMessageHandler extends PrintAdvanceMessageHandler {
default void printErrorMessage(OllamaMessage errorMessage) { default void printErrorMessage(OllamaMessage errorMessage) {
printError(errorMessage.getContent()); 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);
}
}
} }
@@ -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 {}
}
@@ -3,6 +3,7 @@ package me.neurodock.core.files;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.Pair; import me.neurodock.core.Pair;
import me.neurodock.core.files.tools.ReadFileTool; import me.neurodock.core.files.tools.ReadFileTool;
import me.neurodock.core.files.tools.WriteFileTool;
import me.neurodock.ollama.OllamaTool; import me.neurodock.ollama.OllamaTool;
import org.intellij.lang.annotations.MagicConstant; import org.intellij.lang.annotations.MagicConstant;
@@ -52,11 +53,12 @@ public class FileHandler {
* @return * @return
*/ */
public static ArrayList<Pair<? extends OllamaTool, String>> getTools() { public static ArrayList<Pair<? extends OllamaTool, String>> getTools() {
ArrayList<Pair<? extends OllamaTool, String>> tools = new ArrayList<>(); ArrayList<Pair<? extends OllamaTool, String>> 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{ public Path resolve(String relative) throws IOException{
@@ -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);
}
}
}
@@ -1,10 +1,13 @@
package me.neurodock.ollama; package me.neurodock.ollama;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.ToolCallingRender;
import me.neurodock.ollama.exceptions.OllamaToolErrorException; import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.Optional;
/** /**
* Represents a tool that Ollama can call. * Represents a tool that Ollama can call.
*/ */
@@ -34,6 +37,18 @@ public abstract class OllamaFunctionTool implements OllamaTool {
return ret.toString(); 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 * The name of the tool
* This is used by Ollama to know what the tool is * This is used by Ollama to know what the tool is
@@ -64,7 +79,7 @@ public abstract class OllamaFunctionTool implements OllamaTool {
/** /**
* The function of the tool.<br> * The function of the tool.<br>
* This is used by Ollama to call the tool.<br> * This is used by Ollama to call the tool.<br>
* 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 * @param args The arguments to pass to the tool, if any
* @return The response from the tool, if null return {@link OllamaToolResponse} * @return The response from the tool, if null return {@link OllamaToolResponse}
* @throws OllamaToolErrorException If the tool encounters an error * @throws OllamaToolErrorException If the tool encounters an error
@@ -5,7 +5,7 @@ import org.json.JSONObject;
/** /**
* Represents an error from a tool.<br> * Represents an error from a tool.<br>
* 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 { public class OllamaToolErrorException extends RuntimeException {
/** /**
+6
View File
@@ -1,6 +1,12 @@
package plugin; package plugin;
import me.neurodock.plugin.Plugin; import me.neurodock.plugin.Plugin;
import me.neurodock.plugin.PluginMetadata;
import org.jetbrains.annotations.NotNull;
public class Test extends Plugin { public class Test extends Plugin {
@Override
public @NotNull PluginMetadata getMetadata() {
return null;
}
} }
+4 -4
View File
@@ -6,13 +6,13 @@ import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaPerameter; import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaToolResponse; import me.neurodock.ollama.OllamaToolResponse;
import me.zacharias.chat.plugin.annotation.OllamaTool; //import me.zacharias.chat.plugin.annotation.OllamaTool;
import me.zacharias.chat.plugin.annotation.injectons.InjectPlugin; //import me.zacharias.chat.plugin.annotation.injectons.InjectPlugin;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@OllamaTool //@OllamaTool
public class Tool extends OllamaFunctionTool { public class Tool extends OllamaFunctionTool {
@InjectPlugin(classType = Test.class) //@InjectPlugin(classType = Test.class)
Test core; Test core;
@Override @Override
@@ -5,11 +5,8 @@ import me.neurodock.core.Pair;
import me.neurodock.core.PrintMessageHandler; import me.neurodock.core.PrintMessageHandler;
import me.neurodock.core.files.FileHandlerLocation; import me.neurodock.core.files.FileHandlerLocation;
import me.neurodock.core.memory.CoreMemory; import me.neurodock.core.memory.CoreMemory;
import me.neurodock.mal.api.MALAPITool;
import me.neurodock.ollama.*; import me.neurodock.ollama.*;
import me.neurodock.ollama.utils.SystemMessage; import me.neurodock.ollama.utils.SystemMessage;
import me.neurodock.genius.GeniusTools;
import me.neurodock.wikipedia.WikipediaTool;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.*; import java.io.*;
@@ -172,7 +169,7 @@ public class Display {
writeLog("User: " + message); writeLog("User: " + message);
core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, message.toString())); core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, message.toString()));
//System.out.println(ollamaObject.toString()); //System.out.println(ollamaObject.toString());
core.handleResponce(core.qurryOllama()); core.handleResponse(core.qurryOllama());
} }
} }
} catch (Exception e) { } catch (Exception e) {
@@ -22,6 +22,7 @@ import org.jspecify.annotations.NonNull;
import java.io.*; import java.io.*;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.net.SocketException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
@@ -194,11 +195,27 @@ public class PythonRunner extends OllamaFunctionTool {
return; 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) catch (Exception e)
{ {
writeLog("Failed to ping docker: " + e.getMessage()); writeLog("Failed to ping docker: " + e.getMessage());
e.printStackTrace(); 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; dockerClient = null;
return; return;
} }
@@ -445,7 +462,7 @@ public class PythonRunner extends OllamaFunctionTool {
dockerClient.removeContainerCmd(containerId).exec(); 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) { catch (Exception e) {
throw new OllamaToolErrorException(name(), "Docker unavailable"); throw new OllamaToolErrorException(name(), "Docker unavailable");
@@ -10,8 +10,8 @@ def connect(data):
s.connect((HOST, PORT)) s.connect((HOST, PORT))
data = data + "\n" data = data + "\n"
s.sendall(data.encode("utf-8")) s.sendall(data.encode("utf-8"))
responce = s.recv(4096) response = s.recv(4096)
return responce.decode("utf-8") return response.decode("utf-8")
def print_output(text): def print_output(text):
data = {"function": "print_output", "container_id": CONTAINER_ID, "text": text} data = {"function": "print_output", "container_id": CONTAINER_ID, "text": text}
+8 -1
View File
@@ -25,10 +25,17 @@ subprojects {
implementation("org.jetbrains:annotations:23.1.0") implementation("org.jetbrains:annotations:23.1.0")
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
} }
test { tasks.named('test', Test) {
useJUnitPlatform() useJUnitPlatform()
maxHeapSize = '1G'
testLogging {
events "passed"
}
} }
task sourcesJar(type: Jar, dependsOn: classes) { task sourcesJar(type: Jar, dependsOn: classes) {
+2 -2
View File
@@ -12,8 +12,8 @@ def connect(data):
s.connect((HOST, PORT)) s.connect((HOST, PORT))
data = data + "\n" data = data + "\n"
s.sendall(data.encode("utf-8")) s.sendall(data.encode("utf-8"))
responce = s.recv(4096) response = s.recv(4096)
return responce.decode("utf-8") return response.decode("utf-8")
def print_output(text): def print_output(text):
data = {"function": "print_output", "container_id": CONTAINER_ID, "text": text} data = {"function": "print_output", "container_id": CONTAINER_ID, "text": text}