From 87dc1a0359642b742f1c3eef2e30fd5976216cb9 Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Sun, 19 May 2024 12:03:28 +0300 Subject: [PATCH 1/1] Initial version --- .gitignore | 12 + COPYING | 121 +++++ alyverkko-cli | 6 + alyverkko-cli.yaml | 12 + doc/index.org | 465 ++++++++++++++++++ install | 82 +++ logo.png | Bin 0 -> 4012 bytes maven.xml | 15 + pom.xml | 193 ++++++++ .../eu/svjatoslav/alyverkko_cli/AiTask.java | 251 ++++++++++ .../eu/svjatoslav/alyverkko_cli/Command.java | 10 + .../eu/svjatoslav/alyverkko_cli/Main.java | 53 ++ .../eu/svjatoslav/alyverkko_cli/Utils.java | 13 + .../commands/ListModelsCommand.java | 33 ++ .../commands/MailCorrespondentCommand.java | 192 ++++++++ .../configuration/Configuration.java | 114 +++++ .../configuration/ConfigurationModel.java | 52 ++ .../svjatoslav/alyverkko_cli/model/Model.java | 50 ++ .../alyverkko_cli/model/ModelLibrary.java | 108 ++++ .../svjatoslav/alyverkko_cli/AiTaskTest.java | 24 + tools/open with IntelliJ IDEA | 54 ++ tools/update web site | 31 ++ 22 files changed, 1891 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100755 alyverkko-cli create mode 100644 alyverkko-cli.yaml create mode 100644 doc/index.org create mode 100755 install create mode 100644 logo.png create mode 100644 maven.xml create mode 100644 pom.xml create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/AiTask.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/Command.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/Main.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/Utils.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/commands/MailCorrespondentCommand.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationModel.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/model/Model.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/model/ModelLibrary.java create mode 100644 src/test/java/eu/svjatoslav/alyverkko_cli/AiTaskTest.java create mode 100755 tools/open with IntelliJ IDEA create mode 100755 tools/update web site diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0da3832 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/.idea/ +/.settings/ +/target/ +/*.iml +/*.log +/test/ + +/doc/apidocs/ +/doc/graphs/ +/doc/index.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..0e00b35 --- /dev/null +++ b/alyverkko-cli.yaml @@ -0,0 +1,12 @@ +mail_directory: "/home/user/AI/mail" +models_directory: "/home/user/AI/models" +default_temperature: 0.7 +llama_cpp_executable_path: "/home/user/AI/llama.cpp/main" +batch_thread_count: 10 +thread_count: 6 +models: + - alias: "wizard" + filesystem_path: "WizardLM-2-8x22B.Q5_K_M-00001-of-00005.gguf" + context_size_tokens: 64000 + end_of_text_marker: null + diff --git a/doc/index.org b/doc/index.org new file mode 100644 index 0000000..3670b99 --- /dev/null +++ b/doc/index.org @@ -0,0 +1,465 @@ +#+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 + +* General +- 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]]. + +* Introduction +:PROPERTIES: +:GPTEL_TOPIC: introduction +:END: + +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 around 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 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: + +- *Automated Report Generation*: The AI can analyze large datasets + overnight and produce comprehensive reports by morning, saving + countless hours of manual analysis. +- *Code Development Support*: Developers could use the AI to generate + code snippets or even entire modules based on specifications + provided, which they can then integrate and refine during their + workday. +- *Content Creation*: The AI can draft articles, create outlines for + books, or compile research notes, providing a solid foundation for + content creators to edit and finalize. +- *Data Processing*: It can process and organize large volumes of + unstructured data, such as customer feedback or scientific + observations, into structured formats ready for human review. + +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. + +* Setup +** 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]]. My favorite is + [[https://huggingface.co/MaziyarPanahi/WizardLM-2-8x22B-GGUF/tree/main][WizardLM-2-8x22B]] for strong problem solving skills. + +Follow instructions for obtaining and building Älyverkko CLI on your +computer that runs Debian 12 operating system: + +1. Ensure that you have Java Development Kit (JDK) installed on your + system. + : sudo apt-get install openjdk-17-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. + +** 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 + +*** 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 + mail_directory: "/home/user/AI/mail" + models_directory: "/home/user/AI/models" + default_temperature: 0.7 + llama_cpp_executable_path: "/home/user/AI/llama.cpp/main" + batch_thread_count: 10 + thread_count: 6 + models: + - alias: "wizard" + filesystem_path: "WizardLM-2-8x22B.Q5_K_M-00001-of-00005.gguf" + context_size_tokens: 64000 + end_of_text_marker: null + - alias: "maid" + filesystem_path: "daringmaid-20b.Q4_K_M.gguf" + context_size_tokens: 4096 + end_of_text_marker: null + +#+end_src + +*** Configuration file syntax + +Here are available parameters: + +- mail_directory :: Directory where AI will look for files that + contain problems to solve. + +- models_directory :: Directory where AI models are stored. + - This option is mandatory. + +- default_temperature :: Defines the default temperature for AI + responses, affecting randomness in the generation process. Lower + values make the AI more deterministic and higher values make it more + creative or random. + - Default value: 0.7 + +- llama_cpp_executable_path :: Specifies the file path to the + *llama.cpp* *main* executable. + - Example Value: /home/user/AI/llama.cpp/main + - This option is mandatory. + +- batch_thread_count :: Specifies the number of threads to use for + input prompt processing. CPU computing power is usually the + bottleneck here. + - Default value: 10 + +- 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. + - Default value: 6 + +- models :: List of available large language models. First model in + the list would be used by default. + + - alias :: Short model alias. + - 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*. + +*** 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 + +** Starting 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 mail + +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 +:PROPERTIES: +:ID: 883d6e7c-60e0-422b-8c00-5cdc9dfec20d +:END: + +The Älyverkko CLI application expects input files for processing in +the form of plain text files within the specified mail directory +(configured in the [[id:0fcdae48-81c5-4ae1-bdb9-64ae74e87c45][YAML configuration file]]). Each file should begin +with a `TOCOMPUTE:` marker on the first line to be considered for +processing. + +When the application 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. + +Suggested way to use mail processing mode is to prepare assignments +within the Älyverkko CLI mail directory using normal text editor. Feel +free to save intermediary states. Once AI assignment is ready, add +: TOCOMPUTE: +to the beginning of the file and save one last time. Älyverkko CLI +will detect new task within one second 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 +:PROPERTIES: +:GPTEL_TOPIC: create-new-topic-dedicated-file +:END: + +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 =ai-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 ai-topic-files-directory "/home/user/my-ai-mail-directory/" + "Directory where topic files are stored. Set it to directory you want to use.") + +(defun ai-new-topic () + "Create and open a topic file in the specified directory." + (interactive) + (let ((topic (read-string "Enter topic name: "))) + (let ((file-path (concat ai-topic-files-directory topic ".org"))) + (if (not (file-exists-p file-path)) + (with-temp-file file-path + (insert "#+TITLE: " topic "\n\n"))) + (find-file file-path) + (goto-char (point-max)) + (org-mode)))) +#+end_src + +***** Easily signal to AI that problem statement is ready for solving +When *ai-compute* function is triggered, it inserts "TOCOMPUTE:" line +at the beginning of file and saves it. Marking it for processing by +AI. + +#+begin_src elisp :results none + (defun ai-compute () + "Inserts 'TOCOMPUTE:' at the beginning of the buffer." + (interactive) + (goto-char (point-min)) ; Move to the beginning of the buffer + (insert "TOCOMPUTE:\n") ; Insert the string followed by a new line + (save-buffer) ; Save the buffer + ) +#+end_src + +* TODO + +Ideas to be possibly implemented in the future: + +** System operation +- Implement CPU nice priority for inference processes to minimize the + impact on system responsiveness during heavy computations. +- Enable model selection per individual inference task, allowing for + dynamic adjustment based on task requirements. +- Allow specification of custom prompts for each inference task to + tailor interactions precisely. +- Introduce an aliasing system for frequently used prompts, + streamlining the prompt selection process. +- Consider implementing a plugin architecture to allow third-party + developers to extend Älyverkko CLI's functionality with custom + modules or integrations. +- Possibility to easily pause and resume Älyverkko CLI without loosing + in-progress computation. Unix process stop and continue signals + could possibly be used. +** Data management +- Develop a feature to recursively aggregate files into a single + document using Emacs org-mode syntax, facilitating the preparation + of comprehensive problem statements for AI processing. + - Ensure that binary files are excluded from this aggregation + process to maintain text readability and compatibility. + +** Configuration and logging +- Implement a fallback mechanism to use a system-wide configuration + file located in `/etc/` if no user-specific configuration is found, + enhancing out-of-the-box usability for new users. +- Introduce optional logging of `llama.cpp` output to aid in debugging + and performance monitoring without cluttering the standard output. + +** Integration with external services +- Add capabilities to connect with Jira, fetch content, and + potentially update issues or comments based on AI processing + results. +- Implement similar integration with Confluence for content retrieval + and updates. +- Extend the application's reach by adding the ability to interact + with arbitrary web sites, enabling information extraction and + automation of web-based tasks. + +** Tooling enhancements +- Incorporate Python script execution capabilities directly by the AI, + expanding the range of available data manipulation and processing + tools. +- Integrate relational database access to leverage structured data + storage and retrieval in workflows. +- Enable web request functionality to interact with RESTful APIs or + scrape web content as part of task execution. +- Introduce a notebook feature that allows the AI to maintain and + reference its own notes, fostering context retention across tasks. + +** Multimedia processing +- Extend the application's capabilities to include voice capture and + processing, opening up new avenues for interaction beyond text-based + communication. +- Implement image capture and processing features, enabling tasks that + involve image analysis or content extraction from visual data. + +** Task queue management +- Refactor the task queue mechanism to support: + - Multiple task sources, including a REST API endpoint for + programmatic task submission. + - Load balancing across multiple executors (instances of Älyverkko + CLI) with dynamic registration and unregistration without system + interruption. + - Task priority assignments, ensuring that critical tasks are + processed in a timely manner. + +** User interface development +- Create a web-based UI to provide users with an interface for task + submission and result retrieval, improving accessibility and user + experience. +- Integrate Quality of Service (QoS) concepts within the UI to ensure + equitable resource allocation among users. +- Implement administrative features for managing user accounts and + system resources, maintaining a secure and efficient operating + environment. diff --git a/install b/install new file mode 100755 index 0000000..0596533 --- /dev/null +++ b/install @@ -0,0 +1,82 @@ +#!/bin/bash + +# Define constants for paths and filenames +INSTALL_DIR="/opt/alyverkko-cli" +BINARY="alyverkko-cli" +DESKTOP_FILE="alyverkko-cli.desktop" +ICON_FILE="$INSTALL_DIR/logo.png" +APPLICATIONS_DIR="/usr/share/applications" +SYSTEM_SERVICE_DIR="/etc/systemd/system" +CONFIG_DIR="${HOME}/.config/" + +# Ask user if they want to install systemd service +echo "Do you want to install Älyverkko CLI as a systemd service? (y/N)" +read install_service + +# Build the application +mvn --settings maven.xml clean package + +# Install the binary and jar +sudo mkdir -p "$INSTALL_DIR" +sudo rm -f "$INSTALL_DIR/$BINARY.jar" +sudo cp target/"$BINARY".jar "$INSTALL_DIR/$BINARY.jar" +sudo cp "$BINARY" "$INSTALL_DIR/$BINARY" + +# Create a symbolic link to run the program from CLI +sudo rm -f /usr/bin/"$BINARY" +sudo ln -s "$INSTALL_DIR/$BINARY" /usr/bin/"$BINARY" + +# Install the desktop launcher +cat < /dev/null +[Desktop Entry] +Type=Application +Terminal=true +Name=Älyverkko CLI +Comment=Runner for artificial neural network service +Icon=$ICON_FILE +Exec=/opt/alyverkko-cli/$BINARY mail +Categories=Development; +EOF + +# Make sure the desktop file is executable +sudo chmod 644 "$APPLICATIONS_DIR/$DESKTOP_FILE" + +# Copy the icon (assuming you have an icon named alyverkko-cli.svg) +sudo cp logo.png "$ICON_FILE" + +# Pre-deploy example configuration YAML file +sudo mkdir -p "$CONFIG_DIR" + +if [ -f "$CONFIG_DIR/alyverkko-cli.yaml" ]; then + echo "Configuration file already exists. Skipping pre-deployment." +else + sudo cp alyverkko-cli.yaml "$CONFIG_DIR" +fi + +# Install systemd service if requested +if [[ $install_service == [Yy] ]]; then + +cat < /dev/null +[Unit] +Description=Älyverkko CLI daemon in mail mode +After=network.target + +[Service] +User=$USER +ExecStart=/opt/alyverkko-cli/alyverkko-cli mail +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 + echo "Systemd service installed, enabled and started. Service status is:" + systemctl -l status alyverkko-cli +fi + +echo "Installation complete." diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..468758e81c57f304665f959dcd47446445a7f026 GIT binary patch literal 4012 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 literal 0 HcmV?d00001 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..be4cbfd --- /dev/null +++ b/pom.xml @@ -0,0 +1,193 @@ + + 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.2 + + + 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.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/AiTask.java b/src/main/java/eu/svjatoslav/alyverkko_cli/AiTask.java new file mode 100644 index 0000000..1c5a608 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/AiTask.java @@ -0,0 +1,251 @@ +package eu.svjatoslav.alyverkko_cli; + +import eu.svjatoslav.alyverkko_cli.model.Model; + +import java.io.*; +import java.nio.file.Files; + +import static eu.svjatoslav.alyverkko_cli.Main.configuration; +import static java.lang.String.join; + +public class AiTask { + public static final String AI_RESPONSE_MARKER = "ASSISTANT:"; + private static final String LLAMA_CPP_META_INFO_MARKER = "llm_load_print_meta: "; + + private final String aiQuery; + private final Model model; + private final Float temperature; + File inputFile; + + /** + * Creates a new AI task. + * + * @param input Problem statement to be used for the AI task. + * @param model The model to be used for the AI task. + * @param temperature The temperature to be used for the AI inference process. + */ + public AiTask(String input, Model model, Float temperature) { + this.aiQuery = buildAiQuery(input); + this.model = model; + this.temperature = temperature == null ? configuration.getDefaultTemperature() : temperature; + } + + private String buildAiQuery(String input) { + StringBuilder sb = new StringBuilder(); + + sb.append("SYSTEM:\nThis conversation involves a user and AI assistant where the AI " + + "is expected to provide not only immediate responses but also detailed and " + + "well-reasoned analysis. The AI should consider all aspects of the query " + + "and deliver insights based on logical deductions and comprehensive understanding." + + "AI assistant should reply using emacs org-mode syntax.\n" + + "Quick recap: *this is bold* [[http://domain.org][This is link]]\n" + + "* Heading level 1\n" + + "** Heading level 2\n" + + "| Col 1 Row 1 | Col 2 Row 1 |\n" + + "| Col 1 Row 2 | Col 2 Row 2 |\n" + + "#+BEGIN_SRC python\n" + + " print ('Hello, world!')\n" + + "#+END_SRC\n\n"); + + + String filteredInput = filterParticipantsInUserInput(input); + + // if filtered input does not start with "USER:", add it + if (!filteredInput.startsWith("USER:")) { + filteredInput = "USER:\n" + filteredInput; + } + + sb.append(filteredInput).append("\n").append(AI_RESPONSE_MARKER); + return sb.toString(); + } + + public static String filterParticipantsInUserInput(String input) { + StringBuilder result = new StringBuilder(); + String[] lines = input.split("\n"); + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + if (i > 0) result.append("\n"); + if ("* ASSISTANT:".equals(line)) line = "ASSISTANT:"; + if ("* USER:".equals(line)) line = "USER:"; + result.append(line); + } + return result.toString(); + } + + public static String filterParticipantsInAiResponse(String response) { + StringBuilder result = new StringBuilder(); + String[] lines = response.split("\n"); + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + if (i > 0) result.append("\n"); + if ("ASSISTANT:".equals(line)) line = "* ASSISTANT:"; + if ("USER:".equals(line)) line = "* USER:"; + result.append(line); + } + result.append("\n* USER:\n"); + return result.toString(); + } + + /** + * Compute the AI task. + * @return The result of the AI task. + */ + public String runAiQuery() throws InterruptedException, IOException { + try { + initializeInputFile(); + + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.command(getCliCommand().split("\\s+")); // Splitting the command string into parts + + Process process = processBuilder.start(); + handleErrorThread(process); + StringBuilder result = new StringBuilder(); + Thread outputThread = handleResultThread(process, result); + process.waitFor(); // Wait for the main AI computing process to finish + outputThread.join(); // Wait for the output thread to finish + return filterParticipantsInAiResponse(cleanupAiResponse(result.toString())); + } finally { + deleteTemporaryFile(); + } + } + + /** + * Initializes the input file for the AI task. + */ + private void initializeInputFile() throws IOException { + // write AI input to file + inputFile = createTemporaryFile(); + Files.write(inputFile.toPath(), aiQuery.getBytes()); + } + + /** + * Creates and starts a thread to handle the error stream of an AI inference process. + * + * @param process the process to read the error stream from. + */ + 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(); + } + + + /** + * Handles a single line from the error stream of an AI inference process. + * If the line contains meta-info, it is printed to the console. + * + * @param line the line to be handled. + */ + private static void handleErrorStreamLine(String line) { + if (line.startsWith(LLAMA_CPP_META_INFO_MARKER)) { + // Print the meta-info to console + System.out.println(line.substring(LLAMA_CPP_META_INFO_MARKER.length())); + return; + } + + // Print the error to console + Utils.printRedMessageToConsole(line); + } + + /** + * Gets the full command to be executed by the AI inference process. + * + * @return the full command to be executed by the AI inference process. + */ + private String getCliCommand() { + + return join(" ", + configuration.getLlamaCppExecutablePath().getAbsolutePath(), + "--model " + model.filesystemPath, + "--threads " + configuration.getThreadCount(), + "--threads-batch " + configuration.getBatchThreadCount(), + "--mirostat 2", + "--log-disable", + "--temp " + temperature, + "--ctx-size " + model.contextSizeTokens, + "--batch-size 8", + "-n -1", + "--repeat_penalty 1.1", + "--file " + inputFile); + + } + + + /** + * Creates and starts a thread to handle the result of the AI inference process. + * The result is read from the process's input stream and saved in a StringBuilder. + * + * @param process the process to read the result from. + * @param result the StringBuilder to save the result in. + * @return the thread that handles the result. + */ + 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"); // Display each line as it's being read + result.append(aiResultLine).append("\n"); // Save the result + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + outputThread.start(); + return outputThread; + } + + /** + * Returns the temporary file for the AI to work with. + */ + private File createTemporaryFile() throws IOException { + File file = Files.createTempFile("ai-inference", ".tmp").toFile(); + file.deleteOnExit(); + return file; + } + + /** + * Cleans up the AI response by removing unnecessary text. + * @param result the AI response string to be cleaned up. + * @return the cleaned-up AI response.k + */ + private String cleanupAiResponse(String result) { + + // remove text before AI response marker + int aIResponseIndex = result.lastIndexOf(AI_RESPONSE_MARKER); + if (aIResponseIndex != -1) { + result = result.substring(aIResponseIndex + AI_RESPONSE_MARKER.length()); + } + + // remove text after end of text marker, if it exists + if (model.endOfTextMarker != null) { + int endOfTextMarkerIndex = result.indexOf(model.endOfTextMarker); + if (endOfTextMarkerIndex != -1) { + result = result.substring(0, endOfTextMarkerIndex); + } + } + return result + "\n"; + } + + 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()); + } + } + } + + public static String runAiQuery(String problemStatement, Model model, Float temperature) throws IOException, InterruptedException { + AiTask ai = new AiTask(problemStatement, model, temperature); + return ai.runAiQuery(); + } + +} 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..18767b9 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/Command.java @@ -0,0 +1,10 @@ +package eu.svjatoslav.alyverkko_cli; + +import java.io.IOException; + +public interface Command { + + String getName(); + + void execute(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..4070734 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java @@ -0,0 +1,53 @@ +package eu.svjatoslav.alyverkko_cli; + +import eu.svjatoslav.alyverkko_cli.commands.ListModelsCommand; +import eu.svjatoslav.alyverkko_cli.configuration.Configuration; +import eu.svjatoslav.alyverkko_cli.commands.MailCorrespondentCommand; + +import java.io.IOException; +import java.util.Optional; + +import static java.util.Arrays.copyOfRange; + +public class Main { + + private final java.util.List commands = java.util.Arrays.asList( + new ListModelsCommand(), + new MailCorrespondentCommand() + ); + + public static void main(final String[] args) throws IOException, InterruptedException { + new Main().handleCommand(args); + } + + public static Configuration configuration; + + 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.getName().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.execute(remainingArgs); + } + + private void showHelp() { + System.out.println("Älyverkko CLI\n"); + System.out.println("Available commands:"); + commands.forEach(cmd -> System.out.println(" " + cmd.getName())); + } + +} \ No newline at end of file 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..6523174 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/Utils.java @@ -0,0 +1,13 @@ +package eu.svjatoslav.alyverkko_cli; + +public class Utils { + + 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/ListModelsCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java new file mode 100644 index 0000000..188fedd --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java @@ -0,0 +1,33 @@ +package eu.svjatoslav.alyverkko_cli.commands; + +import eu.svjatoslav.alyverkko_cli.Command; +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.*; + +import java.io.IOException; + +import static eu.svjatoslav.alyverkko_cli.Main.configuration; +import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration; + +public class ListModelsCommand implements Command { + + @Override + public String getName() { + return "listmodels"; + } + + @Override + public void execute(String[] cliArguments) throws IOException { + configuration = loadConfiguration(); + 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/MailCorrespondentCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/MailCorrespondentCommand.java new file mode 100644 index 0000000..49e44b0 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/MailCorrespondentCommand.java @@ -0,0 +1,192 @@ +package eu.svjatoslav.alyverkko_cli.commands; + +import eu.svjatoslav.alyverkko_cli.*; +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.DirectoryOption; +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 static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration; +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.ENTRY_CREATE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; + +public class MailCorrespondentCommand implements Command { + + final Parser parser = new Parser(); + + ModelLibrary modelLibrary; + + private WatchService watcher; + + /** + * The directory containing mail files. + */ + DirectoryOption mailDirectoryOption = parser.add(new DirectoryOption("Directory containing mail files")) + .addAliases("--mail", "-m").mustExist(); + + File mailDirectory; + + /** + * Configuration file location. + */ + public FileOption configFileOption = parser.add(new FileOption("Configuration file path")) + .addAliases("--config", "-c").mustExist(); + + private void initialMailScanAndReply() throws IOException, InterruptedException { + File[] files = mailDirectory.listFiles(); + if (files == null) return; + + for (File file : files) + processMailIfNeeded(file); + } + + private boolean isMailProcessingNeeded(File file) throws IOException { + // ignore hidden files + if (file.getName().startsWith(".")) + return false; + + // Check if the file is a mail file (not a directory + if (!file.isFile()) return false; + + return fileHasToComputeMarker(file); + } + + private static boolean fileHasToComputeMarker(File file) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String firstLine = reader.readLine(); + return firstLine != null && firstLine.startsWith("TOCOMPUTE:"); + } + } + + private void processMailIfNeeded(File file) throws IOException, InterruptedException { + if (!isMailProcessingNeeded(file)) { + System.out.println("Ignoring file: " + file.getName() + " (does not need processing for now)"); + return; + } + + System.out.println("\nReplying to mail: " + file.getName()); + + // Read the mail contents, and remove the TOCOMPUTE: prefix from the first line + String mailContents = getFileContentsAsString(file); + mailContents = removeToComputePrefixFile(mailContents); + + // faster model for testing for development time testing + // String modelAlias = "maid"; + // TODO: make model CLI argument + String modelAlias = "wizard"; + + Model model = modelLibrary.findModelByAlias(modelAlias).get(); + String aiGeneratedResponse = AiTask.runAiQuery(mailContents, model, null); + + // Append the AI response to the mail contents + if (!mailContents.startsWith("* USER:\n")) { + mailContents = "* USER:\n" + mailContents; + } + + String newMailContents = mailContents + "\n* ASSISTANT:\n" + aiGeneratedResponse; + + // Write the result to the file + saveToFile(file, newMailContents); + } + + private String removeToComputePrefixFile(String mailContents) { + // Remove the first line from the mail contents + int firstNewLineIndex = mailContents.indexOf('\n'); + if (firstNewLineIndex != -1) { + mailContents = mailContents.substring(firstNewLineIndex + 1); + } + return mailContents; + } + + + @Override + public String getName() { + return "mail"; + } + + @Override + public void execute(String[] cliArguments) throws IOException, InterruptedException { + if (!parser.parse(cliArguments)) { + System.out.println("Failed to parse commandline arguments"); + parser.showHelp(); + return; + } + + configuration = loadConfiguration(configFileOption.isPresent() ? configFileOption.getValue() : null); + if (configuration == null){ + System.out.println("Failed to load configuration file"); + return; + } + + modelLibrary = new ModelLibrary(configuration.getModelsDirectory(), configuration.getModels()); + mailDirectory = mailDirectoryOption.isPresent() ? mailDirectoryOption.getValue() : configuration.getMailDirectory(); + + initializeFileWatcher(); + + // before we start processing incremental changes in directory, we need to process all the existing files + initialMailScanAndReply(); + + System.out.println("Mail correspondent running. Press CTRL+c to terminate."); + + while (true) { + WatchKey key; + try { + key = watcher.take(); + } catch (InterruptedException e) { + System.out.println("Interrupted while waiting for file system events. Exiting."); + break; + } + + System.out.println("Detected filesystem event."); + + // sleep for a while to allow the file to be fully written + Thread.sleep(1000); + + processDetectedFilesystemEvents(key); + + if (!key.reset()) break; + } + + watcher.close(); + } + + private void processDetectedFilesystemEvents(WatchKey key) throws IOException, InterruptedException { + for (WatchEvent event : key.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + + // Skip OVERFLOW event + if (kind == StandardWatchEventKinds.OVERFLOW) continue; + + // Retrieve the file name associated with the event + Path filename = ((WatchEvent) event).context(); + System.out.println("Event: " + kind + " for file: " + filename); + + // Process the event + processFileSystemEvent(kind, filename); + } + } + + private void initializeFileWatcher() throws IOException { + this.watcher = FileSystems.getDefault().newWatchService(); + Paths.get(mailDirectory.getAbsolutePath()).register(watcher, ENTRY_CREATE, ENTRY_MODIFY); + } + + private void processFileSystemEvent(WatchEvent.Kind kind, Path filename) throws IOException, InterruptedException { + if (kind != ENTRY_CREATE && kind != ENTRY_MODIFY) return; + + File file = mailDirectory.toPath().resolve(filename).toFile(); + processMailIfNeeded(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..91ba191 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java @@ -0,0 +1,114 @@ +package eu.svjatoslav.alyverkko_cli.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.annotation.JsonProperty; + + +import java.io.*; +import java.util.List; + +public class Configuration { + + private static final String DEFAULT_CONFIG_FILE_PATH = "~/.config/alyverkko-cli.yaml".replaceFirst("^~", System.getProperty("user.home"));; + + @JsonProperty("mail_directory") + private File mailDirectory; + + @JsonProperty("models_directory") + private File modelsDirectory; + + @JsonProperty("default_temperature") + private float defaultTemperature; + + @JsonProperty("llama_cpp_executable_path") + private File llamaCppExecutablePath; + + @JsonProperty("batch_thread_count") + private int batchThreadCount; + + @JsonProperty("thread_count") + private int threadCount; + + public List getModels() { + return models; + } + + public void setModels(List models) { + this.models = models; + } + + private List models; + + public Configuration() { + } + + public static Configuration loadConfiguration() throws IOException { + return loadConfiguration(null); + } + + public static Configuration loadConfiguration(File configFile) throws IOException { + + if (configFile == null) { + // Load configuration from the default path + configFile = new File(DEFAULT_CONFIG_FILE_PATH); + } + + if (!configFile.exists()) { + System.err.println("Configuration file not found: " + configFile); + return null; + } + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + return mapper.readValue(configFile, Configuration.class); + } + + + public File getMailDirectory() { + return mailDirectory; + } + + public void setMailDirectory(File mailDirectory) { + this.mailDirectory = mailDirectory; + } + + public File getModelsDirectory() { + return modelsDirectory; + } + + public void setModelsDirectory(File modelsDirectory) { + this.modelsDirectory = modelsDirectory; + } + + public float getDefaultTemperature() { + return defaultTemperature; + } + + public void setDefaultTemperature(float defaultTemperature) { + this.defaultTemperature = defaultTemperature; + } + + public File getLlamaCppExecutablePath() { + return llamaCppExecutablePath; + } + + public void setLlamaCppExecutablePath(File llamaCppExecutablePath) { + this.llamaCppExecutablePath = llamaCppExecutablePath; + } + + public int getBatchThreadCount() { + return batchThreadCount; + } + + public void setBatchThreadCount(int batchThreadCount) { + this.batchThreadCount = batchThreadCount; + } + + public int getThreadCount() { + return threadCount; + } + + public void setThreadCount(int threadCount) { + this.threadCount = threadCount; + } +} 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..f48a1f3 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationModel.java @@ -0,0 +1,52 @@ +package eu.svjatoslav.alyverkko_cli.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ConfigurationModel { + + private String alias; + + /** + * Absolute path in the file system where the model is stored. + */ + @JsonProperty("filesystem_path") + private String filesystemPath; + + @JsonProperty("context_size_tokens") + private int contextSizeTokens; + + @JsonProperty("end_of_text_marker") + private String endOfTextMarker; + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public String getFilesystemPath() { + return filesystemPath; + } + + public void setFilesystemPath(String filesystemPath) { + this.filesystemPath = filesystemPath; + } + + public int getContextSizeTokens() { + return contextSizeTokens; + } + + public void setContextSizeTokens(int contextSizeTokens) { + this.contextSizeTokens = contextSizeTokens; + } + + public String getEndOfTextMarker() { + return endOfTextMarker; + } + + public void setEndOfTextMarker(String endOfTextMarker) { + this.endOfTextMarker = endOfTextMarker; + } +} 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..8a49497 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/model/Model.java @@ -0,0 +1,50 @@ +package eu.svjatoslav.alyverkko_cli.model; + +import java.io.File; + +public class Model { + /** + * The path to the file system where the model will be stored. + */ + public final File filesystemPath; + + /** + * The size of the context in terms of tokens. + */ + public final int contextSizeTokens; + + /** + * The alias for the model. + */ + public final String alias; + + /** + * The marker used to signify the end of AI generated text. + */ + public final String endOfTextMarker; + + + /** + * The constructor for the Model class. + * @param filesystemPath The path to the file system where the model will be stored. + * @param contextSizeTokens The size of the context in terms of tokens. + * @param modelAlias The alias for the model. + * @param endOfTextMarker The marker used to signify the end of AI generated text. + */ + public Model(File filesystemPath, int contextSizeTokens, String modelAlias, String endOfTextMarker) { + this.filesystemPath = filesystemPath; + this.contextSizeTokens = contextSizeTokens; + this.alias = modelAlias; + this.endOfTextMarker = endOfTextMarker; + } + + /** + * Prints the details of the model. + */ + 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..b83f277 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/model/ModelLibrary.java @@ -0,0 +1,108 @@ +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; + +public class ModelLibrary { + + /** + * List of all models available in the library. + */ + private final List models; + + private static Model defaultModel; + private final File modelsBaseDirectory; + + /** + * Represents a library of AI models. + * + * @param modelsBaseDirectory the directory containing the models + */ + public ModelLibrary(File modelsBaseDirectory, List configModels) { + this.modelsBaseDirectory = modelsBaseDirectory; + models = new ArrayList<>(); + + + for (ConfigurationModel configModel : configModels) { + addModelFromConfig(configModel); + } + + if (models.isEmpty()) + throw new RuntimeException("No models are defined!"); + + defaultModel = models.get(0); + } + + private void addModelFromConfig(ConfigurationModel configModel) { + + // validate that model actually exists in the filesystem + 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() + )); + } + + + /** + * Adds a given model to the existing models list if it does not already exist. + * + * @param model the model to be added + * @throws RuntimeException if a model with the same alias already exists in the models list + */ + public void addModel(Model model){ + if (findModelByAlias(model.alias).isPresent()) + throw new RuntimeException("Model with alias \"" + model.alias + "\" already exists!"); + + models.add(model); + } + + /** + * Returns a list of all the models. + * + * @return a list of {@link Model} objects representing the models + */ + public List getModels() { + return models; + } + + /** + * Finds a model by its alias. + * + * @param alias the alias of the model to be found + * @return an {@link Optional} containing the model if it was found, or an empty {@link Optional} otherwise + */ + public Optional findModelByAlias(String alias){ + return models.stream() + .filter(model -> model.alias.equals(alias)) + .findFirst(); + } + + public Model getDefaultModel() { + return defaultModel; + } + + /** + * Prints the details of each model in the list. + */ + public void printModels(){ + System.out.println("Available models:\n"); + for (Model model : models) { + model.printModelDetails(); + System.out.println(); + } + } +} diff --git a/src/test/java/eu/svjatoslav/alyverkko_cli/AiTaskTest.java b/src/test/java/eu/svjatoslav/alyverkko_cli/AiTaskTest.java new file mode 100644 index 0000000..f8aaa1f --- /dev/null +++ b/src/test/java/eu/svjatoslav/alyverkko_cli/AiTaskTest.java @@ -0,0 +1,24 @@ +package eu.svjatoslav.alyverkko_cli; + +import org.junit.jupiter.api.Test; + +import static org.testng.AssertJUnit.assertEquals; + +public class AiTaskTest { + + @Test + void testFilterUserInput() { + String input = "* ASSISTANT:\nHello\n* USER:\nHi!"; + String expectedOutput = "ASSISTANT:\nHello\nUSER:\nHi!"; + String output = AiTask.filterParticipantsInUserInput(input); + assertEquals(expectedOutput, output); + } + + @Test + void testFilterUserInputAnyNumberOfSpacesAtTheEnd() { + String input = "* ASSISTANT: \nHello"; + String expectedOutput = "ASSISTANT:\nHello"; + String output = AiTask.filterParticipantsInUserInput(input); + assertEquals(expectedOutput, output); + } +} \ No newline at end of file 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..8be342a --- /dev/null +++ b/tools/update web site @@ -0,0 +1,31 @@ +#!/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 +) + +# 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 \ No newline at end of file -- 2.20.1