1 package eu.svjatoslav.alyverkko_cli.commands;
3 import eu.svjatoslav.alyverkko_cli.*;
4 import eu.svjatoslav.alyverkko_cli.model.Model;
5 import eu.svjatoslav.alyverkko_cli.model.ModelLibrary;
6 import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser;
7 import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.DirectoryOption;
8 import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption;
10 import java.io.BufferedReader;
12 import java.io.FileReader;
13 import java.io.IOException;
14 import java.nio.file.*;
16 import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration;
17 import static eu.svjatoslav.alyverkko_cli.Main.configuration;
18 import static eu.svjatoslav.commons.file.IOHelper.getFileContentsAsString;
19 import static eu.svjatoslav.commons.file.IOHelper.saveToFile;
20 import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
21 import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
23 public class MailCorrespondentCommand implements Command {
25 final Parser parser = new Parser();
27 ModelLibrary modelLibrary;
29 private WatchService watcher;
32 * The directory containing mail files.
34 DirectoryOption mailDirectoryOption = parser.add(new DirectoryOption("Directory containing mail files"))
35 .addAliases("--mail", "-m").mustExist();
40 * Configuration file location.
42 public FileOption configFileOption = parser.add(new FileOption("Configuration file path"))
43 .addAliases("--config", "-c").mustExist();
45 private void initialMailScanAndReply() throws IOException, InterruptedException {
46 File[] files = mailDirectory.listFiles();
47 if (files == null) return;
49 for (File file : files)
50 processMailIfNeeded(file);
53 private boolean isMailProcessingNeeded(File file) throws IOException {
54 // ignore hidden files
55 if (file.getName().startsWith("."))
58 // Check if the file is a mail file (not a directory
59 if (!file.isFile()) return false;
61 return fileHasToComputeMarker(file);
64 private static boolean fileHasToComputeMarker(File file) throws IOException {
65 try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
66 String firstLine = reader.readLine();
67 return firstLine != null && firstLine.startsWith("TOCOMPUTE:");
71 private void processMailIfNeeded(File file) throws IOException, InterruptedException {
72 if (!isMailProcessingNeeded(file)) {
73 System.out.println("Ignoring file: " + file.getName() + " (does not need processing for now)");
77 System.out.println("\nReplying to mail: " + file.getName());
79 // Read the mail contents, and remove the TOCOMPUTE: prefix from the first line
80 String mailContents = getFileContentsAsString(file);
81 mailContents = removeToComputePrefixFile(mailContents);
83 // faster model for testing for development time testing
84 // String modelAlias = "maid";
85 // TODO: make model CLI argument
86 String modelAlias = "wizard";
88 Model model = modelLibrary.findModelByAlias(modelAlias).get();
89 String aiGeneratedResponse = AiTask.runAiQuery(mailContents, model, null);
91 // Append the AI response to the mail contents
92 if (!mailContents.startsWith("* USER:\n")) {
93 mailContents = "* USER:\n" + mailContents;
96 String newMailContents = mailContents + "\n* ASSISTANT:\n" + aiGeneratedResponse;
98 // Write the result to the file
99 saveToFile(file, newMailContents);
102 private String removeToComputePrefixFile(String mailContents) {
103 // Remove the first line from the mail contents
104 int firstNewLineIndex = mailContents.indexOf('\n');
105 if (firstNewLineIndex != -1) {
106 mailContents = mailContents.substring(firstNewLineIndex + 1);
113 public String getName() {
118 public void execute(String[] cliArguments) throws IOException, InterruptedException {
119 if (!parser.parse(cliArguments)) {
120 System.out.println("Failed to parse commandline arguments");
125 configuration = loadConfiguration(configFileOption.isPresent() ? configFileOption.getValue() : null);
126 if (configuration == null){
127 System.out.println("Failed to load configuration file");
131 modelLibrary = new ModelLibrary(configuration.getModelsDirectory(), configuration.getModels());
132 mailDirectory = mailDirectoryOption.isPresent() ? mailDirectoryOption.getValue() : configuration.getMailDirectory();
134 initializeFileWatcher();
136 // before we start processing incremental changes in directory, we need to process all the existing files
137 initialMailScanAndReply();
139 System.out.println("Mail correspondent running. Press CTRL+c to terminate.");
144 key = watcher.take();
145 } catch (InterruptedException e) {
146 System.out.println("Interrupted while waiting for file system events. Exiting.");
150 System.out.println("Detected filesystem event.");
152 // sleep for a while to allow the file to be fully written
155 processDetectedFilesystemEvents(key);
157 if (!key.reset()) break;
163 private void processDetectedFilesystemEvents(WatchKey key) throws IOException, InterruptedException {
164 for (WatchEvent<?> event : key.pollEvents()) {
165 WatchEvent.Kind<?> kind = event.kind();
167 // Skip OVERFLOW event
168 if (kind == StandardWatchEventKinds.OVERFLOW) continue;
170 // Retrieve the file name associated with the event
171 Path filename = ((WatchEvent<Path>) event).context();
172 System.out.println("Event: " + kind + " for file: " + filename);
175 processFileSystemEvent(kind, filename);
179 private void initializeFileWatcher() throws IOException {
180 this.watcher = FileSystems.getDefault().newWatchService();
181 Paths.get(mailDirectory.getAbsolutePath()).register(watcher, ENTRY_CREATE, ENTRY_MODIFY);
184 private void processFileSystemEvent(WatchEvent.Kind<?> kind, Path filename) throws IOException, InterruptedException {
185 if (kind != ENTRY_CREATE && kind != ENTRY_MODIFY) return;
187 File file = mailDirectory.toPath().resolve(filename).toFile();
188 processMailIfNeeded(file);