From: Svjatoslav Agejenko Date: Sun, 30 Nov 2025 22:45:53 +0000 (+0200) Subject: Initial commit X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=0f54672862d9c8c6ea66f1aef124edf56672ffd7;p=alyverkko-cli.git Initial commit --- 0f54672862d9c8c6ea66f1aef124edf56672ffd7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52e4c3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +/.idea/ +/.settings/ +/target/ +/*.iml +/*.log +/test/ + +/doc/apidocs/ +/doc/graphs/ +/doc/index.html +/doc/setup.html + + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/alyverkko-cli b/alyverkko-cli new file mode 100755 index 0000000..aea6d86 --- /dev/null +++ b/alyverkko-cli @@ -0,0 +1,6 @@ +#!/bin/bash + +set -f + +java -Xmx4500m -classpath /opt/alyverkko-cli/* eu.svjatoslav.alyverkko_cli.Main "$@" + diff --git a/alyverkko-cli.yaml b/alyverkko-cli.yaml new file mode 100644 index 0000000..f931d6e --- /dev/null +++ b/alyverkko-cli.yaml @@ -0,0 +1,16 @@ +task_directory: "/home/user/AI/tasks" +models_directory: "/home/user/AI/models" +default_temperature: 0.7 +llama_cpp_dir_path: "/home/user/AI/llama.cpp/" +batch_thread_count: 10 +thread_count: 6 +skills_directory: "/home/user/.config/alyverkko-cli/skills" +models: + - alias: "default" + filesystem_path: "WizardLM-2-8x22B.Q5_K_M-00001-of-00005.gguf" + context_size_tokens: 64000 + end_of_text_marker: null + - alias: "mistral" + filesystem_path: "Mistral-Large-Instruct-2407.Q8_0.gguf" + context_size_tokens: 32768 + end_of_text_marker: null diff --git a/doc/QwQ-32B Termination issue.pdf b/doc/QwQ-32B Termination issue.pdf new file mode 100644 index 0000000..827eb5d Binary files /dev/null and b/doc/QwQ-32B Termination issue.pdf differ diff --git a/doc/index.org b/doc/index.org new file mode 100644 index 0000000..8e8338b --- /dev/null +++ b/doc/index.org @@ -0,0 +1,855 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Älyverkko CLI application +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +* Introduction + +The *Älyverkko CLI* application is a user-friendly tool developed in +Java, specifically tailored to streamline the utilization of expansive +language models through CPU-based computation in batch processing +mode. + +To illustrate its capabilities: Imagine harnessing the power of a vast +language model, boasting approximately 100 billion parameters, solely +relying on CPU computations and leveraging the open-source software +*llama.cpp*. This setup requires a modern consumer-grade CPU and +approximately 128 GB of RAM. To put this into perspective, 128 GB of +RAM is financially comparable to purchasing a high-quality smartphone, +making it an economical option for many users. + +In contrast, executing the same model on an Nvidia GPU could result in +costs that are significantly higher, potentially at least by an order +of magnitude. + +However, there is a trade-off: CPU-based processing for such extensive +models is inherently slow. This means real-time interaction, like +chatting with the AI, wouldn't be practical or enjoyable due to the +lag in response times. Nevertheless, when deployed in a +non-interactive batch mode, this "slow but smart" AI can complete +numerous valuable tasks within a 24-hour window. For instance, it +could generate a substantial amount of code, potentially exceeding +what you could thoroughly review in the same timeframe. Additionally, +it could process more documents than most individuals would be +inclined to read manually. + +The primary objective of the *Älyverkko CLI* project is to identify +and enable applications where this "slow but smart" AI can excel. By +utilizing *llama.cpp* as its inference engine, the project aims to +unlock a variety of uses where batch processing is more advantageous +than real-time interaction. + +Here are some practical examples of tasks suited for the +*Älyverkko CLI* application: + +- [[id:e1b6aa9a-a27d-4019-8bd4-fceb0c606e23][Domain: Natural Language Processing (NLP)]] +- [[id:f4c7d3e0-8c18-4123-a797-871ca73a7580][Domain: Code Generation]] +- [[id:f38360ad-54f6-4f24-b299-f73a9faacabd][Domain: Content Creation]] + +In summary, the *Älyverkko CLI* application opens up a realm of +possibilities for leveraging powerful AI in scenarios where immediate +responses are not critical, but high-quality batch processing output +is highly beneficial. + +Note: project is still in early stage. + +** Use cases +*** Domain: Natural Language Processing (NLP) +:PROPERTIES: +:ID: e1b6aa9a-a27d-4019-8bd4-fceb0c606e23 +:END: + +*Problem Statement:* + +Analyze a set of customer reviews to determine overall sentiment and +extract key features that customers appreciate or dislike about a +product. + +*Usage Procedure:* +1. User collects customer reviews in plain text format within the + configured mail directory. Lets say, about 150 kilobytes of reviews + per input file (this is dictated by AI model available context + size). +2. Each review file is prefixed with "TOCOMPUTE:". +3. The Älyverkko CLI application processes these files, generating + sentiment analysis results and feature extraction insights. +4. Results are appended to the original files in org-mode syntax, + indicating AI responses. + +*** Domain: Code Generation +:PROPERTIES: +:ID: f4c7d3e0-8c18-4123-a797-871ca73a7580 +:END: + +*Problem Statement:* + +Generate code snippets for a new software module based on detailed +specifications provided by the developer. + +*Usage Procedure:* + +1. Developer writes specifications in a text file within the mail + directory, prefixed with "TOCOMPUTE:". Text file also contains + relevant parts of the program source code and + documentation. Älyverkko CLI *joinfiles* subcommand can be used to + facilitate such text file preparation. + +2. The Älyverkko CLI application processes this file and generates the + corresponding code snippets. The generated code is appended to the + original specifications file, organized using org-mode syntax. + +3. Developer can review proposed changes and then integrate them back + into original program source code. + +Note: Most of the [[https://www3.svjatoslav.eu/projects/alyverkko-cli/][Älyverkko CLI]] program code is written in such a way +by AI. [[https://www2.svjatoslav.eu/gitweb/?p=alyverkko-cli.git;a=blob;f=tools/implement+idea;h=02b0ceb260693a6c9733f221e52a0e6c5fce0a36;hb=HEAD][This script]] is used to facilitate the process. + +*** Domain: Content Creation +:PROPERTIES: +:ID: f38360ad-54f6-4f24-b299-f73a9faacabd +:END: + +*Problem Statement:* + +Draft an outline for a book on science fiction and improve its plot. + +*Usage Procedure:* + +1. The book author writes a brief describing the outline of the plot + and his book main idea for the novel. + +2. *Älyverkko CLI* processes this description and generates an outline + with suggested headings and suggests possible improvements to the + plot. + +Here is [[https://www.svjatoslav.eu/writing/Whispers%20in%20the%20Stream%20of%20Time.html][example sci-fi book]] that was written with the help of +*Älyverkko CLI*. + +* Getting started + +When you first encounter Älyverkko CLI, the setup process might seem +involved compared to cloud-based AI services. That's completely +understandable! Let me walk you through why each step exists and how +it ultimately creates a powerful, private, and cost-effective AI +assistant that works /for you/. + +** Why Bother With This Setup? (The Big Picture) + +Before diving into steps, let's address the elephant in the room: *Why +go through this setup when ChatGPT is just a click away?* + +Because Älyverkko CLI solves a fundamentally different problem: + +- ✨ *Privacy by design*: All processing happens on your machine - no + data ever leaves your computer +- 💰 *Cost efficiency*: Run 70B+ parameter models without paying per + token (128GB RAM ≈ cost of a smartphone) +- ⚙️ *Full control*: Tweak every parameter to match your specific needs +- 📦 *Offline capability*: Works without internet connection +- 🕒 *Batch processing*: Perfect for "set it and forget it" workflows + while you sleep + +This isn't designed for real-time chatting (CPU inference is slow), +but for substantial tasks where quality matters more than speed: code +generation, document analysis, content creation, etc. + +** Your Setup Journey - What to Expect + +Here's what you'll be doing, explained simply with /why/ each step +matters: + +*** 1. Installing Java & Maven (The Foundation) + +- *What*: Install JDK 21+ and Apache Maven +- *Why*: Älyverkko CLI is written in Java - these tools let you build + and run the application. + - *Don't worry*: On Debian/Ubuntu, it's just + : sudo apt install openjdk-21-jdk maven + +*Key insight*: Java was chosen because it's cross-platform, +memory-safe, and perfect for long-running background processes like +our AI task processor. + +*** 2. Building llama.cpp (Your AI Engine) + +- *What*: Download and compile the llama.cpp project from GitHub. +- *Why*: This is the actual "brain" that runs large language models on + *your CPU*. We build from source (rather than using prebuilt + binaries) so it can optimize for /your specific CPU/ - squeezing out + *maximum performance from your hardware. + +*** 3. Adding AI Models (The Brains) +- *What*: Download GGUF format model files (typically 4-30GB each) +- *Where*: From Hugging Face Hub ([[https://huggingface.co/models?search=gguf][search "GGUF"]]). +- *Why*: These contain the actual neural networks that power the AI. +- *Don't worry*: Start with one model (like Mistral 7B) - you can add + more later. +- *Key insight*: GGUF format was created specifically for CPU + inference. + +#+begin_quote +❓ Why not smaller models? Larger models (even running slowly on CPU) +produce significantly better results for complex tasks - it's worth +the wait. +#+end_quote + +*** 4. Running the Interactive Wizard (=alyverkko-cli wizard=) +- *What*: Launch the configuration wizard that asks simple questions. +- *Why*: To connect all the pieces without you needing to edit complex + YAML files. +- *Don't worry*: It's interactive! You'll answer questions like "Where did + you put your AI models?" with easy prompts. +- *Key insight*: This creates your personal + =~/.config/alyverkko-cli.yaml= file. + +#+begin_quote +🌟 Pro tip: The wizard automatically detects your models and suggests +reasonable defaults - you're not starting from scratch. +#+end_quote + +*** 5. Setting Up "Skills" (Your Custom Instructions) + +- *What*: Create simple YAML files defining how the AI should behave + for different tasks. +- *Why*: So you don't have to rewrite instructions every time ("be a + coding assistant" vs "be a writing editor"). +- *Don't worry*: Sample skills come pre-configured - you can modify + them gradually. +- *Key insight*: Skills let you create specialized AI personas without + changing models. + +#+begin_quote +Example: Your =writer.yaml= skill might instruct the AI to "always +provide well-reasoned responses in academic tone" +#+end_quote + +*** 6. Preparing Your First Task (The Magic Moment) +- *What*: Create a text file with your request, prefixed with + *TOCOMPUTE:* +- *Why*: This triggers the background processing system +- *Key insight*: File-based interaction isn't primitive - it's + intentional design for batch processing. + +** Why Files Instead of a Fancy UI? + +You might wonder: /Why deal with text files when everything has +beautiful interfaces these days?/ + +Because *this is designed for productivity, not conversation*: + +1. *No waiting around*: With CPU inference, responses take + minutes/hours. File-based workflow lets you queue tasks and get + back to work. +2. *Natural integration*: Works with your existing text editor (VS + Code, Emacs, etc.) rather than forcing a new interface. +3. *Version control friendly*: You can track changes to + prompts/responses with Git. +4. *Resource efficient*: No heavy GUI consuming precious RAM needed + for AI models. +5. *Scriptable*: Easily integrate with other tools in your workflow. + +Think of it like email versus phone calls - sometimes asynchronous +communication is actually /more/ productive. + +** The Light at the End of the Tunnel + +After initial setup (which typically takes 30-60 minutes), here's what +you get: + +- ✅ A silent background process that automatically processes tasks +- ✅ Complete privacy - no data ever leaves your machine +- ✅ The ability to run state-of-the-art models without expensive + hardware. +- ✅ A system that keeps working while you sleep - queue up 10 tasks + before bed, get results in the morning. + +You fill find that after the first few processed tasks, the initial +setup effort feels worthwhile. You're not just getting another +chatbot - you're building a personal AI workstation tailored to your +specific needs. The initial investment pays dividends every time you +need serious AI power without compromise. + +* Installation +** Requirements +*Operating System:* + +Älyverkko CLI is developed and tested on Debian 12 "Bookworm". It +should work on any modern Linux distribution with minimal adjustments +to the installation process. + +*Dependencies:* +- Java Development Kit (JDK) 17 or higher +- Apache Maven for building the project + +*Hardware Requirements:* +- Modern multi-core CPU. +- The more RAM you have, the smarter AI model you can use. For + example, at least 64 GB of RAM is needed to run decent AI models + with sufficiently large context. +- Sufficient disk space to store large language models and + input/output data. + +** Installation +:PROPERTIES: +:ID: 0b705a37-9b84-4cd5-878a-fedc9ab09b12 +:END: +At the moment, to use Älyverkko CLI, you need to: +- Download sources and build [[https://github.com/ggerganov/llama.cpp][llama.cpp]] project. +- Download [[id:f5740953-079b-40f4-87d8-b6d1635a8d39][sources]] and build Älyverkko CLI project. +- Download one or more pre-trained large language models in GGUF + format. Hugging Face repository [[https://huggingface.co/models?search=GGUF][has lot of them]]. + +Follow instructions for obtaining and building Älyverkko CLI on your +computer that runs Debian 13 operating system: + +1. Ensure that you have Java Development Kit (JDK) installed on your + system. + : sudo apt-get install openjdk-21-jdk + +2. Ensure that you have Apache Maven installed: + : sudo apt-get install maven + +3. Clone the [[id:f5740953-079b-40f4-87d8-b6d1635a8d39][code repository]] or download the [[id:f5740953-079b-40f4-87d8-b6d1635a8d39][source code]] for the + `alyverkko-cli` application to your local machine. + +4. Navigate to the root directory of the cloned/downloaded project in + your terminal. + +5. Execute the installation script by running + : ./install + + This script will compile the application and install it to + directory + : /opt/alyverkko-cli + + To facilitate usage from command-line, it will also define + system-wide command *alyverkko-cli* as well as "Älyverkko CLI" + launcher in desktop applications menu. + +6. Prepare Älyverkko CLI [[id:0fcdae48-81c5-4ae1-bdb9-64ae74e87c45][configuration]] file. + +7. Verify that the application has been installed correctly by running + *alyverkko-cli* in your terminal. + +** Alyverkko CLI daemon configuration +:PROPERTIES: +:ID: 0fcdae48-81c5-4ae1-bdb9-64ae74e87c45 +:END: +Älyverkko CLI application configuration is done by editing YAML +formatted configuration file. + +Configuration file should be placed under current user home directory: +: ~/.config/alyverkko-cli.yaml + +*** Key Parameters Explained +**** Core Directories + +- =mail_directory=: Where task files are placed for processing +- =models_directory=: Contains GGUF model files +- =skills_directory=: Contains YAML skill definition files +- =llama_cli_path=: Path to llama.cpp's executable + +**** Generation Parameters + +- =default_temperature=: Creativity control (0-3, higher = more + creative) +- =default_top_p=: Nucleus sampling threshold (0.0-1., higher = more + diverse) +- =default_repeat_penalty=: Penalty for repetition (>0.0, 1.0 = no + penalty) + +**** Performance Tuning + +- =thread_count=: Sets the number of threads to be used by the AI + during response generation. RAM data transfer speed is usually + bottleneck here. When RAM bandwidth is saturated, increasing thread + count will no longer increase processing speed, but it will still + keep CPU cores unnecessarily busy. + +- =batch_thread_count=: Specifies the number of threads to use for + input prompt processing. CPU computing power is usually the + bottleneck here. + +**** Model-Specific Settings + +Each model in the =models= list can have: + +- =alias=: Short model alias. Model with alias "default" would be used + by default. + +- =filesystem_path=: File name of the model as located within + *models_directory* + +- =context_size_tokens=: Context size in tokens that model was trained + on. + +- =end_of_text_marker=: Some models produce certain markers to + indicate end of their output. If specified here, Älyverkko CLI can + identify and remove them so that they don't leak into + conversation. Default value is: *null*. + +- =temperature=, =top_p=, =repeat_penalty=: Model-specific overrides + +*** Configuration file example + +The application is configured using a YAML-formatted configuration +file. Below is an example of how the configuration file might look: + +#+begin_src yaml + task_directory: "/home/user/AI/tasks" + models_directory: "/home/user/AI/models" + skills_directory: "/home/user/AI/skills" + llama_cli_path: "/home/user/AI/llama.cpp/build/bin/llama-cli" + + # Processing parameters + default_temperature: 0.7 + default_top_p: 0.9 + default_repeat_penalty: 1.0 + thread_count: 6 + batch_thread_count: 10 + + # Model definitions + models: + - alias: "default" + filesystem_path: "model.gguf" + context_size_tokens: 64000 + end_of_text_marker: null + temperature: 0.8 # Optional model-specific parameter + top_p: 0.9 # Optional + repeat_penalty: 1.1 # Optional + - alias: "mistral" + filesystem_path: "Mistral-Large-Instruct-2407.Q8_0.gguf" + context_size_tokens: 32768 + end_of_text_marker: null +#+end_src + +*** Parameter Precedence Hierarchy + +For *temperature*, *top_p*, and *repeat_penalty* parameters, values +are determined using this priority order (highest to lowest): + +1. *Skill-specific value* (from skill's YAML file) +2. *Model-specific value* (from model configuration) +3. *Global default value* (from main configuration) + +This allows fine-grained control where more specific configurations +override broader ones. + +*** Enlisting available models +Once Älyverkko CLI is installed and properly configured, you can run +following command at commandline to see what models are available to +it: + +: alyverkko-cli listmodels + +Note: Models that reference missing files will be automatically marked +with "-missing" suffix in their alias by configuration wizard. You can +manually remove this suffix after fixing the model file path. + +*** Self test +The *selftest* command performs a series of checks to ensure the +system is configured correctly: + +: alyverkko-cli selftest + +It verifies: +- Configuration file integrity. +- Model directory existence. +- The presence of the *llama.cpp* executable. + +** Skill concept and configuration +*** Skill File Format + +Skills are defined in YAML files stored in the *skills_directory*. + +Each skill file contains: + +#+begin_src yaml +prompt: "Full system prompt text here" +temperature: 0.8 # Optional +top_p: 0.9 # Optional +repeat_penalty: 1.1 # Optional +#+end_src + +The system prompt must contain ** which gets replaced with +the actual user prompt during execution. + +*** Example Skill File +: writer.yaml + +#+begin_src yaml + temperature: 0.9 + top_p: 0.95 + prompt: | + <|im_start|>system + User will provide you with task that needs to be solved along with + existing relevant information. + + You are artificial general intelligence system that always provides well reasoned responses. + <|im_end|> + <|im_start|>user + /think Solve following problem: + + + + <|im_end|> + <|im_start|>assistant +#+end_src + +** Starting process daemon + +Älyverkko CLI keeps continuously listening for and processing tasks +from a specified mail directory. + +There are multiple alternative ways to start Älyverkko CLI in mail +processing mode: + +**** Start via command line interface + +1. Open your terminal. + +2. Run the command: + : alyverkko-cli process + +3. The application will start monitoring the configured mail directory + for incoming messages and process them accordingly in endless loop. + +4. To terminate Älyverkko CLI, just hit *CTRL+c* on the keyboard, or + close terminal window. + +**** Start using your desktop environment application launcher + +1. Access the application launcher or application menu on your desktop + environment. + +2. Search for "Älyverkko CLI". + +3. Click on the icon to start the application. It will open its own + terminal. + +4. If you want to stop Älyverkko CLI, just close terminal window. + +**** Start in the background as systemd system service + +During Älyverkko CLI [[id:0b705a37-9b84-4cd5-878a-fedc9ab09b12][installation]], installation script will prompt you +if you want to install *systemd* service. If you chose *Y*, Alyverkko +CLI would be immediately started in the background as a system +service. Also it will be automatically started on every system reboot. + +To view service status, use: +: systemctl -l status alyverkko-cli + +If you want to stop or disable service, you can do so using systemd +facilities: + +: sudo systemctl stop alyverkko-cli +: sudo systemctl disable alyverkko-cli + +* Usage +** Task file format + +Task files follow a specific structure that begins with a header line: + +#+begin_example +TOCOMPUTE: [parameters] +[User prompt content] +#+end_example + +*** Task File Header Format + +The first line *must* begin with exactly =TOCOMPUTE:= followed by +space-separated key-value pairs: + +#+begin_example +TOCOMPUTE: skill=default model=mistral priority=10 +#+end_example + +Valid parameters in the header: +- =skill=[name]=: Specifies which skill to use (defaults to "default") +- =model=[alias]=: Specifies which AI model to use (defaults to "default") +- =priority=[integer]=: Higher integers mean higher priority (default: 0) + +*** Processed File Format + +After AI processing completes, a new file is created with: +1. First line: =DONE: skill=[name] model=[alias] duration=[time]= +2. =* USER:= section containing original user prompt +3. =* ASSISTANT:= section containing AI response + +: DONE: skill=writer model=default duration=5m +: ... + +** Task preparation +:PROPERTIES: +:ID: 4b7900e4-77c1-45e7-9c54-772d0d3892ea +:END: + +The Älyverkko CLI application expects input files for processing in +the form of plain text files within the specified tasks directory +(configured in the [[id:0fcdae48-81c5-4ae1-bdb9-64ae74e87c45][YAML configuration file]]). + +Suggested usage flow is to prepare AI assignments within the Älyverkko +CLI mail directory using normal text editor. Once AI assignment is +ready for processing, you should [[id:883d6e7c-60e0-422b-8c00-5cdc9dfec20d][initiate AI processing]] on that file. + +*** "joinfiles" command +*Note:* See also alternative solution with similar goal: [[https://github.com/aerugo/prelude][prelude]]. + +The *joinfiles* command is a utility for aggregating the contents of +multiple files into a single document, which can then be processed by +AI. This is particularly useful for preparing comprehensive problem +statements from various source files, such as software project +directories or collections of text documents. + +**** Usage + +To use the *joinfiles* command, specify the source directory +containing the files you wish to join and a topic name that will be +used to generate the output file name: + +#+begin_example +alyverkko-cli joinfiles -s /path/to/source/directory -t "my_topic" +#+end_example + +If desired, you can also specify a glob pattern to match only certain files within the directory: + +#+begin_example +alyverkko-cli joinfiles -s /path/to/source/directory -p "*.java" -t "my_topic" +#+end_example + +After joining the files, you can choose to open the resulting document +in text editor for further editing or review: + +#+begin_example +alyverkko-cli joinfiles -t "my_topic" --edit +#+end_example + +**** Options + +- **-s, --src-dir**: Specifies the source directory from which to join + files. + +- **-p, --pattern**: An optional glob pattern to match specific files + within the source directory. + +- **-t, --topic**: The topic name that will be used as a basis for the + output file name and should reflect the subject matter of the joined + content. + +- **-e, --edit**: Opens the joined file in text editor after the join + operation is complete. + +**** Example Use Case + +Imagine you have a software project with various source files that you +want to analyze using AI. You can use the *joinfiles* command to +create a single document for processing: + +#+begin_example +alyverkko-cli joinfiles -s /path/to/project -p "*.java" -t "software_analysis" --edit +#+end_example + +This will recursively search the project directory for Java source +files, aggregate their contents into a file named +*software_analysis.org* (within AI processor input files directory), +and open text editor on the file, so that you can add your analysis +instructions or problem statement. Finally you [[id:883d6e7c-60e0-422b-8c00-5cdc9dfec20d][Initiate AI processing]] +and after some time, you will get results and the end of the file. + +** Initiate AI processing +:PROPERTIES: +:ID: 883d6e7c-60e0-422b-8c00-5cdc9dfec20d +:END: + +Once your task file is prepared, you should place *TOCOMPUTE:* marker +on the first line of that file, so that it will be considered for +processing. + +When the Älyverkko CLI detects a new or modified file in the mail +directory: + +1. It checks if file has "TOCOMPUTE:" on the first line. If no, file + is ignored. Otherwise Älyverkko CLI continues processing the file. + +2. It reads the content of the file and feeds it as an input for an AI + model to generate a response. + +4. Once the AI has generated a response, the application appends this + response to the original mail contents within the same file, using + org-mode syntax to distinguish between the user's query and the + assistant's reply. The updated file will contain both the original + query (prefixed with: "* USER:*") and the AI's response (prefixed + with "* ASSISTANT:"), ensuring a clear and organized conversation + thread. "TOCOMPUTE:" is removed from the beginning of the file to + avoid processing same file again. + +Note: During AI task file preparation, feel free to save intermediary +states as often as needed because AI engine will keep ignoring the +file until *TOCOMPUTE:* line appears. Once AI assignment is ready, add +: TOCOMPUTE: +to the beginning of the file and save one last time. Älyverkko CLI +will detect new task approximately within one second after file is +saved and will start processing it. + +If your text editor automatically reloads file when it was changed by +other process in the filesystem, AI response will appear within text +editor as soon as AI response is ready. If needed, you can add further +queries at the end of the file and re-add "TOCOMPUTE:" at the +beginning of the file. This way AI will process file again and file +becomes stateful conversation. If you use GNU Emacs text editor, you +can benefit from [[id:25038854-c905-4b26-9670-cca06600223e][purpose-built GNU Emacs utilities]]. + +** Helpful GNU Emacs utilities +:PROPERTIES: +:ID: 25038854-c905-4b26-9670-cca06600223e +:END: + +Note: GNU Emacs and following Emacs Lisp utilities are not required to +use Älyverkko CLI. Their purpose is to increase comfort for existing +GNU Emacs users. + +*** Easily compose new problem statement for AI from emacs + +The Elisp function *ai-new-topic* facilitates the creation and opening +of a new Org-mode file dedicated to a user-defined topic within a +specified directory. Now you can use this file within emacs to compose +you problem statement to AI. + +When *ai-new-topic* function triggered, it first prompts the user to +input a topic name. This name will serve as the basis for the filename +and the title within the document. + +The function then constructs a file path by concatenating the +pre-defined =alyverkko-topic-files-directory= (which should be set to +your topics directory), the topic name, and the =.org= extension. If a +file with this path does not already exist, the function will create a +new file and open it for editing. + +#+begin_src elisp :results none + + (defvar alyverkko-task-files-directory "/home/user/my-ai-tasks-directory/" + "Directory where task files are stored.") + + (defun alyverkko-new-task () + "Create and open a task file in the specified directory." + (interactive) + (let ((task (read-string "Enter task name: "))) + (let ((file-path (concat alyverkko-task-files-directory task ".org"))) + (if (not (file-exists-p file-path)) + (with-temp-file file-path + )) + (find-file file-path) + (goto-char (point-max)) + (org-mode)))) + +#+end_src + +*** Easily signal to AI that problem statement is ready for solving + +The function =alyverkko-compute= is designed to enhance the workflow +of users working with the Älyverkko CLI application by automating the +process of marking text files for computation with a specific AI model +and prompt. + +When function is invoked, it detects available prompts and allows user +to select one. + +Thereafter function detects available models from Älyverkko CLI +configuration file and allows user to select one. + +Finally function inserts at the beginning of currently opened file +something like this: + +: TOCOMPUTE: prompt= model= priority= + +- Adjust *prompt-dir* variable to point to your prompts directory. +- Adjust *config-file* variable to point to your Älyverkko CLI + configuration file path. + +#+begin_src emacs-lisp + +(defun alyverkko-compute () + "Interactively pick a skill, model, and priority, then insert a +TOCOMPUTE line at the top of the current buffer. + +Adjust `skill-dir` and `config-file` to match your setup." + (interactive) + (let ((skill-dir "~/.config/alyverkko-cli/skills/") + (config-file "~/.config/alyverkko-cli/alyverkko-cli.yaml") + models) + + ;; Harvest model aliases from the Älyverkko CLI config + (with-temp-buffer + (insert-file-contents config-file) + (goto-char (point-min)) + (when (search-forward-regexp "^models:" nil t) + (while (search-forward-regexp "^\\s-+- alias: \"\\([^\"]+\\)\"" nil t) + (push (match-string 1) models)))) + + (if (file-exists-p skill-dir) + (let* ((files (directory-files skill-dir t "\\`[^.].*\\.yaml\\'")) + (aliases (mapcar #'file-name-base files))) + (if aliases + (let* ((selected-alias (completing-read "Select skill: " aliases)) + (model (completing-read "Select AI model: " models)) + (priority (number-to-string + (read-number "Priority (integer, default 0): " 0)))) + (alyverkko-insert-tocompute-line selected-alias model priority)) + (message "No skill files found."))) + (message "Skill directory not found.")))) + +(defun alyverkko-insert-tocompute-line (skill-alias model &optional priority) + "Insert a TOCOMPUTE line with SKILL-ALIAS, MODEL, and PRIORITY at +the top of the current buffer." + (save-excursion + (goto-char (point-min)) + (insert (format "TOCOMPUTE: skill=%s model=%s priority=%s\n" + skill-alias model (or priority "0"))) + (save-buffer))) + +#+end_src + +* Getting the source code +- This program is free software: released under Creative Commons Zero + (CC0) license. + +- Program author: + - Svjatoslav Agejenko + - Homepage: https://svjatoslav.eu + - Email: mailto://svjatoslav@svjatoslav.eu + +- [[https://www.svjatoslav.eu/projects/][Other software projects hosted at svjatoslav.eu]] + +** Source code +:PROPERTIES: +:ID: f5740953-079b-40f4-87d8-b6d1635a8d39 +:END: +- [[https://www2.svjatoslav.eu/gitweb/?p=alyverkko-cli.git;a=snapshot;h=HEAD;sf=tgz][Download latest snapshot in TAR GZ format]] + +- [[https://www2.svjatoslav.eu/gitweb/?p=alyverkko-cli.git;a=summary][Browse Git repository online]] + +- Clone Git repository using command: + : git clone https://www3.svjatoslav.eu/git/alyverkko-cli.git + +- See [[https://www3.svjatoslav.eu/projects/alyverkko-cli/apidocs/][JavaDoc]]. + +- See [[https://www3.svjatoslav.eu/projects/alyverkko-cli/graphs/][automatically generated class diagrams]]. Here is [[https://www3.svjatoslav.eu/projects/javainspect/legend.png][legend]] to help + understand diagrams. Diagrams were generated using [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect tool]]. + +* Feature ideas + +- In task directory ignore binary files, use joinfiles command as + example how to ignore binary files. Perhaps extract plain text file + detection into some utility class. + +- Make text editor configurable in application properties file. + +- Extend the application's capabilities to include voice capture and + processing. + +- Implement image capture and processing features. diff --git a/doc/pausing and resuming.pdf b/doc/pausing and resuming.pdf new file mode 100644 index 0000000..a1a2be2 Binary files /dev/null and b/doc/pausing and resuming.pdf differ diff --git a/install b/install new file mode 100755 index 0000000..9439a1e --- /dev/null +++ b/install @@ -0,0 +1,96 @@ +#!/bin/bash + +SYSTEMD_SERVICE_FILE="/etc/systemd/system/alyverkko-cli.service" + + +# Function to install binary and jar +install_to_opt() { + sudo rm -rf /opt/alyverkko-cli/ + sudo mkdir -p /opt/alyverkko-cli/ + sudo chmod 755 /opt/alyverkko-cli/ + + sudo cp target/alyverkko-cli.jar "/opt/alyverkko-cli/alyverkko-cli.jar" + sudo cp "alyverkko-cli" "/opt/alyverkko-cli/alyverkko-cli" + sudo chmod +x "/opt/alyverkko-cli/alyverkko-cli" + sudo cp logo.png "/opt/alyverkko-cli/logo.png" + + sudo ln -sf "/opt/alyverkko-cli/alyverkko-cli" /usr/bin/alyverkko-cli +} + +# Function to install the desktop launcher +install_desktop_entry() { + sudo cp launchers/alyverkko-cli*.desktop /usr/share/applications/ + sudo chmod 644 /usr/share/applications/alyverkko-cli*.desktop +} + +# Function to install systemd service +install_systemd_service() { + + cat < /dev/null +[Unit] +Description=Älyverkko CLI daemon in task processor mode +After=network.target + +[Service] +User=$USER +ExecStart=/opt/alyverkko-cli/alyverkko-cli process +WorkingDirectory=/opt/alyverkko-cli +Nice=10 +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + + sudo systemctl daemon-reload + sudo systemctl enable alyverkko-cli + sudo systemctl start alyverkko-cli + sleep 1 + echo "Systemd service installed, enabled and started. Service status is:" + systemctl --no-pager -l status alyverkko-cli +} + + +# Function to pre-deploy example configuration YAML file +install_config_file() { + local alyverkko_config_dir="${HOME}/.config/alyverkko-cli" + + if [ ! -d "$alyverkko_config_dir" ]; then + mkdir -p "$alyverkko_config_dir" + cp alyverkko-cli.yaml "$alyverkko_config_dir/" + else + echo "Configuration directory already exists: $alyverkko_config_dir" + fi +} + +# Main installation function +main() { + # Build the application + mvn --settings maven.xml clean package + + install_to_opt + install_desktop_entry + install_config_file + + # Check if systemd service already exists + if [ -f "$SYSTEMD_SERVICE_FILE" ]; then + echo "Systemd service is already installed." + # Display the status without hanging + echo "Service status is:" + systemctl --no-pager -l status alyverkko-cli + else + # Install systemd service if requested + echo "Do you want to install Älyverkko CLI as a systemd service? (y/N)" + read install_service + + if [[ $install_service == [Yy] ]]; then + install_systemd_service + fi + fi + + echo "Installation complete." +} + +# Call the main installation function +main diff --git a/launchers/alyverkko-cli-pause.desktop b/launchers/alyverkko-cli-pause.desktop new file mode 100644 index 0000000..ae51de9 --- /dev/null +++ b/launchers/alyverkko-cli-pause.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Name=Pause Älyverkko CLI +Comment=Freeze the current llama-cli job +Icon=media-playback-pause +Exec=/usr/bin/killall -STOP llama-cli +Terminal=false +Categories=Development; diff --git a/launchers/alyverkko-cli-resume.desktop b/launchers/alyverkko-cli-resume.desktop new file mode 100644 index 0000000..029fad7 --- /dev/null +++ b/launchers/alyverkko-cli-resume.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Name=Resume Älyverkko CLI +Comment=Unfreeze the current llama-cli job +Icon=media-playback-pause +Exec=/usr/bin/killall -CONT llama-cli +Terminal=false +Categories=Development; diff --git a/launchers/alyverkko-cli.desktop b/launchers/alyverkko-cli.desktop new file mode 100644 index 0000000..54378ee --- /dev/null +++ b/launchers/alyverkko-cli.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Terminal=true +Name=Älyverkko CLI +Comment=Runner for artificial neural network service +Icon=/opt/alyverkko-cli/logo.png +Exec=/opt/alyverkko-cli/alyverkko-cli process +Categories=Development; diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..468758e Binary files /dev/null and b/logo.png differ diff --git a/maven.xml b/maven.xml new file mode 100644 index 0000000..505327a --- /dev/null +++ b/maven.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..624e71c --- /dev/null +++ b/pom.xml @@ -0,0 +1,209 @@ + + 4.0.0 + eu.svjatoslav + alyverkko-cli + 1.0-SNAPSHOT + Älyverkko CLI + AI engine wrapper + + + UTF-8 + UTF-8 + + + + svjatoslav.eu + https://svjatoslav.eu + + + + + eu.svjatoslav + svjatoslavcommons + 1.8 + + + eu.svjatoslav + cli-helper + 1.3 + + + org.testng + testng + 7.7.0 + test + + + org.junit.jupiter + junit-jupiter + RELEASE + test + + + com.fasterxml.jackson.core + jackson-databind + 2.13.4.1 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.13.0 + + + org.apache.commons + commons-lang3 + 3.12.0 + + + org.apache.commons + commons-io + 1.3.2 + + + org.projectlombok + lombok + 1.18.32 + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + 11 + true + UTF-8 + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.4 + + + attach-javadocs + + jar + + + + + + + + foo + bar + + + + ${java.home}/bin/javadoc + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4.3 + + UTF-8 + + + + + maven-assembly-plugin + + + + + eu.svjatoslav.alyverkko_cli.Main + + + + jar-with-dependencies + + alyverkko-cli + false + + + + + package-jar-with-dependencies + package + + single + + + + jar-with-dependencies + + + + eu.svjatoslav.alyverkko_cli.Main + + + + + + + + + + + org.apache.maven.wagon + wagon-ssh-external + 2.6 + + + + + + + + svjatoslav.eu + svjatoslav.eu + scpexe://svjatoslav.eu:10006/srv/maven + + + svjatoslav.eu + svjatoslav.eu + scpexe://svjatoslav.eu:10006/srv/maven + + + + + + svjatoslav.eu + Svjatoslav repository + https://www3.svjatoslav.eu/maven/ + + + + + scm:git:ssh://n0@svjatoslav.eu/home/git/repositories/alyverkko-cli.git + scm:git:ssh://n0@svjatoslav.eu/home/git/repositories/alyverkko-cli.git + + + + + diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/Command.java b/src/main/java/eu/svjatoslav/alyverkko_cli/Command.java new file mode 100644 index 0000000..01b48b3 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/Command.java @@ -0,0 +1,32 @@ +package eu.svjatoslav.alyverkko_cli; + +import java.io.IOException; + +/** + *

Base interface for all subcommands in the Älyverkko CLI. Each command must define its name and execution logic. + *

Commands typically: + *

    + *
  • Parse their own specific arguments
  • + *
  • Access the global configuration
  • + *
  • Handle I/O operations
  • + *
+ * + *

Commands should be stateless and self-contained, using the configuration object for persistent data when needed. + */ +public interface Command { + + /** + * @return the subcommand's name. + */ + String getCommandName(); + + /** + * Called to carry out the specific subcommand. Typically, reads + * command-line arguments and performs the desired action. + * + * @param args arguments passed after the subcommand name. + * @throws IOException if I/O operations fail. + * @throws InterruptedException if the operation is interrupted. + */ + void executeCommand(String[] args) throws IOException, InterruptedException; +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java b/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java new file mode 100644 index 0000000..289ede4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java @@ -0,0 +1,83 @@ +package eu.svjatoslav.alyverkko_cli; + +import eu.svjatoslav.alyverkko_cli.commands.*; +import eu.svjatoslav.alyverkko_cli.commands.task_processor.TaskProcessorCommand; +import eu.svjatoslav.alyverkko_cli.configuration.Configuration; + +import java.io.IOException; +import java.util.Optional; + +import static java.util.Arrays.copyOfRange; + +/** + * The main entry point for the Älyverkko CLI application. + * It processes subcommands such as "wizard", "selftest", "joinfiles", + * "mail", and "listmodels". + */ +public class Main { + + /** + * The list of all supported subcommands. + */ + private final java.util.List commands = java.util.Arrays.asList( + new ListModelsCommand(), + new TaskProcessorCommand(), + new JoinFilesCommand(), + new WizardCommand() + ); + + /** + * The active, loaded configuration for the entire application. + * May be null if the configuration is not loaded properly. + */ + public static Configuration configuration; + + /** + * Application entry point. Dispatches to a subcommand if one is + * specified; otherwise shows usage help. + * + * @param args command-line arguments; the first is the subcommand name. + */ + public static void main(final String[] args) throws IOException, InterruptedException { + new Main().handleCommand(args); + } + + /** + * Attempts to find and execute the subcommand specified in the given arguments, + * or prints a help message if no command is found. + * + * @param args the command-line arguments. + * @throws IOException if an I/O error occurs during command execution. + * @throws InterruptedException if the command is interrupted. + */ + public void handleCommand(String[] args) throws IOException, InterruptedException { + if (args.length == 0) { + showHelp(); + return; + } + + String commandName = args[0].toLowerCase(); + Optional commandOptional = commands.stream() + .filter(cmd -> cmd.getCommandName().equals(commandName)) + .findFirst(); + + if (!commandOptional.isPresent()) { + System.out.println("Unknown command: " + commandName); + showHelp(); + return; + } + + Command command = commandOptional.get(); + String[] remainingArgs = copyOfRange(args, 1, args.length); + command.executeCommand(remainingArgs); + } + + /** + * Displays a basic help message, listing available commands. + */ + private void showHelp() { + System.out.println("Älyverkko CLI\n"); + System.out.println("Available commands:"); + commands.forEach(cmd -> System.out.println(" " + cmd.getCommandName())); + } +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/Utils.java b/src/main/java/eu/svjatoslav/alyverkko_cli/Utils.java new file mode 100644 index 0000000..87e8531 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/Utils.java @@ -0,0 +1,23 @@ +package eu.svjatoslav.alyverkko_cli; + +/** + *

General utility functions for the Älyverkko CLI application. Currently provides ANSI color output capabilities for + * console messages. + *

Color formatting follows standard ANSI escape sequences, with specific methods for common message types like errors. + *

For future extensions, this class could include additional helper functions for file operations or string processing. + */ +public class Utils { + + /** + * Prints a message in red text to the console. + * + * @param message the text to print in red. + */ + public static void printRedMessageToConsole(String message) { + // set output color to red + System.out.print("\033[0;31m"); + System.out.print(message + "\n"); + // reset output color + System.out.print("\033[0m"); + } +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java new file mode 100644 index 0000000..aa0ff69 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java @@ -0,0 +1,215 @@ +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.configuration.ConfigurationHelper.getConfigurationFile; +import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.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: + *

+ *   alyverkko-cli joinfiles -s /path/to/source -p "*.java" -t "my_topic" --edit
+ * 
+ */ + +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.getTaskDirectory().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 .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 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"); + } +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java new file mode 100644 index 0000000..2ad1d7c --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java @@ -0,0 +1,53 @@ +package eu.svjatoslav.alyverkko_cli.commands; + +import eu.svjatoslav.alyverkko_cli.Command; +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.ConfigurationHelper.getConfigurationFile; +import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.loadConfiguration; + +/** + *

Displays all available AI models in the configured models directory. This command provides a quick overview of + * currently available models and their metadata. + *

The implementation: + *

    + *
  • Loads the configuration
  • + *
  • Instantiates ModelLibrary
  • + *
  • Prints model details using ModelLibrary's printModels()
  • + *
+ * + *

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 = loadConfiguration(getConfigurationFile(null)); + if (configuration == null){ + System.out.println("Failed to load configuration file"); + return; + } + + System.out.println("Listing models in directory: " + configuration.getModelsDirectory()); + ModelLibrary modelLibrary = new ModelLibrary(configuration.getModelsDirectory(), configuration.getModels()); + modelLibrary.printModels(); + } +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java new file mode 100644 index 0000000..7808a62 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java @@ -0,0 +1,482 @@ +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.configuration.Configuration; +import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper; +import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationModel; +import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser; +import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption; + +import java.io.*; +import java.nio.file.*; +import java.util.ArrayList; +import java.util.List; + +import static eu.svjatoslav.alyverkko_cli.Utils.printRedMessageToConsole; +import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.getConfigurationFile; +import static eu.svjatoslav.commons.cli_helper.CLIHelper.*; + +/** + *

Interactive configuration wizard that helps users validate and fix their configuration files. + * It performs system checks and offers to fix any missing or invalid paths, discovers new models, + * and updates the configuration accordingly. + *

Key workflow steps: + *

    + *
  1. Load or create configuration
  2. + *
  3. Validate core directory paths
  4. + *
  5. Discover and annotate new models
  6. + *
  7. Save updated configuration
  8. + *
+ *

When handling split models (.gguf files with part numbering), the wizard automatically + * detects base models and only adds part-1 files to the configuration. + */ +public class WizardCommand implements Command { + + // Command-line parser to handle wizard arguments + private final Parser cliParser = new Parser(); + + /** + * Optional CLI argument for specifying a configuration file path. + */ + public FileOption configFileOption = cliParser.add(new FileOption("Configuration file path")) + .addAliases("--config", "-c"); + + /** + * The configuration object (loaded or newly created) + */ + private Configuration configuration; + + private File configurationFile; + + private boolean configurationUpdated = false; + private boolean modelsUpdated = false; + + @Override + public String getCommandName() { + return "wizard"; + } + + @Override + public void executeCommand(String[] cliArguments) throws IOException { + // Parse command-line arguments + if (!cliParser.parse(cliArguments)) { + System.out.println("Failed to parse command-line arguments"); + cliParser.showHelp(); + return; + } + + configurationFile = getConfigurationFile(configFileOption); + loadOrCreateConfiguration(); + + checkAndFixGeneralParameters(); + + fixModelEntries(); + + trySaveConfiguration(); + + if (modelsUpdated) { + System.out.println("Configuration has been updated. Please open the configuration file in a text editor to review and adjust model settings as needed."); + } + } + + private void loadOrCreateConfiguration() throws IOException { + validateConfigurationFile(); + + if (configurationFile.exists()) { + System.out.println("Found existing configuration at: \"" + configurationFile.getAbsolutePath() + "\""); + configuration = ConfigurationHelper.loadConfiguration(configurationFile); + } else { + // If no config found, create a fresh one + System.out.println("Existing configuration not found at \"" + + configurationFile.getAbsolutePath() + "\". Initializing new blank configuration."); + configuration = new Configuration(); + configurationUpdated = true; + } + } + + /** + * Validates the configuration file path and checks if it is a valid file. + * If not, prints an error message and exits the program. + */ + private void validateConfigurationFile() { + if (!configurationFile.exists()) return; // No need to check further if it doesn't exist + + // Check if the file is a directory + if (configurationFile.isDirectory()) { + System.err.println("ERROR: Configuration file path incorrectly points to a directory: \"" + configurationFile.getAbsolutePath() + + "\". Please specify a file instead."); + System.exit(1); + } + + // Check if the file is readable + if (!configurationFile.canRead()) { + System.err.println("ERROR: Cannot read configuration file: \"" + configurationFile.getAbsolutePath() + + "\". Please check permissions."); + System.exit(1); + } + + // Check if the file is writable + if (!configurationFile.canWrite()) { + System.err.println("ERROR: Cannot write to configuration file: \"" + configurationFile.getAbsolutePath() + + "\". Please check file permissions."); + System.exit(1); + } + } + + /** + * Step-by-step checking (and possibly fixing) of each main config parameter. + */ + private void checkAndFixGeneralParameters() { + configuration.setTaskDirectory( + checkDirectory( + configuration.getTaskDirectory(), + "Mail directory", + true, + "The mail directory is where the AI will look for tasks to solve. " + + "It should be a directory that you can write to. Please specify new mail directory path.", + true) + ); + + configuration.setModelsDirectory( + checkDirectory( + configuration.getModelsDirectory(), + "Models directory", + null, + "The models directory is where the AI model files (*.gguf) are stored. " + + "Please specify new models directory path.", + true) + ); + + configuration.setSkillsDirectory( + checkDirectory( + configuration.getSkillsDirectory(), + "Prompts directory", + null, + "The prompts directory is where the AI prompt files (*.yaml) are stored. " + + "Please specify new prompts directory path.", + true) + ); + + configuration.setLlamaCliPath( + checkFile( + configuration.getLlamaCliPath(), + "llama.cpp project llama-cli executable file path", + "The llama-cli is commandline engine that runs GGUF language models. " + + "Usually it is located under build/bin/ directory within llama.cpp project.", + true, + true) + ); + + // Default_temperature + Float temperature = configuration.getDefaultTemperature(); + if (temperature == null || temperature < 0f || temperature > 3f) { + configuration.setDefaultTemperature(askFloat( + "Enter default temperature (0-3). Lower => more deterministic, higher => more creative.", + temperature, + 0f, 3f, false + )); + } + // Thread_count + Integer threadCount = configuration.getThreadCount(); + if (threadCount == null || threadCount < 1) { + int defaultThreadCount = Runtime.getRuntime().availableProcessors() / 2; + if (defaultThreadCount < 1) defaultThreadCount = 1; + configuration.setThreadCount(askInteger( + "Enter number of CPU threads for AI generation. Typically RAM bandwidth gets saturated " + + "first and becomes bottleneck before all CPU cores can get fully utilized. So for 12 core CPU" + + " it might be enough to set 6 threads. Increasing this number higher yields diminishing returns.", + defaultThreadCount, + 1, null, false + )); + } + + // Batch thread count + Integer batchThreadCount = configuration.getBatchThreadCount(); + if (batchThreadCount == null || batchThreadCount < 1) { + int defaultThreadCount = Runtime.getRuntime().availableProcessors(); + configuration.setBatchThreadCount( + askInteger( + "\nEnter number of CPU threads for input prompt processing (all cores is often fine).", + defaultThreadCount, + 1, null, false + )); + } + } + + /** + * Validates a directory path and prompts user to fix if needed. + * @param directory current directory value + * @param directoryName name to display to user + * @param writable if directory must be writable (null = no check) + * @param explanation message to show user when prompting + * @param offerToCreate if true, offers to create directory if missing + * @return validated directory path + */ + private File checkDirectory( + File directory, + String directoryName, + Boolean writable, + String explanation, + boolean offerToCreate) { + + while (true) { + // Check if the directory is null + boolean allOk = true; + if (directory == null) { + System.out.println(directoryName + " is not defined."); + allOk = false; + directory = getNewDirectoryPath(explanation, writable); + } + + if (!directory.exists()) { + System.out.println(directoryName + " does not exist: " + directory.getAbsolutePath()); + allOk = false; + // Offer to create it + if (offerToCreate) { + if (askBoolean("Create " + directoryName + " ?", true)) { + boolean created = directory.mkdirs(); + if (!created) { + printRedMessageToConsole("Failed to create \"" + directory + "\". Check permissions"); + } else { + System.out.println(directoryName + " created at: " + directory.getAbsolutePath()); + } + } + } + } + + if (!directory.isDirectory()) { + System.out.println(directoryName + " is not a directory: " + directory.getAbsolutePath()); + allOk = false; + directory = getNewDirectoryPath(explanation, writable); + } + + if (writable != null && writable && !directory.canWrite()) { + System.out.println(directoryName + " is not writable: " + directory.getAbsolutePath()); + allOk = false; + directory = getNewDirectoryPath(explanation, writable); + } + + if (allOk) { + System.out.println(directoryName + " is: " + directory.getAbsolutePath()); + return directory; + } + + configurationUpdated = true; + } + } + + /** + * Validates a file path and prompts user to fix if needed. + * @param file current file value + * @param fileName name to display to user + * @param mustExist if file must exist (null = no check) + * @param explanation message to show user when prompting + * @param executable if file must be executable (null = no check) + * @return validated file path + */ + private File checkFile( + File file, + String fileName, + String explanation, + Boolean mustExist, + Boolean executable) { + + while (true) { + boolean allOk = true; + if (file == null) { + System.out.println(fileName + " is not defined."); + allOk = false; + file = askFile(explanation, null, mustExist, null, null, executable, false); + } + + if (mustExist != null && mustExist && !file.exists()) { + System.out.println(fileName + " does not exist: " + file.getAbsolutePath()); + allOk = false; + file = askFile(explanation, null, mustExist, null, null, executable, false); + } + + if (!file.isFile()) { + System.out.println(fileName + " is not a file: " + file.getAbsolutePath()); + allOk = false; + file = askFile(explanation, null, null, null, null, executable, false); + } + + if (executable != null && executable && !file.canExecute()) { + System.out.println(fileName + " is not executable: " + file.getAbsolutePath()); + allOk = false; + file = askFile(explanation, null, true, null, null, executable, false); + } + + if (allOk) { + System.out.println(fileName + " is: " + file.getAbsolutePath()); + return file; + } + + configurationUpdated = true; + } + } + + private static File getNewDirectoryPath(String directoryName, Boolean writable) { + return askDirectory(directoryName, null, null, null, writable, null, false); + } + + /** + * Saves the config to the default path, verifying if user wants to + * overwrite if it already exists, etc. + */ + private void trySaveConfiguration() { + if (!configurationUpdated) { + System.out.println("No changes made to the configuration. Not saving."); + return; + } + + // ask user if user wants to save configuration + if (!askBoolean("Save configuration to: " + configurationFile, true, false)) + return; + + boolean fileExisted = configurationFile.exists(); + + try { + Files.createDirectories(configurationFile.toPath().getParent()); + try (BufferedWriter writer = Files.newBufferedWriter( + configurationFile.toPath(), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + )) { + new ObjectMapper(new YAMLFactory()).writeValue(writer, configuration); + } + if (fileExisted) { + System.out.println("Existing configuration updated at: " + configurationFile.toPath()); + } else { + System.out.println("New configuration created at: " + configurationFile.toPath()); + } + } catch (IOException e) { + printRedMessageToConsole("Error saving configuration: " + e.getMessage()); + } + } + + /** + * Generates an alias from a .gguf filename by removing non-alphanumeric chars. + */ + private String suggestAlias(String filePath) { + String fileName = new File(filePath).getName(); + // Remove .gguf extension + fileName = fileName.replaceFirst("\\.gguf$", ""); + // Check if it's a split model + if (fileName.matches(".*-\\d{5}-of-\\d{5}")) { + // Extract base name by removing the part numbers + fileName = fileName.replaceFirst("-\\d{5}-of-\\d{5}", ""); + } + // Replace non-alphanumeric characters with hyphens + String alias = fileName.replaceAll("[^a-zA-Z0-9]", "-").toLowerCase(); + // Normalize hyphens and trim leading/trailing hyphens + return alias.replaceAll("-+", "-").replaceAll("^-|-$", ""); + } + + private void fixModelEntries() { + File modelsDir = configuration.getModelsDirectory(); + if (modelsDir == null || !modelsDir.exists() || !modelsDir.isDirectory()) { + System.out.println("Models directory is not properly configured. Skipping model checks."); + return; + } + + List existingModels = configuration.getModels(); + if (existingModels == null) { + existingModels = new ArrayList<>(); + configuration.setModels(existingModels); + configurationUpdated = true; + } + + annotateMissingModels(); + + discoverNewModels(); + } + + private void discoverNewModels() { + // List all .gguf files in models directory + File[] files = configuration.getModelsDirectory().listFiles((dir, name) -> name.endsWith(".gguf")); + if (files == null) return; + + for (File file : files) { + String relativePath = configuration.getModelsDirectory().toPath().relativize(file.toPath()).toString(); + if (isExistingModel(relativePath)) continue; + + processPotentialNewModelFile(file, relativePath); + } + } + + private boolean isExistingModel(String relativePath) { + return configuration.getModels().stream() + .anyMatch(m -> m.getFilesystemPath().equals(relativePath)); + } + + private void processPotentialNewModelFile(File file, String relativePath) { + // Check if it's a split model + if (isSplitModel(file.getName())) { + handleSplitModel(file, relativePath); + } else { + addNewModel(relativePath); + } + } + + private boolean isSplitModel(String fileName) { + return fileName.matches(".*-\\d{5}-of-\\d{5}\\.gguf"); + } + + private void handleSplitModel(File file, String relativePath) { + String baseName = relativePath.replaceFirst("-\\d{5}-of-\\d{5}\\.gguf", ""); + if (configuration.getModels().stream().anyMatch(m -> m.getAlias().startsWith(baseName))) { + return; + } + + // Extract part number + String partNumberStr = relativePath.replaceAll(".*-(\\d{5}-of-\\d{5}\\.gguf)", "$1"); + int partNumber = Integer.parseInt(partNumberStr.split("-of-")[0]); + if (partNumber == 1) { + addNewModel(relativePath); + } + } + + private void addNewModel(String relativePath) { + ConfigurationModel newModel = getNewModel(relativePath); + configuration.getModels().add(newModel); + System.out.println("Added new model: " + newModel.getAlias() + " (" + newModel.getFilesystemPath() + ")"); + configurationUpdated = true; + modelsUpdated = true; + } + + private ConfigurationModel getNewModel(String relativePath) { + String suggestedAlias = suggestAlias(relativePath); + ConfigurationModel newModel = new ConfigurationModel(); + newModel.setAlias(suggestedAlias + "-new"); + newModel.setFilesystemPath(relativePath); + newModel.setContextSizeTokens(32768); // Default context size + newModel.setEndOfTextMarker(null); // Default end marker + return newModel; + } + + private void annotateMissingModels() { + // Process existing models to add/remove -missing suffix + for (ConfigurationModel model : configuration.getModels()) { + File modelFile = new File(configuration.getModelsDirectory(), model.getFilesystemPath()); + if (!modelFile.exists()) { + if (!model.getAlias().endsWith("-missing")) { + model.setAlias(model.getAlias() + "-missing"); + System.out.println("Marked model as missing: " + model.getAlias()); + configurationUpdated = true; + modelsUpdated = true; + } + } else { + if (model.getAlias().endsWith("-missing")) { + model.setAlias(model.getAlias().replaceFirst("-missing$", "")); + System.out.println("Removed -missing suffix from model: " + model.getAlias()); + configurationUpdated = true; + modelsUpdated = true; + } + } + } + } + +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/package-info.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/package-info.java new file mode 100644 index 0000000..871a60b --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/package-info.java @@ -0,0 +1,13 @@ +/** + *

This package implements all subcommands available in the Älyverkko CLI application. Each command class provides a + * specific functionality through the Command interface. + *

Available commands include: + *

    + *
  • Wizard-style configuration builder
  • + *
  • Model listing and management
  • + *
  • File joining for multi-file processing
  • + *
  • Mail-based AI task processing
  • + *
+ */ + +package eu.svjatoslav.alyverkko_cli.commands; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/Task.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/Task.java new file mode 100644 index 0000000..6265dd6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/Task.java @@ -0,0 +1,144 @@ +package eu.svjatoslav.alyverkko_cli.commands.task_processor; + +import eu.svjatoslav.alyverkko_cli.configuration.SkillConfig; +import eu.svjatoslav.alyverkko_cli.model.Model; + +import static eu.svjatoslav.alyverkko_cli.Main.configuration; + +/** + * Represents the data needed to perform a single task based AI query, + * containing prompts and the specific AI model to use. + */ +public class Task { + + /** + * 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 name of the specific skill or capability that the AI should utilize + * when processing the query. This can help determine the focus or purpose + * of the response generated by the AI. + */ + public String skillName; + + public SkillConfig skill; + + /** + * 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; + + /** + * The start time of the query (milliseconds since epoch). + */ + public long startTimeMillis; + + /** + * The end time of the query (milliseconds since epoch). + */ + public long endTimeMillis; + + /** + * Task priority. Bigger integer has higher priority. + */ + public int priority; + + + /** + * Returns a string containing a summary of the {@link Task} 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 + + '}'; + } + + /** + * Calculate effective temperature based on override hierarchy. + */ + public float getEffectiveTemperature() { + // 1. Skill specific temperature has priority + if (skill != null && skill.getTemperature() != null) { + return skill.getTemperature(); + } + + // 2. If not in skill, check if model specifies it + if (model.temperature != null) { + return model.temperature; + } + + // 3. Fall back to global default + return configuration.getDefaultTemperature(); + } + + /** + * Calculates effective top-p value using hierarchy: + * skill-specific → model-specific → global default + * + * @return the applicable top-p value for this query + */ + public float getEffectiveTopP() { + // Skill-specific has highest priority + if (skill != null && skill.getTopP() != null) { + return skill.getTopP(); + } + + // Model-specific next + if (model.topP != null) { + return model.topP; + } + + // Global default as fallback + return configuration.getDefaultTopP(); + } + + /** + * Calculates effective repeat penalty using hierarchy: + * skill-specific → model-specific → global default + * + * @return the applicable repeat penalty for this query + */ + public float getEffectiveRepeatPenalty() { + // Skill-specific has highest priority + if (skill != null && skill.getRepeatPenalty() != null) { + return skill.getRepeatPenalty(); + } + + // Model-specific next + if (model.repeatPenalty != null) { + return model.repeatPenalty; + } + + // Global default as fallback + return configuration.getDefaultRepeatPenalty(); + } + + + public float getEffectiveTopK() { + if (skill != null && skill.getTopK() != null) return skill.getTopK(); + else if (model.topK != null) return model.topK.floatValue(); + else return configuration.getDefaultTopK().floatValue(); + } + + public float getEffectiveMinP() { + if (skill != null && skill.getMinP() != null) return skill.getMinP(); + else if (model.minP != null) return model.minP.floatValue(); + else return configuration.getDefaultMinP().floatValue(); + } + + +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/TaskProcess.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/TaskProcess.java new file mode 100644 index 0000000..92eb873 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/TaskProcess.java @@ -0,0 +1,298 @@ +package eu.svjatoslav.alyverkko_cli.commands.task_processor; + +import eu.svjatoslav.alyverkko_cli.Utils; + +import java.io.*; +import java.nio.file.Files; + +import static eu.svjatoslav.alyverkko_cli.Main.configuration; +import static java.lang.String.join; + +/** + * + * TODO: what if directory disappeared that contained original input file ? Response cannot be written back anymore. + * + *

Executes AI inference tasks through llama.cpp CLI. This class handles the complete workflow + * from prompt construction to response formatting, including temporary file management and process execution. + *

Key processing steps: + *

    + *
  1. Build standardized input prompt
  2. + *
  3. Create a temporary input file
  4. + *
  5. Execute llama.cpp with appropriate parameters
  6. + *
  7. Capture and filter output
  8. + *
  9. Perform cleanup operations
  10. + *
+ * + *

Temperature settings, context size, and thread counts are all derived from the current configuration. + * The response is formatted to match org-mode conventions while preserving original conversation structure. + */ +public class TaskProcess { + + /** + * Marker used by llama.cpp to print metadata. We monitor and display these lines. + */ + private static final String LLAMA_CPP_META_INFO_MARKER = "llm_load_print_meta: "; + + /** + * The mail query defining system prompt, user prompt, and which model to use. + */ + private final Task task; + + /** + * Temporary file used as input to the llama.cpp CLI. + */ + private File inputFile; + + /** + * Creates a new AI task with a given mail query. + * + * @param task the mail query containing model and prompts. + */ + public TaskProcess(Task task) { + this.task = task; + } + + /** + * Builds the prompt text that is fed to llama.cpp, including the system prompt, + * the user prompt, and an "ASSISTANT:" marker signifying where the AI response begins. + * + * @return a string containing the fully prepared query prompt. + */ + private String buildAiQuery() { + return task.systemPrompt.replace("", task.userPrompt); + } + + /** + * Runs the AI query by constructing the prompt, writing it to a temp file, + * invoking llama.cpp, collecting output, and performing any final cleanup. + * + * @return the AI's response in a format suitable for appending back into + * the conversation file. + * @throws InterruptedException if the process is interrupted. + * @throws IOException if reading/writing the file fails or the process fails to start. + */ + public String runAiQuery() throws InterruptedException, IOException { + try { + // Record the start time of the query + task.startTimeMillis = System.currentTimeMillis(); + + // Build input prompt + initializeInputFile(buildAiQuery()); + + // Prepare a process builder + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.command(getCliCommand().split("\\s+")); // Splitting the command string into tokens + + // Start process + Process process = processBuilder.start(); + + // Handle process's error stream + handleErrorThread(process); + + // Handle the process's output stream + StringBuilder result = new StringBuilder(); + Thread outputThread = handleResultThread(process, result); + + // Wait for the process to finish + process.waitFor(); + + // Wait for the output thread to finish reading + outputThread.join(); + + // Record the end time of the query + task.endTimeMillis = System.currentTimeMillis(); + + // Clean up the AI response: remove partial prompt text, end-of-text marker, etc. + return cleanupAiResponse(result.toString()); + } finally { + deleteTemporaryFile(); + } + } + + /** + * Creates a temporary file for the AI input and writes the prompt to it. + * + * @param aiQuery the final prompt string for the AI to process. + * @throws IOException if file creation or writing fails. + */ + private void initializeInputFile(String aiQuery) throws IOException { + inputFile = createTemporaryFile(); + Files.write(inputFile.toPath(), aiQuery.getBytes()); + } + + /** + * Creates a temporary file that will be used for the AI prompt input. + * + * @return a new {@link File} referencing the created temporary file. + * @throws IOException if the file could not be created. + */ + private File createTemporaryFile() throws IOException { + File file = Files.createTempFile("ai-inference", ".tmp").toFile(); + file.deleteOnExit(); + return file; + } + + /** + * Cleans up the AI response by removing the partial text before the + * AI response marker and after the end-of-text marker, if specified. + * + * @param result the raw output from llama.cpp. + * @return the cleaned AI response. + */ + private String cleanupAiResponse(String result) { + + // remove text after the end of text marker if it exists + if (task.model.endOfTextMarker != null) { + int endOfTextMarkerIndex = result.indexOf(task.model.endOfTextMarker); + if (endOfTextMarkerIndex != -1) { + result = result.substring(0, endOfTextMarkerIndex); + } + } + + return result; + } + + /** + * Returns the full command string used to run the AI inference via llama.cpp. + * + * @return a string representing the command and all arguments. + */ + private String getCliCommand() { + int niceValue = 10; // niceness level for background tasks + String executablePath = configuration.getLlamaCliPath().getAbsolutePath(); + + return join(" ", + "nice", "-n", Integer.toString(niceValue), + executablePath, + "--model " + task.model.filesystemPath, + "--threads " + configuration.getThreadCount(), + "--threads-batch " + configuration.getBatchThreadCount(), + + // Restricts token selection to the smallest possible set of tokens whose cumulative probability + // exceeds the specified threshold P. + "--top-p " + task.getEffectiveTopP(), + + "--repeat-penalty " + task.getEffectiveRepeatPenalty(), + + // Restricts token selection to the K tokens with the highest probabilities. + "--top-k " + task.getEffectiveTopK(), + + // Filters the vocabulary to include only tokens whose + // probability is at least a certain fraction (Min P) of the + // probability of the most likely token. + "--min-p " + task.getEffectiveMinP(), + + // Ensure that model sees the <|im_start|>system … / <|im_start|>user … markup it was trained on + // "--chat-format qwen3", + + // Last n tokens to consider for penalizing repetition + "--repeat-last-n 512", + + // Controls the strength of the penalty for a detected repetition sequence. + //"--dry-multiplier 0.1", + + // In a code we want the model to reuse the same variable names, keywords, and syntax consistently. + // A presence penalty, even a small 0.1, could cause the model to needlessly rename variables. + //"--presence-penalty 1", + + "--mirostat 0", // Disable mirostat + + "--no-display-prompt", + "--no-warmup", + "--flash-attn on", + "--temp " + task.getEffectiveTemperature(), + "--ctx-size " + task.model.contextSizeTokens, + "--batch-size 512", + "--single-turn", // run conversation for a single turn only, then exit when done + "-n -1", + "--file " + inputFile + + // "--cache-type-k q8_0", + // might save RAM, need to test if precision loss is acceptable + + // might save RAM, need to test if precision loss is acceptable + // "--cache-type-v q8_0", + + ); + + + } + + + + + /** + * Spawns a new Thread to handle the error stream from llama.cpp, + * printing lines that contain metadata or errors to the console. + * + * @param process the process whose error stream is consumed. + */ + private static void handleErrorThread(Process process) { + Thread errorThread = new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + while ((line = reader.readLine()) != null) { + handleErrorStreamLine(line); + } + } catch (IOException e) { + System.err.println("Error reading error stream: " + e.getMessage()); + } + }); + errorThread.start(); + } + + /** + * Decides what to do with each line from the error stream: + * if it matches the llama.cpp meta-info marker, print it normally; + * otherwise print as an error. + * + * @param line a line from the llama.cpp error stream. + */ + private static void handleErrorStreamLine(String line) { + if (line.startsWith(LLAMA_CPP_META_INFO_MARKER)) { + // Print the meta-info to the console in normal color + System.out.println(line.substring(LLAMA_CPP_META_INFO_MARKER.length())); + } else { + // Print actual error lines in red + Utils.printRedMessageToConsole(line); + } + } + + /** + * Consumes the standard output (inference result) from the + * llama.cpp process, storing it into a result buffer for further + * cleanup, while simultaneously printing it to the console. + * + * @param process the AI inference process. + * @param result a string builder to accumulate the final result. + * @return the thread that is reading the output stream. + */ + private static Thread handleResultThread(Process process, StringBuilder result) { + Thread outputThread = new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String aiResultLine; + while ((aiResultLine = reader.readLine()) != null) { + System.out.print("AI: " + aiResultLine + "\n"); // Show each line in real-time + result.append(aiResultLine).append("\n"); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + outputThread.start(); + return outputThread; + } + + /** + * Deletes the temporary input file once processing is complete. + */ + private void deleteTemporaryFile() { + if (inputFile != null && inputFile.exists()) { + try { + Files.delete(inputFile.toPath()); + } catch (IOException e) { + System.err.println("Failed to delete temporary file: " + e.getMessage()); + } + } + } +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/TaskProcessorCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/TaskProcessorCommand.java new file mode 100644 index 0000000..acfbbf5 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/TaskProcessorCommand.java @@ -0,0 +1,545 @@ +package eu.svjatoslav.alyverkko_cli.commands.task_processor; + +import eu.svjatoslav.alyverkko_cli.*; +import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper; +import eu.svjatoslav.alyverkko_cli.configuration.SkillConfig; +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.*; + +import static eu.svjatoslav.alyverkko_cli.Main.configuration; +import static eu.svjatoslav.commons.file.IOHelper.getFileContentsAsString; +import static eu.svjatoslav.commons.file.IOHelper.saveToFile; +import static java.nio.file.StandardWatchEventKinds.*; + + +/** + * TODO: What happens when directory gets renamed ? Will event listener reindex all files inside it for processing ? + * + * The TaskProcessorCommand continuously monitors a specified tasks + * directory for new or modified text files, checks if they have a + * "TOCOMPUTE:" marker, and if so, adds them to a priority queue to be + * processed in priority order. Once processed, results are appended to + * the same file. + *

+ * Usage: + *

+ *   alyverkko-cli mail
+ * 
+ */ +public class TaskProcessorCommand 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 taskDirectory; + + /** + * Priority queue of tasks to process, sorted by priority and a + * random tiebreaker. + */ + private final PriorityQueue taskQueue; + + public TaskProcessorCommand() { + Comparator comparator = (a, b) -> { + int priorityCompare = Integer.compare(b.priority, a.priority); + if (priorityCompare != 0) { + return priorityCompare; + } + return a.tiebreaker.compareTo(b.tiebreaker); + }; + this.taskQueue = new PriorityQueue<>(comparator); + } + + /** + * @return the name of this command, i.e., "mail". + */ + @Override + public String getCommandName() { + return "process"; + } + + /** + * Executes the "mail" command, loading configuration, starting a + * WatchService on the mail directory, adding existing files to the + * task queue, and processing tasks in priority order. + * + * @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 executeCommand(String[] cliArguments) throws IOException, InterruptedException { + if (!parser.parse(cliArguments)) { + System.out.println("Failed to parse commandline arguments"); + parser.showHelp(); + return; + } + + configuration = ConfigurationHelper.loadConfiguration(ConfigurationHelper.getConfigurationFile(configFileOption)); + if (configuration == null) { + System.out.println("Failed to load configuration file"); + return; + } + + modelLibrary = new ModelLibrary(configuration.getModelsDirectory(), configuration.getModels()); + taskDirectory = configuration.getTaskDirectory(); + + // Set up a directory watch service + initializeFileWatcher(); + + // Add all existing mail files to the queue + initialTaskScanAndReply(); + + System.out.println("Mail correspondent running. Press CTRL+c to terminate."); + + // Main loop: process tasks from the queue in priority order + while (true) { + // Process the highest priority task if available + if (!taskQueue.isEmpty()) processTask(taskQueue.poll()); + + // Check for filesystem events + WatchKey key = directoryWatcher.poll(); + + // Sleep briefly to allow the file to be fully written + Thread.sleep(1000); + + if (key != null) { + System.out.println("Detected filesystem events in mail directory. Processing ... "); + processDetectedFilesystemEvents(key); + + if (!key.reset()) { + break; + } + } + + } + + directoryWatcher.close(); + } + + private void initialTaskScanAndReply() throws IOException { + Files.walk(taskDirectory.toPath()) + .filter(path -> Files.isRegularFile(path)) + .forEach(path -> { + try { + considerFileForQueuing(path); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + /** + * 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; + } + + // Ensure the file exists + if (!file.exists()) { + 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 { + String firstLine = getFirstLine(file); + return firstLine != null && firstLine.startsWith("TOCOMPUTE:"); + } + + private static String getFirstLine(File file) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + return reader.readLine(); + } + } + + private static void saveAiResponseToFile(File file, Task task, String aiResponse) throws IOException { + StringBuilder resultFileContent = new StringBuilder(); + + // The First line should be "DONE:" with settings used to process this file + resultFileContent.append(getDoneLine(task)); + + resultFileContent.append("* USER:\n"); + + // Append the original user prompt (after the first line) + resultFileContent.append(task.userPrompt).append("\n"); + + // Append the AI response block + resultFileContent + .append("* ASSISTANT:\n") + .append(aiResponse) + .append("\n"); + + File newFile = new File(file.getParentFile(), "DONE: " + file.getName()); + saveToFile(newFile, resultFileContent.toString()); + + if (!file.delete()) { + System.err.println("Failed to delete original file: " + file.getAbsolutePath()); + } + } + + /** + * Processes a task by reading the file, building the MailQuery, + * running the AI query, and saving the response. + * + * @param entry the task entry containing the file path and priority. + */ + private void processTask(TaskQueueEntry entry) throws IOException { + Path filePath = entry.getFilePath(); + File file = filePath.toFile(); + + if (!isMailProcessingNeeded(file)) { + System.out.println("Ignoring file: " + filePath.getFileName() + " (does not need processing now)"); + return; + } + + try { + Task task = buildMailQueryFromTaskFile(file); + TaskProcess aiTask = new TaskProcess(task); + String aiGeneratedResponse = aiTask.runAiQuery(); + + saveAiResponseToFile(file, task, aiGeneratedResponse); + } catch (IOException | InterruptedException | RuntimeException e) { + e.printStackTrace(); + } + } + + + /** + * Builds a string for the first line of the output file indicating + * that processing is done. + * + * @param task the query that was processed. + * @return a string for the first line. + */ + private static String getDoneLine(Task task) { + return "DONE: skill=" + task.skillName + + " model=" + task.model.alias + + " duration=" + getDuration(task.startTimeMillis, task.endTimeMillis) + "\n"; + } + + + private static String getDuration(long startTimeMillis, long endTimeMillis) { + + long durationSeconds = (endTimeMillis - startTimeMillis) / 1000; + + if (durationSeconds < 180) return durationSeconds + "s"; + + long durationMinutes = durationSeconds / 60; + if (durationMinutes < 180) return durationMinutes + "m"; + + long durationHours = durationMinutes / 60; + return durationHours + "h"; + } + + /** + * Builds a MailQuery object from the contents of a file. + * + * @param file the file to read. + * @return the constructed MailQuery. + * @throws IOException if reading the file fails. + */ + private Task buildMailQueryFromTaskFile(File file) throws IOException { + Task result = new Task(); + + String inputFileContent = getFileContentsAsString(file); + int firstNewLineIndex = inputFileContent.indexOf('\n'); + if (firstNewLineIndex == -1) { + throw new IllegalArgumentException("Input file is only one line long."); + } + + String firstLine = inputFileContent.substring(0, firstNewLineIndex); + Map fileProcessingSettings = parseSettings(firstLine); + + // The rest of the file is the user prompt + result.userPrompt = inputFileContent.substring(firstNewLineIndex + 1); + + // Set system prompt + result.skillName = fileProcessingSettings.getOrDefault("skill", "default"); + SkillConfig skill = configuration.getSkillByName(result.skillName); + result.systemPrompt = skill.getPrompt(); + result.skill = skill; + + // Set AI model + String modelAlias = fileProcessingSettings.getOrDefault("model", "default"); + Optional modelOptional = modelLibrary.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; + if (priorityStr != null) { + try { + result.priority = Integer.parseInt(priorityStr); + } catch (NumberFormatException e) { + System.err.println("Invalid priority in file: " + priorityStr); + } + } + + return result; + } + + + /** + * Parses the "TOCOMPUTE:" line, which should look like: + *
TOCOMPUTE: key1=value1 key2=value2 ...
+ * + * @param toComputeLine the line beginning with "TOCOMPUTE:". + * @return a map of settings derived from that line. + */ + private Map 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 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, modification, or deletion), + * and updates the task queue accordingly. + * + * @param key the watch key containing the events. + * @throws IOException if file processing fails. + */ + private void processDetectedFilesystemEvents(WatchKey key) throws IOException { + Path dir = (Path) key.watchable(); + for (WatchEvent event : key.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + if (kind != ENTRY_CREATE && kind != ENTRY_MODIFY && kind != ENTRY_DELETE) { + continue; + } + @SuppressWarnings("unchecked") + WatchEvent ev = (WatchEvent) event; + Path filename = ev.context(); + Path fullPath = dir.resolve(filename); + System.out.printf("Event: %s – %s%n", kind.name(), fullPath); + if (kind == ENTRY_DELETE) { + // Remove any existing tasks for this file + removeTasksForFile(fullPath); + continue; + } + // Handle directory creation + if (Files.isDirectory(fullPath)) { + if (kind == ENTRY_CREATE) { + // Register the new directory and its subdirectories for monitoring + registerAllSubdirectories(fullPath); + // Scan the new directory for existing files to process + Files.walk(fullPath) + .filter(path -> { + try { + return Files.isRegularFile(path) && !Files.isHidden(path); + } catch (IOException e) { + System.err.println("Failed to check if file is hidden: " + path + " - " + e.getMessage()); + return false; // Skip files that cause errors + } + }) + .forEach(path -> { + try { + considerFileForQueuing(path); + } catch (IOException e) { + System.err.println("Failed to process file in new directory: " + path + " - " + e.getMessage()); + } + }); + } + continue; + } + // Handle file events + if (kind == ENTRY_MODIFY) { + // Remove existing tasks for this file before adding new ones + removeTasksForFile(fullPath); + } + // Check if it's a regular, non-hidden file and needs processing + try { + if (Files.isRegularFile(fullPath) && !Files.isHidden(fullPath)) { + considerFileForQueuing(fullPath); + } + } catch (IOException e) { + System.err.println("Failed to check file: " + fullPath + " - " + e.getMessage()); + } + } + } + + private void initializeFileWatcher() throws IOException { + this.directoryWatcher = FileSystems.getDefault().newWatchService(); + registerAllSubdirectories(taskDirectory.toPath()); + } + + private void registerAllSubdirectories(Path path) { + try { + System.out.println("Registering directory for watch service: " + path); + path.register(directoryWatcher, ENTRY_CREATE, ENTRY_MODIFY); + if (Files.isDirectory(path)) { + try (DirectoryStream stream = Files.newDirectoryStream(path)) { + for (Path entry : stream) { + if (Files.isDirectory(entry)) { + registerAllSubdirectories(entry); + } + } + } + } + } catch (IOException e) { + System.err.println("Failed to register directory: " + path + " - " + e.getMessage()); + } + } + + /** + * Adds a file to the task queue if it needs processing. + * + * @param filePath the path to the file to check. + * @throws IOException if reading the first line fails. + */ + private void considerFileForQueuing(Path filePath) throws IOException { + File file = filePath.toFile(); + if (!isMailProcessingNeeded(file)) return; + + String firstLine = getFirstLine(file); + Map settings = parseSettings(firstLine); + int priority = 0; + String priorityStr = settings.get("priority"); + if (priorityStr != null) { + try { + priority = Integer.parseInt(priorityStr); + } catch (NumberFormatException e) { + System.err.println("Invalid priority in file " + filePath.getFileName() + ": " + priorityStr); + } + } + taskQueue.offer(new TaskQueueEntry(filePath, priority)); + } + + /** + * A static nested class representing a task in the queue, with a + * priority and a tiebreaker for sorting. + */ + private static class TaskQueueEntry implements Comparable { + private final Path filePath; + private final int priority; + private final String tiebreaker; + + public TaskQueueEntry(Path filePath, int priority) { + this.filePath = filePath; + this.priority = priority; + this.tiebreaker = UUID.randomUUID().toString(); + } + + public Path getFilePath() { + return filePath; + } + + public int getPriority() { + return priority; + } + + public String getTiebreaker() { + return tiebreaker; + } + + @Override + public int compareTo(TaskQueueEntry other) { + int priorityCompare = Integer.compare(this.priority, other.priority); + if (priorityCompare != 0) { + return -priorityCompare; // higher priority first + } + return this.tiebreaker.compareTo(other.tiebreaker); + } + } + + /** + * Removes all tasks from the queue that match the given file path. + * This is done by draining the queue into a list, filtering out the matching entries, + * and re-adding the remaining ones back into the queue. + * + * @param filePath the file path to match and remove from the queue. + */ + private void removeTasksForFile(Path filePath) { + List remaining = new ArrayList<>(); + + // Drain all elements into a list + while (!taskQueue.isEmpty()) { + TaskQueueEntry entry = taskQueue.poll(); + if (!entry.getFilePath().equals(filePath)) { + remaining.add(entry); + } + } + + // Re-add all remaining entries back to the queue + for (TaskQueueEntry entry : remaining) { + taskQueue.offer(entry); + } + } + +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/package-info.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/package-info.java new file mode 100644 index 0000000..51e74a4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/package-info.java @@ -0,0 +1,12 @@ +/** + *

This subpackage implements the mail-based AI task processing functionality. It watches mail directories for + * new/modified files with specific markers and processes them using AI models. + *

Key components: + *

    + *
  • File monitoring with WatchService
  • + *
  • Prompt parsing and execution logic
  • + *
  • Query object for storing processing parameters
  • + *
+ */ + +package eu.svjatoslav.alyverkko_cli.commands.task_processor; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java new file mode 100644 index 0000000..6872648 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java @@ -0,0 +1,118 @@ +package eu.svjatoslav.alyverkko_cli.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.*; +import java.util.List; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + + +/** + *

Central configuration class storing all application parameters. + * This class is serialized to YAML format for user editing and persistence. + *

Configuration parameters include: + *

    + *
  • Model and prompt directories
  • + *
  • Performance tuning parameters
  • + *
  • Model-specific configurations
  • + *
+ *

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 +public class Configuration { + + /** + * Directory where AI tasks (mail) are placed and discovered. + */ + @JsonProperty("task_directory") + private File taskDirectory; + + /** + * Directory that contains AI model files in GGUF format. + */ + @JsonProperty("models_directory") + private File modelsDirectory; + + /** + * The default "temperature" used by the AI for creative/deterministic + * tradeoff. Ranges roughly between 0 and 3. + */ + @JsonProperty("default_temperature") + private Float defaultTemperature; + + /** + * Default top-p value used when not specified elsewhere. + * Controls diversity of sampling (0.0-1.0, where 1.0 means no restriction). + */ + @JsonProperty("default_top_p") + private Float defaultTopP; + + /** + * Default global Top-K value used when not specified elsewhere. + */ + @JsonProperty("default_top_k") + private Float defaultTopK = 30f; + + /** + * Global minimum-p threshold default (applies if none set). + */ + @JsonProperty("default_min_p") + private Float defaultMinP = 0f; + + /** + * Default repeat penalty used when not specified elsewhere. + * Controls repetition reduction (typically 0.8-2.0, where 1.0 means no penalty). + */ + @JsonProperty("default_repeat_penalty") + private Float defaultRepeatPenalty; + + /** + * The filesystem path to the llama-cli executable, which processes + * AI tasks via llama.cpp. + */ + @JsonProperty("llama_cli_path") + private File llamaCliPath; + + /** + * Number of CPU threads used for input prompt processing. + */ + @JsonProperty("batch_thread_count") + private Integer batchThreadCount; + + /** + * Number of CPU threads used for AI inference. + */ + @JsonProperty("thread_count") + private Integer threadCount; + + /** + * Directory containing text prompt files. Each file is a separate + * "prompt" by alias (the filename minus ".txt"). + */ + @JsonProperty("skills_directory") + private File skillsDirectory; + + /** + * The list of models defined in this configuration. + */ + private List models; + + + /** + * Retrieves the contents of a prompt file by alias, e.g. "writer" + * maps to "writer.txt" in the prompt's directory. + * + * @param skillName the name of the prompt file (without ".txt"). + * @return the full-text content of the prompt file. + * @throws IOException if reading the prompt file fails. + */ + public SkillConfig getSkillByName(String skillName) throws IOException { + File promptFile = new File(skillsDirectory, skillName + ".yaml"); + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + return mapper.readValue(promptFile, SkillConfig.class); + } + +} 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..6c39b8a --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationHelper.java @@ -0,0 +1,58 @@ +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; + +/** + *

Helper class for configuration file operations. Provides methods for loading configurations + * and determining the default configuration file path in the user's home directory. + *

Key functionality includes: + *

    + *
  • Configuration file path resolution
  • + *
  • YAML deserialization
  • + *
  • Error handling for missing configurations
  • + *
+ */ +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); + } +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationModel.java b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationModel.java new file mode 100644 index 0000000..9588068 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationModel.java @@ -0,0 +1,62 @@ +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 ConfigurationModel { + + /** + * 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; +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/SkillConfig.java b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/SkillConfig.java new file mode 100644 index 0000000..2ccd61c --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/SkillConfig.java @@ -0,0 +1,29 @@ +package eu.svjatoslav.alyverkko_cli.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class SkillConfig { + + private String prompt; + + private Float temperature; + + /** + * Skill-specific top-p value overriding model/global defaults. + */ + private Float topP; + + @JsonProperty("top_k") + private Float topK; + + @JsonProperty("min_p") + private Float minP; + + /** + * Skill-specific repeat penalty value overriding model/global defaults. + */ + private Float repeatPenalty; + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/package-info.java b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/package-info.java new file mode 100644 index 0000000..fd21d0b --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/package-info.java @@ -0,0 +1,13 @@ +/** + *

This package handles the configuration system for the Älyverkko CLI application. + * It provides classes for storing and retrieving application-wide settings, including + * model configurations, directory paths, and performance parameters. + *

Configuration is stored in YAML format and includes: + *

    + *
  • Model directory paths
  • + *
  • Mail task directories
  • + *
  • Performance settings (thread counts, temperature)
  • + *
+ */ + +package eu.svjatoslav.alyverkko_cli.configuration; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/model/Model.java b/src/main/java/eu/svjatoslav/alyverkko_cli/model/Model.java new file mode 100644 index 0000000..935ccd6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/model/Model.java @@ -0,0 +1,109 @@ +package eu.svjatoslav.alyverkko_cli.model; + +import java.io.File; + +/** + *

Represents an AI model stored on the filesystem with metadata about its capabilities and identification. + * This class serves as a lightweight container for model information, enabling quick lookup and validation. + *

Models are typically discovered through configuration files and stored in the ModelLibrary for easy access. + *

Key fields include: + *

    + *
  • filesystemPath - Location of the model file
  • + *
  • contextSizeTokens - Maximum token capacity for this model
  • + *
  • alias - User-friendly identifier for the model
  • + *
  • endOfTextMarker - Optional response completion marker
  • + *
+ */ +public class Model { + + /** + * The path to the model file on the filesystem. + */ + public final File filesystemPath; + + /** + * The size of the context (in tokens) that this model is able to handle. + */ + public final int contextSizeTokens; + + /** + * A user-friendly alias for the model, e.g. "default" or "mistral". + */ + public final String alias; + + /** + * An optional marker indicating end of the AI-generated text (e.g., "###"). + * If non-null, it can be used to detect where the model has finished answering. + */ + public final String endOfTextMarker; + + /** + * Model-specific temperature value (null if not configured). + */ + public Float temperature; + + /** + * Model-specific top-p value (null if not configured). + */ + public Float topP; + + /** + * Model-specific Top-K parameter controlling token selection (null if not configured). + */ + public Float topK; + + /** + * Minimum probability threshold for candidate tokens (null if not configured). + */ + public Float minP; + + /** + * Model-specific repeat penalty value (null if not configured). + */ + public Float repeatPenalty; + + /** + * Constructs a {@link Model} instance with all hyperparameters. + * + * @param filesystemPath The path to the model file on the filesystem. + * @param contextSizeTokens Maximum token capacity of this model. + * @param modelAlias User-friendly identifier for the model. + * @param endOfTextMarker Optional response completion marker. + * @param temperature Sampling temperature (higher = more creative) + * @param topP Nucleus sampling threshold (0.0-1.0) + * @param repeatPenalty Penalty for token repetition (>0.0) + * @param topK Token selection cutoff for Top-K sampling (>=1) + * @param minP Minimum relative probability threshold (0.0-1.0) + */ + public Model(File filesystemPath, int contextSizeTokens, + String modelAlias, String endOfTextMarker, + Float temperature, Float topP, Float repeatPenalty, + Float topK, Float minP) { + this.filesystemPath = filesystemPath; + this.contextSizeTokens = contextSizeTokens; + this.alias = modelAlias; + this.endOfTextMarker = endOfTextMarker; + this.temperature = temperature; + this.topP = topP; + this.repeatPenalty = repeatPenalty; + this.topK = topK; + this.minP = minP; + } + + /** + *

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. + *

Typical output: + *

+     * Model: default
+     *   Path: /path/to/model.gguf
+     *   Context size: 32768
+     * 
+ */ + public void printModelDetails() { + System.out.println("Model: " + alias); + System.out.println(" Path: " + filesystemPath); + System.out.println(" Context size: " + contextSizeTokens); + } +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/model/ModelLibrary.java b/src/main/java/eu/svjatoslav/alyverkko_cli/model/ModelLibrary.java new file mode 100644 index 0000000..4c94c92 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/model/ModelLibrary.java @@ -0,0 +1,131 @@ +package eu.svjatoslav.alyverkko_cli.model; + +import eu.svjatoslav.alyverkko_cli.Utils; +import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationModel; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * A container (library) for multiple AI models, providing + * functionality for adding and retrieving models by alias. + */ +public class ModelLibrary { + + /** + * The list of all successfully loaded models in this library. + */ + private final List models; + + /** + * The default model for this library (e.g., the first successfully + * loaded model in the list). + */ + private static Model defaultModel; + + /** + * Base directory containing the model files. + */ + private final File modelsBaseDirectory; + + /** + * Constructs a library of AI models from the provided list of + * {@link ConfigurationModel}s, ignoring those whose paths do not exist. + * + * @param modelsBaseDirectory the root directory where model files are stored. + * @param configModels a list of model configurations. + */ + public ModelLibrary(File modelsBaseDirectory, List configModels) { + this.modelsBaseDirectory = modelsBaseDirectory; + this.models = new ArrayList<>(); + + for (ConfigurationModel configModel : configModels) { + addModelFromConfig(configModel); + } + + if (models.isEmpty()) { + throw new RuntimeException("No models are defined!"); + } + + defaultModel = models.get(0); + } + + /** + * Attempts to construct a {@link Model} from configuration, verifying file existence. + */ + private void addModelFromConfig(ConfigurationModel configModel) { + File modelFile = new File(modelsBaseDirectory, configModel.getFilesystemPath()); + if (!modelFile.exists()) { + Utils.printRedMessageToConsole("WARN: Model file not found: " + modelFile.getAbsolutePath() + " . Skipping model."); + return; + } + + addModel(new Model( + modelFile, + configModel.getContextSizeTokens(), + configModel.getAlias(), + configModel.getEndOfTextMarker(), + + configModel.getTemperature(), + configModel.getTopP(), + configModel.getRepeatPenalty(), + + configModel.getTopK(), + configModel.getMinP() + )); + + } + + /** + * Adds a model to the library if no model with the same alias + * already exists. + * + * @param model the model to add. + * @throws RuntimeException if a model with the same alias already exists. + */ + public void addModel(Model model) { + if (findModelByAlias(model.alias).isPresent()) { + throw new RuntimeException("Model with alias \"" + model.alias + "\" already exists!"); + } + models.add(model); + } + + /** + * @return the list of loaded models in this library. + */ + public List getModels() { + return models; + } + + /** + * Finds a model by its alias in this library. + * + * @param alias the model alias to look for. + * @return an {@link Optional} describing the found model, or empty if none match. + */ + public Optional findModelByAlias(String alias) { + return models.stream() + .filter(model -> model.alias.equals(alias)) + .findFirst(); + } + + /** + * @return the default model (first loaded model). + */ + public Model getDefaultModel() { + return defaultModel; + } + + /** + * 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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/model/package-info.java b/src/main/java/eu/svjatoslav/alyverkko_cli/model/package-info.java new file mode 100644 index 0000000..5d8b074 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/model/package-info.java @@ -0,0 +1,12 @@ +/** + *

This package defines the model management system for the Älyverkko CLI. + * It includes classes for representing AI models and maintaining a library of available models. + *

Key features: + *

    + *
  • Model metadata storage and validation
  • + *
  • Default model selection and management
  • + *
  • File system integration for model discovery
  • + *
+ */ + +package eu.svjatoslav.alyverkko_cli.model; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/package-info.java b/src/main/java/eu/svjatoslav/alyverkko_cli/package-info.java new file mode 100644 index 0000000..a3057d0 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/package-info.java @@ -0,0 +1,14 @@ +/** + *

This package contains the core components of the Älyverkko CLI application, including the main entry point, + * command interfaces, and utility classes. It serves as the central hub for orchestrating subcommands and core + * application behavior. + *

Key responsibilities include: + *

    + *
  • Command registration and execution
  • + *
  • Configuration loading and management
  • + *
  • Basic utility functions for colored console output
  • + *
+ * + */ + +package eu.svjatoslav.alyverkko_cli; \ No newline at end of file diff --git a/tools/implement idea b/tools/implement idea new file mode 100755 index 0000000..6472735 --- /dev/null +++ b/tools/implement idea @@ -0,0 +1,12 @@ +#!/bin/bash +cd "${0%/*}"; if [ "$1" != "T" ]; then gnome-terminal -e "'$0' T"; exit; fi; +cd .. + +read -p "Enter the topic name: " TOPIC_NAME + +alyverkko-cli joinfiles -t "$TOPIC_NAME" --src-dir . --pattern "*.org" +alyverkko-cli joinfiles -t "$TOPIC_NAME" --src-dir . --pattern "*.java" +alyverkko-cli joinfiles -t "$TOPIC_NAME" --src-dir . --pattern "implement*" +alyverkko-cli joinfiles -t "$TOPIC_NAME" --src-dir . --pattern "install" +alyverkko-cli joinfiles -t "$TOPIC_NAME" --src-dir . --pattern "uninstall" +alyverkko-cli joinfiles -t "$TOPIC_NAME" --edit diff --git a/tools/open with IntelliJ IDEA b/tools/open with IntelliJ IDEA new file mode 100755 index 0000000..304bf94 --- /dev/null +++ b/tools/open with IntelliJ IDEA @@ -0,0 +1,54 @@ +#!/bin/bash + +# This script launches IntelliJ IDEA with the current project +# directory. The script is designed to be run by double-clicking it in +# the GNOME Nautilus file manager. + +# First, we change the current working directory to the directory of +# the script. + +# "${0%/*}" gives us the path of the script itself, without the +# script's filename. + +# This command basically tells the system "change the current +# directory to the directory containing this script". + +cd "${0%/*}" + +# Then, we move up one directory level. +# The ".." tells the system to go to the parent directory of the current directory. +# This is done because we assume that the project directory is one level up from the script. +cd .. + +# Now, we use the 'setsid' command to start a new session and run +# IntelliJ IDEA in the background. 'setsid' is a UNIX command that +# runs a program in a new session. + +# The command 'idea .' opens IntelliJ IDEA with the current directory +# as the project directory. The '&' at the end is a UNIX command that +# runs the process in the background. The '> /dev/null' part tells +# the system to redirect all output (both stdout and stderr, denoted +# by '&') that would normally go to the terminal to go to /dev/null +# instead, which is a special file that discards all data written to +# it. + +setsid idea . &>/dev/null & + +# The 'disown' command is a shell built-in that removes a shell job +# from the shell's active list. Therefore, the shell will not send a +# SIGHUP to this particular job when the shell session is terminated. + +# '-h' option specifies that if the shell receives a SIGHUP, it also +# doesn't send a SIGHUP to the job. + +# '$!' is a shell special parameter that expands to the process ID of +# the most recent background job. +disown -h $! + + +sleep 2 + +# Finally, we use the 'exit' command to terminate the shell script. +# This command tells the system to close the terminal window after +# IntelliJ IDEA has been opened. +exit diff --git a/tools/update web site b/tools/update web site new file mode 100755 index 0000000..5d3b485 --- /dev/null +++ b/tools/update web site @@ -0,0 +1,35 @@ +#!/bin/bash +cd "${0%/*}"; if [ "$1" != "T" ]; then gnome-terminal -e "'$0' T"; exit; fi; + +cd .. + +# Build the project jar file and the apidocs. +mvn clean package + +# Export org to html using emacs in batch mode +( + cd doc/ + + rm -f index.html + emacs --batch -l ~/.emacs --visit=index.org --funcall=org-html-export-to-html --kill + + #rm setup.html + #emacs --batch -l ~/.emacs --visit=setup.org --funcall=org-html-export-to-html --kill +) + +# Generate class diagrams. See: https://www3.svjatoslav.eu/projects/javainspect/ +rm -rf doc/graphs/ +mkdir -p doc/graphs/ +javainspect -j target/alyverkko-cli-*-SNAPSHOT.jar -d doc/graphs/ -n "all classes" -t png -ho +meviz index -w doc/graphs/ -t "Älyverkko CLI program classes" + +# Copy the apidocs to the doc folder so that they can be uploaded to the server. +rm -rf doc/apidocs/ +cp -r target/apidocs/ doc/ + +# Upload project homepage to the server. +rsync -avz --delete -e 'ssh -p 10006' doc/ n0@www3.svjatoslav.eu:/mnt/big/projects/alyverkko-cli/ + +echo "" +echo "Press ENTER to close this window." +read diff --git a/uninstall b/uninstall new file mode 100755 index 0000000..b4a4474 --- /dev/null +++ b/uninstall @@ -0,0 +1,13 @@ +#!/bin/bash + +SYSTEMD_SERVICE_FILE="/etc/systemd/system/alyverkko-cli.service" + +sudo systemctl stop alyverkko-cli +sudo systemctl disable alyverkko-cli +sudo rm "$SYSTEMD_SERVICE_FILE" +sudo rm -rf /opt/alyverkko-cli/ + +read -p "Do you want to remove user configuration as well? (y/N) " remove_config +if [[ $remove_config == [Yy] ]]; then + sudo rm -rf "${HOME}/.config/alyverkko-cli" +fi