refactor(Core): decompose handleResponse and add ToolCallingRender system
build / build (push) Has been cancelled
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:
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user