diff --git a/Core/build.gradle b/Core/build.gradle index cbfb7d6..d512bf3 100644 --- a/Core/build.gradle +++ b/Core/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'me.zacharias.neurodock' -version = '1.5' +version = '1.6' dependencies { implementation project(":Plugin-API") diff --git a/Core/src/main/java/me/zacharias/chat/core/Core.java b/Core/src/main/java/me/zacharias/chat/core/Core.java index c212267..3a1f1ca 100644 --- a/Core/src/main/java/me/zacharias/chat/core/Core.java +++ b/Core/src/main/java/me/zacharias/chat/core/Core.java @@ -75,9 +75,15 @@ public class Core { public static File CACHE_DIRECTORY; static { + // This should not be enforced like this, instead this should read a data field, or wait for an init to be called.... + // Unsure of how to properly do this at this time, however. setDataDirectory("AI-Chat"); } + /** + * Set the data directory in appropriate locations depending on the host OS, falling back to $WORKING_DIR/data + * @param dataDirectory the data directory to use + */ public static void setDataDirectory(String dataDirectory) { String data; @@ -120,11 +126,181 @@ public class Core { } } + /** + * Creates the base directories required by the application. + *

+ * TODO: These paths should be resolved to OS-appropriate locations + * rather than being relative to the working directory. + */ + private void initDirectories() { + ensureDir("./logs/"); + ensureDir("./pythonFiles/"); + ensureDir("./messages/"); + } + + /** + * Ensures that a directory exists at the given path, creating it if it does not. + * + * @param path The path of the directory to create + */ + private static void ensureDir(String path) { + File dir = new File(path); + if (!dir.exists()) dir.mkdir(); + } + + /** + * Constructs the Ollama API {@link #url} from {@link #ollamaIP} and {@link #ollamaPort}. + * + * @throws RuntimeException If the resulting URL is malformed or the URI syntax is invalid + */ + private void initOllamaUrl() { + try { + url = new URI("http://" + ollamaIP + ":" + ollamaPort + "/api/chat").toURL(); + } catch (MalformedURLException | URISyntaxException e) { + throw new RuntimeException("Failed to construct Ollama URL", e); + } + } + + /** + * Initializes the {@link #logWriter}, rotating any pre-existing log file beforehand. + * + * @throws RuntimeException If the log file cannot be created or opened for writing + * @see #rotateLogFile() + */ + private void initLogWriter() { + try { + if (logFile.exists()) { + rotateLogFile(); + } + logWriter = new BufferedWriter(new FileWriter(logFile)); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize log writer", e); + } + } + + /** + * Rotates the existing log file by renaming it to a timestamped filename derived from + * its first line, then recreates {@link #logFile} as a fresh {@code latest.log}. + *

+ * If the existing log file is empty, it is simply deleted and recreated. + * + * @throws IOException If the log file cannot be read, renamed, deleted, or recreated + */ + private void rotateLogFile() throws IOException { + try (BufferedReader br = new BufferedReader(new FileReader(logFile))) { + String line = br.readLine(); + if (line != null) { + String date = line.substring(0, line.indexOf(">")).replaceAll("[/:]", "-"); + logFile.renameTo(new File(logFile.getParentFile(), date + ".log")); + logFile = new File("./logs/latest.log"); + } else { + System.out.println("Existing log file is empty, overwriting it!"); + logFile.delete(); + } + } + logFile.createNewFile(); + } + + /** + * Initializes the {@link #scheduler} and schedules periodic flushing of {@link #logWriter} + * to disk every 3 minutes. + */ + private void initScheduler() { + this.scheduler = Executors.newScheduledThreadPool(1); + scheduler.scheduleAtFixedRate(() -> { + try { + logWriter.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + }, 0, 3, TimeUnit.MINUTES); + } + + /** + * Registers a JVM shutdown hook that shuts down the {@link #scheduler}, + * flushes and closes the {@link #logWriter}, and persists the current + * session's messages to disk. + * + * @see #closeLogWriter() + * @see #saveMessages() + */ + private void initShutdownHook() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + scheduler.shutdownNow(); + closeLogWriter(); + saveMessages(); + })); + } + + /** + * Flushes and closes the {@link #logWriter}. + *

+ * Any {@link IOException} thrown during this process is suppressed, as the writer + * may already be closed by the time the shutdown hook runs. + */ + private void closeLogWriter() { + try { + logWriter.flush(); + logWriter.close(); + } catch (IOException ignore) { + System.out.println("Failed to flush log file, but that is not a problem."); + } + } + + /** + * Persists the current session's messages from the {@link OllamaObject} to two locations: + * a timestamped archive file under {@code ./messages/}, and a rolling {@code messages.json} + * under {@link #DATA_DIR} for resuming the session later. + * + * @see #buildMessagesArray() + * @see #writeMessagesTo(File, JSONArray) + */ + private void saveMessages() { + JSONArray messages = buildMessagesArray(); + + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd_HH-mm-ss")); + writeMessagesTo(new File("./messages/" + timestamp + ".json"), messages); + writeMessagesTo(new File(DATA_DIR, "messages.json"), messages); + } + + /** + * Collects all messages from the {@link OllamaObject} and serializes them into a {@link JSONArray}. + * + * @return A {@link JSONArray} containing all current messages + */ + private JSONArray buildMessagesArray() { + JSONArray messages = new JSONArray(); + for (OllamaMessage message : ollamaObject.getMessages()) { + messages.put(new JSONObject(message.toString())); + } + return messages; + } + + /** + * Writes a {@link JSONArray} of messages to the given file, overwriting it if it already exists. + * + * @param file The target file to write to + * @param messages The message content to write + * @throws RuntimeException If the file cannot be created or written to + */ + private void writeMessagesTo(File file, JSONArray messages) { + try { + if (file.exists()) file.delete(); + file.createNewFile(); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + writer.write(messages.toString()); + } + } catch (IOException e) { + throw new RuntimeException("Failed to write messages to " + file.getPath(), e); + } + } + /** * This function is pending a better name, as this was introduced from being a generic block. This was ugly, and ideally things here should be refactored more. * * TODO: Fix a better name and refactor this method. */ + @Deprecated(forRemoval = true) private void constructCore(){ File dir = new File("./logs/"); if (!dir.exists()) { @@ -224,36 +400,39 @@ public class Core { } })); } - + /** - * Creates a new instance of Core with the provided PrintMessageHandler - * @param printMessageHandler The PrintMessageHandler to use as the default Output + * Creates a new instance of Core with the provided PrintMessageHandler, + * defaulting the Ollama backend to {@code localhost}. + * + * @param printMessageHandler The PrintMessageHandler to use as the default output */ public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler) { - this.printMessageHandler = printMessageHandler; - ollamaIP = "localhost"; - - // TODO: This is a dirty way to not have a masisive init block, please check this methods comment. - constructCore(); + this(printMessageHandler, "localhost"); } /** - * Creates a new instance of Core with the provided PrintMessageHandler, with a specific IP used for Ollama - * @param printMessageHandler The PrintMessageHandler to use as the default Output - * @param ollamaIP The IP used for the Ollama backend + * Creates a new instance of Core with the provided PrintMessageHandler + * and a specific Ollama backend address. + * + * @param printMessageHandler The PrintMessageHandler to use as the default output + * @param ollamaIP The IP or hostname of the Ollama backend */ public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler, @NotNull String ollamaIP) { this.printMessageHandler = printMessageHandler; this.ollamaIP = ollamaIP; - // TODO: This is a dirty way to not have a masisive init block, please check this methods comment. - constructCore(); + initDirectories(); + initOllamaUrl(); + initLogWriter(); + initScheduler(); + initShutdownHook(); } /** * Sets the {@link #ollamaObject} object to the provided argument, - * Also adds the memory base system. see {@link Core#setOllamaObjectNoMemory} if you don't want to add memory functions + * Also adds the memory base system. See {@link Core#setOllamaObjectNoMemory} if you don't want to add memory functions * @param ollamaObject The OllamaObject to use */ public void setOllamaObject(OllamaObject ollamaObject) { @@ -317,7 +496,6 @@ public class Core { { for(Pair tool : tools) { - addTool(tool.getKey(), tool.getValue()); } } @@ -338,16 +516,24 @@ public class Core { return ollamaObject; } + /** + * Removes a tool with a given name + * @param name The tool to remove + */ public void removeTool(String name) { Pair funtionTool = funtionTools.stream().filter(tool -> tool.getKey().name().equalsIgnoreCase(name)).findFirst().orElse(null); - funtionTools.remove(funtionTool); - if(funtionTool.getKey() == null) { - // This should never happens... So if it does, Shit hit the fan - Exception e = new IllegalArgumentException("Function tool with name '"+name+"' does not exist"); - e.printStackTrace(); - System.exit(1); - } - ollamaObject.removeTool(funtionTool.getKey()); + funtionTools.stream() + .filter(tool -> tool.getKey().name().equalsIgnoreCase(name)) + .findFirst() + .ifPresentOrElse(tool -> { + funtionTools.remove(tool); + ollamaObject.removeTool(tool.getKey()); + }, () -> { + + new IllegalArgumentException("Function tool with name '"+name+"' does not exist") + .printStackTrace(); + System.exit(1); + }); } /** @@ -461,8 +647,7 @@ public class Core { if(functions.isEmpty()) { 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 { diff --git a/Core/src/main/java/me/zacharias/chat/core/memory/array/AddArrayMemory.java b/Core/src/main/java/me/zacharias/chat/core/memory/array/AddArrayMemory.java index ead65c3..be8b189 100644 --- a/Core/src/main/java/me/zacharias/chat/core/memory/array/AddArrayMemory.java +++ b/Core/src/main/java/me/zacharias/chat/core/memory/array/AddArrayMemory.java @@ -48,7 +48,7 @@ public class AddArrayMemory extends OllamaFunctionTool { if(!args[0].argument().equals("memory")) { throw new OllamaToolErrorException(name(), "no memory provided"); } - this.memory.addMemory(name(), args[0].argument()); + this.memory.addMemory(name(), (String) args[0].value()); } return new OllamaToolResponse(name(), "Added arrayed memory"); }