From 7e66742cb72338a3bd1a3501b8d1c57cb47344ce Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Sat, 17 May 2025 11:19:37 +0300 Subject: [PATCH 1/1] initial commit --- .gitignore | 13 + COPYING | 121 +++++ alyverkko-cli | 6 + alyverkko-cli.yaml | 16 + doc/QwQ-32B Termination issue.odt | Bin 0 -> 66254 bytes doc/index.org | 506 ++++++++++++++++++ doc/pausing and resuming.odt | Bin 0 -> 72031 bytes doc/setup.org | 289 ++++++++++ install | 108 ++++ logo.png | Bin 0 -> 4012 bytes maven.xml | 15 + pom.xml | 209 ++++++++ .../eu/svjatoslav/alyverkko_cli/AiTask.java | 320 +++++++++++ .../eu/svjatoslav/alyverkko_cli/Command.java | 26 + .../eu/svjatoslav/alyverkko_cli/Main.java | 83 +++ .../eu/svjatoslav/alyverkko_cli/Utils.java | 20 + .../commands/JoinFilesCommand.java | 216 ++++++++ .../commands/ListModelsCommand.java | 45 ++ .../alyverkko_cli/commands/WizardCommand.java | 480 +++++++++++++++++ .../MailCorrespondentCommand.java | 346 ++++++++++++ .../mail_correspondant/MailQuery.java | 41 ++ .../configuration/Configuration.java | 82 +++ .../configuration/ConfigurationHelper.java | 48 ++ .../configuration/ConfigurationModel.java | 38 ++ .../svjatoslav/alyverkko_cli/model/Model.java | 55 ++ .../alyverkko_cli/model/ModelLibrary.java | 126 +++++ .../svjatoslav/alyverkko_cli/AiTaskTest.java | 24 + tools/implement idea | 12 + tools/open with IntelliJ IDEA | 54 ++ tools/update web site | 35 ++ uninstall | 13 + 31 files changed, 3347 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100755 alyverkko-cli create mode 100644 alyverkko-cli.yaml create mode 100644 doc/QwQ-32B Termination issue.odt create mode 100644 doc/index.org create mode 100644 doc/pausing and resuming.odt create mode 100644 doc/setup.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/JoinFilesCommand.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/WizardCommand.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java create mode 100644 src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailQuery.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/ConfigurationHelper.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/implement idea create mode 100755 tools/open with IntelliJ IDEA create mode 100755 tools/update web site create mode 100755 uninstall 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..f9dc574 --- /dev/null +++ b/alyverkko-cli.yaml @@ -0,0 +1,16 @@ +mail_directory: "/home/user/AI/mail" +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 +prompts_directory: "/home/user/.config/alyverkko-cli/prompts" +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.odt b/doc/QwQ-32B Termination issue.odt new file mode 100644 index 0000000000000000000000000000000000000000..c230de4c8b4db62206d30038404b33ce4ca315ae GIT binary patch literal 66254 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 model= + +- 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 () + "Select a prompt and a model interactively, then insert them at the + beginning of the current buffer." + (interactive) + (let ((prompt-dir "~/.config/alyverkko-cli/prompts/") + (config-file "~/.config/alyverkko-cli/alyverkko-cli.yaml") + (models '()) + ) + + (with-temp-buffer + (insert-file-contents config-file) + ;; Move to the beginning of the prompts section + (goto-char (point-min)) + (when (search-forward-regexp "^models:" nil t) + ;; Collect all aliases + (while (search-forward-regexp "^\\s-+- alias: \"\\([^\"]+\\)\"" nil t) + (push (match-string 1) models)))) + + (if (file-exists-p prompt-dir) + (let* ((files (directory-files prompt-dir t "\\`[^.].*\\.txt\\'")) + (aliases (mapcar (lambda (f) (file-name-base f)) files))) + (if aliases + (let ((selected-alias (completing-read "Select prompt alias: " aliases)) + (model (completing-read "Select AI model: " models))) + (alyverkko-insert-tocompute-line selected-alias model)) + (message "No prompt files found."))) + (message "Prompt directory not found.")))) + + (defun alyverkko-insert-tocompute-line (prompt-alias model) + "Inserts TOCOMPUTE line with selected PROMPT-ALIAS and MODEL at the + beginning of the buffer." + (save-excursion + (goto-char (point-min)) + (insert (format "TOCOMPUTE: prompt=%s model=%s\n" prompt-alias model)) + (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]]. + +* TODO + +Ideas to be possibly implemented in the future: + +** Documentation + +- Add example problem statements and resulting solutions on various + domains. Accompany them with precise usage procedure: + - What user did, when, where and how. + - How and where did result appear. + +- Add a section detailing the architecture and design decisions behind + Älyverkko CLI. + +** System operation + +- 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. + +- Explain how to monitor system performance and resource usage during + AI processing tasks. + +- Introduce a comprehensive suite of automated tests to ensure the + reliability and stability of new features before they are released. + +- Address potential security concerns, such as handling sensitive data + in AI processing tasks and securing communication with external + services. + +** Data management + +- in maildir 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. + +** 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. + +- Implement a logging framework with configurable log levels to help + users debug issues 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 preparation and 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. + +- Offer guidance on preparing input files for batch processing, + including best practices for formatting data and structuring problem + statements. + +** 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/doc/pausing and resuming.odt b/doc/pausing and resuming.odt new file mode 100644 index 0000000000000000000000000000000000000000..f5190924fae0f57d5f76b82df6090bee075a58da GIT binary patch literal 72031 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Μ@b~{hdz|sSIYYZ7Hohfs}M1 z(3{vVRB{Q~(+Q@s^*|@h#i?XXB2D6EBGC3bb~i!gUUMqmQ_qC;TxZvpnw(^p=5_R| zT4wtr+70A8zhR$L_mJ=r_1G$Qt}dvs_Zl~1%M+&g#+_saHJ%k&d96%@`j#Yblu0)> ztzGh5vrr#?W%Kw0*@jfQeBQMe&R)9>S&ud0DlO00!{1zwGTF;~C2TxeI{P4gyC_-H zS-!n##MwL|KsqLz(;nTshnfP%nphS#YAL3_S;%;-LcNyRNd^q`z@o?!{ZZUU{KO^uQaH8^9ACJMp^`nSl^pE*%&gR&jqMM^+GQjxE{ocQmN=$-maw3Y06cWCRy1yr10jYj7LF zPe{hGi!NJp;(z>TCL!(&>v_f8Ke;MiAp;ZTduGu!9rHc_l&bSpKG z8j>CHiHS@SyQ*rEipbetO*Y!fPM*yuN@tba9LX~xYCu?GKTy$7keBxEoSM{y2wc5) z)e!oXus#d2+=@-W;EN3hvrd2sMA;fc%|o_=XpfWwtY|0eD)$>*=i1kfltVTKheXm1 z3?J23FjXhtr9t(AfMD$hF39M`Y*6xxUg=cM+ej6KraY}TfrENpnp1)y=H7fyLUQ}* z;lOA{bk#tH806Eb9-V#&oDq@afyYC4HN5b0+Pm5>?^&~)2%6_UaFnsL^Pyr21m+eU z^b(z<8RfvZr|S*7Gp%;Ff*pZwQQM|$1fDG@NXOWilQ`%D{o@nr*QXXMffHe#>#2(T z`*= zsl|6w^splkiBHM$s<&WAHJc?3+Oiu<;*=jW<}E9gc!m_jovwj8_~{}IrnFSJ*F7^Z z&9EC*nLR2xZH+_Y%sSXje)%W!>slcK1pGox+?Ju`K<}RIgIcKuko+9?4!g^}u;xdp z*%$vmG{{Ol3^>AniL;6%?H<{OvL1V@IZ8Qh2%8(mRuJb6jPrDEkXIeuYUh%1U>s9m zv;kGtBv3Y{%|?Yy^E}@*@FmOtvPO?98PUUScg?-uL8ig#%o{aGQ1AL%-e!jt%Wd#I zWj_=(d2-F@2vcR!b6oq1(sM{|z%x6yk_##D>Fi_E0;F)9(myO*-zD2)7CcSw#<_FpBy)ACIw{pMF z`v!Ayg~e4*Z4<&!WB!5DP~fXAaP$43**CkD8%3{#W?S24W~-{a$ZunEL?`}-X8=BbmxKuEhPg50X3+y5CZ1n zXa{%ySDH|s(&o}}l{{iQASP;^cBQW8e)hhwqxJ_`fQDwwZE}2l30HX<39aH1J@&nb zs-v^>mlUudM&Lblp__4)Q8S##Y7-QOz%?gWE?$z^kbDV!*iWqnjbv--@FwF+$5($+ z7)@h%Nmow*bII8ZBYiVDXnpZ1I+mVzdW;SMHsf+}SR@<}ggxRhvJC$?$C}CdxD0KY z16l|b=G~OUO@>0~yUm!7$AY25=@$JZ)^in=K9@f*80u|5wC?tETI)}|ZqxDR?;ZGh zZZ!qhu=gSQsa8&VRGjOa!e(aT^01HSD+`+8Xl<_>;>UQiW_!s>M)v?EZEN5&VUa2j zD1>Jl;V*sYtv*G?YxAgJ^{s53*KN%wrqqgon1D5b8#DgXgrK?|kuB)y$=ZqKMZQxt zBFO~s002(H3jqioJ}rP20JseJS8;6dT9lmoJkuBFdDkz7a74h-K1lels2_(h{n!9# zowpQSUF;m~Jy0;TqpOR%s1VG?2JQ?$Z@Qv{{=~%c6I0}Gm>dx>d$_yMEk`s0hIIcU z8R#dn&fk!sU0t1#Fc-MHz>Q&Y$Dupa1|2 z96S)#E-*)DcOmrmOF^WI{dbn{UjL}(y4WJ%KZYLtal?RLaX%K_3$53GY<})1<=3yk kAFIbjP5zIefq1{K&^j7K#23S^oFDM>qO(AIo@T)R03R(sNB{r; literal 0 HcmV?d00001 diff --git a/doc/setup.org b/doc/setup.org new file mode 100644 index 0000000..ef1c364 --- /dev/null +++ b/doc/setup.org @@ -0,0 +1,289 @@ +:PROPERTIES: +:ID: e469ec1e-402a-476d-a849-662a48eb4f90 +:END: +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Älyverkko CLI application setup +#+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 + +* 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 pretty decent + [[https://huggingface.co/MaziyarPanahi/WizardLM-2-8x22B-GGUF/tree/main][WizardLM-2-8x22B AI model]]. +- 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]]. 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_cli_path: "/home/user/AI/llama.cpp/build/bin/llama-cli" + batch_thread_count: 10 + thread_count: 6 + prompts_directory: "/home/user/.config/alyverkko-cli/prompts" + 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 +#+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. + +- prompts_directory :: Directory where prompts are stored. + - Each prompt is a .txt file with the same name as its alias. + - Example prompts directory content: + #+begin_verse + default.txt + writer.txt + #+end_verse + + Example content for *writer.txt*: + : You are best-selling book writer. + + See more [[id:2109b238-3f2a-4ecb-9f37-d17b52175c82][example prompts that you can try.]] + +- 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_cli_path :: Specifies the filesystem path to the *llama.cpp* + project *llama-cli* executable file. + - Example Value: /home/user/AI/llama.cpp/build/bin/llama-cli + - 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. + - 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*. + + +*** Example prompt files that you can try +:PROPERTIES: +:ID: 2109b238-3f2a-4ecb-9f37-d17b52175c82 +:END: + +Deliver insights: +#+begin_example +This 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. +#+end_example + +Summarize text into LOLcat speak: +#+begin_example +Ur task iz to rite sumree ov wut teh hooman sez, lyk storee, blog, or +tootorial, etc. + +Make teh sumree so fun an captivatin dat teh reeder finks teh author iz +spikin directly to dem. Thiz iz why u must hide teh fact dat it iz a +sumree. No mentshun o' teh authur. Just focus on teh meowssage an sharez. + +Forbidden stuf iz like dis: + +"a thought-provoking piece titled ..." +"the author delves into the ..." +"a comment from a reader named ..." +"This observation leads the author to ..." +"Reflecting on her own experiences as ..." + +Cuz dem stuf talks 'bout da hooman or his writin an distracts teh +reeder. Rite teh sumree so it's just like a shortr version uv teh +preev-yus text, like teh authur self wrote it. + +If iz a storee, kepp teh feelz an emocshuns like teh originnal +tail wud. + +If iz a guide or tutoorial, make sure sumree haz all teh intrestin +explanashuns an steppies, so dat by readin ur version, the same quest +an target can be done just the same as teh originnal mewtoorial. + +If iz nooz, keep teh fax like who, where, when, an most impooortantly +why. Alwayz try an esplains teh whys if possible, add ur pawsome +foughts on motiveyz or why it hapened, so we can lern sumfing an +remeembr teh lesuns. + +Ensure teh key point o' teh originnal writings is kep awl shiny an +bright fer teh reedurs. Also, u gotta translate to lolcat bi talkin +funny. Rite fur teh lulz! +#+end_example + +** 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. + +* 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 diff --git a/install b/install new file mode 100755 index 0000000..188e05b --- /dev/null +++ b/install @@ -0,0 +1,108 @@ +#!/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() { + local desktop_entry_path="/usr/share/applications/alyverkko-cli.desktop" + + cat < /dev/null +[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 mail +Categories=Development; +EOF + + sudo chmod 644 "$desktop_entry_path" +} + +# Function to install systemd service +install_systemd_service() { + + 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 + 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/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..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/AiTask.java b/src/main/java/eu/svjatoslav/alyverkko_cli/AiTask.java new file mode 100644 index 0000000..1ce9a92 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/AiTask.java @@ -0,0 +1,320 @@ +package eu.svjatoslav.alyverkko_cli; + +import eu.svjatoslav.alyverkko_cli.commands.mail_correspondant.MailQuery; + +import java.io.*; +import java.nio.file.Files; + +import static eu.svjatoslav.alyverkko_cli.Main.configuration; +import static java.lang.String.join; + +/** + * Encapsulates the process of running an AI inference query via + * llama.cpp. It prepares an input file, spawns the process, collects + * output, and cleans up temporary files. + */ +public class AiTask { + + /** + * Marker for the AI's response block, used in the constructed prompt string. + */ + public static final String AI_RESPONSE_MARKER = "ASSISTANT:"; + + /** + * 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 MailQuery mailQuery; + + /** + * The temperature (creativity factor) for the AI. + */ + private final Float temperature; + + /** + * Temporary file used as input to the llama.cpp CLI. + */ + private File inputFile; + + /** + * Creates a new AI task with a given mail query. + * + * @param mailQuery the mail query containing model and prompts. + */ + public AiTask(MailQuery mailQuery) { + this.mailQuery = mailQuery; + this.temperature = configuration.getDefaultTemperature(); + } + + /** + * 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() { + StringBuilder sb = new StringBuilder(); + sb.append("SYSTEM:\n").append(mailQuery.systemPrompt).append("\n"); + + String filteredUserPrompt = filterParticipantsInUserInput(mailQuery.userPrompt); + if (!filteredUserPrompt.startsWith("USER:")) { + sb.append("USER:\n"); + } + sb.append(filteredUserPrompt).append("\n"); + + sb.append(AI_RESPONSE_MARKER); + return sb.toString(); + } + + /** + * In the user input, rewrite lines like "* USER:" or "* ASSISTANT:" + * to "USER:" or "ASSISTANT:" so that we standardize them in the final prompt. + * + * @param input the raw user input. + * @return a sanitized or standardized version of the user prompt. + */ + 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(); + } + + /** + * In the AI's response, revert lines like "ASSISTANT:" to "* ASSISTANT:" + * for easier reading in org-mode, plus append a * USER: prompt at the end + * to form the basis for a continuing conversation. + * + * @param response the raw AI response. + * @return a sanitized response for org-mode usage. + */ + 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(); + } + + /** + * 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 { + // Build input prompt + initializeInputFile(buildAiQuery()); + + // Prepare 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 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(); + + // Clean up the AI response: remove partial prompt text, end-of-text marker, etc. + return filterParticipantsInAiResponse(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 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 (mailQuery.model.endOfTextMarker != null) { + int endOfTextMarkerIndex = result.indexOf(mailQuery.model.endOfTextMarker); + if (endOfTextMarkerIndex != -1) { + result = result.substring(0, endOfTextMarkerIndex); + } + } + + return result + "\n"; + } + + /** + * 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 " + mailQuery.model.filesystemPath, + "--threads " + configuration.getThreadCount(), + "--threads-batch " + configuration.getBatchThreadCount(), + "--mirostat 2", + "--flash-attn", + "--cache-type-k q8_0", + "--cache-type-v q8_0", + "--no-warmup", + "--temp " + temperature, + "--ctx-size " + mailQuery.model.contextSizeTokens, + "--batch-size 8", + "--no-conversation", + "-n -1", + "--repeat_penalty 1.1", + "--file " + inputFile + ); + } + + /** + * 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/Command.java b/src/main/java/eu/svjatoslav/alyverkko_cli/Command.java new file mode 100644 index 0000000..dfd9b14 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/Command.java @@ -0,0 +1,26 @@ +package eu.svjatoslav.alyverkko_cli; + +import java.io.IOException; + +/** + * A simple interface for all subcommands used by the Älyverkko CLI. + * Implementing classes define a unique name (e.g., "wizard") and an + * {@code execute} method for the command's logic. + */ +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..f7d36a9 --- /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.mail_correspondant.MailCorrespondentCommand; +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 MailCorrespondentCommand(), + 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..8f42cde --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/Utils.java @@ -0,0 +1,20 @@ +package eu.svjatoslav.alyverkko_cli; + +/** + * Utility functions for miscellaneous tasks such as colored console output. + */ +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..072bb3a --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java @@ -0,0 +1,216 @@ +package eu.svjatoslav.alyverkko_cli.commands; + +import eu.svjatoslav.alyverkko_cli.Command; +import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper; +import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser; +import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.DirectoryOption; +import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.NullOption; +import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.StringOption; +import eu.svjatoslav.commons.string.GlobMatcher; +import org.apache.commons.lang3.StringUtils; + +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.getMailDirectory().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..5078177 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java @@ -0,0 +1,45 @@ +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; + +/** + * Lists all configured models in the system, loading them from the + * user’s configuration and printing them to the console. + */ +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..36616d6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java @@ -0,0 +1,480 @@ +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.*; + +/** + * A single WizardCommand that: + * 1. Loads existing configuration (if any). + * 2. Performs "selftest" style validation checks interactively, + * prompting the user to fix invalid or missing items. + * 3. If no config file exists, it goes through all config parameters + * from scratch. + * 4. Offers to remove model entries that reference missing files. + * 5. Autodiscovers new .gguf files and lets the user add them to the config. + * 6. Saves the resulting (fixed) config file. + * 7. Prints a final pass/fail summary for the user. + */ +public class WizardCommand implements Command { + + // Command-line parser to handle wizard arguments + private final Parser 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.setMailDirectory( + checkDirectory( + configuration.getMailDirectory(), + "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.setPromptsDirectory( + checkDirectory( + configuration.getPromptsDirectory(), + "Prompts directory", + null, + "The prompts directory is where the AI prompt files (*.txt) 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/mail_correspondant/MailCorrespondentCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java new file mode 100644 index 0000000..469f626 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java @@ -0,0 +1,346 @@ +package eu.svjatoslav.alyverkko_cli.commands.mail_correspondant; + +import eu.svjatoslav.alyverkko_cli.*; +import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper; +import eu.svjatoslav.alyverkko_cli.model.Model; +import eu.svjatoslav.alyverkko_cli.model.ModelLibrary; +import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser; +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.HashMap; +import java.util.Map; +import java.util.Optional; + +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; +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; + +/** + * The MailCorrespondentCommand continuously monitors a specified mail + * directory for new or modified text files, checks if they have a + * "TOCOMPUTE:" marker, and if so, processes them with an AI model. + * Once processed, results are appended to the same file. + *

+ * Usage: + *

+ *   alyverkko-cli mail
+ * 
+ */ +public class MailCorrespondentCommand 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 mailDir; + + /** + * @return the name of this command, i.e., "mail". + */ + @Override + public String getCommandName() { + return "mail"; + } + + /** + * Executes the "mail" command, loading configuration, starting a + * WatchService on the mail directory, and running an infinite loop + * that processes newly discovered tasks. + * + * @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 = loadConfiguration(getConfigurationFile(configFileOption)); + if (configuration == null) { + System.out.println("Failed to load configuration file"); + return; + } + + modelLibrary = new ModelLibrary(configuration.getModelsDirectory(), configuration.getModels()); + mailDir = configuration.getMailDirectory(); + + // Set up directory watch service + initializeFileWatcher(); + + // Process any existing files that might already be in the directory + initialMailScanAndReply(); + + System.out.println("Mail correspondent running. Press CTRL+c to terminate."); + + // Main loop: watch for file events + while (true) { + WatchKey key; + try { + key = directoryWatcher.take(); + } catch (InterruptedException e) { + System.out.println("Interrupted while waiting for file system events. Exiting."); + break; + } + + System.out.println("Detected filesystem event."); + + // Sleep briefly to allow the file to be fully written + Thread.sleep(1000); + + processDetectedFilesystemEvents(key); + + if (!key.reset()) { + break; + } + } + + directoryWatcher.close(); + } + + /** + * Performs an initial scan of existing files in the mail directory, + * processing those that need AI inference (i.e., that start with "TOCOMPUTE:"). + * + * @throws IOException if reading files fails. + * @throws InterruptedException if the thread is interrupted. + */ + private void initialMailScanAndReply() throws IOException, InterruptedException { + File[] files = mailDir.listFiles(); + if (files == null) return; + + for (File file : files) { + processMailIfNeeded(file); + } + } + + /** + * 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; + } + + // 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 { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String firstLine = reader.readLine(); + return firstLine != null && firstLine.startsWith("TOCOMPUTE:"); + } + } + + /** + * Processes a file if it has the "TOCOMPUTE:" marker, running an AI + * query and appending the result to the file. Otherwise logs that + * it's being ignored. + * + * @param file the file to possibly process. + * @throws IOException if reading/writing the file fails. + * @throws InterruptedException if the AI query is interrupted. + */ + 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 content + String inputFileContent = getFileContentsAsString(file); + + // Parse the relevant data into a MailQuery object + MailQuery mailQuery = parseInputFileContent(inputFileContent); + + // Create an AiTask and run the query + AiTask aiTask = new AiTask(mailQuery); + String aiGeneratedResponse = aiTask.runAiQuery(); + + // Build new content + StringBuilder resultFileContent = new StringBuilder(); + + // Ensure the user prompt block is labeled if it isn't already + if (!mailQuery.userPrompt.startsWith("* USER:\n")) { + resultFileContent.append("* USER:\n"); + } + resultFileContent.append(mailQuery.userPrompt).append("\n"); + + // Append the AI response block + resultFileContent + .append("* ASSISTANT:\n") + .append(aiGeneratedResponse) + .append("\n"); + + // Write the combined result back to the same file + saveToFile(file, resultFileContent.toString()); + } + + /** + * Converts the raw file content (including the line beginning with "TOCOMPUTE:") + * into a {@link MailQuery} object that the AI can process. + * + * @param inputFileContent the raw contents of the mail file. + * @return a {@link MailQuery} containing the system prompt, user prompt, and the selected model. + * @throws IOException if reading prompt files fails. + */ + private MailQuery parseInputFileContent(String inputFileContent) throws IOException { + MailQuery mailQuery = new MailQuery(); + + // Find the newline that separates "TOCOMPUTE: ..." from the rest + int firstNewLineIndex = inputFileContent.indexOf('\n'); + if (firstNewLineIndex == -1) { + throw new IllegalArgumentException("Input file is only one line long. Content: " + inputFileContent); + } else { + // The user prompt is everything after the first line + mailQuery.userPrompt = inputFileContent.substring(firstNewLineIndex + 1); + } + + // The first line will look like "TOCOMPUTE: model=... prompt=... etc." + String firstLine = inputFileContent.substring(0, firstNewLineIndex); + + // Parse out the key/value pairs + Map settings = parseSettings(firstLine); + + // Look up system prompt from the "prompt" alias + String promptAlias = settings.getOrDefault("prompt", "default"); + mailQuery.systemPrompt = configuration.getPromptByAlias(promptAlias); + + // Resolve model from the "model" alias + String modelAlias = settings.getOrDefault("model", "default"); + Optional modelOptional = modelLibrary.findModelByAlias(modelAlias); + if (!modelOptional.isPresent()) { + throw new IllegalArgumentException("Model with alias '" + modelAlias + "' not found."); + } + mailQuery.model = modelOptional.get(); + + return mailQuery; + } + + /** + * 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 + * or modification), then processes those files if necessary. + * + * @param key the watch key containing the events. + * @throws IOException if file reading/writing fails. + * @throws InterruptedException if the AI process is interrupted. + */ + 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; + } + + // The filename for the event + Path filename = ((WatchEvent) event).context(); + System.out.println("Event: " + kind + " for file: " + filename); + + // Process the file + if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY) { + File file = mailDir.toPath().resolve(filename).toFile(); + processMailIfNeeded(file); + } + } + } + + /** + * Registers the mail directory with a WatchService for ENTRY_CREATE + * and ENTRY_MODIFY events. + * + * @throws IOException if registration fails. + */ + private void initializeFileWatcher() throws IOException { + this.directoryWatcher = FileSystems.getDefault().newWatchService(); + Paths.get(mailDir.getAbsolutePath()).register(directoryWatcher, ENTRY_CREATE, ENTRY_MODIFY); + } +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailQuery.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailQuery.java new file mode 100644 index 0000000..a33584f --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailQuery.java @@ -0,0 +1,41 @@ +package eu.svjatoslav.alyverkko_cli.commands.mail_correspondant; + +import eu.svjatoslav.alyverkko_cli.model.Model; + +/** + * Represents the data needed to perform a single mail-based AI query, + * containing prompts and the specific AI model to use. + */ +public class MailQuery { + + /** + * 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 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; + + /** + * Returns a string containing a summary of the {@link MailQuery} 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 + + '}'; + } +} 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..4e0c52e --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java @@ -0,0 +1,82 @@ +package eu.svjatoslav.alyverkko_cli.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.*; +import java.util.List; + +import static eu.svjatoslav.commons.file.IOHelper.getFileContentsAsString; + +/** + * Encapsulates all user configuration for the Älyverkko CLI application, + * such as model directories, mail directory, default temperature, + * llama-cli path, etc. + */ +@Data +public class Configuration { + + /** + * Directory where AI tasks (mail) are placed and discovered. + */ + @JsonProperty("mail_directory") + private File mailDirectory; + + /** + * 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; + + /** + * 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("prompts_directory") + private File promptsDirectory; + + /** + * 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 prompts directory. + * + * @param alias 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 String getPromptByAlias(String alias) throws IOException { + File promptFile = new File(promptsDirectory, alias + ".txt"); + return getFileContentsAsString(promptFile); + } +} diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationHelper.java b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationHelper.java new file mode 100644 index 0000000..45891db --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationHelper.java @@ -0,0 +1,48 @@ +package eu.svjatoslav.alyverkko_cli.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption; + +import java.io.File; +import java.io.IOException; + +public class ConfigurationHelper { + + /** + * The default path for the YAML config file, typically under the user's home directory. + */ + public static final String DEFAULT_CONFIG_FILE_PATH = "~/.config/alyverkko-cli/alyverkko-cli.yaml".replaceFirst("^~", System.getProperty("user.home")); + + /** + * Loads the configuration from a given file, or from the default + * path if {@code configFile} is null. + * + * @param configFile the file containing the YAML config; may be null. + * @return the {@link Configuration} object, or null if not found/invalid. + * @throws IOException if file I/O fails during reading. + */ + public static Configuration loadConfiguration(File configFile) throws IOException { + + if (!configFile.exists()) { + System.err.println("Configuration file not found: " + configFile); + return null; + } + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + return mapper.readValue(configFile, Configuration.class); + } + + /** + * Returns the configuration file from the given option, or the default path if not present. + * @param configFileOption the CLI option for the config file. + * @return the configuration file to load. + */ + public static File getConfigurationFile(FileOption configFileOption) { + if (configFileOption != null) + if (configFileOption.isPresent()) + return configFileOption.getValue(); + + return new File(DEFAULT_CONFIG_FILE_PATH); + } +} 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..604f327 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationModel.java @@ -0,0 +1,38 @@ +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; + + /** + * 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/model/Model.java b/src/main/java/eu/svjatoslav/alyverkko_cli/model/Model.java new file mode 100644 index 0000000..e985ac1 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/model/Model.java @@ -0,0 +1,55 @@ +package eu.svjatoslav.alyverkko_cli.model; + +import java.io.File; + +/** + * Represents an AI model stored on the filesystem, including details such + * as path, context size, alias, and an optional end-of-text 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; + + /** + * Constructs a {@link Model} instance. + * + * @param filesystemPath The path to the model file on the filesystem. + * @param contextSizeTokens The size of the context in tokens. + * @param modelAlias A short alias by which the model is referenced. + * @param endOfTextMarker Optional text that signifies the end of the AI's output. + */ + public Model(File filesystemPath, int contextSizeTokens, String modelAlias, String endOfTextMarker) { + this.filesystemPath = filesystemPath; + this.contextSizeTokens = contextSizeTokens; + this.alias = modelAlias; + this.endOfTextMarker = endOfTextMarker; + } + + /** + * Prints the model's alias, path, and context size to standard output. + */ + 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..b53f9e4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/model/ModelLibrary.java @@ -0,0 +1,126 @@ +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 the given + * {@link ConfigurationModel}, verifying that the file actually exists. + * + * @param configModel the configuration describing the model. + */ + 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() + )); + } + + /** + * 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/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/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..7507bff --- /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 -- 2.20.1