refactor: redesign plugin system and enhance message handling
build / build (push) Has been cancelled

- Move plugin and tool-related classes to a dedicated `Plugin-API` module.
- Introduce a new plugin lifecycle (`onInit`, `onEnable`, `onDisable`) and a more robust `Tool` API.
- Refactor `Core` to use `PrintAdvanceMessageHandler` for improved handling of assistant messages, tool responses, and errors.
- Rename `PluginLoader` to `Loader` and migrate it to `me.zacharias.chat.plugin.loader`.
- Update build configurations and Java toolchains across modules.
- Clean up obsolete plugin annotations and interfaces in the `Core` module.
This commit is contained in:
2026-05-26 16:28:02 +02:00
parent 339b176480
commit f6610777ae
37 changed files with 510 additions and 132 deletions
+18 -4
View File
@@ -2,15 +2,29 @@ plugins {
id 'java-library'
}
group = 'me.zacharias'
version = '1.3'
group = 'me.zacharias.neurodock'
version = '1.5'
dependencies {
implementation project(":Plugin-API")
api "org.json:json:20250107"
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21)) // Set Java version
languageVersion.set(JavaLanguageVersion.of(javaVersion)) // Set Java version
}
}
// Tell shadow not to interfere with the main publication
shadowJar {
archiveClassifier = 'all' // keeps shadow as -all.jar, not the main artifact
}
configurations {
[apiElements, runtimeElements].each {
it.outgoing.artifacts.removeIf {
it.buildDependencies.getDependencies(null).contains(shadowJar)
}
}
}
@@ -4,7 +4,7 @@ import me.zacharias.chat.core.memory.*;
import me.zacharias.chat.ollama.*;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import me.zacharias.chat.plugin.Plugin;
import me.zacharias.chat.plugin.PluginLoader;
import me.zacharias.chat.plugin.loader.Loader;
import me.zacharias.chat.plugin.exceptions.PluginLoadingException;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
@@ -68,11 +68,7 @@ public class Core {
/**
* The PrintMessageHandler to use.
*/
private final PrintMessageHandler printMessageHandler;
/**
* If color is supported by the PrintMessageHandler.
*/
private boolean supportColor;
private final PrintAdvanceMessageHandler printMessageHandler;
public static String DATA;
public static File DATA_DIR;
@@ -234,9 +230,8 @@ public class Core {
* Creates a new instance of Core with the provided PrintMessageHandler
* @param printMessageHandler The PrintMessageHandler to use as the default Output
*/
public Core(@NotNull PrintMessageHandler printMessageHandler) {
public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler) {
this.printMessageHandler = printMessageHandler;
supportColor = printMessageHandler.color();
ollamaIP = "localhost";
// TODO: This is a dirty way to not have a masisive init block, please check this methods comment.
@@ -248,10 +243,9 @@ public class Core {
* @param printMessageHandler The PrintMessageHandler to use as the default Output
* @param ollamaIP The IP used for the Ollama backend
*/
public Core(@NotNull PrintMessageHandler printMessageHandler, @NotNull String ollamaIP)
public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler, @NotNull String ollamaIP)
{
this.printMessageHandler = printMessageHandler;
supportColor = printMessageHandler.color();
this.ollamaIP = ollamaIP;
// TODO: This is a dirty way to not have a masisive init block, please check this methods comment.
@@ -466,8 +460,9 @@ public class Core {
}
if(functions.isEmpty()) {
ollamaObject.addMessage(new OllamaToolError("Function '"+function.getString("name")+"' does not exist"));
printMessageHandler.printMessage((supportColor ?"\u001b[31m":"")+"Tried funtion call "+function.getString("name")+"("+deconstructOllamaFunctionArguments(argumentArrayList)+") but failed to find it."+(printMessageHandler.color()?"\u001b[0m":""));
OllamaToolError ollamaToolError = new OllamaToolError("Function '" + function.getString("name") + "' does not exist");
ollamaObject.addMessage(ollamaToolError);
printMessageHandler.printErrorMessage(ollamaToolError);
writeLog("Failed function call to "+function.getString("name"));
}
else {
@@ -479,11 +474,12 @@ public class Core {
try {
OllamaToolRespnce function1 = func.function(argumentArrayList.toArray(new OllamaFunctionArgument[0]));
ollamaObject.addMessage(function1);
printMessageHandler.printMessage((supportColor?"\u001b[34m":"")+"Call "+func.name() + "(" + deconstructOllamaFunctionArguments(argumentArrayList) + ")" + (supportColor?"\u001b[0m":""));
printMessageHandler.printMessage(function1);
writeLog("Successfully function call " + func.name() + " output: " + function1.getResponse());
} catch (OllamaToolErrorException e) {
ollamaObject.addMessage(new OllamaToolError(e.getMessage()));
printMessageHandler.printMessage((supportColor?"\u001b[31m":"")+"Tried funtion call " + func.name() + "("+deconstructOllamaFunctionArguments(argumentArrayList)+") but failed due to " + e.getError() + (supportColor?"\u001b[0m":""));
OllamaToolError ollamaToolError = new OllamaToolError(e.getMessage());
ollamaObject.addMessage(ollamaToolError);
printMessageHandler.printErrorMessage(ollamaToolError);
writeLog("ERROR: "+e.getMessage());
}
}
@@ -505,9 +501,10 @@ public class Core {
String message = responce.getJSONObject("message").getString("content");
if(responce.getJSONObject("message").has("content") && !message.isBlank())
{
printMessageHandler.printMessage((supportColor?"\u001b[32m":"")+(LaunchOptions.getInstance().isShowFullMessage()? message : message.replaceAll("(?s)<think>.*?</think>", "")) +(supportColor?"\u001b[0m":""));
OllamaMessage ollamaMessage = new OllamaMessage(OllamaMessageRole.ASSISTANT, message);
printMessageHandler.printMessage(ollamaMessage);
writeLog("Response content: "+ message);
ollamaObject.addMessage(new OllamaMessage(OllamaMessageRole.ASSISTANT, message));
ollamaObject.addMessage(ollamaMessage);
}
}
@@ -541,7 +538,7 @@ public class Core {
if(files == null) {
return;
}
PluginLoader loader = new PluginLoader();
Loader loader = new Loader();
for(File file : files) {
try(FileSystem fs = FileSystems.newFileSystem(file.toPath())){
if(!fs.getPath("/plugin.json").toFile().exists())
@@ -0,0 +1,25 @@
package me.zacharias.chat.core;
import me.zacharias.chat.ollama.OllamaMessage;
import me.zacharias.chat.ollama.OllamaMessageRole;
/**
* Represents a {@link PrintAdvanceMessageHandler}.
* This is used by the {@link Core} to print messages for the user, LLM, Tools, or potentialy System, see {@link OllamaMessageRole}.
* For a simpler version see {@link PrintMessageHandler}
*/
public interface PrintAdvanceMessageHandler {
/**
* Expected to handle the printing of the provided {@link OllamaMessage}.
* @param message The {@link OllamaMessage} requested to be printed from a veriity of sources, see {@link OllamaMessageRole}
*/
void printMessage(OllamaMessage message);
/**
* Default method to print error messages to the user or API Client.
* This uses ANSI escape codes to color the output red if color is supported.
* @param errorMessage The error message to be printed.
* If color is not supported, it will print the message without color.
*/
void printErrorMessage(OllamaMessage errorMessage);
}
@@ -1,16 +1,35 @@
package me.zacharias.chat.core;
import me.zacharias.chat.ollama.OllamaMessage;
import static me.zacharias.chat.ollama.OllamaFunctionArgument.deconstructOllamaFunctionArguments;
/**
* Represents a PrintMessageHandler.
* This is used by the Core to print messages to the user or API Clients.
* The Core uses this to print messages to the user or API Clients.
*/
public interface PrintMessageHandler {
public interface PrintMessageHandler extends PrintAdvanceMessageHandler {
/**
* Handles the printing of a message.
* This is meant to output the message to the user or API Client.
* @param message The message to be printed.
*/
void printMessage(String message);
/**
* This is a default implementation when wrapping {@link PrintAdvanceMessageHandler} to this "simpler" {@link PrintMessageHandler}
*
*
* @param message The {@link OllamaMessage} requested to be printed from a veriity of sources, see {@link me.zacharias.chat.ollama.OllamaMessageRole}
*/
default void printMessage(OllamaMessage message) {
printMessage(switch (message.getRole()) {
case ASSISTANT -> (color()?"\u001b[32m":"")+(LaunchOptions.getInstance().isShowFullMessage()? message.getContent() : message.getContent().replaceAll("(?s)<think>.*?</think>", "")) +(color()?"\u001b[0m":"");
case TOOL -> (color() ?"\u001b[31m":"")+message.getContent()+(color()?"\u001b[0m":"");
case USER, SYSTEM -> (color() ?"\u001b[37m":"")+message.getContent()+(color()?"\u001b[0m":"");
default -> throw new IllegalArgumentException("Invalid message role");
});
}
/**
* Gets if color is supported by the PrintMessageHandler.
@@ -32,4 +51,17 @@ public interface PrintMessageHandler {
printMessage(message);
}
}
/**
* This is a default implementation when wrapping {@link PrintAdvanceMessageHandler} to this "simpler" {@link PrintMessageHandler}
*
*
* Default method to print error messages to the user or API Client.
* This uses ANSI escape codes to color the output red if color is supported.
* @param errorMessage The error message to be printed.
* If color is not supported, it will print the message without color.
*/
default void printErrorMessage(OllamaMessage errorMessage) {
printError(errorMessage.getContent());
}
}
@@ -25,6 +25,20 @@ public class OllamaMessage {
this.content = content;
}
/**
* @return The role of who sent this "message", represented as {@link OllamaMessageRole}
*/
public OllamaMessageRole getRole() {
return role;
}
/**
* @return The "message" or content sent by a source
*/
public String getContent() {
return content;
}
@Override
public String toString() {
JSONObject json = new JSONObject();
@@ -1,6 +1,5 @@
package me.zacharias.chat.ollama;
import com.sun.source.util.Plugin;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.core.LaunchOptions;
import me.zacharias.chat.core.Pair;
@@ -15,10 +14,6 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.lang.reflect.Field;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -452,6 +447,17 @@ public class OllamaObject {
return this;
}
/**
* Initializes the {@link FileHandler} singleton with the given base directory and adds its tools.
* <p>
* The {@link FileHandler} is intentionally constructed here rather than eagerly,
* as file access tooling should only be initialized if explicitly requested during
* object construction. Calling this after {@link #build()} is technically possible
* but not intended.
* </p>
* @param baseDirectory the base directory for file access, see {@link FileHandlerLocation}
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addFileTools(@MagicConstant(valuesFromClass = FileHandlerLocation.class) String baseDirectory)
{
new FileHandler(baseDirectory);
@@ -1,5 +0,0 @@
package me.zacharias.chat.plugin;
public class Plugin {
private final PluginMetadata metadata = null;
}
@@ -1,62 +0,0 @@
package me.zacharias.chat.plugin;
import org.json.JSONObject;
import java.lang.reflect.Field;
import java.nio.file.FileSystem;
import java.nio.file.Path;
public class PluginLoader {
public PluginLoader() {
}
public Plugin loadPlugin(FileSystem pluginJar, JSONObject pluginMeta) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
String pluginName = pluginMeta.getString("name");
String pluginEntryPoint = pluginMeta.getString("entryPoint");
String pluginVersion = pluginMeta.getString("version");
String pluginDescription = pluginMeta.optString("description", "No description provided");
String[] pluginAuthors = pluginMeta.optJSONArray("author") != null ?
pluginMeta.getJSONArray("author").toList().toArray(new String[0]) : new String[0];
PluginMetadata pluginMetadata = new PluginMetadata() {
@Override
public String getName() {
return pluginName;
}
@Override
public String getVersion() {
return pluginVersion;
}
@Override
public String getDescription() {
return pluginDescription;
}
@Override
public String[] getAuthor() {
return pluginAuthors;
}
@Override
public String entryPoint() {
return pluginEntryPoint;
}
};
try {
Plugin plugin = (Plugin) Class.forName(pluginEntryPoint).newInstance();
Field metadataField = plugin.getClass().getDeclaredField("metadata");
metadataField.set(plugin, pluginMetadata);
if (metadataField.get(plugin) == null)
throw new IllegalStateException("Plugin metadata field is null for plugin: " + pluginName);
}catch (NoSuchFieldException e)
{
throw new IllegalStateException("Plugin class " + pluginEntryPoint + " does not have a 'metadata' field", e);
}
return null;
//ClassLoader classLoader = pluginJar.
}
}
@@ -1,9 +0,0 @@
package me.zacharias.chat.plugin;
public interface PluginMetadata {
public String getName();
public String getVersion();
public String getDescription();
public String[] getAuthor();
public String entryPoint();
}
@@ -1,12 +0,0 @@
package me.zacharias.chat.plugin.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface OllamaTool {
}
@@ -1,14 +0,0 @@
package me.zacharias.chat.plugin.annotation.injectons;
import me.zacharias.chat.plugin.Plugin;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
public @interface InjectPlugin {
Class<? extends Plugin> classType() default Plugin.class;
}
@@ -1,7 +0,0 @@
package me.zacharias.chat.plugin.exceptions;
public class PluginLoadingException extends RuntimeException {
public PluginLoadingException(String message, String pluginName) {
super("Plugin: \""+pluginName+"\"> "+message);
}
}
@@ -0,0 +1,62 @@
package me.zacharias.chat.plugin.loader;
import jdk.jshell.spi.ExecutionControl;
import me.zacharias.chat.core.Pair;
import me.zacharias.chat.ollama.*;
import me.zacharias.chat.plugin.Plugin;
import me.zacharias.chat.plugin.PluginMetadata;
import me.zacharias.chat.plugin.exceptions.ToolRuntimeException;
import me.zacharias.chat.plugin.tool.Tool;
import me.zacharias.chat.plugin.tool.ToolResponse;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
public class Loader {
private final ArrayList<Pair<Plugin, PluginMetadata>> plugins = new ArrayList<>();
/**
* Mock example!
* @return
*/
public OllamaTool getTool() {
// Mock example!
Tool tool = null;
return new OllamaFunctionTool() {
@Override
public @NotNull String name() {
return tool.name();
}
@Override
public String description() {
return tool.description();
}
@Override
public @NotNull OllamaPerameter parameters() {
// TODO: make a wrapper around ToolPerameters to OllamaPeramer
throw new RuntimeException(new ExecutionControl.NotImplementedException("To be Implemented"));
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
// Wrap OllamaFunctionArguments[] to ether ToolArgument[] or ToolArguments(current implementation)
try {
ToolResponse toolResponse = tool.callTool(null);
} catch (ToolRuntimeException e) {
throw new RuntimeException(e);
}
// Wrap ToolResponce to an OllamaToolRespnce(Well I see a typo here now)
return null;
}
@Override
public String toString() {
// Replaces the toString which is used thru out the Core and OllamaObject to get the JSON representation of a Function Tool
return tool.getToolJSON();
}
};
}
}
+3 -2
View File
@@ -6,6 +6,7 @@ import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.plugin.annotation.OllamaTool;
import me.zacharias.chat.plugin.annotation.injectons.InjectPlugin;
import org.jetbrains.annotations.NotNull;
@OllamaTool
public class Tool extends OllamaFunctionTool {
@@ -13,7 +14,7 @@ public class Tool extends OllamaFunctionTool {
Test core;
@Override
public String name() {
public @NotNull String name() {
return "";
}
@@ -23,7 +24,7 @@ public class Tool extends OllamaFunctionTool {
}
@Override
public OllamaPerameter parameters() {
public @NotNull OllamaPerameter parameters() {
return null;
}