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