import static java.util.Arrays.copyOfRange;
-/**
- * The main entry point for the Älyverkko CLI application.
- * It processes subcommands such as "wizard", "selftest", "joinfiles",
- * "process", etc...
- */
public class Main {
/**
* The list of all supported subcommands.
*/
private final java.util.List<Command> commands = java.util.Arrays.asList(
- new ListModelsCommand(),
new TaskProcessorCommand(),
- new JoinFilesCommand(),
new WizardCommand(),
new AddTaskHeaderCommand()
);
import eu.svjatoslav.alyverkko_cli.Command;
import eu.svjatoslav.alyverkko_cli.configuration.Configuration;
-import eu.svjatoslav.alyverkko_cli.configuration.Model;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* This command recursively adds a TOCOMPUTE header to all non-hidden files in the current directory
- * that do not already have one. It prompts the user for skill, model, priority values,
+ * that do not already have one. It prompts the user for skill and priority values,
* and optional custom processing instructions, validates their existence in the configuration,
* and then processes the files accordingly.
* <p>
* </pre>
* The command will interactively prompt for:
* - Skill name (must exist in skills directory)
- * - Model alias (must exist in configuration models)
* - Priority value (integer, defaults to 0)
* - Custom processing instructions (optional, multi-line input; press Enter twice to finish)
* <p>
}
/**
- * Executes the addheader command. Loads configuration, prompts user for skill, model, priority,
+ * Executes the addheader command. Loads configuration, prompts user for skill, priority,
* and optional custom instructions, validates them, and processes all files in the current directory recursively.
*
* @param cliArguments command-line arguments (unused in this command)
*/
@Override
public void executeCommand(String[] cliArguments) throws IOException, InterruptedException {
- // Load configuration to validate skills and models
+ // Load configuration to validate skills
Configuration config = loadConfiguration(getConfigurationFile(null));
if (config == null) {
System.err.println("ERROR: Failed to load configuration file");
Scanner scanner = new Scanner(System.in);
String skill = promptForSkill(scanner, config);
- String model = promptForModel(scanner, config);
int priority = promptForPriority(scanner);
// Prompt for custom processing instructions
}
System.out.println("\nProcessing files in current directory...");
- processDirectory(new File("."), skill, model, priority, customInstructions.toString());
+ processDirectory(new File("."), skill, priority, customInstructions.toString());
System.out.println("\nProcessing complete!");
}
}
}
- /**
- * Prompts user for model alias and validates it exists in configuration.
- *
- * @param scanner Scanner for user input
- * @param config Current configuration
- * @return Validated model alias
- */
- private String promptForModel(Scanner scanner, Configuration config) {
- List<Model> models = config.getModels();
- if (models == null || models.isEmpty()) {
- System.err.println("ERROR: No models configured. Please check your configuration file.");
- System.exit(1);
- }
-
- Map<String, Model> modelMap = new HashMap<>();
- for (Model model : models) {
- modelMap.put(model.getAlias(), model);
- }
-
- while (true) {
- System.out.print("Enter model alias: ");
- String alias = scanner.nextLine().trim();
- if (modelMap.containsKey(alias)) {
- return alias;
- }
- System.out.println("ERROR: Model '" + alias + "' not found. Available models: " + String.join(", ", modelMap.keySet()));
- }
- }
-
/**
* Prompts user for priority value with validation.
*
*
* @param dir Directory to process
* @param skill Skill name to include in TOCOMPUTE header
- * @param model Model alias to include in TOCOMPUTE header
* @param priority Priority value to include in TOCOMPUTE header
* @param customInstructions Optional custom processing instructions to prepend after header
* @throws IOException if file operations fail
*/
- private void processDirectory(File dir, String skill, String model, int priority, String customInstructions) throws IOException {
+ private void processDirectory(File dir, String skill, int priority, String customInstructions) throws IOException {
File[] files = dir.listFiles();
if (files == null) return;
for (File file : files) {
if (file.isDirectory()) {
- processDirectory(file, skill, model, priority, customInstructions);
+ processDirectory(file, skill, priority, customInstructions);
} else if (file.isFile() && !file.getName().startsWith(".")) {
- processFile(file, skill, model, priority, customInstructions);
+ processFile(file, skill, priority, customInstructions);
}
}
}
*
* @param file File to process
* @param skill Skill name for TOCOMPUTE header
- * @param model Model alias for TOCOMPUTE header
* @param priority Priority value for TOCOMPUTE header
* @param customInstructions Optional custom processing instructions to insert after header
* @throws IOException if file operations fail
*/
- private void processFile(File file, String skill, String model, int priority, String customInstructions) throws IOException {
+ private void processFile(File file, String skill, int priority, String customInstructions) throws IOException {
if (fileHasToComputeMarker(file)) {
System.out.println("Skipped (already has header): " + file.getAbsolutePath());
return;
}
String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
- String header = String.format("TOCOMPUTE: skill=%s model=%s priority=%d\n", skill, model, priority);
+ String header = String.format("TOCOMPUTE: skill=%s priority=%d\n", skill, priority);
String newContent = header + customInstructions + content;
Files.write(file.toPath(), newContent.getBytes(StandardCharsets.UTF_8));
+++ /dev/null
-package eu.svjatoslav.alyverkko_cli.commands;
-
-import eu.svjatoslav.alyverkko_cli.Command;
-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;
-import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.StringOption;
-import eu.svjatoslav.commons.string.GlobMatcher;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.*;
-
-import static eu.svjatoslav.alyverkko_cli.Main.configuration;
-import static eu.svjatoslav.alyverkko_cli.Utils.getConfigurationFile;
-import static eu.svjatoslav.alyverkko_cli.Utils.loadConfiguration;
-
-/**
- * The JoinFilesCommand aggregates multiple files (optionally matching
- * a specific pattern) into a single file for AI processing, typically
- * in the mail directory.
- *
- * Usage Example:
- * <pre>
- * alyverkko-cli joinfiles -s /path/to/source -p "*.java" -t "my_topic" --edit
- * </pre>
- */
-
-public class JoinFilesCommand implements Command {
-
- /**
- * A command-line parser to handle joinfiles arguments.
- */
- final Parser parser = new Parser();
-
- /**
- * Directory from which files will be joined.
- */
- public DirectoryOption sourceDirectoryOption = parser.add(new DirectoryOption("Directory to join files from"))
- .addAliases("--src-dir", "-s")
- .mustExist();
-
- /**
- * Pattern for matching files, such as "*.java".
- */
- public StringOption patternOption = parser.add(new StringOption("Pattern to match files"))
- .addAliases("--pattern", "-p");
-
- /**
- * Topic name, used as the basis for the output file name.
- */
- public StringOption topic = parser.add(new StringOption("Topic of the joined files"))
- .addAliases("--topic", "-t")
- .setMandatory();
-
- /**
- * If present, open the joined file using a text editor afterward.
- */
- public NullOption editOption = parser.add(new NullOption("Edit the joined file using text editor"))
- .addAliases("--edit", "-e");
-
- /**
- * The base directory for recursion when joining files.
- */
- public Path sourceBaseDirectory;
-
- /**
- * The pattern used to filter files for joining, e.g. "*.java".
- */
- public String fileNamePattern = null;
-
- /**
- * The resulting output file that aggregates all matched files.
- */
- File outputFile;
-
- /**
- * @return the name of this command, i.e., "joinfiles".
- */
- @Override
- public String getCommandName() {
- return "joinfiles";
- }
-
- /**
- * Executes the command that joins files from a specified directory
- * (matching an optional pattern) into one output file in the mail
- * directory. Optionally, it can open the output file in an editor.
- *
- * @param cliArguments the command-line arguments after "joinfiles".
- * @throws IOException if any IO operations fail.
- */
- @Override
- public void executeCommand(String[] cliArguments) throws IOException {
- configuration = loadConfiguration(getConfigurationFile(null));
- if (configuration == null){
- System.out.println("Failed to load configuration file");
- return;
- }
-
- if (!parser.parse(cliArguments)) {
- System.out.println("Failed to parse command-line arguments");
- parser.showHelp();
- return;
- }
-
- // Build the path to the target file that is relative to the mail directory
- outputFile = configuration.getTasksDirectory().toPath().resolve(topic.getValue() + ".org").toFile();
-
- if (patternOption.isPresent()) {
- fileNamePattern = patternOption.getValue();
- joinFiles();
- }
-
- if (editOption.isPresent()) {
- openFileWithEditor();
- }
- }
-
- /**
- * Opens the joined file with a text editor. Currently uses a
- * command "emc" as an example—adapt as needed.
- *
- * @throws IOException if the launch of the editor fails.
- */
- private void openFileWithEditor() throws IOException {
- String[] cmd = {"emc", outputFile.getAbsolutePath()};
- Runtime.getRuntime().exec(cmd);
- }
-
- /**
- * Joins the matching files from the configured source directory
- * into a single file named {@code <topic>.org} in the mail directory.
- *
- * @throws IOException if reading or writing files fails.
- */
- private void joinFiles() throws IOException {
- boolean appendToFile = outputFile.exists();
-
- if (sourceDirectoryOption.isPresent()) {
- sourceBaseDirectory = sourceDirectoryOption.getValue().toPath();
- } else {
- sourceBaseDirectory = Paths.get(".");
- }
-
- try (BufferedWriter writer = Files.newBufferedWriter(
- outputFile.toPath(), StandardCharsets.UTF_8,
- appendToFile ? StandardOpenOption.APPEND : StandardOpenOption.CREATE)) {
-
- // Recursively join files that match the pattern
- joinFilesRecursively(sourceBaseDirectory, writer);
- }
-
- System.out.println("Files have been joined into: " + outputFile.getAbsolutePath());
- }
-
- /**
- * Recursively traverses the specified directory and writes the contents
- * of files that match the specified {@link #fileNamePattern}.
- *
- * @param directoryToIndex the directory to be searched recursively.
- * @param writer the writer to which file contents are appended.
- * @throws IOException if file reading fails.
- */
- private void joinFilesRecursively(Path directoryToIndex, BufferedWriter writer) throws IOException {
- try (DirectoryStream<Path> stream = Files.newDirectoryStream(directoryToIndex)) {
- for (Path entry : stream) {
- if (Files.isDirectory(entry)) {
- joinFilesRecursively(entry, writer);
- } else if (Files.isRegularFile(entry)) {
- String fileName = entry.getFileName().toString();
-
- boolean match = GlobMatcher.match(fileName, fileNamePattern);
- if (match) {
- System.out.println("Joining file: " + fileName);
- writeFile(writer, entry);
- }
- }
- }
- }
- }
-
- /**
- * Writes the contents of a single file to the specified writer,
- * including a small header containing the file path.
- *
- * @param writer the writer to which file contents are appended.
- * @param entry the file to read and write.
- * @throws IOException if file reading or writing fails.
- */
- private void writeFile(BufferedWriter writer, Path entry) throws IOException {
- writeFileHeader(writer, entry);
-
- String fileContent = new String(Files.readAllBytes(entry), StandardCharsets.UTF_8);
-
- // remove empty lines from the beginning and end of the file
- fileContent = fileContent.replaceAll("(?m)^\\s*$", "");
-
- writer.write(fileContent + "\n");
- }
-
- /**
- * Writes a small header line to indicate which file is being appended.
- *
- * @param writer the writer to which the header is appended.
- * @param entry the path of the current file.
- * @throws IOException if writing fails.
- */
- private void writeFileHeader(BufferedWriter writer, Path entry) throws IOException {
- String relativePath = sourceBaseDirectory.relativize(entry).toString();
- writer.write("* file: " + relativePath + "\n\n");
- }
-}
+++ /dev/null
-package eu.svjatoslav.alyverkko_cli.commands;
-
-import eu.svjatoslav.alyverkko_cli.Command;
-import eu.svjatoslav.alyverkko_cli.Utils;
-
-import java.io.IOException;
-
-import static eu.svjatoslav.alyverkko_cli.Main.configuration;
-
-/**
- * <p>Displays all available AI models in the configured models directory. This command provides a quick overview of
- * currently available models and their metadata.
- * <p>The implementation:
- * <ul>
- * <li>Loads the configuration</li>
- * <li>Instantiates ModelLibrary</li>
- * <li>Prints model details using ModelLibrary's printModels()</li>
- * </ul>
- *
- * <p>This command is primarily intended for administrative use to verify model availability before running tasks.
- */
-public class ListModelsCommand implements Command {
-
- /**
- * @return the name of this command, i.e., "listmodels".
- */
- @Override
- public String getCommandName() {
- return "listmodels";
- }
-
- /**
- * Executes the command to load the user's configuration and list
- * all known AI models, printing them to stdout.
- *
- * @param cliArguments the command-line arguments after "listmodels".
- * @throws IOException if loading configuration fails.
- */
- @Override
- public void executeCommand(String[] cliArguments) throws IOException {
- configuration = Utils.loadConfiguration(Utils.getConfigurationFile(null));
- if (configuration == null){
- System.out.println("Failed to load configuration file");
- return;
- }
-
- System.out.println("Available models:");
- configuration.printModels();
- }
-}
package eu.svjatoslav.alyverkko_cli.commands.task_processor;
-import eu.svjatoslav.alyverkko_cli.configuration.Model;
import eu.svjatoslav.alyverkko_cli.configuration.SkillConfig;
import static eu.svjatoslav.alyverkko_cli.Main.configuration;
*/
public String userPrompt;
- /**
- * The AI model to be used for processing this query.
- */
- public Model model;
-
/**
* The start time of the query (milliseconds since epoch).
*/
return "Task{" +
"systemPrompt='" + systemPrompt + '\'' +
", userPrompt='" + userPrompt + '\'' +
- ", model=" + model +
'}';
}
* Calculates the effective timeout in milliseconds using the following hierarchy:
* <ol>
* <li>Skill-specific timeout (highest priority)</li>
- * <li>Model-specific timeout</li>
* <li>Global default timeout (lowest priority)</li>
* </ol>
*/
if (skill != null && skill.getTimeoutMillis() != null) {
return skill.getTimeoutMillis();
}
- if (model.getTimeoutMillis() != null) {
- return model.getTimeoutMillis();
- }
return configuration.getDefaultTimeoutMillis();
}
}
private String buildRequestBody() throws IOException {
Map<String, Object> body = new HashMap<>();
- body.put("model", task.model.getAlias());
Map<String, String> systemMessage = new HashMap<>();
systemMessage.put("role", "system");
package eu.svjatoslav.alyverkko_cli.commands.task_processor;
import eu.svjatoslav.alyverkko_cli.*;
-import eu.svjatoslav.alyverkko_cli.configuration.Model;
import eu.svjatoslav.alyverkko_cli.configuration.SkillConfig;
import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser;
import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption;
resultFileContent.append(task.userPrompt).append("\n");
// Check for the final answer indicator in the model's configuration
- String finalAnswerIndicator = task.model.getFinalAnswerIndicator();
+ String finalAnswerIndicator = null;
if (finalAnswerIndicator != null && !finalAnswerIndicator.isEmpty()) {
int index = aiResponse.indexOf(finalAnswerIndicator);
if (index != -1) {
*/
private static String getDoneLine(Task task) {
return "DONE: skill=" + task.skillName +
- " model=" + task.model.getAlias() +
" duration=" + getDuration(task.startTimeMillis, task.endTimeMillis) + "\n";
}
/**
* Builds a Task object from the contents of a file.
- * <p>
- * This method now implements a three-level hierarchy for model selection:
- * 1. Explicit model specified in TOCOMPUTE line (the highest priority)
- * 2. Model alias defined in the skill configuration (if present)
- * 3. Default "default" model (the lowest priority)
*
* @param file the file to read.
- * @return the constructed MailQuery.
+ * @return the constructed Task.
* @throws IOException if reading the file fails.
*/
private Task buildTaskFromFile(File file) throws IOException {
result.systemPrompt = skill.getSystemPrompt();
result.skill = skill;
- // Set AI model using hierarchy: TOCOMPUTE > skill config > default
- String modelAlias = fileProcessingSettings.getOrDefault("model",
- skill.getModelAlias() != null ? skill.getModelAlias() : "default");
- Optional<Model> modelOptional = configuration.findModelByAlias(modelAlias);
- if (!modelOptional.isPresent()) {
- throw new IllegalArgumentException("Model with alias '" + modelAlias + "' not found.");
- }
- result.model = modelOptional.get();
-
// Set priority
String priorityStr = fileProcessingSettings.get("priority");
result.priority = 0;
package eu.svjatoslav.alyverkko_cli.configuration;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.*;
-import java.util.List;
-import java.util.Optional;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
/**
* <p>Central configuration class storing all application parameters.
* This class is serialized to YAML format for user editing and persistence.
- * <p>Configuration parameters include:
- * <ul>
- * <li>Task and prompt directories</li>
- * <li>Server connection settings</li>
- * <li>Model-specific configurations</li>
- * </ul>
- * <p>All paths are resolved relative to the user's home directory by default, but can be customized. The class provides
- * direct access to prompt content for AI query construction.
*/
@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
public class Configuration {
/**
/**
* The default timeout in milliseconds for AI processing tasks. A value of 0 or null means no timeout.
- * This serves as the lowest-priority fallback when no skill-specific or model-specific timeout is set.
*/
@JsonProperty("default_timeout_millis")
private Long defaultTimeoutMillis;
-
- /**
- * The list of models defined in this configuration.
- */
- private List<Model> models;
-
-
/**
* Retrieves the contents of a prompt file by alias, e.g. "writer"
* maps to "writer.txt" in the prompt's directory.
return mapper.readValue(promptFile, SkillConfig.class);
}
-
-
- /**
- * Prints the details of each model in the library to standard output.
- */
- public void printModels() {
- System.out.println("Available models:\n");
- for (Model model : models) {
- model.printModelDetails();
- System.out.println();
- }
- }
-
- public String getModelFullFilesystemPath(Model model) {
- return model.getFilesystemPath();
- }
-
- public Optional<Model> findModelByAlias(String modelAlias) {
- for (Model model : models) {
- if (model.getAlias().equals(modelAlias)) return Optional.of(model);
- }
- return Optional.empty();
- }
}
+++ /dev/null
-package eu.svjatoslav.alyverkko_cli.configuration;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
-
-
-/**
- * Represents a single AI model configuration entry, including alias,
- * path to the model file, token context size, and an optional
- * end-of-text marker.
- */
-@Data
-public class Model {
-
- /**
- * A short name for the model, e.g., "default" or "mistral".
- */
- private String alias;
-
- /**
- * Model-specific temperature value overriding global default.
- */
- private Float temperature;
-
- /**
- * Model-specific top-p value overriding global default.
- */
- @JsonProperty("top_p")
- private Float topP;
-
- @JsonProperty("min_p")
- private Float minP;
-
- @JsonProperty("top_k")
- private Float topK;
-
- /**
- * Model-specific repeat penalty value overriding global default.
- */
- @JsonProperty("repeat_penalty")
- private Float repeatPenalty;
-
- /**
- * The path to the model file (GGUF, etc.), relative to
- * {@link Configuration#getModelsDirectory()} or fully qualified.
- */
- @JsonProperty("filesystem_path")
- private String filesystemPath;
-
- /**
- * The maximum context size the model supports, in tokens.
- */
- @JsonProperty("context_size_tokens")
- private int contextSizeTokens;
-
- /**
- * Optional text marker signifying the end of text for this model.
- * If non-null, it will be used to strip trailing tokens from the AI response.
- */
- @JsonProperty("end_of_text_marker")
- private String endOfTextMarker;
-
- /**
- * Optional string that indicates the start of the final answer in the model's output.
- * When specified, the response is split into ASSISTANT and FINAL ANSWER sections based on this indicator.
- */
- @JsonProperty("final_answer_indicator")
- private String finalAnswerIndicator;
-
- /**
- * Maximum time in milliseconds allowed for AI processing for this model. If null, no timeout is set for this model.
- * This value overrides the global default timeout but is overridden by skill-specific timeouts.
- */
- @JsonProperty("timeout_millis")
- private Long timeoutMillis;
-
- /**
- * <p>Prints the model's metadata to standard output in a consistent format. This includes the model's alias,
- * filesystem path, and context token capacity. The output format is designed to be both human-readable and
- * machine-parsable when needed.
- * <p>Typical output:
- * <pre>
- * Model: default
- * Path: /path/to/model.gguf
- * Context size: 32768
- * </pre>
- */
- public void printModelDetails() {
- System.out.println("Model: " + alias);
- System.out.println(" Path: " + filesystemPath);
- System.out.println(" Context size: " + contextSizeTokens);
- }
-
-}