feat(plugin): scaffold RPCP plugin loading core
build / build (push) Has been cancelled

- Add JarFile-based plugin.json manifest reading and validation
- Implement reflection-based plugin class instantiation (Data or no-arg constructor)
- Wrap Tool objects into OllamaFunctionTool with basic parameter/response handling
- Implement ToolArguments storage and ToolResponse record
- Extract RPCP_SOURCE constant; add plugin name validation
- Update Data interface: getLoadedPlugins() returns LoadedPlugin[]
- Fix launcher package references (me.zacharias.chat → me.neurodock)

TODO: Extract FileSystem from JAR, populate PluginMetadata, complete
OllamaFunctionArgument↔ToolArguments wrapping, invoke plugin lifecycle hooks

Signed-off-by: zacharias <alienfromdia@proton.me>
This commit is contained in:
2026-06-15 22:09:55 +02:00
parent d016ad9a48
commit b74c5e9e97
12 changed files with 244 additions and 51 deletions
+56 -4
View File
@@ -3,6 +3,9 @@ package me.neurodock.core;
import me.neurodock.core.memory.*;
import me.neurodock.ollama.*;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import me.neurodock.plugin.Data;
import me.neurodock.plugin.LoadedPlugin;
import me.neurodock.plugin.Plugin;
import me.neurodock.plugin.loader.Loader;
import me.neurodock.plugin.exceptions.PluginLoadingException;
import org.intellij.lang.annotations.MagicConstant;
@@ -15,14 +18,19 @@ import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import static me.neurodock.ollama.OllamaFunctionArgument.deconstructOllamaFunctionArguments;
import static me.neurodock.plugin.Plugin.UNKNOWN_PLUGIN;
/**
* The Main class for the System, responsible for managing the OllamaObject, tools, and the Ollama API.
@@ -822,21 +830,65 @@ public class Core {
if(files == null) {
return;
}
LoaderPluginData data = new LoaderPluginData();
Loader loader = new Loader();
for(File file : files) {
try(FileSystem fs = FileSystems.newFileSystem(file.toPath())){
if(!fs.getPath("/plugin.json").toFile().exists())
try(JarFile jar = new JarFile(file)){
JarEntry pluginJsonFile = jar.getJarEntry("/plugin.json");
if(pluginJsonFile == null)
{
throw new PluginLoadingException("Plugin does not contain a plugin.json file", file.getName());
}
//JSONObject pluginJson = new JSONObject(new String(fs.getPath("/plugin.json").toFile().readAllBytes()));
//Plugin plugin = loader.loadPlugin(pluginJson, fs);
if(jar.getJarEntry("/me/neurodock/plugin/Plugin") != null)
throw new PluginLoadingException("Plugin bundles NeuroDock API classes. Consider using compileOnly", file.getName());
StringBuilder pluginJsonData = new StringBuilder();
String tmp = null;
BufferedReader inReader = new BufferedReader(new InputStreamReader(jar.getInputStream(pluginJsonFile)));
while((tmp = inReader.readLine()) != null)
{
pluginJsonData.append(tmp);
}
JSONObject pluginJson = new JSONObject(pluginJsonData.toString());
LoadedPlugin plugin = loader.loadPlugin(pluginJson, file.toPath(), data);
data.addPlugin(plugin);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private static class LoaderPluginData extends Data {
ArrayList<LoadedPlugin> plugins = new ArrayList<>();
@Override
public File getDataDictionary() {
return DATA_DIR;
}
@Override
public LoadedPlugin[] getLoadedPlugins() {
return plugins.toArray(new LoadedPlugin[0]);
}
@Override
public File getCacheDirectory() {
return CACHE_DIRECTORY;
}
public void addPlugin(LoadedPlugin plugin)
{
plugins.add(plugin);
}
public void removePlugin(LoadedPlugin plugin)
{
plugins.remove(plugin);
}
}
/**
* Represents the source of a tool.
* <p>
@@ -3,14 +3,33 @@ package me.neurodock.plugin.loader;
import jdk.jshell.spi.ExecutionControl;
import me.neurodock.core.Pair;
import me.neurodock.ollama.*;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import me.neurodock.plugin.Data;
import me.neurodock.plugin.LoadedPlugin;
import me.neurodock.plugin.Plugin;
import me.neurodock.plugin.PluginMetadata;
import me.neurodock.plugin.exceptions.PluginLoadingException;
import me.neurodock.plugin.exceptions.ToolRuntimeException;
import me.neurodock.plugin.tool.Tool;
import me.neurodock.plugin.tool.ToolArguments;
import me.neurodock.plugin.tool.ToolResponse;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import static me.neurodock.plugin.Plugin.UNKNOWN_PLUGIN;
public class Loader {
private final ArrayList<Pair<Plugin, PluginMetadata>> plugins = new ArrayList<>();
@@ -19,44 +38,110 @@ public class Loader {
* Mock example!
* @return
*/
public OllamaTool getTool() {
// Mock example!
Tool tool = null;
public OllamaTool[] getTools(Plugin plugin) {
return new OllamaFunctionTool() {
@Override
public @NotNull String name() {
return tool.name();
}
ArrayList<OllamaTool> tools = new ArrayList<>();
@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 @NotNull OllamaToolResponse 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);
for(Tool tool : plugin.getTools()) {
tools.add(new OllamaFunctionTool() {
@Override
public @NotNull String name() {
return tool.name();
}
// 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();
@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 @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
// Wrap OllamaFunctionArguments[] to ether ToolArgument[] or ToolArguments(current implementation)
ToolArguments toolArgs = new ToolArguments();
for (OllamaFunctionArgument arg : args) {
toolArgs.addArgument(arg.argument(), arg.value());
}
ToolResponse toolResponse;
try {
toolResponse = tool.callTool(toolArgs);
} catch (ToolRuntimeException e) {
throw new OllamaToolErrorException(name(), e);
}
// Wrap ToolResponce to an OllamaToolRespnce(Well I see a typo here now)
return new OllamaToolResponse(toolResponse.name(), toolResponse.response());
}
@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();
}
});
}
return tools.toArray(tools.toArray(new OllamaTool[0]));
}
public LoadedPlugin loadPlugin(JSONObject pluginJson, Path pluginJar, Data data) {
if(!pluginJson.has("name") || !(pluginJson.get("name") instanceof String pluginName))
throw new PluginLoadingException("Malformed plugin json", UNKNOWN_PLUGIN);
if(!pluginJson.has("entryPoint") || !(pluginJson.get("entryPoint") instanceof String pluginEntryPoint))
throw new PluginLoadingException("Malformed plugin json", pluginName);
Path pluginEntryPointFile = fs.getPath("/"+pluginEntryPoint.replaceAll("\\.", "/")+".class");
if(!Files.exists(pluginEntryPointFile)) throw new PluginLoadingException("Missing plugin entrypoint", pluginName);
Plugin plugin = null;
URLClassLoader classLoader;
try {
classLoader = new URLClassLoader(
new URL[]{pluginJar.toUri().toURL()},
getClass().getClassLoader()
);
Class<?> pluginClass = classLoader.loadClass(pluginEntryPoint);
if(!Plugin.class.isAssignableFrom(pluginClass)) throw new PluginLoadingException(
"Entrypoint does not implement Plugin",
pluginName
);
Class<? extends Plugin> typedClass =
pluginClass.asSubclass(Plugin.class);
for(Constructor<?> constructor : typedClass.getDeclaredConstructors())
{
Parameter[] parameters = constructor.getParameters();
if(parameters.length == 1 &&
parameters[0].getType() == Data.class)
{
plugin = (Plugin) constructor.newInstance(data);
}
else if(parameters.length == 0)
{
plugin = (Plugin) constructor.newInstance();
}
}
};
}catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | IOException e)
{
throw new PluginLoadingException("Failed to load plugin jar", e, pluginName);
}
if(plugin == null)
{
throw new PluginLoadingException(
"Missing proper constructor",
pluginName
);
}
return new LoadedPlugin(plugin, classLoader, null/*TODO: MUST BE REPLACED WITH ACTUAL PLUGIN METADATA! this is read from the PluginEntyPoint*/);
}
}
@@ -4,6 +4,6 @@ import java.io.File;
public abstract class Data {
public abstract File getDataDictionary();
public abstract LoadedPlugins getLoadedPlugins();
public abstract LoadedPlugin[] getLoadedPlugins();
public abstract File getCacheDirectory();
}
@@ -0,0 +1,9 @@
package me.neurodock.plugin;
public record LoadedPlugin(
Plugin plugin,
ClassLoader loader,
PluginMetadata metaData
) {
}
@@ -1,5 +0,0 @@
package me.neurodock.plugin;
public class LoadedPlugins {
// TODO: Implement
}
@@ -9,6 +9,10 @@ import org.jetbrains.annotations.NotNull;
* Entry point for the plugin, this is required for the plugin to work, and you MUST annotate it with {@link PluginEntryPoint}
*/
public abstract class Plugin {
/**
* This is a magic constant plugin name used for when a plugin's name cant be loaded
*/
public static final String UNKNOWN_PLUGIN = "UNKNOWN PLUGIN";
/**
* This shuld be used if {@link PluginEntryPoint#requireData()} is false (default)
@@ -29,12 +33,12 @@ public abstract class Plugin {
public void onInit(){}
/**
* This will be called if the core gets a "fetal" exception or fails to fetch certain info
* This will be called if the core gets a "fetal" exception or fails to fetch certain info AND the plugin was able to be loaded.
*/
public void onDisable(){}
/**
* This will be called after all plugins have been initialised; here it is safe to perform potential stuff where you load data from optional dependencies, or similar things
* This will be called after all plugins have been initialized; here it is safe to perform potential stuff where you load data from optional dependencies, or similar things
*/
public void onEnable(){}
@@ -9,5 +9,10 @@ import java.lang.annotation.Target;
@Target(ElementType.TYPE)
public @interface PluginEntryPoint {
boolean requireData() default true;
/**
* NOT YET IMPLEMENTED!
* @return An array of plugins this plugin depends on
*/
String[] dependencies() default {};
}
@@ -4,4 +4,9 @@ public class PluginLoadingException extends RuntimeException {
public PluginLoadingException(String message, String pluginName) {
super("Plugin: \""+pluginName+"\"> "+message);
}
public PluginLoadingException(String message, Throwable cause, String pluginName)
{
super("Plugin: \""+pluginName+"\"> "+message, cause);
}
}
@@ -6,6 +6,11 @@ import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
public abstract class Tool {
/**
* Magic constant for RPCP sources.<br>
* This is solely here to replace the case of a magic string being used in the code
*/
public static final String RPCP_SOURCE = "RPCP";
/**
* The parent plugin of this tool, used by {@link Tool#getToolJSON()}.
@@ -29,7 +34,7 @@ public abstract class Tool {
ret.put("type", "function");
JSONObject function = new JSONObject();
function.put("name", String.format("%s_%s_RPCP", name(), PLUGIN.getMetadata().getName()).replaceAll(" ", "_"));
function.put("name", String.format("%s_%s_%s", name(), PLUGIN.getMetadata().getName(), RPCP_SOURCE).replaceAll(" ", "_"));
if(description() != null) {
function.put("description", description());
}
@@ -1,4 +1,30 @@
package me.neurodock.plugin.tool;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
public class ToolArguments {
Map<String, Object> arguments = new HashMap<>();
public void addArguments(Map<String, Object> args)
{
arguments.putAll(args);
}
public void addArgument(String name, Object value)
{
arguments.put(name, value);
}
public Object getArgument(String name)
{
if(!arguments.containsKey(name)) throw new NoSuchElementException("Trying to access missing argument");
return arguments.get(name);
}
public boolean hasArgument(String name)
{
return arguments.containsKey(name);
}
}
@@ -1,12 +1,14 @@
package me.neurodock.plugin.tool;
public class ToolResponse {
public record ToolResponse(String name, String response) {
public static ToolResponse empty(Tool tool) {
return new ToolResponse();
return new ToolResponse(tool.name(), "");
}
public static ToolResponse empty(String toolName)
{
return new ToolResponse();
return new ToolResponse(toolName, "");
}
}
+6 -1
View File
@@ -1,5 +1,6 @@
plugins {
id 'java'
id 'application'
}
version = '1.0-SNAPSHOT'
@@ -11,10 +12,14 @@ dependencies {
jar{
manifest {
attributes 'Main-Class': 'me.zacharias.chat.launcher.Launcher'
attributes 'Main-Class': 'me.neurodock.launcher.Launcher'
}
}
application {
mainClass = 'me.neurodock.launcher.Launcher'
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(javaVersion)) // Set Java version