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*/);
}
}