diff --git a/Core/src/main/java/me/neurodock/core/Core.java b/Core/src/main/java/me/neurodock/core/Core.java index d3ef67b..0643a8a 100644 --- a/Core/src/main/java/me/neurodock/core/Core.java +++ b/Core/src/main/java/me/neurodock/core/Core.java @@ -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 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. *

diff --git a/Core/src/main/java/me/neurodock/plugin/loader/Loader.java b/Core/src/main/java/me/neurodock/plugin/loader/Loader.java index 1dbe0eb..2b2b3c5 100644 --- a/Core/src/main/java/me/neurodock/plugin/loader/Loader.java +++ b/Core/src/main/java/me/neurodock/plugin/loader/Loader.java @@ -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> 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 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 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*/); } } diff --git a/Plugin-API/src/main/java/me/neurodock/plugin/Data.java b/Plugin-API/src/main/java/me/neurodock/plugin/Data.java index 3a04092..f9df5ef 100644 --- a/Plugin-API/src/main/java/me/neurodock/plugin/Data.java +++ b/Plugin-API/src/main/java/me/neurodock/plugin/Data.java @@ -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(); } diff --git a/Plugin-API/src/main/java/me/neurodock/plugin/LoadedPlugin.java b/Plugin-API/src/main/java/me/neurodock/plugin/LoadedPlugin.java new file mode 100644 index 0000000..faacce7 --- /dev/null +++ b/Plugin-API/src/main/java/me/neurodock/plugin/LoadedPlugin.java @@ -0,0 +1,9 @@ +package me.neurodock.plugin; + +public record LoadedPlugin( + Plugin plugin, + ClassLoader loader, + PluginMetadata metaData +) { + +} diff --git a/Plugin-API/src/main/java/me/neurodock/plugin/LoadedPlugins.java b/Plugin-API/src/main/java/me/neurodock/plugin/LoadedPlugins.java deleted file mode 100644 index e32e52e..0000000 --- a/Plugin-API/src/main/java/me/neurodock/plugin/LoadedPlugins.java +++ /dev/null @@ -1,5 +0,0 @@ -package me.neurodock.plugin; - -public class LoadedPlugins { - // TODO: Implement -} diff --git a/Plugin-API/src/main/java/me/neurodock/plugin/Plugin.java b/Plugin-API/src/main/java/me/neurodock/plugin/Plugin.java index 9288eb5..4281abb 100644 --- a/Plugin-API/src/main/java/me/neurodock/plugin/Plugin.java +++ b/Plugin-API/src/main/java/me/neurodock/plugin/Plugin.java @@ -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(){} diff --git a/Plugin-API/src/main/java/me/neurodock/plugin/annotations/PluginEntryPoint.java b/Plugin-API/src/main/java/me/neurodock/plugin/annotations/PluginEntryPoint.java index 61d07d5..062a5c5 100644 --- a/Plugin-API/src/main/java/me/neurodock/plugin/annotations/PluginEntryPoint.java +++ b/Plugin-API/src/main/java/me/neurodock/plugin/annotations/PluginEntryPoint.java @@ -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 {}; } diff --git a/Plugin-API/src/main/java/me/neurodock/plugin/exceptions/PluginLoadingException.java b/Plugin-API/src/main/java/me/neurodock/plugin/exceptions/PluginLoadingException.java index 5720a7d..e96398b 100644 --- a/Plugin-API/src/main/java/me/neurodock/plugin/exceptions/PluginLoadingException.java +++ b/Plugin-API/src/main/java/me/neurodock/plugin/exceptions/PluginLoadingException.java @@ -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); + } } diff --git a/Plugin-API/src/main/java/me/neurodock/plugin/tool/Tool.java b/Plugin-API/src/main/java/me/neurodock/plugin/tool/Tool.java index 993f8b7..e93b200 100644 --- a/Plugin-API/src/main/java/me/neurodock/plugin/tool/Tool.java +++ b/Plugin-API/src/main/java/me/neurodock/plugin/tool/Tool.java @@ -6,6 +6,11 @@ import org.jetbrains.annotations.NotNull; import org.json.JSONObject; public abstract class Tool { + /** + * Magic constant for RPCP sources.
+ * 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()); } diff --git a/Plugin-API/src/main/java/me/neurodock/plugin/tool/ToolArguments.java b/Plugin-API/src/main/java/me/neurodock/plugin/tool/ToolArguments.java index f65a5ff..7432716 100644 --- a/Plugin-API/src/main/java/me/neurodock/plugin/tool/ToolArguments.java +++ b/Plugin-API/src/main/java/me/neurodock/plugin/tool/ToolArguments.java @@ -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 arguments = new HashMap<>(); + + public void addArguments(Map 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); + } } diff --git a/Plugin-API/src/main/java/me/neurodock/plugin/tool/ToolResponse.java b/Plugin-API/src/main/java/me/neurodock/plugin/tool/ToolResponse.java index 8745b3b..aff8e42 100644 --- a/Plugin-API/src/main/java/me/neurodock/plugin/tool/ToolResponse.java +++ b/Plugin-API/src/main/java/me/neurodock/plugin/tool/ToolResponse.java @@ -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, ""); } + + } diff --git a/launcher/build.gradle b/launcher/build.gradle index 47c1d4f..477807d 100644 --- a/launcher/build.gradle +++ b/launcher/build.gradle @@ -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