Improve selftest
[alyverkko-cli.git] / src / main / java / eu / svjatoslav / alyverkko_cli / commands / MailCorrespondentCommand.java
1 package eu.svjatoslav.alyverkko_cli.commands;
2
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;
9
10 import java.io.BufferedReader;
11 import java.io.File;
12 import java.io.FileReader;
13 import java.io.IOException;
14 import java.nio.file.*;
15
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;
22
23 public class MailCorrespondentCommand implements Command {
24
25     final Parser parser = new Parser();
26
27     ModelLibrary modelLibrary;
28
29     private WatchService watcher;
30
31     /**
32      * The directory containing mail files.
33      */
34     DirectoryOption mailDirectoryOption = parser.add(new DirectoryOption("Directory containing mail files"))
35             .addAliases("--mail", "-m").mustExist();
36
37     File mailDirectory;
38
39     /**
40      * Configuration file location.
41      */
42     public FileOption configFileOption = parser.add(new FileOption("Configuration file path"))
43             .addAliases("--config", "-c").mustExist();
44
45     private void initialMailScanAndReply() throws IOException, InterruptedException {
46         File[] files = mailDirectory.listFiles();
47         if (files == null) return;
48
49         for (File file : files)
50             processMailIfNeeded(file);
51     }
52
53     private boolean isMailProcessingNeeded(File file) throws IOException {
54         // ignore hidden files
55         if (file.getName().startsWith("."))
56             return false;
57
58         // Check if the file is a mail file (not a directory
59         if (!file.isFile()) return false;
60
61         return fileHasToComputeMarker(file);
62     }
63
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:");
68         }
69     }
70
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)");
74             return;
75         }
76
77         System.out.println("\nReplying to mail: " + file.getName());
78
79         // Read the mail contents, and remove the TOCOMPUTE: prefix from the first line
80         String mailContents = getFileContentsAsString(file);
81         mailContents = removeToComputePrefixFile(mailContents);
82
83         // faster model for testing for development time testing
84         // String modelAlias = "maid";
85         // TODO: make model CLI argument
86         String modelAlias = "wizard";
87
88         Model model = modelLibrary.findModelByAlias(modelAlias).get();
89         String aiGeneratedResponse = AiTask.runAiQuery(mailContents, model, null);
90
91         // Append the AI response to the mail contents
92         if (!mailContents.startsWith("* USER:\n")) {
93             mailContents = "* USER:\n" + mailContents;
94         }
95
96         String newMailContents = mailContents + "\n* ASSISTANT:\n" + aiGeneratedResponse;
97
98         // Write the result to the file
99         saveToFile(file, newMailContents);
100     }
101
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);
107         }
108         return mailContents;
109     }
110
111
112     @Override
113     public String getName() {
114         return "mail";
115     }
116
117     @Override
118     public void execute(String[] cliArguments) throws IOException, InterruptedException {
119         if (!parser.parse(cliArguments)) {
120             System.out.println("Failed to parse commandline arguments");
121             parser.showHelp();
122             return;
123         }
124
125         configuration = loadConfiguration(configFileOption.isPresent() ? configFileOption.getValue() : null);
126         if (configuration == null){
127             System.out.println("Failed to load configuration file");
128             return;
129         }
130
131         modelLibrary = new ModelLibrary(configuration.getModelsDirectory(), configuration.getModels());
132         mailDirectory = mailDirectoryOption.isPresent() ? mailDirectoryOption.getValue() : configuration.getMailDirectory();
133
134         initializeFileWatcher();
135
136         // before we start processing incremental changes in directory, we need to process all the existing files
137         initialMailScanAndReply();
138
139         System.out.println("Mail correspondent running. Press CTRL+c to terminate.");
140
141         while (true) {
142             WatchKey key;
143             try {
144                 key = watcher.take();
145             } catch (InterruptedException e) {
146                 System.out.println("Interrupted while waiting for file system events. Exiting.");
147                 break;
148             }
149
150             System.out.println("Detected filesystem event.");
151
152             // sleep for a while to allow the file to be fully written
153             Thread.sleep(1000);
154
155             processDetectedFilesystemEvents(key);
156
157             if (!key.reset()) break;
158         }
159
160         watcher.close();
161     }
162
163     private void processDetectedFilesystemEvents(WatchKey key) throws IOException, InterruptedException {
164         for (WatchEvent<?> event : key.pollEvents()) {
165             WatchEvent.Kind<?> kind = event.kind();
166
167             // Skip OVERFLOW event
168             if (kind == StandardWatchEventKinds.OVERFLOW) continue;
169
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);
173
174             // Process the event
175             processFileSystemEvent(kind, filename);
176         }
177     }
178
179     private void initializeFileWatcher() throws IOException {
180         this.watcher = FileSystems.getDefault().newWatchService();
181         Paths.get(mailDirectory.getAbsolutePath()).register(watcher, ENTRY_CREATE, ENTRY_MODIFY);
182     }
183
184     private void processFileSystemEvent(WatchEvent.Kind<?> kind, Path filename) throws IOException, InterruptedException {
185         if (kind != ENTRY_CREATE && kind != ENTRY_MODIFY) return;
186
187         File file = mailDirectory.toPath().resolve(filename).toFile();
188         processMailIfNeeded(file);
189     }
190
191
192 }