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
+126 -54
View File
@@ -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<Pair<OllamaFunctionTool, String>> 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<OllamaFunctionArgument> 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.
*
* <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());
}
else checkIfResponceMessage(responce);
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()) {
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());
}
}
@@ -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);
}
@@ -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);
}
}
}
@@ -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.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<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{
@@ -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;
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.<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
* @return The response from the tool, if null return {@link OllamaToolResponse}
* @throws OllamaToolErrorException If the tool encounters an error
@@ -5,7 +5,7 @@ import org.json.JSONObject;
/**
* 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 {
/**
+6
View File
@@ -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;
}
}
+4 -4
View File
@@ -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