package eu.svjatoslav.alyverkko_cli;
-import eu.svjatoslav.alyverkko_cli.commands.MailQuery;
+import eu.svjatoslav.alyverkko_cli.commands.mail_correspondant.MailQuery;
import java.io.*;
import java.nio.file.Files;
package eu.svjatoslav.alyverkko_cli;
import eu.svjatoslav.alyverkko_cli.commands.*;
+import eu.svjatoslav.alyverkko_cli.commands.mail_correspondant.MailCorrespondentCommand;
import eu.svjatoslav.alyverkko_cli.configuration.Configuration;
import java.io.IOException;
+++ /dev/null
-package eu.svjatoslav.alyverkko_cli.commands;
-
-import eu.svjatoslav.alyverkko_cli.*;
-import eu.svjatoslav.alyverkko_cli.model.Model;
-import eu.svjatoslav.alyverkko_cli.model.ModelLibrary;
-import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser;
-import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
-import java.nio.file.*;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
-import static eu.svjatoslav.alyverkko_cli.Main.configuration;
-import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration;
-import static eu.svjatoslav.commons.file.IOHelper.getFileContentsAsString;
-import static eu.svjatoslav.commons.file.IOHelper.saveToFile;
-import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
-import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
-
-/**
- * The MailCorrespondentCommand continuously monitors a specified mail
- * directory for new or modified text files, checks if they have a
- * "TOCOMPUTE:" marker, and if so, processes them with an AI model.
- * Once processed, results are appended to the same file.
- *
- * Usage:
- * <pre>
- * alyverkko-cli mail
- * </pre>
- */
-public class MailCorrespondentCommand implements Command {
-
- /**
- * A command-line parser to handle "mail" command arguments.
- */
- final Parser parser = new Parser();
-
- /**
- * Optional CLI argument for specifying a configuration file path.
- */
- public FileOption configFileOption = parser.add(new FileOption("Configuration file path"))
- .addAliases("--config", "-c")
- .mustExist();
-
- /**
- * The library of available models, constructed from configuration.
- */
- ModelLibrary modelLibrary;
-
- /**
- * The WatchService instance for monitoring file system changes in
- * the mail directory.
- */
- private WatchService directoryWatcher;
-
- /**
- * The directory that we continuously watch for new tasks.
- */
- File mailDir;
-
- /**
- * @return the name of this command, i.e., "mail".
- */
- @Override
- public String getName() {
- return "mail";
- }
-
- /**
- * Executes the "mail" command, loading configuration, starting a
- * WatchService on the mail directory, and running an infinite loop
- * that processes newly discovered tasks.
- *
- * @param cliArguments the command-line arguments following the "mail" subcommand.
- * @throws IOException if reading/writing tasks fails.
- * @throws InterruptedException if the WatchService is interrupted.
- */
- @Override
- public void execute(String[] cliArguments) throws IOException, InterruptedException {
- if (!parser.parse(cliArguments)) {
- System.out.println("Failed to parse commandline arguments");
- parser.showHelp();
- return;
- }
-
- configuration = loadConfiguration(configFileOption.isPresent() ? configFileOption.getValue() : null);
- if (configuration == null) {
- System.out.println("Failed to load configuration file");
- return;
- }
-
- modelLibrary = new ModelLibrary(configuration.getModelsDirectory(), configuration.getModels());
- mailDir = configuration.getMailDirectory();
-
- // Set up directory watch service
- initializeFileWatcher();
-
- // Process any existing files that might already be in the directory
- initialMailScanAndReply();
-
- System.out.println("Mail correspondent running. Press CTRL+c to terminate.");
-
- // Main loop: watch for file events
- while (true) {
- WatchKey key;
- try {
- key = directoryWatcher.take();
- } catch (InterruptedException e) {
- System.out.println("Interrupted while waiting for file system events. Exiting.");
- break;
- }
-
- System.out.println("Detected filesystem event.");
-
- // Sleep briefly to allow the file to be fully written
- Thread.sleep(1000);
-
- processDetectedFilesystemEvents(key);
-
- if (!key.reset()) {
- break;
- }
- }
-
- directoryWatcher.close();
- }
-
- /**
- * Performs an initial scan of existing files in the mail directory,
- * processing those that need AI inference (i.e., that start with "TOCOMPUTE:").
- *
- * @throws IOException if reading files fails.
- * @throws InterruptedException if the thread is interrupted.
- */
- private void initialMailScanAndReply() throws IOException, InterruptedException {
- File[] files = mailDir.listFiles();
- if (files == null) return;
-
- for (File file : files) {
- processMailIfNeeded(file);
- }
- }
-
- /**
- * Checks if a file needs to be processed by verifying that it:
- * 1) is not hidden,
- * 2) is a regular file,
- * 3) starts with "TOCOMPUTE:" in the first line.
- *
- * @param file the file to inspect.
- * @return true if the file meets the criteria for AI processing.
- * @throws IOException if reading the file fails.
- */
- private boolean isMailProcessingNeeded(File file) throws IOException {
- // ignore hidden files
- if (file.getName().startsWith(".")) {
- return false;
- }
-
- // Check if it's a regular file
- if (!file.isFile()) {
- return false;
- }
-
- // Ensure the first line says "TOCOMPUTE:"
- return fileHasToComputeMarker(file);
- }
-
- /**
- * Inspects the first line of the file to see if it starts with "TOCOMPUTE:".
- *
- * @param file the file to read.
- * @return true if the file's first line starts with "TOCOMPUTE:".
- * @throws IOException if file reading fails.
- */
- private static boolean fileHasToComputeMarker(File file) throws IOException {
- try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
- String firstLine = reader.readLine();
- return firstLine != null && firstLine.startsWith("TOCOMPUTE:");
- }
- }
-
- /**
- * Processes a file if it has the "TOCOMPUTE:" marker, running an AI
- * query and appending the result to the file. Otherwise logs that
- * it's being ignored.
- *
- * @param file the file to possibly process.
- * @throws IOException if reading/writing the file fails.
- * @throws InterruptedException if the AI query is interrupted.
- */
- private void processMailIfNeeded(File file) throws IOException, InterruptedException {
- if (!isMailProcessingNeeded(file)) {
- System.out.println("Ignoring file: " + file.getName() + " (does not need processing for now)");
- return;
- }
-
- System.out.println("\nReplying to mail: " + file.getName());
-
- // Read the mail content
- String inputFileContent = getFileContentsAsString(file);
-
- // Parse the relevant data into a MailQuery object
- MailQuery mailQuery = parseInputFileContent(inputFileContent);
-
- // Create an AiTask and run the query
- AiTask aiTask = new AiTask(mailQuery);
- String aiGeneratedResponse = aiTask.runAiQuery();
-
- // Build new content
- StringBuilder resultFileContent = new StringBuilder();
-
- // Ensure the user prompt block is labeled if it isn't already
- if (!mailQuery.userPrompt.startsWith("* USER:\n")) {
- resultFileContent.append("* USER:\n");
- }
- resultFileContent.append(mailQuery.userPrompt).append("\n");
-
- // Append the AI response block
- resultFileContent
- .append("* ASSISTANT:\n")
- .append(aiGeneratedResponse)
- .append("\n");
-
- // Write the combined result back to the same file
- saveToFile(file, resultFileContent.toString());
- }
-
- /**
- * Converts the raw file content (including the line beginning with "TOCOMPUTE:")
- * into a {@link MailQuery} object that the AI can process.
- *
- * @param inputFileContent the raw contents of the mail file.
- * @return a {@link MailQuery} containing the system prompt, user prompt, and the selected model.
- * @throws IOException if reading prompt files fails.
- */
- private MailQuery parseInputFileContent(String inputFileContent) throws IOException {
- MailQuery mailQuery = new MailQuery();
-
- // Find the newline that separates "TOCOMPUTE: ..." from the rest
- int firstNewLineIndex = inputFileContent.indexOf('\n');
- if (firstNewLineIndex == -1) {
- throw new IllegalArgumentException("Input file is only one line long. Content: " + inputFileContent);
- } else {
- // The user prompt is everything after the first line
- mailQuery.userPrompt = inputFileContent.substring(firstNewLineIndex + 1);
- }
-
- // The first line will look like "TOCOMPUTE: model=... prompt=... etc."
- String firstLine = inputFileContent.substring(0, firstNewLineIndex);
-
- // Parse out the key/value pairs
- Map<String, String> settings = parseSettings(firstLine);
-
- // Look up system prompt from the "prompt" alias
- String promptAlias = settings.getOrDefault("prompt", "default");
- mailQuery.systemPrompt = configuration.getPromptByAlias(promptAlias);
-
- // Resolve model from the "model" alias
- String modelAlias = settings.getOrDefault("model", "default");
- Optional<Model> modelOptional = modelLibrary.findModelByAlias(modelAlias);
- if (!modelOptional.isPresent()) {
- throw new IllegalArgumentException("Model with alias '" + modelAlias + "' not found.");
- }
- mailQuery.model = modelOptional.get();
-
- return mailQuery;
- }
-
- /**
- * Parses the "TOCOMPUTE:" line, which should look like:
- * <pre>TOCOMPUTE: key1=value1 key2=value2 ...</pre>
- *
- * @param toComputeLine the line beginning with "TOCOMPUTE:".
- * @return a map of settings derived from that line.
- */
- private Map<String, String> parseSettings(String toComputeLine) {
- if (!toComputeLine.startsWith("TOCOMPUTE:")) {
- throw new IllegalArgumentException("Invalid TOCOMPUTE line: " + toComputeLine);
- }
-
- // If there's nothing beyond "TOCOMPUTE:", just return an empty map
- if (toComputeLine.length() <= "TOCOMPUTE: ".length()) {
- return new HashMap<>();
- }
-
- // Example format: "TOCOMPUTE: prompt=writer model=mistral"
- String[] parts = toComputeLine.substring("TOCOMPUTE: ".length()).split("\\s+");
- Map<String, String> settings = new HashMap<>();
-
- for (String part : parts) {
- String[] keyValue = part.split("=");
- if (keyValue.length == 2) {
- settings.put(keyValue[0], keyValue[1]);
- }
- }
- return settings;
- }
-
- /**
- * Handles the filesystem events from the WatchService (e.g. file creation
- * or modification), then processes those files if necessary.
- *
- * @param key the watch key containing the events.
- * @throws IOException if file reading/writing fails.
- * @throws InterruptedException if the AI process is interrupted.
- */
- private void processDetectedFilesystemEvents(WatchKey key) throws IOException, InterruptedException {
- for (WatchEvent<?> event : key.pollEvents()) {
- WatchEvent.Kind<?> kind = event.kind();
-
- // Skip OVERFLOW event
- if (kind == StandardWatchEventKinds.OVERFLOW) {
- continue;
- }
-
- // The filename for the event
- Path filename = ((WatchEvent<Path>) event).context();
- System.out.println("Event: " + kind + " for file: " + filename);
-
- // Process the file
- if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY) {
- File file = mailDir.toPath().resolve(filename).toFile();
- processMailIfNeeded(file);
- }
- }
- }
-
- /**
- * Registers the mail directory with a WatchService for ENTRY_CREATE
- * and ENTRY_MODIFY events.
- *
- * @throws IOException if registration fails.
- */
- private void initializeFileWatcher() throws IOException {
- this.directoryWatcher = FileSystems.getDefault().newWatchService();
- Paths.get(mailDir.getAbsolutePath()).register(directoryWatcher, ENTRY_CREATE, ENTRY_MODIFY);
- }
-}
+++ /dev/null
-package eu.svjatoslav.alyverkko_cli.commands;
-
-import eu.svjatoslav.alyverkko_cli.model.Model;
-
-/**
- * Represents the data needed to perform a single mail-based AI query,
- * containing prompts and the specific AI model to use.
- */
-public class MailQuery {
-
- /**
- * The system prompt text that sets the context or role for the AI.
- * This is often used to establish rules or background instructions
- * for how the assistant should behave.
- */
- public String systemPrompt;
-
- /**
- * The user's prompt text (the main request or query).
- */
- public String userPrompt;
-
- /**
- * The AI model to be used for processing this query.
- */
- public Model model;
-
- /**
- * Returns a string containing a summary of the {@link MailQuery} object.
- *
- * @return a string with the system prompt, user prompt, and model info.
- */
- @Override
- public String toString() {
- return "MailQuery{" +
- "systemPrompt='" + systemPrompt + '\'' +
- ", userPrompt='" + userPrompt + '\'' +
- ", model=" + model +
- '}';
- }
-}
import eu.svjatoslav.alyverkko_cli.AiTask;
import eu.svjatoslav.alyverkko_cli.Command;
import eu.svjatoslav.alyverkko_cli.Main;
+import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser;
+import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption;
import java.io.File;
import java.io.IOException;
return "selftest";
}
+ final Parser parser = new Parser();
+
+ /**
+ * Optional CLI argument for specifying a configuration file path.
+ */
+ public FileOption configFileOption = parser.add(new FileOption("Configuration file path"))
+ .addAliases("--config", "-c")
+ .mustExist();
+
/**
* Executes the self-test, checking if various directories, files, and
* configurations exist and are valid.
// Perform selftest checks here
System.out.println("Starting selftest...");
- configuration = loadConfiguration();
+ if (!parser.parse(cliArguments)) {
+ System.out.println("Failed to parse commandline arguments");
+ parser.showHelp();
+ return;
+ }
+
+ configuration = loadConfiguration(configFileOption.isPresent() ? configFileOption.getValue() : null);
+ if (configuration == null) {
+ System.out.println("Failed to load configuration file");
+ return;
+ }
+
boolean didAllChecksPass = true;
// Check if the configuration is loaded
--- /dev/null
+package eu.svjatoslav.alyverkko_cli.commands.mail_correspondant;
+
+import eu.svjatoslav.alyverkko_cli.*;
+import eu.svjatoslav.alyverkko_cli.model.Model;
+import eu.svjatoslav.alyverkko_cli.model.ModelLibrary;
+import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser;
+import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.file.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import static eu.svjatoslav.alyverkko_cli.Main.configuration;
+import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration;
+import static eu.svjatoslav.commons.file.IOHelper.getFileContentsAsString;
+import static eu.svjatoslav.commons.file.IOHelper.saveToFile;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+
+/**
+ * The MailCorrespondentCommand continuously monitors a specified mail
+ * directory for new or modified text files, checks if they have a
+ * "TOCOMPUTE:" marker, and if so, processes them with an AI model.
+ * Once processed, results are appended to the same file.
+ *
+ * Usage:
+ * <pre>
+ * alyverkko-cli mail
+ * </pre>
+ */
+public class MailCorrespondentCommand implements Command {
+
+ /**
+ * A command-line parser to handle "mail" command arguments.
+ */
+ final Parser parser = new Parser();
+
+ /**
+ * Optional CLI argument for specifying a configuration file path.
+ */
+ public FileOption configFileOption = parser.add(new FileOption("Configuration file path"))
+ .addAliases("--config", "-c")
+ .mustExist();
+
+ /**
+ * The library of available models, constructed from configuration.
+ */
+ ModelLibrary modelLibrary;
+
+ /**
+ * The WatchService instance for monitoring file system changes in
+ * the mail directory.
+ */
+ private WatchService directoryWatcher;
+
+ /**
+ * The directory that we continuously watch for new tasks.
+ */
+ File mailDir;
+
+ /**
+ * @return the name of this command, i.e., "mail".
+ */
+ @Override
+ public String getName() {
+ return "mail";
+ }
+
+ /**
+ * Executes the "mail" command, loading configuration, starting a
+ * WatchService on the mail directory, and running an infinite loop
+ * that processes newly discovered tasks.
+ *
+ * @param cliArguments the command-line arguments following the "mail" subcommand.
+ * @throws IOException if reading/writing tasks fails.
+ * @throws InterruptedException if the WatchService is interrupted.
+ */
+ @Override
+ public void execute(String[] cliArguments) throws IOException, InterruptedException {
+ if (!parser.parse(cliArguments)) {
+ System.out.println("Failed to parse commandline arguments");
+ parser.showHelp();
+ return;
+ }
+
+ configuration = loadConfiguration(configFileOption.isPresent() ? configFileOption.getValue() : null);
+ if (configuration == null) {
+ System.out.println("Failed to load configuration file");
+ return;
+ }
+
+ modelLibrary = new ModelLibrary(configuration.getModelsDirectory(), configuration.getModels());
+ mailDir = configuration.getMailDirectory();
+
+ // Set up directory watch service
+ initializeFileWatcher();
+
+ // Process any existing files that might already be in the directory
+ initialMailScanAndReply();
+
+ System.out.println("Mail correspondent running. Press CTRL+c to terminate.");
+
+ // Main loop: watch for file events
+ while (true) {
+ WatchKey key;
+ try {
+ key = directoryWatcher.take();
+ } catch (InterruptedException e) {
+ System.out.println("Interrupted while waiting for file system events. Exiting.");
+ break;
+ }
+
+ System.out.println("Detected filesystem event.");
+
+ // Sleep briefly to allow the file to be fully written
+ Thread.sleep(1000);
+
+ processDetectedFilesystemEvents(key);
+
+ if (!key.reset()) {
+ break;
+ }
+ }
+
+ directoryWatcher.close();
+ }
+
+ /**
+ * Performs an initial scan of existing files in the mail directory,
+ * processing those that need AI inference (i.e., that start with "TOCOMPUTE:").
+ *
+ * @throws IOException if reading files fails.
+ * @throws InterruptedException if the thread is interrupted.
+ */
+ private void initialMailScanAndReply() throws IOException, InterruptedException {
+ File[] files = mailDir.listFiles();
+ if (files == null) return;
+
+ for (File file : files) {
+ processMailIfNeeded(file);
+ }
+ }
+
+ /**
+ * Checks if a file needs to be processed by verifying that it:
+ * 1) is not hidden,
+ * 2) is a regular file,
+ * 3) starts with "TOCOMPUTE:" in the first line.
+ *
+ * @param file the file to inspect.
+ * @return true if the file meets the criteria for AI processing.
+ * @throws IOException if reading the file fails.
+ */
+ private boolean isMailProcessingNeeded(File file) throws IOException {
+ // ignore hidden files
+ if (file.getName().startsWith(".")) {
+ return false;
+ }
+
+ // Check if it's a regular file
+ if (!file.isFile()) {
+ return false;
+ }
+
+ // Ensure the first line says "TOCOMPUTE:"
+ return fileHasToComputeMarker(file);
+ }
+
+ /**
+ * Inspects the first line of the file to see if it starts with "TOCOMPUTE:".
+ *
+ * @param file the file to read.
+ * @return true if the file's first line starts with "TOCOMPUTE:".
+ * @throws IOException if file reading fails.
+ */
+ private static boolean fileHasToComputeMarker(File file) throws IOException {
+ try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+ String firstLine = reader.readLine();
+ return firstLine != null && firstLine.startsWith("TOCOMPUTE:");
+ }
+ }
+
+ /**
+ * Processes a file if it has the "TOCOMPUTE:" marker, running an AI
+ * query and appending the result to the file. Otherwise logs that
+ * it's being ignored.
+ *
+ * @param file the file to possibly process.
+ * @throws IOException if reading/writing the file fails.
+ * @throws InterruptedException if the AI query is interrupted.
+ */
+ private void processMailIfNeeded(File file) throws IOException, InterruptedException {
+ if (!isMailProcessingNeeded(file)) {
+ System.out.println("Ignoring file: " + file.getName() + " (does not need processing for now)");
+ return;
+ }
+
+ System.out.println("\nReplying to mail: " + file.getName());
+
+ // Read the mail content
+ String inputFileContent = getFileContentsAsString(file);
+
+ // Parse the relevant data into a MailQuery object
+ MailQuery mailQuery = parseInputFileContent(inputFileContent);
+
+ // Create an AiTask and run the query
+ AiTask aiTask = new AiTask(mailQuery);
+ String aiGeneratedResponse = aiTask.runAiQuery();
+
+ // Build new content
+ StringBuilder resultFileContent = new StringBuilder();
+
+ // Ensure the user prompt block is labeled if it isn't already
+ if (!mailQuery.userPrompt.startsWith("* USER:\n")) {
+ resultFileContent.append("* USER:\n");
+ }
+ resultFileContent.append(mailQuery.userPrompt).append("\n");
+
+ // Append the AI response block
+ resultFileContent
+ .append("* ASSISTANT:\n")
+ .append(aiGeneratedResponse)
+ .append("\n");
+
+ // Write the combined result back to the same file
+ saveToFile(file, resultFileContent.toString());
+ }
+
+ /**
+ * Converts the raw file content (including the line beginning with "TOCOMPUTE:")
+ * into a {@link MailQuery} object that the AI can process.
+ *
+ * @param inputFileContent the raw contents of the mail file.
+ * @return a {@link MailQuery} containing the system prompt, user prompt, and the selected model.
+ * @throws IOException if reading prompt files fails.
+ */
+ private MailQuery parseInputFileContent(String inputFileContent) throws IOException {
+ MailQuery mailQuery = new MailQuery();
+
+ // Find the newline that separates "TOCOMPUTE: ..." from the rest
+ int firstNewLineIndex = inputFileContent.indexOf('\n');
+ if (firstNewLineIndex == -1) {
+ throw new IllegalArgumentException("Input file is only one line long. Content: " + inputFileContent);
+ } else {
+ // The user prompt is everything after the first line
+ mailQuery.userPrompt = inputFileContent.substring(firstNewLineIndex + 1);
+ }
+
+ // The first line will look like "TOCOMPUTE: model=... prompt=... etc."
+ String firstLine = inputFileContent.substring(0, firstNewLineIndex);
+
+ // Parse out the key/value pairs
+ Map<String, String> settings = parseSettings(firstLine);
+
+ // Look up system prompt from the "prompt" alias
+ String promptAlias = settings.getOrDefault("prompt", "default");
+ mailQuery.systemPrompt = configuration.getPromptByAlias(promptAlias);
+
+ // Resolve model from the "model" alias
+ String modelAlias = settings.getOrDefault("model", "default");
+ Optional<Model> modelOptional = modelLibrary.findModelByAlias(modelAlias);
+ if (!modelOptional.isPresent()) {
+ throw new IllegalArgumentException("Model with alias '" + modelAlias + "' not found.");
+ }
+ mailQuery.model = modelOptional.get();
+
+ return mailQuery;
+ }
+
+ /**
+ * Parses the "TOCOMPUTE:" line, which should look like:
+ * <pre>TOCOMPUTE: key1=value1 key2=value2 ...</pre>
+ *
+ * @param toComputeLine the line beginning with "TOCOMPUTE:".
+ * @return a map of settings derived from that line.
+ */
+ private Map<String, String> parseSettings(String toComputeLine) {
+ if (!toComputeLine.startsWith("TOCOMPUTE:")) {
+ throw new IllegalArgumentException("Invalid TOCOMPUTE line: " + toComputeLine);
+ }
+
+ // If there's nothing beyond "TOCOMPUTE:", just return an empty map
+ if (toComputeLine.length() <= "TOCOMPUTE: ".length()) {
+ return new HashMap<>();
+ }
+
+ // Example format: "TOCOMPUTE: prompt=writer model=mistral"
+ String[] parts = toComputeLine.substring("TOCOMPUTE: ".length()).split("\\s+");
+ Map<String, String> settings = new HashMap<>();
+
+ for (String part : parts) {
+ String[] keyValue = part.split("=");
+ if (keyValue.length == 2) {
+ settings.put(keyValue[0], keyValue[1]);
+ }
+ }
+ return settings;
+ }
+
+ /**
+ * Handles the filesystem events from the WatchService (e.g. file creation
+ * or modification), then processes those files if necessary.
+ *
+ * @param key the watch key containing the events.
+ * @throws IOException if file reading/writing fails.
+ * @throws InterruptedException if the AI process is interrupted.
+ */
+ private void processDetectedFilesystemEvents(WatchKey key) throws IOException, InterruptedException {
+ for (WatchEvent<?> event : key.pollEvents()) {
+ WatchEvent.Kind<?> kind = event.kind();
+
+ // Skip OVERFLOW event
+ if (kind == StandardWatchEventKinds.OVERFLOW) {
+ continue;
+ }
+
+ // The filename for the event
+ Path filename = ((WatchEvent<Path>) event).context();
+ System.out.println("Event: " + kind + " for file: " + filename);
+
+ // Process the file
+ if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY) {
+ File file = mailDir.toPath().resolve(filename).toFile();
+ processMailIfNeeded(file);
+ }
+ }
+ }
+
+ /**
+ * Registers the mail directory with a WatchService for ENTRY_CREATE
+ * and ENTRY_MODIFY events.
+ *
+ * @throws IOException if registration fails.
+ */
+ private void initializeFileWatcher() throws IOException {
+ this.directoryWatcher = FileSystems.getDefault().newWatchService();
+ Paths.get(mailDir.getAbsolutePath()).register(directoryWatcher, ENTRY_CREATE, ENTRY_MODIFY);
+ }
+}
--- /dev/null
+package eu.svjatoslav.alyverkko_cli.commands.mail_correspondant;
+
+import eu.svjatoslav.alyverkko_cli.model.Model;
+
+/**
+ * Represents the data needed to perform a single mail-based AI query,
+ * containing prompts and the specific AI model to use.
+ */
+public class MailQuery {
+
+ /**
+ * The system prompt text that sets the context or role for the AI.
+ * This is often used to establish rules or background instructions
+ * for how the assistant should behave.
+ */
+ public String systemPrompt;
+
+ /**
+ * The user's prompt text (the main request or query).
+ */
+ public String userPrompt;
+
+ /**
+ * The AI model to be used for processing this query.
+ */
+ public Model model;
+
+ /**
+ * Returns a string containing a summary of the {@link MailQuery} object.
+ *
+ * @return a string with the system prompt, user prompt, and model info.
+ */
+ @Override
+ public String toString() {
+ return "MailQuery{" +
+ "systemPrompt='" + systemPrompt + '\'' +
+ ", userPrompt='" + userPrompt + '\'' +
+ ", model=" + model +
+ '}';
+ }
+}