refactor: redesign plugin system and enhance message handling
build / build (push) Has been cancelled
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:
+18
-4
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user