From ca019669e01f5d270c4b51eb35f0a5b3b3e9e213 Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Wed, 26 Mar 2025 03:20:43 +0200 Subject: [PATCH] Trying to unite wizard and selfcheck commands. --- .../eu/svjatoslav/alyverkko_cli/Main.java | 1 - .../commands/JoinFilesCommand.java | 6 +- .../commands/ListModelsCommand.java | 5 +- .../commands/SelftestCommand.java | 140 ----- .../alyverkko_cli/commands/WizardCommand.java | 567 ++++++++++++------ .../MailCorrespondentCommand.java | 8 +- .../configuration/Configuration.java | 38 -- .../configuration/ConfigurationHelper.java | 48 ++ 8 files changed, 446 insertions(+), 367 deletions(-) delete mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/commands/SelftestCommand.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationHelper.java diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java b/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java index 25fa1fd..ed81cfb 100644 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java @@ -23,7 +23,6 @@ public class Main { new ListModelsCommand(), new MailCorrespondentCommand(), new JoinFilesCommand(), - new SelftestCommand(), new WizardCommand() ); diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java index b72b440..1e644ed 100644 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java @@ -1,6 +1,7 @@ package eu.svjatoslav.alyverkko_cli.commands; import eu.svjatoslav.alyverkko_cli.Command; +import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper; import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser; import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.DirectoryOption; import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.NullOption; @@ -15,7 +16,8 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import static eu.svjatoslav.alyverkko_cli.Main.configuration; -import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration; +import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.getConfigurationFile; +import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.loadConfiguration; /** * The JoinFilesCommand aggregates multiple files (optionally matching @@ -93,7 +95,7 @@ public class JoinFilesCommand implements Command { */ @Override public void execute(String[] cliArguments) throws IOException { - configuration = loadConfiguration(); + configuration = loadConfiguration(getConfigurationFile(null)); if (configuration == null){ System.out.println("Failed to load configuration file"); return; diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java index 2b42712..56128a6 100644 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java @@ -6,7 +6,8 @@ import eu.svjatoslav.alyverkko_cli.model.ModelLibrary; import java.io.IOException; import static eu.svjatoslav.alyverkko_cli.Main.configuration; -import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration; +import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.getConfigurationFile; +import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.loadConfiguration; /** * Lists all configured models in the system, loading them from the @@ -31,7 +32,7 @@ public class ListModelsCommand implements Command { */ @Override public void execute(String[] cliArguments) throws IOException { - configuration = loadConfiguration(); + configuration = loadConfiguration(getConfigurationFile(null)); if (configuration == null){ System.out.println("Failed to load configuration file"); return; diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/SelftestCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/SelftestCommand.java deleted file mode 100644 index cfe7a2e..0000000 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/SelftestCommand.java +++ /dev/null @@ -1,140 +0,0 @@ -package eu.svjatoslav.alyverkko_cli.commands; - -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; - -import static eu.svjatoslav.alyverkko_cli.Main.configuration; -import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration; - -/** - * Performs a series of checks to ensure that the Älyverkko CLI is - * configured correctly, including verifying directories, executables, - * and model definitions. - */ -public class SelftestCommand implements Command { - - /** - * @return the name of this command, i.e., "selftest". - */ - @Override - public String getName() { - 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. - * - * @param cliArguments the command-line arguments following the "selftest" subcommand. - * @throws IOException if there are issues loading the configuration file. - */ - @Override - public void execute(String[] cliArguments) throws IOException { - // Perform selftest checks here - System.out.println("Starting selftest..."); - - 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 - if (Main.configuration == null) { - System.err.println("Configuration not found or invalid."); - didAllChecksPass = false; - } else { - // Validate models directory - if (!Main.configuration.getModelsDirectory().exists() || !Main.configuration.getModelsDirectory().isDirectory()) { - System.err.println("Models directory does not exist or is not a directory: " + Main.configuration.getModelsDirectory()); - didAllChecksPass = false; - } - - // Validate llama-cli executable path - if (!Main.configuration.getLlamaCliPath().exists() || !Main.configuration.getLlamaCliPath().isFile()) { - System.err.println("llama-cli executable path does not point to existing file: " + Main.configuration.getLlamaCliPath()); - didAllChecksPass = false; - } - - // Validate mail directory - if (!Main.configuration.getMailDirectory().exists() || !Main.configuration.getMailDirectory().isDirectory()) { - System.err.println("Mail directory does not exist or is not a directory: " + Main.configuration.getMailDirectory()); - didAllChecksPass = false; - } - - // Ensure that there is at least one prompt file - File promptsDirectory = Main.configuration.getPromptsDirectory(); - if (promptsDirectory == null) { - System.err.println("Prompts directory is not defined in the configuration."); - didAllChecksPass = false; - } else { - if (!promptsDirectory.exists() || !promptsDirectory.isDirectory()) { - System.err.println("Prompts directory does not exist or is not a directory: " + promptsDirectory); - didAllChecksPass = false; - } else { - if (promptsDirectory.listFiles() == null || promptsDirectory.listFiles().length == 0) { - System.err.println("No prompt files found in the prompts directory: " + promptsDirectory); - didAllChecksPass = false; - } - } - } - - // Validate models - if (Main.configuration.getModels().isEmpty()) { - System.err.println("No models are defined in the configuration."); - didAllChecksPass = false; - } - - // Validate default temperature - if (Main.configuration.getDefaultTemperature() < 0 || Main.configuration.getDefaultTemperature() > 3) { - System.err.println("Default temperature must be between 0 and 3."); - didAllChecksPass = false; - } - - // Validate thread count - if (Main.configuration.getThreadCount() < 1) { - System.err.println("Thread count must be at least 1."); - didAllChecksPass = false; - } - - // Validate batch thread count - if (Main.configuration.getBatchThreadCount() < 1) { - System.err.println("Batch thread count must be at least 1."); - didAllChecksPass = false; - } - - // Validate models again (redundant check but ensures clarity) - if (Main.configuration.getModels().isEmpty()) { - System.err.println("No models are defined in the configuration."); - didAllChecksPass = false; - } - } - - if (didAllChecksPass) { - System.out.println("Selftest completed successfully."); - } - } -} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java index 00c3c74..3994ef7 100644 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java @@ -3,59 +3,62 @@ package eu.svjatoslav.alyverkko_cli.commands; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import eu.svjatoslav.alyverkko_cli.Command; +import eu.svjatoslav.alyverkko_cli.Utils; import eu.svjatoslav.alyverkko_cli.configuration.Configuration; import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationModel; +import eu.svjatoslav.commons.cli_helper.CLIHelper; import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser; +import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption; import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.NullOption; import org.apache.commons.lang3.StringUtils; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.List; -import java.util.Scanner; +import java.io.*; +import java.nio.file.*; +import java.util.*; import java.util.stream.Collectors; -import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.DEFAULT_CONFIG_FILE_PATH; -import static eu.svjatoslav.commons.cli_helper.CLIHelper.*; +import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.getConfigurationFile; +import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.loadConfiguration; +import static eu.svjatoslav.commons.cli_helper.CLIHelper.askBoolean; +import static eu.svjatoslav.commons.cli_helper.CLIHelper.askFloat; +import static eu.svjatoslav.commons.cli_helper.CLIHelper.askInteger; +import static java.lang.Boolean.TRUE; /** - * The WizardCommand provides an interactive, console-based - * configuration wizard for setting up the Älyverkko CLI application. + * A single WizardCommand that: + * 1. Loads existing configuration (if any). + * 2. Performs "selftest" style validation checks interactively, + * prompting the user to fix invalid or missing items. + * 3. If no config file exists, it goes through all config parameters + * from scratch. + * 4. Offers to remove model entries that reference missing files. + * 5. Autodiscovers new .gguf files and lets the user add them to the config. + * 6. Saves the resulting (fixed) config file. + * 7. Prints a final pass/fail summary for the user. */ public class WizardCommand implements Command { + // Command-line parser to handle wizard arguments private final Parser parser = new Parser(); - /** - * Command-line option to force overwriting an existing configuration file. - */ + // If present, force overwriting existing config private final NullOption forceOverwriteOption = parser.add(new NullOption("Force overwrite existing configuration")) .addAliases("--force", "-f"); /** - * An existing configuration, if one is found on disk. + * Optional CLI argument for specifying a configuration file path. */ - private Configuration existingConfig; + public FileOption configFileOption = parser.add(new FileOption("Configuration file path")) + .addAliases("--config", "-c"); + + // The config object (loaded or newly created) + private Configuration config; - /** - * @return the name of this command, i.e., "wizard". - */ @Override public String getName() { return "wizard"; } - /** - * Executes this command, launching an interactive configuration - * wizard in the console. - * - * @param cliArguments the command-line arguments following the "wizard" subcommand. - * @throws IOException in case of IO failures while reading/writing config files. - */ @Override public void execute(String[] cliArguments) throws IOException { if (!parser.parse(cliArguments)) { @@ -64,163 +67,242 @@ public class WizardCommand implements Command { return; } - // Load existing configuration if it exists - existingConfig = Configuration.loadConfiguration(new File(DEFAULT_CONFIG_FILE_PATH)); - if (existingConfig != null) { - System.out.println("Found existing configuration. Current values will be used as defaults."); - } - Configuration newConfig = new Configuration(); + // 1. Load existing config if possible + File configFile = getConfigurationFile(configFileOption); - System.out.println("\nStarting Älyverkko CLI configuration wizard.\n"); - - // Mail Directory - String mailDirectory = getInputWithDefault( - "\nEnter the mail directory for AI tasks. This is where your task files will be stored.", - existingConfig != null ? existingConfig.getMailDirectory().getPath() : "~/.config/alyverkko-cli/mail" - ); - newConfig.setMailDirectory(new File(mailDirectory)); - - // Models Directory - String modelsDirectory = getInputWithDefault( - "\nEnter the directory for AI models. This should contain your GGUF model files.", - existingConfig != null ? existingConfig.getModelsDirectory().getPath() : "~/.config/alyverkko-cli/models" - ); - newConfig.setModelsDirectory(new File(modelsDirectory)); + if (configFile.exists() && configFile.isFile()) { + System.out.println("Found existing configuration at " + configFile.getAbsolutePath()); + System.out.println("I will check whether everything is valid, and if not, allow you to fix it.\n"); + config = loadConfiguration(configFile); + } else { + // If no config found, create a fresh one + System.out.println("No existing configuration found. Let's create one!\n"); + config = new Configuration(); + } - // Prompts Directory - String promptsDirectory = getInputWithDefault( - "\nEnter the directory for prompts. This should contain text files with your AI prompts.", - existingConfig != null ? existingConfig.getPromptsDirectory().getPath() : "~/.config/alyverkko-cli/prompts" - ); - newConfig.setPromptsDirectory(new File(promptsDirectory)); + // 2. Validate and fix each parameter + checkAndFixAllParameters(); - // Llama CLI Path - String llamaCliExecutablePath = getInputWithDefault( - "\nEnter the path to the llama-cli executable. This is the command-line interface for llama.cpp.", - existingConfig != null ? existingConfig.getLlamaCliPath().getPath() : "/usr/local/bin/llama-cli" - ); - newConfig.setLlamaCliPath(new File(llamaCliExecutablePath)); + // 3. Validate the models: remove or fix broken references, autodiscover new models, etc. + fixModelEntries(); - // Default Temperature - float defaultTemperature = askFloat( - "\nEnter default temperature (0-2). A lower value makes the AI more deterministic, while a higher value makes it more creative.", - existingConfig != null ? existingConfig.getDefaultTemperature() : 0.7f, - 0f, 2f - ); - newConfig.setDefaultTemperature(defaultTemperature); - - // Thread Counts - int threadCount = askInteger( - "\nEnter number of CPU threads for AI response generation. " + - "RAM data transfer speed is usually the bottleneck here. " + - "On a typical PC, while you might have 12 CPU cores, " + - "RAM bandwidth can get already saturated with only 6 CPU threads. " + - "Once RAM bandwidth is saturated, using more CPU threads " + - "will not improve performance while it will keep CPU cores needlessly busy.", - existingConfig != null ? existingConfig.getThreadCount() : 6, - 1, null - ); - newConfig.setThreadCount(threadCount); + // 4. If the user is satisfied, attempt to save + trySaveConfiguration(configFile); - int batchThreadCount = askInteger( - "\nEnter number of CPU threads for input prompt processing. " + - "CPU computing power is usually the bottleneck here (not the RAM bandwidth). " + - "So you can utilize all CPU cores that you have here.", - existingConfig != null ? existingConfig.getBatchThreadCount() : 10, - 1, null - ); - newConfig.setBatchThreadCount(batchThreadCount); + // 5. Final selftest pass + boolean finalOk = doFinalCheck(); + if (finalOk) { + System.out.println("\nConfiguration looks good. You're all set! You can now run:"); + System.out.println(" alyverkko-cli mail (to start the mail-based AI service)"); + System.out.println(" alyverkko-cli joinfiles ... (to prepare tasks for AI)\n"); + } else { + System.out.println("\nSome checks did not pass, but we've saved the best config we could."); + System.out.println("You might want to run 'wizard' again or manually edit the YAML file:\n " + + configFile.getAbsolutePath() + "\n"); + } + } - // Models Setup - System.out.println("\nModel configuration."); - List models = new ArrayList<>(); - if (existingConfig != null) { - models.addAll(existingConfig.getModels()); + /** + * Step-by-step checking (and possibly fixing) of each main config parameter. + */ + private void checkAndFixAllParameters() { + // 2.1 mail_directory + while (true) { + File mailDir = config.getMailDirectory(); + if (mailDir == null || !mailDir.isDirectory() || !mailDir.exists()) { + System.out.println("Mail directory is missing or invalid."); + String defaultVal = (mailDir == null) ? "~/.config/alyverkko-cli/mail" : mailDir.getPath(); + String userInput = CLIHelper.askString( + "Enter the mail directory path (will create if it doesn't exist)", + defaultVal + ); + mailDir = new File(userInput); + config.setMailDirectory(mailDir); + + // Attempt to create if not exist + if (!mailDir.exists()) { + boolean created = mailDir.mkdirs(); + if (!created) { + Utils.printRedMessageToConsole("Failed to create mail directory. Check permissions?"); + } + } + } else { + // If valid, confirm or let user fix + if (askBoolean("Mail directory is: " + mailDir.getAbsolutePath() + " . Is this OK?", TRUE)) { + break; + } + // Otherwise loop around to let them reenter + config.setMailDirectory(null); // Force re-check + } } - if (askBoolean("Would you like to try to autodiscover available undefined models ?", true)) { - discoverAndSuggestNewModels(newConfig.getModelsDirectory(), models); + // 2.2 models_directory + while (true) { + File modelsDir = config.getModelsDirectory(); + if (modelsDir == null || !modelsDir.isDirectory() || !modelsDir.exists()) { + System.out.println("Models directory is missing or invalid."); + String defaultVal = (modelsDir == null) ? "~/.config/alyverkko-cli/models" : modelsDir.getPath(); + String userInput = CLIHelper.askString( + "Enter the models directory path (where your .gguf files are located)", + defaultVal + ); + modelsDir = new File(userInput); + config.setModelsDirectory(modelsDir); + + // Attempt to create if not exist + if (!modelsDir.exists()) { + boolean created = modelsDir.mkdirs(); + if (!created) { + Utils.printRedMessageToConsole("Failed to create models directory. Check permissions?"); + } + } + } else { + if (askBoolean("Models directory is: " + modelsDir.getAbsolutePath() + " . Is this OK?", true)) { + break; + } + config.setModelsDirectory(null); + } } - System.out.println("\nYou can now add models manually."); + // 2.3 prompts_directory while (true) { - String alias = askString("Enter desired model alias (leave empty to finish adding models): "); - if (StringUtils.isBlank(alias)) break; + File promptsDir = config.getPromptsDirectory(); + if (promptsDir == null || !promptsDir.isDirectory() || !promptsDir.exists()) { + System.out.println("Prompts directory is missing or invalid."); + String defaultVal = (promptsDir == null) ? "~/.config/alyverkko-cli/prompts" : promptsDir.getPath(); + String userInput = CLIHelper.askString( + "Enter the prompts directory path", + defaultVal + ); + promptsDir = new File(userInput); + config.setPromptsDirectory(promptsDir); + + // Attempt to create if not exist + if (!promptsDir.exists()) { + boolean created = promptsDir.mkdirs(); + if (!created) { + Utils.printRedMessageToConsole("Failed to create prompts directory. Check permissions?"); + } + } + } else { + if (askBoolean("Prompts directory is: " + promptsDir.getAbsolutePath() + " . Is this OK?", true)) { + break; + } + config.setPromptsDirectory(null); + } + } - String filePath = getInputWithDefault( - "\nEnter filesystem path relative to models directory. " + - "Press Tab for suggestions or Enter to accept default.", - null - ); + // 2.4 llama_cli_path + while (true) { + File llamaCli = config.getLlamaCliPath(); + boolean valid = (llamaCli != null && llamaCli.isFile() && llamaCli.exists()); + if (!valid) { + System.out.println("The 'llama-cli' executable path is missing or invalid."); + String defaultVal = (llamaCli == null) ? "/usr/local/bin/llama-cli" : llamaCli.getPath(); + String userInput = CLIHelper.askString( + "Enter the path to the llama-cli executable", + defaultVal + ); + llamaCli = new File(userInput); + config.setLlamaCliPath(llamaCli); + } else { + if (askBoolean("Llama-cli path is: " + llamaCli.getAbsolutePath() + " . Is this OK?", true)) { + break; + } + config.setLlamaCliPath(null); + } + } - int contextSize = askInteger( - "\nEnter context size in tokens. A higher value allows the model to remember more context.", - 64000, - 1024, - 128000 - ); + // 2.5 default_temperature + float oldTemp = config.getDefaultTemperature(); + if (oldTemp < 0.0f || oldTemp > 3.0f) { + oldTemp = 0.7f; + } + float newTemp = askFloat( + "\nEnter default temperature (0-3). Lower => more deterministic, higher => more creative.", + oldTemp, + 0f, 3f + ); + config.setDefaultTemperature(newTemp); - ConfigurationModel model = new ConfigurationModel(); - model.setAlias(alias); - model.setFilesystemPath(filePath); - model.setContextSizeTokens(contextSize); - models.add(model); + // 2.6 thread_count + int oldThreadCount = config.getThreadCount(); + if (oldThreadCount < 1) { + oldThreadCount = 6; } + int newThreadCount = askInteger( + "\nEnter number of CPU threads for AI generation. Typically 6 for a 12-core CPU, for example.", + oldThreadCount, + 1, null + ); + config.setThreadCount(newThreadCount); - newConfig.setModels(models); + // 2.7 batch_thread_count + int oldBatchThreadCount = config.getBatchThreadCount(); + if (oldBatchThreadCount < 1) { + oldBatchThreadCount = 10; + } + int newBatchCount = askInteger( + "\nEnter number of CPU threads for input prompt processing (all cores is often fine).", + oldBatchThreadCount, + 1, null + ); + config.setBatchThreadCount(newBatchCount); + } - // Write configuration - Path configPath = Path.of(DEFAULT_CONFIG_FILE_PATH); - if (Files.exists(configPath) && !forceOverwriteOption.isPresent()) { - if (!askBoolean("Configuration file already exists. Overwrite?", false)) { - System.out.println("Configuration not saved. Run with --force to overwrite."); - return; - } + /** + * Offer to remove model references in config if the file does not exist. + * Then autodiscover new .gguf files and offer to add them. + * Then let user manually add more if desired. + */ + private void fixModelEntries() { + if (config.getModels() == null) { + config.setModels(new ArrayList<>()); } - // Create parent directories - Files.createDirectories(configPath.getParent()); + // Remove references to nonexistent model files + removeDeadModelReferences(); - // Save configuration - try (var writer = Files.newBufferedWriter(configPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { - new ObjectMapper(new YAMLFactory()).writeValue(writer, newConfig); + // Offer to autodiscover new model files + if (askBoolean("\nAttempt to autodiscover new .gguf model files in the models directory?", true)) { + discoverAndSuggestNewModels(config.getModelsDirectory(), config.getModels()); } - System.out.println("\nConfiguration saved to: " + configPath); - System.out.println("You can now run 'alyverkko-cli selftest' to verify everything is set up correctly."); + // Let user manually add new models + addModelsManually(); } /** - * Prompts the user for an input value, showing a default value if provided. - * - * @param prompt the prompt to display to the user. - * @param defaultValue the default value to use if the user simply presses Enter. - * @return the user's input, or the default if no input is provided. + * Loops through the config's list of models. If the file doesn't exist, + * let user decide whether to remove that model or keep it (perhaps to fix the path). */ - private String getInputWithDefault(String prompt, String defaultValue) { - Scanner scanner = new Scanner(System.in); - - if (defaultValue == null) { - System.out.print(prompt + ": "); - } else { - System.out.print(prompt + " [" + defaultValue + "]: "); + private void removeDeadModelReferences() { + Iterator it = config.getModels().iterator(); + while (it.hasNext()) { + ConfigurationModel cm = it.next(); + File f = new File(config.getModelsDirectory(), cm.getFilesystemPath()); + if (!f.exists()) { + System.out.println("\nModel with alias '" + cm.getAlias() + + "' references missing file: " + f.getAbsolutePath()); + boolean remove = askBoolean("Remove this from config?", true); + if (remove) { + it.remove(); + } else { + // Let them fix the filesystem_path or do so in next iteration + System.out.println("Leaving it as-is for now. You can fix the path manually if needed."); + } + } } - String input = scanner.nextLine().trim(); - - return input.isEmpty() ? defaultValue : input; } /** - * Attempts to discover new .gguf model files in the given models directory, and - * suggests adding them to the configuration if they aren't already defined. - * - * @param modelsDir the directory containing model files. - * @param existingModels existing model definitions to compare against. + * Scans the models directory for .gguf files not yet in the config, + * and offers to add them with user-chosen alias, etc. */ private void discoverAndSuggestNewModels(File modelsDir, List existingModels) { if (!modelsDir.exists()) { - System.out.println("Models directory does not exist. Please create it first."); + System.out.println("Models directory does not exist. Please create it first or fix path above."); return; } @@ -231,18 +313,30 @@ public class WizardCommand implements Command { .filter(file -> file.getName().endsWith(".gguf") && !file.getName().contains("-of-")) .collect(Collectors.toList()); - List existingAliases = existingModels.stream() - .map(ConfigurationModel::getAlias) + List existingPaths = existingModels.stream() + .map(ConfigurationModel::getFilesystemPath) .collect(Collectors.toList()); for (File modelFile : modelFiles) { String relativePath = modelsDir.toPath().relativize(modelFile.toPath()).toString(); - - boolean alreadyExists = existingModels.stream() - .anyMatch(m -> m.getFilesystemPath().equals(relativePath)); - - if (!alreadyExists) { - suggestAddingModel(existingModels, relativePath, existingAliases); + if (!existingPaths.contains(relativePath)) { + System.out.println("\nDiscovered new model file: " + relativePath); + String guessAlias = suggestAlias(relativePath); + System.out.println(" Suggest alias: " + guessAlias); + + if (askBoolean("Would you like to add this model?", false)) { + ConfigurationModel cm = new ConfigurationModel(); + cm.setAlias(CLIHelper.askString("Enter alias for this model", guessAlias)); + cm.setFilesystemPath(relativePath); + int ctxSize = askInteger( + "Enter context size in tokens (e.g. 64000).", + 64000, 1024, 128000 + ); + cm.setContextSizeTokens(ctxSize); + cm.setEndOfTextMarker(null); + existingModels.add(cm); + existingPaths.add(relativePath); + } } } } catch (IOException e) { @@ -251,41 +345,152 @@ public class WizardCommand implements Command { } /** - * Suggests to the user adding a newly-discovered model to the configuration. - * - * @param existingModels the list of existing models to which we might add a new one. - * @param relativePath the model file's path relative to the models directory. - * @param existingAliases the aliases already defined for known models. + * Lets the user add one or more new models by specifying alias, path, context size, etc. */ - private void suggestAddingModel(List existingModels, String relativePath, List existingAliases) { - String alias = suggestAlias(relativePath); - System.out.println("\nFound new model: " + relativePath); - System.out.println("Suggested alias: " + alias); - if (askBoolean("Would you like to add this model? (y/N) ", false)) { + private void addModelsManually() { + System.out.println("\nNow you can add models manually, if you wish."); + while (true) { + String alias = CLIHelper.askString("Enter model alias (leave empty to finish adding models): "); + if (StringUtils.isBlank(alias)) { + break; + } + String filePath = CLIHelper.askString("Enter filesystem path relative to models directory: "); + if (StringUtils.isBlank(filePath)) { + System.out.println("Skipping because no path given."); + continue; + } + int contextSize = askInteger( + "Enter context size in tokens (e.g. 64000).", + 64000, 1024, 128000 + ); + + // Check for duplicates + boolean alreadyExists = config.getModels().stream() + .anyMatch(m -> m.getAlias().equals(alias)); + if (alreadyExists) { + Utils.printRedMessageToConsole("Model with alias '" + alias + "' already exists! Skipping."); + continue; + } + ConfigurationModel model = new ConfigurationModel(); model.setAlias(alias); - model.setFilesystemPath(relativePath); - int contextSize = askInteger("\nEnter context size in tokens. A higher value allows the model to remember more context.", - 64000, - 1024, - null - ); + model.setFilesystemPath(filePath); model.setContextSizeTokens(contextSize); - existingModels.add(model); - existingAliases.add(alias); + model.setEndOfTextMarker(null); + + config.getModels().add(model); } } /** - * Generates a suggested model alias by removing non-alphanumeric characters, - * converting to lower case, and normalizing multiple dashes. - * - * @param filePath path string from which to suggest an alias. - * @return a suggested alias, derived from the model file name. + * Saves the config to the default path, verifying if user wants to + * overwrite if it already exists, etc. + */ + private void trySaveConfiguration(File configFile) { + boolean alreadyExists = Files.exists(configFile.toPath()); + + if (alreadyExists && !forceOverwriteOption.isPresent()) { + boolean userOk = askBoolean("\nConfiguration file already exists. Overwrite it?", true); + if (!userOk) { + System.out.println("Not overwriting. If you want to do so, run again with --force."); + return; + } + } + + try { + Files.createDirectories(configFile.toPath().getParent()); + try (BufferedWriter writer = Files.newBufferedWriter( + configFile.toPath(), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + )) { + new ObjectMapper(new YAMLFactory()).writeValue(writer, config); + } + System.out.println("\nConfiguration saved to: " + configFile.toPath()); + } catch (IOException e) { + Utils.printRedMessageToConsole("Error saving configuration: " + e.getMessage()); + } + } + + /** + * Runs a final check across the config to confirm everything is + * valid. Returns true if all checks pass, false otherwise. + */ + private boolean doFinalCheck() { + System.out.println("\nPerforming final checks...\n"); + boolean allOk = true; + + // Check mail dir + if (config.getMailDirectory() == null || !config.getMailDirectory().isDirectory()) { + System.err.println("Mail directory is invalid: " + config.getMailDirectory()); + allOk = false; + } + + // Check llama-cli + if (config.getLlamaCliPath() == null || !config.getLlamaCliPath().isFile()) { + System.err.println("llama-cli path is invalid: " + config.getLlamaCliPath()); + allOk = false; + } + + // Check prompts dir + if (config.getPromptsDirectory() == null || !config.getPromptsDirectory().isDirectory()) { + System.err.println("Prompts directory is invalid: " + config.getPromptsDirectory()); + allOk = false; + } else { + File[] pFiles = config.getPromptsDirectory().listFiles(); + if (pFiles == null || pFiles.length == 0) { + System.out.println("Warning: No prompt files found in " + config.getPromptsDirectory()); + // not necessarily fatal, so we won't mark allOk = false + } + } + + // Check models + if (config.getModels() == null || config.getModels().isEmpty()) { + System.err.println("No models are defined in the configuration."); + allOk = false; + } else { + // Check each model file + for (ConfigurationModel m : config.getModels()) { + File f = new File(config.getModelsDirectory(), m.getFilesystemPath()); + if (!f.exists()) { + System.err.println("Model alias '" + m.getAlias() + + "' references missing file: " + f.getAbsolutePath()); + allOk = false; + } + } + } + + // Check temperature + if (config.getDefaultTemperature() < 0 || config.getDefaultTemperature() > 3) { + System.err.println("Default temperature must be between 0 and 3. Found: " + config.getDefaultTemperature()); + allOk = false; + } + + if (config.getThreadCount() < 1) { + System.err.println("thread_count must be >= 1. Found: " + config.getThreadCount()); + allOk = false; + } + + if (config.getBatchThreadCount() < 1) { + System.err.println("batch_thread_count must be >= 1. Found: " + config.getBatchThreadCount()); + allOk = false; + } + + if (allOk) { + System.out.println("All final checks passed!\n"); + } else { + System.out.println("\nSome checks failed (see messages above)."); + } + return allOk; + } + + /** + * Generates an alias from a .gguf filename by removing non-alphanumeric chars. */ private String suggestAlias(String filePath) { String fileName = new File(filePath).getName(); String alias = fileName.replaceAll("[^a-zA-Z0-9]", "-").toLowerCase(); return alias.replaceAll("-+", "-").replaceAll("^-|-$", ""); } + } diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java index 7bb24ee..c83f2d4 100644 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java @@ -1,6 +1,7 @@ package eu.svjatoslav.alyverkko_cli.commands.mail_correspondant; import eu.svjatoslav.alyverkko_cli.*; +import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper; import eu.svjatoslav.alyverkko_cli.model.Model; import eu.svjatoslav.alyverkko_cli.model.ModelLibrary; import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser; @@ -16,7 +17,8 @@ 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.alyverkko_cli.configuration.ConfigurationHelper.getConfigurationFile; +import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.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; @@ -27,7 +29,7 @@ import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; * 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: *

  *   alyverkko-cli mail
@@ -88,7 +90,7 @@ public class MailCorrespondentCommand implements Command {
             return;
         }
 
-        configuration = loadConfiguration(configFileOption.isPresent() ? configFileOption.getValue() : null);
+        configuration = loadConfiguration(getConfigurationFile(configFileOption));
         if (configuration == null) {
             System.out.println("Failed to load configuration file");
             return;
diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java
index 3970e2f..dfa178b 100644
--- a/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java
+++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java
@@ -18,12 +18,6 @@ import static eu.svjatoslav.commons.file.IOHelper.getFileContentsAsString;
 @Data
 public class Configuration {
 
-    /**
-     * The default path for the YAML config file, typically under the user's home directory.
-     */
-    public static final String DEFAULT_CONFIG_FILE_PATH = "~/.config/alyverkko-cli/alyverkko-cli.yaml"
-            .replaceFirst("^~", System.getProperty("user.home"));
-
     /**
      * Directory where AI tasks (mail) are placed and discovered.
      */
@@ -74,38 +68,6 @@ public class Configuration {
      */
     private List models;
 
-    /**
-     * Loads the configuration from the default file path.
-     *
-     * @return the {@link Configuration} object, or null if the file doesn't exist or fails parsing.
-     * @throws IOException if file I/O fails during reading.
-     */
-    public static Configuration loadConfiguration() throws IOException {
-        return loadConfiguration(null);
-    }
-
-    /**
-     * Loads the configuration from a given file, or from the default
-     * path if {@code configFile} is null.
-     *
-     * @param configFile the file containing the YAML config; may be null.
-     * @return the {@link Configuration} object, or null if not found/invalid.
-     * @throws IOException if file I/O fails during reading.
-     */
-    public static Configuration loadConfiguration(File configFile) throws IOException {
-        if (configFile == null) {
-            // Load configuration from the default path
-            configFile = new File(DEFAULT_CONFIG_FILE_PATH);
-        }
-
-        if (!configFile.exists()) {
-            System.err.println("Configuration file not found: " + configFile);
-            return null;
-        }
-
-        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
-        return mapper.readValue(configFile, Configuration.class);
-    }
 
     /**
      * Retrieves the contents of a prompt file by alias, e.g. "writer"
diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationHelper.java b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationHelper.java
new file mode 100644
index 0000000..45891db
--- /dev/null
+++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationHelper.java
@@ -0,0 +1,48 @@
+package eu.svjatoslav.alyverkko_cli.configuration;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption;
+
+import java.io.File;
+import java.io.IOException;
+
+public class ConfigurationHelper {
+
+    /**
+     * The default path for the YAML config file, typically under the user's home directory.
+     */
+    public static final String DEFAULT_CONFIG_FILE_PATH = "~/.config/alyverkko-cli/alyverkko-cli.yaml".replaceFirst("^~", System.getProperty("user.home"));
+
+    /**
+     * Loads the configuration from a given file, or from the default
+     * path if {@code configFile} is null.
+     *
+     * @param configFile the file containing the YAML config; may be null.
+     * @return the {@link Configuration} object, or null if not found/invalid.
+     * @throws IOException if file I/O fails during reading.
+     */
+    public static Configuration loadConfiguration(File configFile) throws IOException {
+
+        if (!configFile.exists()) {
+            System.err.println("Configuration file not found: " + configFile);
+            return null;
+        }
+
+        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
+        return mapper.readValue(configFile, Configuration.class);
+    }
+
+    /**
+     * Returns the configuration file from the given option, or the default path if not present.
+     * @param configFileOption the CLI option for the config file.
+     * @return the configuration file to load.
+     */
+    public static File getConfigurationFile(FileOption configFileOption) {
+        if (configFileOption != null)
+            if (configFileOption.isPresent())
+                return configFileOption.getValue();
+
+        return new File(DEFAULT_CONFIG_FILE_PATH);
+    }
+}
-- 
2.20.1