├── .github └── workflows │ └── main.yml ├── CITATION.cff ├── LICENSE ├── LICENSE-cutest.txt ├── LICENSE-inih.txt ├── README.md └── src ├── .gitignore ├── CuTest.c ├── CuTest.h ├── Makefile ├── ai-cli-activate-bash.sh ├── ai-cli-config ├── ai_cli.5 ├── ai_cli.7 ├── ai_cli.c ├── all_tests.c ├── config.c ├── config.h ├── config_test.c ├── fetch_anthropic.c ├── fetch_anthropic.h ├── fetch_anthropic_test.c ├── fetch_hal.c ├── fetch_hal.h ├── fetch_llamacpp.c ├── fetch_llamacpp.h ├── fetch_llamacpp_test.c ├── fetch_openai.c ├── fetch_openai.h ├── fetch_openai_test.c ├── ini.c ├── ini.h ├── rl_driver.c ├── support.c ├── support.h ├── support_test.c └── unit_test.h /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: ai-cli CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macOS-latest] 17 | 18 | steps: 19 | - uses: actions/checkout@main 20 | 21 | - name: Install dependencies (Ubuntu) 22 | if: runner.os == 'Linux' 23 | run: sudo apt-get update && sudo apt-get install libcurl4 libcurl4-openssl-dev libjansson4 libjansson-dev libreadline-dev 24 | 25 | - name: Install dependencies (macOS) 26 | if: runner.os == 'macOS' 27 | run: | 28 | brew update 29 | brew upgrade || true 30 | brew install jansson readline 31 | 32 | - name: Make 33 | run: make -C src 34 | 35 | - name: Unit test 36 | run: make -C src unit-test 37 | 38 | - name: Global name verification 39 | run: make -C src verify-global-defs 40 | 41 | - name: End-to-end test 42 | run: make -C src e2e-test 43 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: ai-cli-lib 3 | message: >- 4 | If you use this software, please cite, using the metadata from this file, 5 | both the article from preferred-citation and the software itself. 6 | preferred-citation: 7 | date-released: "2023-11-01" 8 | doi: "10.1109/MS.2023.3307170" 9 | title: "Commands as AI Conversations" 10 | authors: 11 | - family-names: "Spinellis" 12 | given-names: "Diomidis" 13 | type: "article" 14 | volume: 40 15 | issue: 6 16 | journal: "IEEE Software" 17 | start: 22 18 | end: 26 19 | type: software 20 | authors: 21 | - given-names: Diomidis 22 | family-names: Spinellis 23 | email: dds@aueb.gr 24 | affiliation: Athens University of Economics and Business 25 | orcid: 'https://orcid.org/0000-0003-4231-1897' 26 | repository-code: 'https://github.com/dspinellis/ai-cli-lib' 27 | keywords: 28 | - CLI 29 | - LLM 30 | - Generative AI 31 | - Unix 32 | - Shell 33 | - Readline 34 | - command line interface 35 | license: Apache-2.0 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-cutest.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2003 Asim Jalis 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software in 13 | a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 16 | 2. Altered source versions must be plainly marked as such, and must not 17 | be misrepresented as being the original software. 18 | 19 | 3. This notice may not be removed or altered from any source 20 | distribution. 21 | -------------------------------------------------------------------------------- /LICENSE-inih.txt: -------------------------------------------------------------------------------- 1 | 2 | The "inih" library is distributed under the New BSD license: 3 | 4 | Copyright (c) 2009, Ben Hoyt 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | * Neither the name of Ben Hoyt nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY BEN HOYT ''AS IS'' AND ANY 19 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL BEN HOYT BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://img.shields.io/github/actions/workflow/status/dspinellis/ai-cli-lib/main.yml?branch=main) 2 | 3 | # ai-cli-lib: AI help for CLI programs 4 | The __ai-cli__ 5 | library detects programs that offer interactive command-line editing 6 | through the __readline__ library, 7 | and modifies their interface to allow obtaining help from a GPT 8 | large language model server, such as Anthropic's or OpenAI's, 9 | or one provided through a 10 | [llama.cpp](https://github.com/ggerganov/llama.cpp) server. 11 | Think of it as a command line copilot. 12 | 13 | 14 | ![Demonstration](https://gist.githubusercontent.com/dspinellis/b25d9b3c3e6d6a6260774c32dc7be817/raw/d74986b42bd463a8f2fb57f59f094b2c154bf6a5/ai-cli.gif) 15 | 16 | ## Build 17 | The _ai-cli_ 18 | library has been built and tested under the Debian GNU/Linux (bullseye) 19 | distribution 20 | (natively under version 11 and the x86_64 and armv7l architectures 21 | and under Windows Subsystem for Linux version 2), 22 | under macOS (Ventura 13.4) on the arm64 architecture 23 | using Homebrew packages and executables linked against GNU Readline 24 | (not the macOS-supplied _editline_ compatibility layer), 25 | and under Cygwin (3.4.7). 26 | On Linux, 27 | in addition to _make_, a C compiler, and the GNU C library, 28 | the following packages are required: 29 | `libcurl4-openssl-dev` 30 | `libjansson-dev` 31 | `libreadline-dev`. 32 | On macOS, in addition to an Xcode installation, the following Homebrew 33 | packages are required: 34 | `jansson` 35 | `readline`. 36 | On Cygwin in addition to _make_, a C compiler, and the GNU C library, 37 | the following packages are required: 38 | `libcurl-devel`, 39 | `libjansson-devel`, 40 | `libreadline-devel`. 41 | Package names may be different on other systems. 42 | 43 | ```sh 44 | cd src 45 | make 46 | ``` 47 | 48 | ## Test 49 | ### Unit testing 50 | ```sh 51 | cd src 52 | make unit-test 53 | ``` 54 | 55 | ### End-to-end testing 56 | ```sh 57 | cd src 58 | make e2e-test 59 | ``` 60 | This will provide you a simple read-print loop where you can test the 61 | _ai-cli_ library's capability to 62 | link with the Readline API of third party programs. 63 | 64 | ## Install 65 | ```sh 66 | cd src 67 | 68 | # Global installation for all users 69 | sudo make install 70 | 71 | # Local installation for the user executing the command 72 | make install PREFIX=~ 73 | ``` 74 | 75 | ## Run 76 | * Configure the _ai-cli_ library to be activated when your _bash_ 77 | shell starts up by adding the following lines in your `.bashrc` file 78 | (ideally near its beginning for performance reasons). 79 | Adjust the provided path match the _ai-cli_ library installation path; 80 | it is currently set for a local installation in your home directory. 81 | ```bash 82 | # Initialize the ai-cli library 83 | source $HOME/share/ai-cli/ai-cli-activate-bash.sh 84 | ``` 85 | * Alternatively, implement one of the following system-specific configurations. 86 | * Under __Linux__ and __Cygwin__ set the `LD_PRELOAD` environment variable 87 | to load the library using its full path. 88 | For example, under _bash_ run 89 | `export LD_PRELOAD=/usr/local/lib/ai_cli.so` (global installation) or 90 | `export LD_PRELOAD=/home/myname/lib/ai_cli.so` (local installation). 91 | * Under __macOS__ set the `DYLD_INSERT_LIBRARIES` environment variable to 92 | load the library using its full path. 93 | For example, under _bash_ run 94 | `export DYLD_INSERT_LIBRARIES=/Users/myname/lib/ai_cli.dylib`. 95 | Also set the `DYLD_LIBRARY_PATH` environment variable to include 96 | the Homebrew library directory, e.g. 97 | `export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH`. 98 | * Perform one of the following. 99 | * Obtain your 100 | [Anthropic API key](https://console.anthropic.com/settings/keys) 101 | or 102 | [OpenAI API key](https://platform.openai.com/api-keys) 103 | and configure it in the `.aicliconfig` file in your home directory. 104 | This is done with a `key={key}` entry in the file's 105 | `[anthropic]` or `[openai]` section. 106 | In addition, add `api=anthropic` or `api=openai` in the file's 107 | `[general]` section. 108 | See the file [ai-cli-config](src/ai-cli-config) to understand how configuration 109 | files are structured. 110 | Anthropic currently provides free trial credits to new users. 111 | Note that OpenAI API access requires a different (usage-based) 112 | subscription from the ChatGPT one. 113 | * Configure a [llama.cpp](https://github.com/ggerganov/llama.cpp) server 114 | and list its `endpoint` (e.g. `endpoint=http://localhost:8080/completion` 115 | in the configuration file's `[llamacpp]` section. 116 | In addition, add `api=llamacpp` in the file's `[general]` section. 117 | In brief running a _llama.cpp_ server involves 118 | * compiling [llama.cpp](https://github.com/ggerganov/llama.cpp) (ideally 119 | with GPU support), 120 | * downloading, converting, and quantizing suitable model 121 | files (use files with more than 7 billion parameters only on GPUs 122 | with sufficient memory to hold them), 123 | * Running the server with a command such as `server -m models/llama-2-13b-chat/ggml-model-q4_0.gguf -c 2048 --n-gpu-layers 100`. 124 | * Run the interactive command-line programs, such as 125 | _bash_, _mysql_, _psql_, _gdb_, _sqlite3_, _bc_, as you normally would. 126 | * If the program you want to prompt in natural language isn't linked 127 | with the GNU Readline library, you can still make it work with Readline, 128 | by invoking it through [rlwrap](https://github.com/hanslub42/rlwrap). 129 | This however looses the program-specific context provision, because 130 | the program's name appears to The _ai-cli_ library as `rlwrap`. 131 | * To obtain AI help, enter a natural language prompt and press `^X-a` (Ctrl-X followed by a) 132 | in the (default) _Emacs_ key binding mode or `V` if you have configured 133 | _vi_ key bindings. 134 | * Keep in mind that by default _ai-cli-lib_ is sending previously entered 135 | commands as context to the model engine you are using. 136 | This may leak secrets that you enter, for example by setting an environment 137 | variable to contain a key or by configuring a database password. 138 | To avoid this problem configure the `context` setting to zero, 139 | or use the command-line program's offered method to avoid storing 140 | an entered line. 141 | For instance, in _bash_ you can do this by starting the line with a 142 | space character. 143 | 144 | 145 | ### Note for macOS users 146 | Note that macOS ships with the _editline_ line-editing library, 147 | which is currently not compatible with the _ai-cli_ library 148 | (it has been designed to tap onto GNU Readline). 149 | However, Homebrew tools link with GNU Readline, so they can be used 150 | with the _ai-cli_ library. 151 | To find out whether a tool you're using links with GNU Readline (`libreadline`) 152 | or with _editline_ (`libedit`), 153 | use the _which_ command to determine the command's full 154 | path, and then the _otool_ command to see the libraries it is linked with. 155 | In the example below, 156 | `/usr/bin/sqlite3` isn't linked with GNU Readline, 157 | but `/opt/homebrew/opt/sqlite/bin/sqlite3` is linked with _editline_. 158 | 159 | ``` 160 | $ which sqlite3 161 | /usr/bin/sqlite3 162 | 163 | $ otool -L /usr/bin/sqlite3 164 | /usr/bin/sqlite3: 165 | /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0) 166 | /usr/lib/libedit.3.dylib (compatibility version 2.0.0, current version 3.0.0) 167 | /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3) 168 | 169 | $ otool -L /opt/homebrew/opt/sqlite/bin/sqlite3 170 | /opt/homebrew/opt/sqlite/bin/sqlite3: 171 | /opt/homebrew/opt/readline/lib/libreadline.8.dylib (compatibility version 8.2.0, current version 8.2.0) 172 | /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0) 173 | /usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11) 174 | /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3) 175 | 176 | ``` 177 | 178 | Consequently, 179 | if you want to use the capabilities of the _ai-cli_ library, configure your system 180 | to use the Homebrew commands in preference to the ones supplied with macOS. 181 | 182 | 183 | ## Reference documentation 184 | The _ai-cli_ reference documentation is provided as Unix manual 185 | pages. 186 | * [ai-cli(7) — library](https://dspinellis.github.io/manview/?src=https%3A%2F%2Fraw.githubusercontent.com%2Fdspinellis%2Fai-cli%2Fmain%2Fsrc%2Fai_cli.7&name=ai_cli(7)&link=https%3A%2F%2Fgithub.com%2Fdspinellis%2Fai-cli) 187 | * [ai-cli(5) — configuration](https://dspinellis.github.io/manview/?src=https%3A%2F%2Fraw.githubusercontent.com%2Fdspinellis%2Fai-cli%2Fmain%2Fsrc%2Fai_cli.5&name=ai_cli(5)&link=https%3A%2F%2Fgithub.com%2Fdspinellis%2Fai-cli) 188 | 189 | ## Contribute 190 | Contributions are welcomed through GitHub pull requests. 191 | Before working on something substantial, 192 | open an issue to signify your interest and coordinate with others. 193 | Particular useful are: 194 | * multi-shot prompts for systems not yet supported 195 | (see the [ai-cli-config](src/ai-cli-config) file), 196 | * support for other large language models 197 | (start from the [openai_fetch.c](src/openai_fetch.c) file), 198 | * support for other libraries (mainly [editline](https://man.netbsd.org/editline.3)), 199 | * ports to other platforms and distributions. 200 | 201 | ## See also 202 | * Diomidis Spinellis. [Commands as AI Conversations](https://doi.org/10.1109/MS.2023.3307170). _IEEE Software_ 40(6), November/December 2023. doi: 10.1109/MS.2023.3307170 203 | * [edX open online course on Unix tools for data, software, and production engineering](https://www.spinellis.gr/unix/?ai-cli) 204 | * Agarwal, Mayank et al. [NeurIPS 2020 NLC2CMD Competition: Translating Natural Language to Bash Commands](https://arxiv.org/pdf/2103.02523.pdf). ArXiv abs/2103.02523 (2021): n. pag. 205 | * [celery-ai](https://github.com/ortegaalfredo/celery-ai): similar idea, but without program-specific prompts; works by monitoring keyboard events through [pynput](https://pynput.readthedocs.io/). 206 | * [ChatGDB](https://github.com/pgosar/ChatGDB): a _gdb_-specific front-end. 207 | * [ai-cli](https://github.com/abhagsain/ai-cli): a (similarly named) command line interface to AI models. 208 | * [Warp AI](https://www.warp.dev/warp-ai): A terminal with integrated AI capabilities. 209 | 210 | ## Acknowledgements 211 | * API requests are made using [libcurl](https://curl.se/libcurl/). 212 | * The configuration file parsing is based on [inih](https://github.com/benhoyt/inih). 213 | * Unit testing uses [CuTest](https://github.com/ennorehling/cutest). 214 | * JSON is parsed using [Jansson](https://github.com/akheron/jansson/). 215 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .aicliconfig 3 | .gdb_history 4 | ai_cli.dll 5 | ai_cli.dylib 6 | ai_cli.so 7 | ai-cli.log 8 | all-tests 9 | all-tests.exe 10 | eg.py 11 | rl_driver 12 | rl_driver.exe 13 | Session.vim 14 | tags 15 | -------------------------------------------------------------------------------- /src/CuTest.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "CuTest.h" 9 | 10 | /*-------------------------------------------------------------------------* 11 | * CuStr 12 | *-------------------------------------------------------------------------*/ 13 | 14 | char* CuStrAlloc(size_t size) 15 | { 16 | char* newStr = (char*) malloc( sizeof(char) * (size) ); 17 | return newStr; 18 | } 19 | 20 | char* CuStrCopy(const char* old) 21 | { 22 | size_t len = strlen(old); 23 | char* newStr = CuStrAlloc(len + 1); 24 | strcpy(newStr, old); 25 | return newStr; 26 | } 27 | 28 | /*-------------------------------------------------------------------------* 29 | * CuString 30 | *-------------------------------------------------------------------------*/ 31 | 32 | void CuStringInit(CuString* str) 33 | { 34 | str->length = 0; 35 | str->size = STRING_MAX; 36 | str->buffer = (char*) malloc(sizeof(char) * str->size); 37 | str->buffer[0] = '\0'; 38 | } 39 | 40 | CuString* CuStringNew(void) 41 | { 42 | CuString* str = (CuString*) malloc(sizeof(CuString)); 43 | str->length = 0; 44 | str->size = STRING_MAX; 45 | str->buffer = (char*) malloc(sizeof(char) * str->size); 46 | str->buffer[0] = '\0'; 47 | return str; 48 | } 49 | 50 | void CuStringDelete(CuString *str) 51 | { 52 | if (!str) return; 53 | free(str->buffer); 54 | free(str); 55 | } 56 | 57 | void CuStringResize(CuString* str, size_t newSize) 58 | { 59 | str->buffer = (char*) realloc(str->buffer, sizeof(char) * newSize); 60 | str->size = newSize; 61 | } 62 | 63 | void CuStringAppend(CuString* str, const char* text) 64 | { 65 | size_t length; 66 | 67 | if (text == NULL) { 68 | text = "NULL"; 69 | } 70 | 71 | length = strlen(text); 72 | if (str->length + length + 1 >= str->size) 73 | CuStringResize(str, str->length + length + 1 + STRING_INC); 74 | str->length += length; 75 | strcat(str->buffer, text); 76 | } 77 | 78 | void CuStringAppendChar(CuString* str, char ch) 79 | { 80 | char text[2]; 81 | text[0] = ch; 82 | text[1] = '\0'; 83 | CuStringAppend(str, text); 84 | } 85 | 86 | void CuStringAppendFormat(CuString* str, const char* format, ...) 87 | { 88 | va_list argp; 89 | char buf[HUGE_STRING_LEN]; 90 | va_start(argp, format); 91 | vsprintf(buf, format, argp); 92 | va_end(argp); 93 | CuStringAppend(str, buf); 94 | } 95 | 96 | void CuStringInsert(CuString* str, const char* text, size_t pos) 97 | { 98 | size_t length = strlen(text); 99 | if (pos > str->length) 100 | pos = str->length; 101 | if (str->length + length + 1 >= str->size) 102 | CuStringResize(str, str->length + length + 1 + STRING_INC); 103 | memmove(str->buffer + pos + length, str->buffer + pos, (str->length - pos) + 1); 104 | str->length += length; 105 | memcpy(str->buffer + pos, text, length); 106 | } 107 | 108 | /*-------------------------------------------------------------------------* 109 | * CuTest 110 | *-------------------------------------------------------------------------*/ 111 | 112 | void CuTestInit(CuTest* t, const char* name, TestFunction function) 113 | { 114 | t->name = CuStrCopy(name); 115 | t->failed = 0; 116 | t->ran = 0; 117 | t->message = NULL; 118 | t->function = function; 119 | t->jumpBuf = NULL; 120 | } 121 | 122 | CuTest* CuTestNew(const char* name, TestFunction function) 123 | { 124 | CuTest* tc = CU_ALLOC(CuTest); 125 | CuTestInit(tc, name, function); 126 | return tc; 127 | } 128 | 129 | void CuTestDelete(CuTest *t) 130 | { 131 | if (!t) return; 132 | free(t->name); 133 | free(t); 134 | } 135 | 136 | void CuTestRun(CuTest* tc) 137 | { 138 | jmp_buf buf; 139 | tc->jumpBuf = &buf; 140 | if (setjmp(buf) == 0) 141 | { 142 | tc->ran = 1; 143 | (tc->function)(tc); 144 | } 145 | tc->jumpBuf = 0; 146 | } 147 | 148 | static void CuFailInternal(CuTest* tc, const char* file, int line, CuString* string) 149 | { 150 | char buf[HUGE_STRING_LEN]; 151 | 152 | sprintf(buf, "%s:%d: ", file, line); 153 | CuStringInsert(string, buf, 0); 154 | 155 | tc->failed = 1; 156 | tc->message = string->buffer; 157 | if (tc->jumpBuf != 0) longjmp(*(tc->jumpBuf), 0); 158 | } 159 | 160 | void CuFail_Line(CuTest* tc, const char* file, int line, const char* message2, const char* message) 161 | { 162 | CuString string; 163 | 164 | CuStringInit(&string); 165 | if (message2 != NULL) 166 | { 167 | CuStringAppend(&string, message2); 168 | CuStringAppend(&string, ": "); 169 | } 170 | CuStringAppend(&string, message); 171 | CuFailInternal(tc, file, line, &string); 172 | } 173 | 174 | void CuAssert_Line(CuTest* tc, const char* file, int line, const char* message, int condition) 175 | { 176 | if (condition) return; 177 | CuFail_Line(tc, file, line, NULL, message); 178 | } 179 | 180 | void CuAssertStrEquals_LineMsg(CuTest* tc, const char* file, int line, const char* message, 181 | const char* expected, const char* actual) 182 | { 183 | CuString string; 184 | if ((expected == NULL && actual == NULL) || 185 | (expected != NULL && actual != NULL && 186 | strcmp(expected, actual) == 0)) 187 | { 188 | return; 189 | } 190 | 191 | CuStringInit(&string); 192 | if (message != NULL) 193 | { 194 | CuStringAppend(&string, message); 195 | CuStringAppend(&string, ": "); 196 | } 197 | CuStringAppend(&string, "expected <"); 198 | CuStringAppend(&string, expected); 199 | CuStringAppend(&string, "> but was <"); 200 | CuStringAppend(&string, actual); 201 | CuStringAppend(&string, ">"); 202 | CuFailInternal(tc, file, line, &string); 203 | } 204 | 205 | void CuAssertSizeTEquals_LineMsg(CuTest* tc, const char* file, int line, const char* message, 206 | size_t expected, size_t actual) 207 | { 208 | char buf[STRING_MAX]; 209 | if (expected == actual) return; 210 | sprintf(buf, "expected <%zu> but was <%zu>", expected, actual); 211 | CuFail_Line(tc, file, line, message, buf); 212 | } 213 | 214 | void CuAssertIntEquals_LineMsg(CuTest* tc, const char* file, int line, const char* message, 215 | int expected, int actual) 216 | { 217 | char buf[STRING_MAX]; 218 | if (expected == actual) return; 219 | sprintf(buf, "expected <%d> but was <%d>", expected, actual); 220 | CuFail_Line(tc, file, line, message, buf); 221 | } 222 | 223 | void CuAssertULongEquals_LineMsg(CuTest* tc, const char* file, int line, const char* message, 224 | unsigned long expected, unsigned long actual) 225 | { 226 | char buf[STRING_MAX]; 227 | if (expected == actual) return; 228 | sprintf(buf, "expected <%lu> but was <%lu>", expected, actual); 229 | CuFail_Line(tc, file, line, message, buf); 230 | } 231 | 232 | void CuAssertDblEquals_LineMsg(CuTest* tc, const char* file, int line, const char* message, 233 | double expected, double actual, double delta) 234 | { 235 | char buf[STRING_MAX]; 236 | if (fabs(expected - actual) <= delta) return; 237 | sprintf(buf, "expected <%f> but was <%f>", expected, actual); 238 | 239 | CuFail_Line(tc, file, line, message, buf); 240 | } 241 | 242 | void CuAssertPtrEquals_LineMsg(CuTest* tc, const char* file, int line, const char* message, 243 | void* expected, void* actual) 244 | { 245 | char buf[STRING_MAX]; 246 | if (expected == actual) return; 247 | sprintf(buf, "expected pointer <0x%p> but was <0x%p>", expected, actual); 248 | CuFail_Line(tc, file, line, message, buf); 249 | } 250 | 251 | 252 | /*-------------------------------------------------------------------------* 253 | * CuSuite 254 | *-------------------------------------------------------------------------*/ 255 | 256 | void CuSuiteInit(CuSuite* testSuite) 257 | { 258 | testSuite->count = 0; 259 | testSuite->failCount = 0; 260 | memset(testSuite->list, 0, sizeof(testSuite->list)); 261 | } 262 | 263 | CuSuite* CuSuiteNew(void) 264 | { 265 | CuSuite* testSuite = CU_ALLOC(CuSuite); 266 | CuSuiteInit(testSuite); 267 | return testSuite; 268 | } 269 | 270 | void CuSuiteDelete(CuSuite *testSuite) 271 | { 272 | unsigned int n; 273 | for (n=0; n < MAX_TEST_CASES; n++) 274 | { 275 | if (testSuite->list[n]) 276 | { 277 | CuTestDelete(testSuite->list[n]); 278 | } 279 | } 280 | free(testSuite); 281 | 282 | } 283 | 284 | void CuSuiteAdd(CuSuite* testSuite, CuTest *testCase) 285 | { 286 | assert(testSuite->count < MAX_TEST_CASES); 287 | testSuite->list[testSuite->count] = testCase; 288 | testSuite->count++; 289 | } 290 | 291 | void CuSuiteAddSuite(CuSuite* testSuite, CuSuite* testSuite2) 292 | { 293 | int i; 294 | for (i = 0 ; i < testSuite2->count ; ++i) 295 | { 296 | CuTest* testCase = testSuite2->list[i]; 297 | CuSuiteAdd(testSuite, testCase); 298 | } 299 | } 300 | 301 | void CuSuiteRun(CuSuite* testSuite) 302 | { 303 | int i; 304 | for (i = 0 ; i < testSuite->count ; ++i) 305 | { 306 | CuTest* testCase = testSuite->list[i]; 307 | CuTestRun(testCase); 308 | if (testCase->failed) { testSuite->failCount += 1; } 309 | } 310 | } 311 | 312 | void CuSuiteSummary(CuSuite* testSuite, CuString* summary) 313 | { 314 | int i; 315 | for (i = 0 ; i < testSuite->count ; ++i) 316 | { 317 | CuTest* testCase = testSuite->list[i]; 318 | CuStringAppend(summary, testCase->failed ? "F" : "."); 319 | } 320 | CuStringAppend(summary, "\n\n"); 321 | } 322 | 323 | void CuSuiteDetails(CuSuite* testSuite, CuString* details) 324 | { 325 | int i; 326 | int failCount = 0; 327 | 328 | if (testSuite->failCount == 0) 329 | { 330 | int passCount = testSuite->count - testSuite->failCount; 331 | const char* testWord = passCount == 1 ? "test" : "tests"; 332 | CuStringAppendFormat(details, "OK (%d %s)\n", passCount, testWord); 333 | } 334 | else 335 | { 336 | if (testSuite->failCount == 1) 337 | CuStringAppend(details, "There was 1 failure:\n"); 338 | else 339 | CuStringAppendFormat(details, "There were %d failures:\n", testSuite->failCount); 340 | 341 | for (i = 0 ; i < testSuite->count ; ++i) 342 | { 343 | CuTest* testCase = testSuite->list[i]; 344 | if (testCase->failed) 345 | { 346 | failCount++; 347 | CuStringAppendFormat(details, "%d) %s: %s\n", 348 | failCount, testCase->name, testCase->message); 349 | } 350 | } 351 | CuStringAppend(details, "\n!!!FAILURES!!!\n"); 352 | 353 | CuStringAppendFormat(details, "Runs: %d ", testSuite->count); 354 | CuStringAppendFormat(details, "Passes: %d ", testSuite->count - testSuite->failCount); 355 | CuStringAppendFormat(details, "Fails: %d\n", testSuite->failCount); 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/CuTest.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | 11 | #define CUTEST_VERSION "CuTest 1.5" 12 | 13 | /* CuString */ 14 | 15 | char* CuStrAlloc(size_t size); 16 | char* CuStrCopy(const char* old); 17 | 18 | #define CU_ALLOC(TYPE) ((TYPE*) malloc(sizeof(TYPE))) 19 | 20 | #define HUGE_STRING_LEN 8192 21 | #define STRING_MAX 256 22 | #define STRING_INC 256 23 | 24 | typedef struct 25 | { 26 | size_t length; 27 | size_t size; 28 | char* buffer; 29 | } CuString; 30 | 31 | void CuStringInit(CuString* str); 32 | CuString* CuStringNew(void); 33 | void CuStringRead(CuString* str, const char* path); 34 | void CuStringAppend(CuString* str, const char* text); 35 | void CuStringAppendChar(CuString* str, char ch); 36 | void CuStringAppendFormat(CuString* str, const char* format, ...); 37 | void CuStringInsert(CuString* str, const char* text, size_t pos); 38 | void CuStringResize(CuString* str, size_t newSize); 39 | void CuStringDelete(CuString* str); 40 | 41 | /* CuTest */ 42 | 43 | typedef struct CuTest CuTest; 44 | 45 | typedef void (*TestFunction)(CuTest *); 46 | 47 | struct CuTest 48 | { 49 | char* name; 50 | TestFunction function; 51 | int failed; 52 | int ran; 53 | const char* message; 54 | jmp_buf *jumpBuf; 55 | }; 56 | 57 | void CuTestInit(CuTest* t, const char* name, TestFunction function); 58 | CuTest* CuTestNew(const char* name, TestFunction function); 59 | void CuTestRun(CuTest* tc); 60 | void CuTestDelete(CuTest *t); 61 | 62 | /* Internal versions of assert functions -- use the public versions */ 63 | void CuFail_Line(CuTest* tc, const char* file, int line, const char* message2, const char* message); 64 | void CuAssert_Line(CuTest* tc, const char* file, int line, const char* message, int condition); 65 | void CuAssertStrEquals_LineMsg(CuTest* tc, 66 | const char* file, int line, const char* message, 67 | const char* expected, const char* actual); 68 | void CuAssertIntEquals_LineMsg(CuTest* tc, 69 | const char* file, int line, const char* message, 70 | int expected, int actual); 71 | void CuAssertSizeTEquals_LineMsg(CuTest* tc, 72 | const char* file, int line, const char* message, 73 | size_t expected, size_t actual); 74 | void CuAssertULongEquals_LineMsg(CuTest* tc, 75 | const char* file, int line, const char* message, 76 | unsigned long expected, unsigned long actual); 77 | void CuAssertDblEquals_LineMsg(CuTest* tc, 78 | const char* file, int line, const char* message, 79 | double expected, double actual, double delta); 80 | void CuAssertPtrEquals_LineMsg(CuTest* tc, 81 | const char* file, int line, const char* message, 82 | void* expected, void* actual); 83 | 84 | /* public assert functions */ 85 | 86 | #define CuFail(tc, ms) CuFail_Line( (tc), __FILE__, __LINE__, NULL, (ms)) 87 | #define CuAssert(tc, ms, cond) CuAssert_Line((tc), __FILE__, __LINE__, (ms), (cond)) 88 | #define CuAssertTrue(tc, cond) CuAssert_Line((tc), __FILE__, __LINE__, "assert failed", (cond)) 89 | 90 | #define CuAssertStrEquals(tc,ex,ac) CuAssertStrEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac)) 91 | #define CuAssertStrEquals_Msg(tc,ms,ex,ac) CuAssertStrEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac)) 92 | #define CuAssertIntEquals(tc,ex,ac) CuAssertIntEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac)) 93 | #define CuAssertIntEquals_Msg(tc,ms,ex,ac) CuAssertIntEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac)) 94 | #define CuAssertSizeTEquals(tc,ex,ac) CuAssertSizeTEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac)) 95 | #define CuAssertSizeTEquals_Msg(tc,ms,ex,ac) CuAssertSizeTEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac)) 96 | #define CuAssertULongEquals(tc,ex,ac) CuAssertULongEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac)) 97 | #define CuAssertULongEquals_Msg(tc,ms,ex,ac) CuAssertULongEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac)) 98 | #define CuAssertDblEquals(tc,ex,ac,dl) CuAssertDblEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac),(dl)) 99 | #define CuAssertDblEquals_Msg(tc,ms,ex,ac,dl) CuAssertDblEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac),(dl)) 100 | #define CuAssertPtrEquals(tc,ex,ac) CuAssertPtrEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac)) 101 | #define CuAssertPtrEquals_Msg(tc,ms,ex,ac) CuAssertPtrEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac)) 102 | 103 | #define CuAssertPtrNotNull(tc,p) CuAssert_Line((tc),__FILE__,__LINE__,"null pointer unexpected",(p != NULL)) 104 | #define CuAssertPtrNotNullMsg(tc,msg,p) CuAssert_Line((tc),__FILE__,__LINE__,(msg),(p != NULL)) 105 | 106 | /* CuSuite */ 107 | 108 | #define MAX_TEST_CASES 1024 109 | 110 | #define SUITE_ADD_TEST(SUITE,TEST) CuSuiteAdd(SUITE, CuTestNew(#TEST, TEST)) 111 | 112 | typedef struct 113 | { 114 | int count; 115 | CuTest* list[MAX_TEST_CASES]; 116 | int failCount; 117 | 118 | } CuSuite; 119 | 120 | 121 | void CuSuiteInit(CuSuite* testSuite); 122 | CuSuite* CuSuiteNew(void); 123 | void CuSuiteDelete(CuSuite *testSuite); 124 | void CuSuiteAdd(CuSuite* testSuite, CuTest *testCase); 125 | void CuSuiteAddSuite(CuSuite* testSuite, CuSuite* testSuite2); 126 | void CuSuiteRun(CuSuite* testSuite); 127 | void CuSuiteSummary(CuSuite* testSuite, CuString* summary); 128 | void CuSuiteDetails(CuSuite* testSuite, CuString* details); 129 | int RunAllTests(void) ; 130 | 131 | #ifdef __cplusplus 132 | } 133 | #endif 134 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # ai_cli - readline wrapper to obtain a generative AI suggestion 3 | # 4 | # GNU Makefile for building, installing, and testing ai-cli-lib 5 | # 6 | # Copyright 2023-2024 Diomidis Spinellis 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | # 20 | 21 | # Help: Set PREFIX=~ for a local install; PREFIX=/usr for a system install. 22 | # Help: By default PREFIX is set to install in /usr/local. 23 | PREFIX ?= /usr/local 24 | LIBPREFIX ?= "$(PREFIX)/lib" 25 | MANPREFIX ?= "$(PREFIX)/share/man/" 26 | SHAREPREFIX ?= "$(PREFIX)/share/ai-cli" 27 | 28 | PROGS=rl_driver $(SHARED_LIB) 29 | ACTIVATION_SCRIPTS=$(wildcard ai-cli-activate-*) 30 | RL_SRC=ai_cli.c config.c ini.c fetch_anthropic.c fetch_hal.c fetch_openai.c \ 31 | fetch_llamacpp.c support.c 32 | TEST_SRC=$(wildcard *_test.c) 33 | LIB=-lcurl -ljansson 34 | 35 | # Maximum configuration file size length; allocate on the stack; check errors 36 | # Report all warnings, treat warnings as errors 37 | CFLAGS=-DINI_MAX_LINE=2048 -DINI_USE_STACK=0 -DINI_ALLOW_REALLOC=1 -DINI_STOP_ON_FIRST_ERROR=1 -Wall -Werror 38 | LDFLAGS=-L/usr/local/lib 39 | 40 | # Help: Set DEBUG=1 to compile with debug information. 41 | ifdef DEBUG 42 | CFLAGS+=-O0 -g 43 | else 44 | CFLAGS+=-O2 45 | endif 46 | 47 | OS := $(shell uname) 48 | ifeq ($(OS), Darwin) 49 | HOMEBREW_PREFIX := $(shell brew --prefix) 50 | READLINE_INCLUDE=$(shell brew info readline | sed -n 's/export CPPFLAGS=//p') 51 | CFLAGS += -I$(HOMEBREW_PREFIX)/include $(READLINE_INCLUDE) -DMACOS 52 | 53 | READLINE_LIB=$(shell brew info readline | sed -n 's/export LDFLAGS=//p') 54 | LDFLAGS += -L$(HOMEBREW_PREFIX)/lib $(READLINE_LIB) 55 | 56 | DLL_EXTENSION=dylib 57 | SHARED_FLAGS=-dynamiclib -undefined dynamic_lookup 58 | PRELOAD_VAR=DYLD_INSERT_LIBRARIES 59 | SET_ADD_LIB=DYLD_LIBRARY_PATH=/opt/homebrew/lib:$$DYLD_LIBRARY_PATH 60 | else 61 | ifeq ($(findstring CYGWIN,$(OS)),CYGWIN) 62 | DLL_EXTENSION=dll 63 | SHARED_FLAGS=-shared -fPIC 64 | PRELOAD_VAR=LD_PRELOAD 65 | SHARED_LIB_LIB=$(LIB) -lreadline 66 | else 67 | DLL_EXTENSION=so 68 | SHARED_FLAGS=-shared -fPIC 69 | PRELOAD_VAR=LD_PRELOAD 70 | endif 71 | endif 72 | 73 | CFLAGS += '-DDLL_EXTENSION="$(DLL_EXTENSION)"' 74 | 75 | SHARED_LIB=ai_cli.$(DLL_EXTENSION) 76 | 77 | all: $(PROGS) 78 | 79 | rl_driver: rl_driver.c 80 | $(CC) $(CFLAGS) $(LDFLAGS) rl_driver.c $(LIB) -lreadline -o $@ 81 | 82 | $(SHARED_LIB): $(RL_SRC) 83 | $(CC) $(SHARED_FLAGS) $(CFLAGS) $(LDFLAGS) $(RL_SRC) -o $@ -ldl $(SHARED_LIB_LIB) 84 | 85 | verify-global-defs: # Help: Verify prefix of globally visible definitions 86 | $(CC) $(CFLAGS) $(RL_SRC) -c 87 | # Check that global not undefined (U) definitions are prefixed 88 | # The output (grep success) signifies unprefixed global defs 89 | ! nm *.o | sed 's/^ /x/' | awk '$$2 ~ /[A-TVZ]/ {print $$3}' | grep -Ev '^_?(acl|ini|curl)' 90 | 91 | e2e-run: $(PROGS) # Help: Invoke the library with a readline read/print loop 92 | $(PRELOAD_VAR)=`pwd`/$(SHARED_LIB) $(SET_ADD_LIB) ./rl_driver 93 | 94 | e2e-test: $(PROGS) # Help: Test the readline hook 95 | printf 'Open the pod bay doors HAL\eV\030A' | \ 96 | $(PRELOAD_VAR)=`pwd`/$(SHARED_LIB) $(SET_ADD_LIB) AI_CLI_general_api=hal ./rl_driver | \ 97 | grep Dave 98 | 99 | all-tests: $(TEST_SRC) $(RL_SRC) 100 | $(CC) -DUNIT_TEST $(CFLAGS) $(LDFLAGS) all_tests.c -DUNIT_TEST $(TEST_SRC) $(RL_SRC) CuTest.c $(LIB) -ldl -lreadline -o $@ 101 | 102 | unit-test: all-tests # Help: Run unit tests 103 | ./all-tests 104 | 105 | clean: # Help: Remove generated files 106 | rm -f $(PROGS) all-tests 107 | 108 | install: ai_cli.$(DLL_EXTENSION) # Help: Install library and manual pages 109 | @mkdir -p $(DESTDIR)$(MANPREFIX)/man5 110 | @mkdir -p $(DESTDIR)$(MANPREFIX)/man7 111 | @mkdir -p $(DESTDIR)$(LIBPREFIX) 112 | @mkdir -p $(DESTDIR)$(SHAREPREFIX) 113 | install $(SHARED_LIB) $(DESTDIR)$(LIBPREFIX)/ 114 | install -m 644 ai_cli.5 $(DESTDIR)$(MANPREFIX)/man5 115 | install -m 644 ai_cli.7 $(DESTDIR)$(MANPREFIX)/man7 116 | install -m 644 ai-cli-config $(DESTDIR)$(SHAREPREFIX)/config 117 | for s in $(ACTIVATION_SCRIPTS) ; do \ 118 | sed -e "s|__LIBPREFIX__|$(LIBPREFIX)|" $$s >$(DESTDIR)$(SHAREPREFIX)/$$s ; \ 119 | done 120 | 121 | help: # Help: Show this help message 122 | @echo 'The following make targets are available.' 123 | @sed -n 's/^\([^:]*:\).*# [H]elp: \(.*\)/printf "%-20s %s\\n" "\1" "\2"/p' Makefile | sh | sort 124 | @echo 125 | @echo 'You can modify the operation of make with the following assignments.' 126 | @sed -n 's/^# [H]elp: \(.*\)/\1/p' Makefile 127 | -------------------------------------------------------------------------------- /src/ai-cli-activate-bash.sh: -------------------------------------------------------------------------------- 1 | # 2 | # ai_cli - readline wrapper to obtain a generative AI suggestion 3 | # 4 | # Bash commands to activate ai-cli 5 | # 6 | # Copyright 2023-2024 Diomidis Spinellis 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | # 20 | 21 | if [ "$0" != bash -a "$0" != -bash ] ; then 22 | echo 'The ai-cli-lib activation script must be sourced, not executed.' 1>&2 23 | exit 1 24 | fi 25 | 26 | AI_CLI_LIB=("__LIBPREFIX__/ai_cli."*) 27 | if [[ $- == *i* # Shell is interactive 28 | && -r ~/.aicliconfig # User has configured script 29 | && -r $AI_CLI_LIB # Library installed 30 | && "$LD_PRELOAD" != *$AI_CLI_LIB* ]] # Variable not set 31 | then 32 | # Linux and Cygwin (is also used for checking) 33 | if [ -z "$LD_PRELOAD" ] ; then 34 | export LD_PRELOAD="$AI_CLI_LIB" 35 | else 36 | LD_PRELOAD="$LD_PRELOAD:$AI_CLI_LIB" 37 | fi 38 | 39 | # macOS 40 | if [ -z "$DYLD_LIBRARY_PATH" ] ; then 41 | export DYLD_LIBRARY_PATH=/opt/homebrew/lib 42 | else 43 | DYLD_LIBRARY_PATH="/opt/homebrew/lib:$DYLD_LIBRARY_PATH" 44 | fi 45 | if [ -z "$DYLD_INSERT_LIBRARIES" ] ; then 46 | export DYLD_INSERT_LIBRARIES="$AI_CLI_LIB" 47 | else 48 | DYLD_INSERT_LIBRARIES="$LD_PRELOAD:$AI_CLI_LIB" 49 | fi 50 | 51 | if shopt -q login_shell ; then 52 | exec -l bash 53 | else 54 | exec bash 55 | fi 56 | fi 57 | -------------------------------------------------------------------------------- /src/ai-cli-config: -------------------------------------------------------------------------------- 1 | [prompt] 2 | system = You are an assistant who provides executable commands for the %s command-line interface. You only provide the requested command on a single line, without any explanations, hints or other adornments. Stop providing output after providing the requested command. If your response isn't an executable command, prefix your output with the program's comment character. 3 | context = 3 4 | 5 | [openai] 6 | endpoint = https://api.openai.com/v1/chat/completions 7 | model = gpt-3.5-turbo 8 | ; The following is more capable, but requires having an established 9 | ; payment relationship. All models including limits are listed at 10 | ; https://platform.openai.com/account/limits 11 | ; model = gpt-4 12 | temperature = 0.7 13 | ; User-specific: add it in a local protected file 14 | ; key = 15 | 16 | [anthropic] 17 | endpoint = https://api.anthropic.com/v1/messages 18 | version = 2023-06-01 19 | ; Most capable. Cost (3/2024): Input: $15 / MTok, Output: $75 / MTok 20 | model = claude-3-opus-20240229 21 | ; User-specific: add it in a local protected file 22 | ; key = 23 | max_tokens = 256 24 | 25 | [llamacpp] 26 | endpoint = http://localhost:8080/completion 27 | 28 | ; Key bindings 29 | [binding] 30 | vi = V 31 | emacs = \C-xa 32 | 33 | ; Multishot command-specific prompts 34 | [prompt-gdb] 35 | comment = # 36 | user-1 = Disable breakpoint number 4 37 | assistant-1 = delete 4 38 | user-2 = break on line 67 of foo.cpp when flag is false, 39 | assistant-3 = break foo.cpp:67 if !flag 40 | 41 | [prompt-bash] 42 | comment = # 43 | user-1 = List files in current directory 44 | assistant-1 = ls 45 | user-2 = How many JavaScript files in the current directory contain the word bar? 46 | assistant-2 = grep -lw bar *.js | wc -l 47 | user-3 = xyzzy 48 | assistant-3 = # Sorry I can't help. 49 | 50 | [prompt-sqlite3] 51 | comment = -- 52 | user-1 = Show available tables 53 | assistant-1 = .tables 54 | user-2 = Show average of grade in table students 55 | assistant-2 = SELECT AVG(grade) FROM students; 56 | user-3 = xyzzy 57 | assistant-3 = -- Sorry I can't help. 58 | 59 | [prompt-bc] 60 | comment = # 61 | user-1 = Calculate 2 raised to the 64th power 62 | assistant-1 = 2^64 63 | user-2 = Increment variable i by 1 64 | assistant-2 = i++ 65 | user-3 = xyzzy 66 | assistant-3 = /* Sorry I can't help. */ 67 | 68 | [prompt-rl_driver] 69 | comment = # 70 | -------------------------------------------------------------------------------- /src/ai_cli.5: -------------------------------------------------------------------------------- 1 | .TH AI_READLINE 5 "2024-03-17" "Diomidis Spinellis" \" -*- 2 | \" nroff -* 3 | 4 | .SH NAME 5 | .B ai_cli 6 | \- Configuration file format 7 | 8 | .SH SYNOPSIS 9 | The operation of 10 | .BR ai_cli (7) 11 | is controlled by ini-style 12 | configuration files 13 | searched in the order specified in the 14 | .I FILES 15 | section of library's manual page. 16 | 17 | .SH DESCRIPTION 18 | Configuration files are searched and loaded in the order specified in 19 | .BR ai_cli (7). 20 | Entries in configuration files read later can override previous ones. 21 | 22 | In general, system-wide configuration files are used to specify 23 | default behaviors, 24 | while user configuration files are used for individual tailoring, 25 | such as the specification of the API key or prompts for commands 26 | that are not supported by default. 27 | 28 | The following sections describe the supported contents of each 29 | configuration file section. 30 | All section names and options are specified in lowercase. 31 | 32 | .SH [PROMPT] SECTION OPTION 33 | .PP 34 | \fIsystem=\fR 35 | .RS 4 36 | The system prompt specifying to context through which the 37 | LLM prompting is made. 38 | A single 39 | .I %s 40 | sequence is replaced when the prompt is sent out as an API request 41 | by the name of the command being executed 42 | (e.g. 43 | .IR mysql ). 44 | .RE 45 | 46 | .PP 47 | \fIcontext=\fR 48 | .RS 4 49 | The number of previous commands that will be provided as context. 50 | More context can provide better responses, 51 | but also increases the operation's cost. 52 | .RE 53 | 54 | .SH [BINDING] SECTION OPTIONS 55 | .PP 56 | \fIvi=\fR 57 | .RS 4 58 | The single-letter vi movement key on which the AI help is bound. 59 | It can be used when vi key bindings are in effect. 60 | .RE 61 | 62 | .PP 63 | \fIemacs=\fR 64 | .RS 4 65 | The Emacs key sequence on which the AI help is bound. 66 | It is specified using 67 | .BR readline (3) 68 | format. 69 | It can be used when Emacs key bindings are in effect. 70 | .RE 71 | 72 | .SH [GENERAL] SECTION OPTIONS 73 | .PP 74 | \fIapi=\fR 75 | .RS 4 76 | Specify the API to use: one of anthropic, hal, llamacpp, or openai. 77 | .RE 78 | 79 | .PP 80 | \fIlogfile=\fR 81 | .RS 4 82 | Specify a path where requests and responses will be logged. 83 | Useful for debugging and research purposes. 84 | .RE 85 | 86 | .PP 87 | \fIresponse_prefix=\fR 88 | .RS 4 89 | When set, the responses will prefixed with the specified string, 90 | e.g. \fItime\fP or \fI#\fP, 91 | followed by a space, before being added to the edit buffer. 92 | .RE 93 | 94 | .PP 95 | \fItimestamp=\fR 96 | .RS 4 97 | Setting \fItimestamp\fP to \fItrue\fP will cause the log file 98 | to include the timestamp (in ISO format) of each request or response. 99 | .RE 100 | 101 | .SH [ANTHROPIC] SECTION OPTIONS 102 | These options tailor the behavior of queries made to the Anthropic servers. 103 | Refer to the 104 | .UR "https://docs.anthropic.com/claude/reference/messages_post" 105 | Anthropic API documentation 106 | .UE 107 | for more details. 108 | 109 | .PP 110 | \fImodel=\fR 111 | .RS 4 112 | The Anthropic model to use, e.g. 113 | .IR claude-3-opus-20240229 . 114 | This affects performance and pricing. 115 | .RE 116 | .PP 117 | \fIendpoint=\fR 118 | .RS 4 119 | The URL of the API endpoint, e.g. \fChttps://api.anthropic.com/v1/messages\fP. 120 | .RE 121 | .PP 122 | \fImax_tokens=\fR 123 | .PP 124 | \fItemperature=\fR 125 | .PP 126 | \fItop_k=\fR 127 | .PP 128 | \fItop_p=\fR 129 | .PP 130 | \fIversion=\fR 131 | .RS 4 132 | The version to supply to the 133 | .I anthropic-version 134 | HTTP header field. 135 | .RE 136 | .PP 137 | 138 | .SH [LLAMACPP] SECTION OPTIONS 139 | These options tailor the behavior of the llama.cpp 140 | queries. 141 | Refer to the 142 | .UR "https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md" 143 | llama.cpp server documentation 144 | .UE 145 | for more details. 146 | 147 | .PP 148 | \fIendpoint=\fR 149 | .RS 4 150 | The URL of the API endpoint, e.g. \fChttp://localhost:8080/completion\fP. 151 | .RE 152 | 153 | .PP 154 | \fItemperature=\fR 155 | .PP 156 | \fItop_k=\fR 157 | .PP 158 | \fItop_p=\fR 159 | .PP 160 | \fIn_predict=\fR 161 | .PP 162 | \fIn_keep=\fR 163 | .PP 164 | \fItfs_z=\fR 165 | .PP 166 | \fItypical_p=\fR 167 | .PP 168 | \fIrepeat_penalty=\fR 169 | .PP 170 | \fIrepeat_last_n=\fR 171 | .PP 172 | \fIpenalize_nl=\fR 173 | .PP 174 | \fIpresence_penalty=\fR 175 | .PP 176 | \fIfrequency_penalty=\fR 177 | .PP 178 | \fImirostat=\fR 179 | .PP 180 | \fImirostat_tau=\fR 181 | .PP 182 | \fImirostat_eta=\fR 183 | .PP 184 | \fIseed=\fR 185 | 186 | .SH [OPENAI] SECTION OPTIONS 187 | These options tailor the behavior of the OpenAI 188 | queries. 189 | Refer to the 190 | .UR "https://platform.openai.com/docs/models" 191 | OpenAI API documentation 192 | .UE 193 | for more details. 194 | 195 | .PP 196 | \fIendpoint=\fR 197 | .RS 4 198 | The URL of the API endpoint. 199 | .RE 200 | .PP 201 | \fImodel=\fR 202 | .RS 4 203 | The OpenAI model to use, e.g. 204 | .IR gpt-3.5-turbo . 205 | This affects performance and pricing. 206 | .RE 207 | 208 | .PP 209 | \fItemperature=\fR 210 | .RS 4 211 | The sampling temperature to use as a value between 0 and 2. 212 | Higher values, such as 0.8, will make the output more random, 213 | while lower values, such as 0.2, will make it more focused and deterministic. 214 | .RE 215 | 216 | .PP 217 | \fIkey=\fR 218 | .RS 4 219 | OpenAI API access key. 220 | Ensure this is specified in a configuration file that is not readable 221 | by entities unauthorized to make OpenAI API requests with the given key. 222 | .RE 223 | 224 | .SH [PROMPT-] SECTION OPTIONS 225 | A series of sections starting with 226 | .B prompt- 227 | followed by the name of a program, 228 | such as 229 | .B bash 230 | or 231 | .BR mysql , 232 | are used to provide program-specific configuration 233 | options for values associated with the global 234 | .B prompt 235 | section (e.g. 236 | .B system 237 | and 238 | .BR context ), 239 | the program's comment string, 240 | and also multishot example prompts for improving the obtained responses. 241 | The multishot example prompts are provided by 242 | .B ai_cli 243 | at the beginning of each API request. 244 | 245 | .PP 246 | \fIcomment=\fR 247 | .RS 4 248 | The line comment sequence for the corresponding tool. 249 | If this is defined, then prompt lines are stored in the 250 | history and provided as context in the form of line comments. 251 | .RE 252 | 253 | .PP 254 | \fIuser-n=\fR 255 | .RS 4 256 | A user prompt in natural language. 257 | The \fIn\fP placeholder can take values 1-3. 258 | .RE 259 | 260 | .PP 261 | \fIassistant-n=\fR 262 | .RS 4 263 | The ideal response to the user prompt for the program being 264 | specified in the corresponding section. 265 | .RE 266 | 267 | .SH EXAMPLE 268 | .RS 269 | .nf 270 | [general] 271 | api = openai 272 | 273 | [prompt] 274 | system = You're an assistant providing executable commands for %s. 275 | context = 3 276 | 277 | [openai] 278 | endpoint = https://api.openai.com/v1/chat/completions 279 | model = gpt-3.5-turbo 280 | temperature = 1.0 281 | key = sk-hjgds5hljfgs8dfw4ljghljfhfFER344FFFggf84fssddG4k 282 | 283 | [llamacpp] 284 | endpoint = http://localhost:8080/completion 285 | 286 | [binding] 287 | vi = V 288 | emacs = \\C-xa 289 | 290 | [prompt-bash] 291 | context=4 292 | user-1 = List files in current directory 293 | assistant-1 = ls 294 | user-2 = What is the current time and date? 295 | assistant-2 = date 296 | .RE 297 | .fi 298 | 299 | .SH FILES 300 | The names and order of configuration files are documented in 301 | .BR ai_cli (7). 302 | 303 | .SH SEE ALSO 304 | .BR ai_cli (7). 305 | 306 | .SH AUTHOR 307 | Diomidis Spinellis (dds@aueb.gr) 308 | 309 | .SH COPYRIGHT 310 | Copyright 2023-2024 Diomidis Spinellis 311 | 312 | Licensed under the Apache License, Version 2.0 (the "License"); 313 | you may not use this file except in compliance with the License. 314 | You may obtain a copy of the License at 315 | 316 | http://www.apache.org/licenses/LICENSE-2.0 317 | 318 | Unless required by applicable law or agreed to in writing, software 319 | distributed under the License is distributed on an "AS IS" BASIS, 320 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 321 | See the License for the specific language governing permissions and 322 | limitations under the License. 323 | -------------------------------------------------------------------------------- /src/ai_cli.7: -------------------------------------------------------------------------------- 1 | .TH AI_READLINE 7 "2023-08-10" "Diomidis Spinellis" \" -*- 2 | \" nroff -* 3 | 4 | .SH NAME 5 | .B ai_cli 6 | \- AI enhancement for interactive command-line programs 7 | 8 | .SH SYNOPSIS 9 | Set the 10 | .B LD_PRELOAD 11 | (under Linux) or 12 | .B DYLD_INSERT_LIBRARIES 13 | (under macOS) 14 | environment variable to load the library using its full path. 15 | Obtain your Anthropic or OpenAI API key and configure it in the 16 | .B .aicliconfig 17 | file. 18 | Alternatively, setup a running 19 | .UR "https://github.com/ggerganov/llama.cpp" 20 | llama.cpp 21 | .UE 22 | server and configure its endpoint. 23 | Run the interactive command-line programs, such as 24 | .BR bash , 25 | .BR mysql , 26 | .BR psql , 27 | .BR gdb , 28 | as you normally would. 29 | To obtain AI help, 30 | enter a prompt and press 31 | .B "^X-a" 32 | (Ctrl-X followed by a) 33 | in the (default) 34 | .B Emacs 35 | key binding mode or 36 | .B V 37 | if you have configured 38 | .B vi 39 | key bindings. 40 | 41 | .SH DESCRIPTION 42 | The 43 | .B ai_cli 44 | library detects programs that link against 45 | .BR readline (3) 46 | and modifies their interface to allow obtaining help from a 47 | generative pre-trained transformer large language model (LLM). 48 | Specifically, 49 | it creates a 50 | .BR readline (3) 51 | function named 52 | .BR query_ai , 53 | and binds it to (configurable) 54 | .BR Emacs 55 | and 56 | .BR vi 57 | keys. 58 | When the corresponding key or key sequence is pressed, 59 | suitable prompts are sent to the LLM API endpoint, 60 | and the prompt is replaced with the obtained response. 61 | (The prompt is added in the command-line history for further editing.) 62 | The LLM query contains context from previous commmands 63 | (but not their output), 64 | which allows prompts to refine commands as needed. 65 | 66 | The syntax to force preload of the library according to the shell 67 | is as follows. 68 | 69 | Bash, Ksh and the traditional Bourne shell (for Linux and macOS): 70 | 71 | export LD_PRELOAD=/usr/lib/ai_cli.so 72 | 73 | or 74 | 75 | export DYLD_INSERT_LIBRARIES=/usr/lib/ai_cli.dylib 76 | 77 | C Shell: 78 | 79 | setenv LD_PRELOAD=/usr/lib/ai_cli.so 80 | 81 | The setting can be made permanent by adding the command 82 | to a shell's configuration file, such as 83 | .BR .bashrc , 84 | .BR zshrc ", or" 85 | .BR .cshrc . 86 | 87 | The operation of 88 | .B ai_cli 89 | can be configured through the files and environment variables listed in the 90 | .I FILES 91 | section and the format documented in 92 | .BR ai_cli (5). 93 | 94 | .SH ENVIRONMENT VARIABLES 95 | All configuration entries specified through 96 | .BR ai_cli (5) 97 | can be overridden by supplying to a program's 98 | invocation an environment variable named 99 | \fCAI_CLI_\fIsection\fC_\fIoption\fR, 100 | where dashes are replaced with underscores. 101 | For example, an environment variable setting could be 102 | \fCAI_CLI_prompt_gdb_user_1=Disable breakpoint 3\fP. 103 | 104 | .SH FILES 105 | .IR /usr/share/ai-cli/config , 106 | .IR /usr/local/share/ai-cli/config , 107 | .IR ai-cli-config , 108 | .IR $HOME/share/ai-cli/config , 109 | .IR $HOME/.aicliconfig , 110 | .I .aicliconfig 111 | \- locations searched for 112 | .B ai_cli 113 | configuration files. 114 | 115 | .SH SEE ALSO 116 | .BR ai_cli (5). 117 | 118 | .SH BUGS 119 | .B ai_cli 120 | will not work with programs that are linked statically with 121 | .BR readline (3) 122 | rather than using it as a shared library object. 123 | 124 | .SH AUTHOR 125 | Diomidis Spinellis (dds@aueb.gr) 126 | 127 | .SH COPYRIGHT 128 | Copyright 2023-2024 Diomidis Spinellis 129 | 130 | Licensed under the Apache License, Version 2.0 (the "License"); 131 | you may not use this file except in compliance with the License. 132 | You may obtain a copy of the License at 133 | 134 | http://www.apache.org/licenses/LICENSE-2.0 135 | 136 | Unless required by applicable law or agreed to in writing, software 137 | distributed under the License is distributed on an "AS IS" BASIS, 138 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 139 | See the License for the specific language governing permissions and 140 | limitations under the License. 141 | -------------------------------------------------------------------------------- /src/ai_cli.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai_cli - readline wrapper to obtain a generative AI suggestion 4 | * 5 | * Copyright 2023-2024 Diomidis Spinellis 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | #define _GNU_SOURCE 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #include "config.h" 31 | #include "support.h" 32 | 33 | #include "fetch_anthropic.h" 34 | #include "fetch_hal.h" 35 | #include "fetch_llamacpp.h" 36 | #include "fetch_openai.h" 37 | 38 | /* 39 | * Dynamically obtained pointer to readline(3) variables.. 40 | * This avoids a undefined symbol errors when a program isn't linked with 41 | * readline. 42 | */ 43 | static char **rl_line_buffer_ptr; 44 | static int *rl_end_ptr; 45 | static int *rl_point_ptr; 46 | static Keymap vi_movement_keymap_ptr; 47 | static int *history_length_ptr; 48 | 49 | // Loaded configuration 50 | static config_t config; 51 | 52 | // API fetch function, e.g. acl_fetch_openai or acl_fetch_llamacpp 53 | 54 | static char * (*fetch)(config_t *config, const char *prompt, int history_length); 55 | 56 | /* 57 | * Add the specified prompt to the RL history, as a comment if the 58 | * comment prefix is defined. 59 | * Return the length of the added comment. 60 | */ 61 | static int 62 | add_commented_prompt_to_history(const char *prompt) 63 | { 64 | if (prompt == NULL) 65 | return 0; 66 | 67 | if (!config.prompt_comment_set) { 68 | add_history(prompt); 69 | return 0; 70 | } 71 | 72 | char *commented_prompt; 73 | acl_safe_asprintf(&commented_prompt, "%s %s", config.prompt_comment, 74 | prompt); 75 | add_history(commented_prompt); 76 | rl_replace_line(commented_prompt, 0); 77 | *rl_point_ptr = *rl_end_ptr; 78 | rl_redisplay(); 79 | free(commented_prompt); 80 | return strlen(config.prompt_comment); 81 | } 82 | 83 | /* 84 | * The user has has asked for AI to be queried on the typed text 85 | * Replace the user's text with the queried on 86 | */ 87 | static int 88 | query_ai(int count, int key) 89 | { 90 | static char *prev_response; 91 | 92 | if (prev_response) { 93 | free(prev_response); 94 | prev_response = NULL; 95 | } 96 | 97 | int comment_len = add_commented_prompt_to_history(*rl_line_buffer_ptr); 98 | char *response = fetch(&config, *rl_line_buffer_ptr + comment_len, 99 | *history_length_ptr); 100 | if (!response) 101 | return -1; 102 | rl_crlf(); 103 | rl_on_new_line(); 104 | rl_delete_text(0, *rl_end_ptr); 105 | *rl_point_ptr = 0; 106 | if (config.general_response_prefix_set) { 107 | rl_insert_text(config.general_response_prefix); 108 | rl_insert_text(" "); 109 | } 110 | rl_insert_text(response); 111 | prev_response = response; 112 | /* 113 | * The readline_internal_teardown() function will restore 114 | * the original history line iff the line being edited 115 | * was originally in the history, AND the line has changed. 116 | * Avoid this by clearing the undo list. 117 | * This results in the commented prompt rather than the 118 | * ucommented prompt being stored in the history. 119 | */ 120 | rl_free_undo_list(); 121 | return 0; 122 | } 123 | 124 | 125 | /* 126 | * This is called when the dynamic library is loaded. 127 | * If the program is linked with readline(3), 128 | * read configuration and set keybindings for AI completion. 129 | */ 130 | #ifndef UNIT_TEST 131 | __attribute__((constructor)) static 132 | #endif 133 | void 134 | setup(void) 135 | { 136 | /* 137 | * See if readline(3) is linked and obtain required symbols 138 | * This avoids undefined symbol errors for programs not 139 | * using readline and also the initialization overhead. 140 | */ 141 | dlerror(); 142 | rl_line_buffer_ptr = dlsym(RTLD_DEFAULT, "rl_line_buffer"); 143 | if (dlerror()) 144 | return; // Program not linked with readline(3) 145 | 146 | /* 147 | * GNU awk 5.2.1 under Debian bookworm and maybe also other 148 | * versions is distributed with a persistent memory allocator (PMA) 149 | * library, which requires initialization with pma_init 150 | * before using it. If the allocator is not initialized calls 151 | * to malloc will (such as those * made by rl_add_defun() in this 152 | * function) will fail with a fatal error, such as the following. 153 | * (null): fatal: node.c:1075:more_blocks: freep: cannot allocate 154 | * 11200 bytes of memory: [unrelated error message] 155 | * To avoid this problem, exit if called from awk. 156 | * In the future more programs may need to get deny-listed here. 157 | */ 158 | const char *program_name = acl_short_program_name(); 159 | if (strcmp(program_name, "awk") == 0 160 | || strcmp(program_name, "gawk") == 0) 161 | return; 162 | 163 | // Obtain remaining variable symbols 164 | rl_end_ptr = dlsym(RTLD_DEFAULT, "rl_end"); 165 | rl_point_ptr = dlsym(RTLD_DEFAULT, "rl_point"); 166 | vi_movement_keymap_ptr = dlsym(RTLD_DEFAULT, "vi_movement_keymap"); 167 | history_length_ptr = dlsym(RTLD_DEFAULT, "history_length"); 168 | 169 | acl_read_config(&config); 170 | 171 | if (!config.prompt_system) { 172 | fprintf(stderr, "No default ai-cli configuration loaded. Installation problem?\n"); 173 | return; 174 | } 175 | 176 | // Require a given configuration value 177 | #define REQUIRE(section, value) do { \ 178 | if (!config.section ## _ ## value ## _set) { \ 179 | fprintf(stderr, "Missing %s value in [%s] configuration section.\n", #value, #section); \ 180 | return; \ 181 | } \ 182 | } while (0); 183 | 184 | REQUIRE(general, api); 185 | 186 | if (strcmp(config.general_api, "openai") == 0) { 187 | fetch = acl_fetch_openai; 188 | REQUIRE(openai, key); 189 | REQUIRE(openai, endpoint); 190 | } else if (strcmp(config.general_api, "anthropic") == 0) { 191 | fetch = acl_fetch_anthropic; 192 | REQUIRE(anthropic, key); 193 | REQUIRE(anthropic, endpoint); 194 | REQUIRE(anthropic, version); 195 | } else if (strcmp(config.general_api, "hal") == 0) { 196 | fetch = acl_fetch_hal; 197 | } else if (strcmp(config.general_api, "llamacpp") == 0) { 198 | fetch = acl_fetch_llamacpp; 199 | REQUIRE(llamacpp, endpoint); 200 | } else { 201 | fprintf(stderr, "Unsupported API: [%s].\n", config.general_api); 202 | return; 203 | } 204 | if (config.general_verbose) 205 | fprintf(stderr, "API set to %s\n", config.general_api); 206 | 207 | // Add named function, making it available to the user 208 | rl_add_defun("query-ai", query_ai, -1); 209 | 210 | /* 211 | * Bind the function to Emacs and vi. 212 | * Programs linked with libedit (editline) lack rl_bind_keyseq and 213 | * will fail if it's executed. To avoid this, first test if the 214 | * function i* s available. 215 | */ 216 | if (config.binding_emacs && dlsym(RTLD_DEFAULT, "rl_bind_keyseq")) 217 | rl_bind_keyseq(config.binding_emacs, query_ai); 218 | if (config.binding_vi) 219 | rl_bind_key_in_map(*config.binding_vi, query_ai, vi_movement_keymap_ptr); 220 | } 221 | -------------------------------------------------------------------------------- /src/all_tests.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Unit test driver 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include 22 | 23 | #include "CuTest.h" 24 | 25 | CuSuite* cu_config_suite(); 26 | CuSuite* cu_fetch_anthropic_suite(); 27 | CuSuite* cu_fetch_openai_suite(); 28 | CuSuite* cu_fetch_llamacpp_suite(); 29 | CuSuite* cu_support_suite(); 30 | 31 | void 32 | run_all_tests(void) 33 | { 34 | CuString *output = CuStringNew(); 35 | CuSuite* suite = CuSuiteNew(); 36 | 37 | CuSuiteAddSuite(suite, cu_config_suite()); 38 | CuSuiteAddSuite(suite, cu_fetch_anthropic_suite()); 39 | CuSuiteAddSuite(suite, cu_fetch_openai_suite()); 40 | CuSuiteAddSuite(suite, cu_fetch_llamacpp_suite()); 41 | CuSuiteAddSuite(suite, cu_support_suite()); 42 | 43 | CuSuiteRun(suite); 44 | CuSuiteSummary(suite, output); 45 | CuSuiteDetails(suite, output); 46 | printf("%s\n", output->buffer); 47 | } 48 | 49 | int main(void) 50 | { 51 | run_all_tests(); 52 | } 53 | -------------------------------------------------------------------------------- /src/config.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Configuration parsing and access 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #define _GNU_SOURCE 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include "config.h" 29 | #include "support.h" 30 | #include "ini.h" 31 | 32 | 33 | static const char hidden_config_name[] = ".aicliconfig"; 34 | 35 | /* 36 | * Prefixes for providing n-shot user and assistant prompts in ini files. 37 | * Example: 38 | * [prompt-gdb] 39 | * user-1 = Disable breakpoint number 4 40 | * assistant-1 = delete 4 41 | */ 42 | static const char prompt_ini_prefix[] = "prompt-"; 43 | static const char user_ini_prefix[] = "user-"; 44 | static const char assistant_ini_prefix[] = "assistant-"; 45 | 46 | /* 47 | * Prefixes for providing n-shot user and assistant prompts in 48 | * environment variables. Examples: 49 | * AI_CLI_prompt_gdb_user_1=Disable breakpoint number 4 50 | * AI_CLI_prompt_gdb_assistant_1=delete 4 51 | */ 52 | static const char env_prompt_prefix[] = "AI_CLI_prompt_"; 53 | static const char user_env_prefix[] = "user_"; 54 | static const char assistant_env_prefix[] = "assistant_"; 55 | 56 | // Return true if the specified string starts with the given prefix 57 | STATIC bool 58 | starts_with(const char *string, const char *prefix) 59 | { 60 | return memcmp(string, prefix, strlen(prefix)) == 0; 61 | } 62 | 63 | /* 64 | * Return the identifier associated with a prompt environment variable 65 | * (e.g. sqlite3 for AI_CLI_prompt_sqlite3_system) 66 | * in dynamically allocated memory. 67 | * Return null if the variable does not contain an identifier. 68 | */ 69 | STATIC char * 70 | prompt_id(const char *entry) 71 | { 72 | // prompt_name is something like {id}_system 73 | const char *id_begin = entry + sizeof(env_prompt_prefix) - 1; 74 | const char *id_end = strchr(id_begin, '_'); 75 | if (!id_end) 76 | return NULL; 77 | return acl_range_strdup(id_begin, id_end); 78 | } 79 | 80 | /* 81 | * Return the 0-based prompt ordinal number n associated with the specified 82 | * prefixed name. The name string is suffixed with -n. 83 | * Return -1 if the string doesn't exactly match a positive integer 84 | * up to the number of allowed prompts. 85 | */ 86 | STATIC int 87 | prompt_number(const char *name, const char *prompt_prefix) 88 | { 89 | int result = acl_strtocard(name + strlen(prompt_prefix)); 90 | return result <= 0 || result > NPROMPTS ? -1 : result - 1; 91 | } 92 | 93 | // Return true if the string contains the value true 94 | static bool 95 | strtobool(const char *string) 96 | { 97 | return strcmp(string, "true") == 0; 98 | } 99 | 100 | /* 101 | * Set configuration values from environment variables or to the 102 | * given configuration file value for non-program specific configuration 103 | * values.. 104 | * If section is NULL, then set all (non program-specific) configuration 105 | * variables from any environment variables corresponding to them 106 | * (AI_CLI_section_name). In this case the section, name, and value parameters 107 | * are not used. 108 | * 109 | * If section is not NULL, set the configuration value from 110 | * the specified section, name, and value. 111 | * In this case return 1 if success 0 otherwise. 112 | */ 113 | static int 114 | fixed_matcher(config_t *pconfig, const char* section, 115 | const char* name, const char* value) 116 | { 117 | /* 118 | * Set the specified section, s, and name, n, from the corresponding 119 | * environment variable or to the given configuration file value 120 | * available through the variable named value. 121 | * The configuration or environment string value is converted to the 122 | * reqired type using the supplied function fn. 123 | */ 124 | #define MATCH(s, n, fn) do { \ 125 | if (section) { \ 126 | if (strcmp(section, #s) == 0 && strcmp(name, #n) == 0) { \ 127 | pconfig->s ## _ ## n = fn(value); \ 128 | pconfig->s ## _ ## n ## _set = true; \ 129 | return 1; \ 130 | } \ 131 | } else { \ 132 | const char *env = getenv("AI_CLI_" #s "_" #n); \ 133 | if (env) { \ 134 | pconfig->s ## _ ## n = fn(env); \ 135 | pconfig->s ## _ ## n ## _set = true; \ 136 | } \ 137 | } \ 138 | } while(0) 139 | 140 | // In section, key alphabetic order 141 | MATCH(anthropic, endpoint, acl_safe_strdup); 142 | MATCH(anthropic, key, acl_safe_strdup); 143 | MATCH(anthropic, max_tokens, atoi); 144 | MATCH(anthropic, model, acl_safe_strdup); 145 | MATCH(anthropic, temperature, atof); 146 | MATCH(anthropic, top_k, atoi); 147 | MATCH(anthropic, top_p, atof); 148 | MATCH(anthropic, version, acl_safe_strdup); 149 | 150 | MATCH(binding, emacs, acl_safe_strdup); 151 | MATCH(binding, vi, acl_safe_strdup); 152 | 153 | MATCH(general, api, acl_safe_strdup); 154 | MATCH(general, logfile, acl_safe_strdup); 155 | MATCH(general, response_prefix, acl_safe_strdup); 156 | MATCH(general, timestamp, strtobool); 157 | MATCH(general, verbose, strtobool); 158 | 159 | MATCH(llamacpp, endpoint, acl_safe_strdup); 160 | MATCH(llamacpp, frequency_penalty, atof); 161 | MATCH(llamacpp, mirostat, atoi); 162 | MATCH(llamacpp, mirostat_eta, atof); 163 | MATCH(llamacpp, mirostat_tau, atof); 164 | MATCH(llamacpp, n_keep, atoi); 165 | MATCH(llamacpp, n_predict, atoi); 166 | MATCH(llamacpp, penalize_nl, strtobool); 167 | MATCH(llamacpp, presence_penalty, atof); 168 | MATCH(llamacpp, repeat_last_n, atoi); 169 | MATCH(llamacpp, repeat_penalty, atof); 170 | MATCH(llamacpp, seed, atoi); 171 | MATCH(llamacpp, temperature, atof); 172 | MATCH(llamacpp, tfs_z, atof); 173 | MATCH(llamacpp, top_k, atoi); 174 | MATCH(llamacpp, top_p, atof); 175 | MATCH(llamacpp, typical_p, atof); 176 | 177 | MATCH(openai, endpoint, acl_safe_strdup); 178 | MATCH(openai, key, acl_safe_strdup); 179 | MATCH(openai, model, acl_safe_strdup); 180 | MATCH(openai, temperature, atof); 181 | 182 | MATCH(prompt, context, acl_strtocard); 183 | MATCH(prompt, system, acl_safe_strdup); 184 | 185 | return 0; 186 | } 187 | 188 | /* 189 | * Set program-specific prompt configuration values. 190 | * 191 | * Return 1 if success 0 otherwise. 192 | */ 193 | static int 194 | fixed_program_matcher(config_t *pconfig, const char* name, const char* value) 195 | { 196 | /* 197 | * Set the specified prompt configuration name, n, from the 198 | * corresponding given configuration value. 199 | * The value is converted to the required type and storage 200 | * using the supplied function fn. 201 | */ 202 | #define MATCH_PROGRAM(n, fn) do { \ 203 | if (strcmp(name, #n) == 0) { \ 204 | pconfig->prompt_ ## n = fn(value); \ 205 | pconfig->prompt_ ## n ## _set = true; \ 206 | if (pconfig->general_verbose) \ 207 | fprintf(stderr, "Overriding general config `%s' with program `%s'-specific value `%s'\n", #n, pconfig->program_name, value); \ 208 | return 1; \ 209 | } \ 210 | } while (0) 211 | MATCH_PROGRAM(comment, acl_safe_strdup); 212 | MATCH_PROGRAM(context, acl_strtocard); 213 | MATCH_PROGRAM(system, acl_safe_strdup); 214 | 215 | return 0; 216 | } 217 | 218 | 219 | /* 220 | * Callback for the configuration .ini file reader 221 | * Called with the section, name, and value read 222 | */ 223 | static int 224 | config_handler(void* user, const char* section, const char* name, 225 | const char* value) 226 | { 227 | config_t *pconfig = (config_t *)user; 228 | 229 | if (pconfig->general_verbose) 230 | fprintf(stderr, "Config [%s]: %s=%s\n", section, name, value); 231 | 232 | if (fixed_matcher(pconfig, section, name, value)) 233 | return 1; 234 | 235 | if (!starts_with(section, prompt_ini_prefix)) 236 | acl_errorf("Unknown configuration section [%s], name `%s'.", section, name); 237 | 238 | /* 239 | * A program specific section. It can provide user or assistant 240 | * n-shot prompt, such as: 241 | * 242 | * [prompt-gdb] 243 | * user-1 = Disable breakpoint number 4 244 | * 245 | * or a program-specific values, such as: 246 | * 247 | * [prompt-bc] 248 | * system = The bc command is already invoked 249 | * context = 1 250 | */ 251 | const char *program_name = section + sizeof(prompt_ini_prefix) - 1; 252 | 253 | // Skip matching of programs other than ours 254 | if (strcmp(program_name, pconfig->program_name) != 0) 255 | return 1; 256 | 257 | if (fixed_program_matcher(pconfig, name, value)) 258 | return 1; 259 | 260 | if (starts_with(name, user_ini_prefix)) { 261 | int n = prompt_number(name, user_ini_prefix); 262 | if (n == -1) 263 | acl_errorf("Invalid prompt number, section [%s], name `%s', value `%s'.", section, name, value); 264 | pconfig->prompt_user[n] = strdup(value); 265 | return 1; 266 | } else if (starts_with(name, assistant_ini_prefix)) { 267 | int n = prompt_number(name, assistant_ini_prefix); 268 | if (n == -1) 269 | acl_errorf("Invalid prompt number, section [%s], name `%s', value `%s'.", section, name, value); 270 | pconfig->prompt_assistant[n] = strdup(value); 271 | return 1; 272 | } 273 | acl_errorf("Unknown configuration section [%s], name `%s'.", section, name); 274 | return 0; /* unknown section/name, error */ 275 | } 276 | 277 | /* 278 | * Override or set configuration values based on set environment variables 279 | * of the form AI_CLI_section_name. 280 | * Prompt settings are specified as AI_CLI_prompt_id, where id can be 281 | * a program name. 282 | * Alternatively the program name can be specified through the 283 | * AI_CLI_prompt_{id}_name variable. 284 | * User and assistant prompts are specified as AI_CLI_prompt_{id}_user_{n} 285 | * and AI_CLI_prompt_{id}_assistant_{n}. 286 | * The system prompt is specified as AI_CLI_prompt_{id}_system. 287 | */ 288 | static void 289 | env_override(config_t *config) 290 | { 291 | // Match all fixed configuration values 292 | (void)fixed_matcher(config, NULL, NULL, NULL); 293 | 294 | /* 295 | * Now look for prompt configurations in environment variables. 296 | * A user or assistant n-shot prompt, such as: 297 | * AI_CLI_prompt_gdb_user_1=Disable breakpoint number 4 298 | * or a program-specific system prompt, such as: 299 | * AI_CLI_prompt_bc_system=The bc command is already invoked 300 | */ 301 | char **env; 302 | extern char **environ; 303 | for (env = environ; *env; env++) { 304 | char *entry = *env; 305 | if (!starts_with(entry, env_prompt_prefix)) 306 | continue; 307 | // E.g. sqlite3 or gitconfig (which will be named git-config) 308 | char *program_name = prompt_id(entry); 309 | if (!program_name) 310 | acl_errorf("Missing program identifier in prompt environment variable %s", entry); 311 | 312 | // Skip matching of programs other than ours 313 | if (strcmp(program_name, config->program_name) != 0) { 314 | free(program_name); 315 | continue; 316 | } 317 | 318 | char *prompt_name_begin = entry + sizeof(env_prompt_prefix) + 319 | strlen(program_name); 320 | const char *prompt_name_end = strchr(prompt_name_begin, '='); 321 | if (!prompt_name_end) 322 | acl_errorf("Missing value in prompt environment variable %s", entry); 323 | char *prompt_name = acl_range_strdup(prompt_name_begin, prompt_name_end); 324 | const char *prompt_value = prompt_name_end + 1; 325 | 326 | if (starts_with(prompt_name, user_env_prefix)) { 327 | int n = prompt_number(prompt_name, user_env_prefix); 328 | if (n == -1) 329 | acl_errorf("Invalid prompt value in environment variable %s", entry); 330 | else 331 | config->prompt_user[n] = strdup(prompt_value); 332 | } else if (starts_with(prompt_name, assistant_env_prefix)) { 333 | int n = prompt_number(prompt_name, assistant_env_prefix); 334 | if (n == -1) 335 | acl_errorf("Invalid prompt value in environment variable %s", entry); 336 | else 337 | config->prompt_assistant[n] = strdup(prompt_value); 338 | } else if (!fixed_program_matcher(config, prompt_name, prompt_value)) 339 | acl_errorf("Invalid name in environment variable %s", entry); 340 | free(program_name); 341 | free(prompt_name); 342 | } 343 | } 344 | 345 | static void 346 | ini_checked_parse(const char* filename, ini_handler handler, config_t *config) 347 | { 348 | if (config->general_verbose) 349 | fprintf(stderr, "Config reading %s\n", filename); 350 | int val = ini_parse(filename, handler, config); 351 | // When unable to open file val is -1, which we ignore 352 | if (val > 0) 353 | acl_errorf("%s:%d:1: Initialization file error", filename, val); 354 | } 355 | 356 | /* 357 | * Read the configuration file from diverse directories into config. 358 | */ 359 | void 360 | acl_read_config(config_t *config) 361 | { 362 | config->program_name = acl_short_program_name(); 363 | 364 | ini_checked_parse("/usr/share/ai-cli/config", config_handler, config); 365 | ini_checked_parse("/usr/local/share/ai-cli/config", config_handler, config); 366 | ini_checked_parse("ai-cli-config", config_handler, config); 367 | 368 | // $HOME/.aicliconfig 369 | char *home_dir; 370 | if ((home_dir = getenv("HOME")) != NULL) { 371 | char *home_config; 372 | 373 | acl_safe_asprintf(&home_config, "%s/%s", home_dir, "share/ai-cli/config"); 374 | ini_checked_parse(home_config, config_handler, config); 375 | free(home_config); 376 | 377 | acl_safe_asprintf(&home_config, "%s/%s", home_dir, hidden_config_name); 378 | ini_checked_parse(home_config, config_handler, config); 379 | free(home_config); 380 | } 381 | 382 | // .aicliconfig 383 | ini_checked_parse(hidden_config_name, config_handler, config); 384 | env_override(config); 385 | } 386 | 387 | #if defined(UNIT_TEST) 388 | /* 389 | * Read the configuration file from the specified file path into config. 390 | * This allows testing the config handler. 391 | */ 392 | void 393 | read_file_config(config_t *config, const char *file_path) 394 | { 395 | // Allow unit tests to override this value 396 | if (!config->program_name) 397 | config->program_name = acl_short_program_name(); 398 | 399 | ini_checked_parse(file_path, config_handler, config); 400 | env_override(config); 401 | } 402 | #endif 403 | 404 | /* 405 | * Return the system role prompt string in dynamically allocated memory. 406 | */ 407 | char * 408 | acl_system_role_get(config_t *config) 409 | { 410 | char *system_role; 411 | acl_safe_asprintf(&system_role, config->prompt_system, config->program_name); 412 | return system_role; 413 | } 414 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Configuration parsing and access 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #pragma once 22 | 23 | #include 24 | 25 | #include "unit_test.h" 26 | 27 | // Number of supported n-shot prompts 28 | #define NPROMPTS 3 29 | 30 | typedef struct { 31 | // Name of running program (not a configuration item) 32 | const char *program_name; 33 | 34 | // All settings are listed in section, key alphabetic order 35 | // (Unless otherwise specified) 36 | 37 | // Anthropic API 38 | // See https://docs.anthropic.com/claude/reference/messages_post 39 | const char *anthropic_endpoint; // API endpoint URL 40 | const char *anthropic_key; // API key 41 | int anthropic_max_tokens; // Max output tokens 42 | const char *anthropic_model; // Name (e.g. claude-3-opus-20240229) 43 | double anthropic_temperature; 44 | int anthropic_top_k; 45 | double anthropic_top_p; 46 | const char *anthropic_version; // API version, e.g. 2023-06-01 47 | 48 | // Single character for invoking AI help in Vi mode 49 | const char *binding_vi; 50 | // Character sequence for invoking AI help in Emacs mode 51 | const char *binding_emacs; 52 | 53 | const char *general_api; // API to use 54 | const char *general_logfile; // File to log requests and responses 55 | const char *general_response_prefix; // Added in pasted responses 56 | bool general_timestamp; // Timestamp log entries 57 | bool general_verbose; // Verbose program operation 58 | 59 | const char *llamacpp_endpoint; // API endpoint URL 60 | // Other llama.cpp parameters in the order documented in 61 | // https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md 62 | double llamacpp_temperature; 63 | int llamacpp_top_k; 64 | double llamacpp_top_p; 65 | int llamacpp_n_predict; 66 | int llamacpp_n_keep; 67 | double llamacpp_tfs_z; 68 | double llamacpp_typical_p; 69 | double llamacpp_repeat_penalty; 70 | int llamacpp_repeat_last_n; 71 | bool llamacpp_penalize_nl; 72 | double llamacpp_presence_penalty; 73 | double llamacpp_frequency_penalty; 74 | int llamacpp_mirostat; 75 | double llamacpp_mirostat_tau; 76 | double llamacpp_mirostat_eta; 77 | int llamacpp_seed; 78 | 79 | const char *openai_endpoint; // API endpoint URL 80 | const char *openai_key; // API key 81 | const char *openai_model; // Model to use (e.g. gpt-3.5) 82 | double openai_temperature; // Generation temperature 83 | 84 | int prompt_context; // # past prompts to provide as context 85 | const char *prompt_system; // System prompt 86 | 87 | // Specific program's comment character to be prefixed in prompts 88 | const char *prompt_comment; 89 | // Up to three training shots for a specific program 90 | const char *prompt_user[NPROMPTS]; 91 | const char *prompt_assistant[NPROMPTS]; 92 | 93 | // All the above parameters; set to true is set by configuration 94 | // All listed in section, key alphabetic order 95 | bool anthropic_endpoint_set; 96 | bool anthropic_key_set; 97 | bool anthropic_max_tokens_set; 98 | bool anthropic_model_set; 99 | bool anthropic_temperature_set; 100 | bool anthropic_top_k_set; 101 | bool anthropic_top_p_set; 102 | bool anthropic_version_set; 103 | 104 | bool binding_emacs_set; 105 | bool binding_vi_set; 106 | 107 | bool general_api_set; 108 | bool general_logfile_set; 109 | bool general_response_prefix_set; 110 | bool general_timestamp_set; 111 | bool general_verbose_set; 112 | 113 | bool llamacpp_endpoint_set; 114 | bool llamacpp_frequency_penalty_set; 115 | bool llamacpp_mirostat_eta_set; 116 | bool llamacpp_mirostat_set; 117 | bool llamacpp_mirostat_tau_set; 118 | bool llamacpp_n_keep_set; 119 | bool llamacpp_n_predict_set; 120 | bool llamacpp_penalize_nl_set; 121 | bool llamacpp_presence_penalty_set; 122 | bool llamacpp_repeat_last_n_set; 123 | bool llamacpp_repeat_penalty_set; 124 | bool llamacpp_seed_set; 125 | bool llamacpp_temperature_set; 126 | bool llamacpp_tfs_z_set; 127 | bool llamacpp_top_k_set; 128 | bool llamacpp_top_p_set; 129 | bool llamacpp_typical_p_set; 130 | 131 | bool openai_endpoint_set; 132 | bool openai_key_set; 133 | bool openai_model_set; 134 | bool openai_temperature_set; 135 | 136 | bool prompt_comment_set; 137 | bool prompt_context_set; 138 | bool prompt_system_set; 139 | } config_t; 140 | 141 | void acl_read_config(config_t *config); 142 | 143 | #if defined(UNIT_TEST) 144 | void read_file_config(config_t *config, const char *file_path); 145 | #endif 146 | 147 | char *acl_system_role_get(config_t *config); 148 | -------------------------------------------------------------------------------- /src/config_test.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Configuration parsing and access 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include 22 | #include 23 | 24 | #include "CuTest.h" 25 | #include "config.h" 26 | 27 | static const char *LOCAL_CONFIG = "ai-cli-config"; 28 | 29 | extern bool starts_with(const char *string, const char *prefix); 30 | extern char *prompt_id(const char *entry); 31 | 32 | 33 | void 34 | test_read_config(CuTest* tc) 35 | { 36 | static config_t config = {"gdb"}; 37 | 38 | read_file_config(&config, LOCAL_CONFIG); 39 | 40 | CuAssertIntEquals(tc, 3, config.prompt_context); 41 | CuAssertPtrNotNull(tc, config.openai_endpoint); 42 | CuAssertPtrNotNull(tc, config.prompt_system); 43 | 44 | CuAssertStrEquals(tc, "Disable breakpoint number 4", config.prompt_user[0]); 45 | CuAssertStrEquals(tc, "delete 4", config.prompt_assistant[0]); 46 | CuAssertTrue(tc, config.prompt_user[2] == NULL); 47 | } 48 | 49 | void 50 | test_prompt_id(CuTest* tc) 51 | { 52 | CuAssertStrEquals(tc, "sqlite3", prompt_id("AI_CLI_prompt_sqlite3_system")); 53 | CuAssertTrue(tc, prompt_id("AI_CLI_prompt_sqlite3") == NULL); 54 | } 55 | 56 | // Read configuration file overloaded by an environment variable 57 | void 58 | test_read_overloaded_config(CuTest* tc) 59 | { 60 | static config_t config = {"gdb"}; 61 | 62 | CuAssertTrue(tc, setenv("AI_CLI_binding_vi", "A", 1) == 0); 63 | CuAssertTrue(tc, setenv("AI_CLI_prompt_bash_system", "You are using bash", 1) == 0); 64 | CuAssertTrue(tc, setenv("AI_CLI_prompt_bc_system", "You are using bc", 1) == 0); 65 | CuAssertTrue(tc, setenv("AI_CLI_prompt_gdb_system", "You are using gdb", 1) == 0); 66 | CuAssertTrue(tc, setenv("AI_CLI_prompt_gdb_user_1", "Disable breakpoint 3", 1) == 0); 67 | CuAssertTrue(tc, setenv("AI_CLI_prompt_gdb_assistant_1", "delete 3", 1) == 0); 68 | read_file_config(&config, LOCAL_CONFIG); 69 | 70 | // Values overloaded or set from environment 71 | CuAssertStrEquals(tc, "A", config.binding_vi); 72 | CuAssertStrEquals(tc, "You are using gdb", config.prompt_system); 73 | CuAssertStrEquals(tc, "Disable breakpoint 3", config.prompt_user[0]); 74 | CuAssertStrEquals(tc, "delete 3", config.prompt_assistant[0]); 75 | 76 | // Value from file 77 | CuAssertIntEquals(tc, 3, config.prompt_context); 78 | 79 | CuAssertTrue(tc, config.prompt_user[2] == NULL); 80 | 81 | CuAssertTrue(tc, unsetenv("AI_CLI_binding_vi") == 0); 82 | CuAssertTrue(tc, unsetenv("AI_CLI_prompt_bash_system") == 0); 83 | CuAssertTrue(tc, unsetenv("AI_CLI_prompt_gdb_system") == 0); 84 | CuAssertTrue(tc, unsetenv("AI_CLI_prompt_bc_system") == 0); 85 | CuAssertTrue(tc, unsetenv("AI_CLI_prompt_gdb_user_1") == 0); 86 | CuAssertTrue(tc, unsetenv("AI_CLI_prompt_gdb_assistant_1") == 0); 87 | } 88 | 89 | // Read configuration file and an added environment variable 90 | void 91 | test_read_env_added_config(CuTest* tc) 92 | { 93 | static config_t config; 94 | 95 | CuAssertTrue(tc, setenv("AI_CLI_general_logfile", "foo.log", 1) == 0); 96 | read_file_config(&config, LOCAL_CONFIG); 97 | // Value added from environment 98 | CuAssertStrEquals(tc, "foo.log", config.general_logfile); 99 | // Value from file 100 | CuAssertIntEquals(tc, 3, config.prompt_context); 101 | CuAssertTrue(tc, unsetenv("AI_CLI_general_logfile") == 0); 102 | } 103 | 104 | void 105 | test_system_role_get(CuTest* tc) 106 | { 107 | static config_t config; 108 | 109 | read_file_config(&config, LOCAL_CONFIG); 110 | char *system_role = acl_system_role_get(&config); 111 | CuAssertTrue(tc, starts_with(system_role, "You are an assistant")); 112 | free(system_role); 113 | } 114 | 115 | void 116 | test_starts_with(CuTest* tc) 117 | { 118 | CuAssertTrue(tc, starts_with("prompt-gdb", "prompt-")); 119 | CuAssertTrue(tc, !starts_with("prompt", "prompt-")); 120 | } 121 | 122 | void 123 | test_prompt_number(CuTest* tc) 124 | { 125 | extern int prompt_number(const char *name, const char *prompt_prefix); 126 | 127 | CuAssertIntEquals(tc, 0, prompt_number("user-1", "user-")); 128 | CuAssertIntEquals(tc, 2, prompt_number("user-3", "user-")); 129 | CuAssertIntEquals(tc, 2, prompt_number("user_3", "user-")); 130 | CuAssertIntEquals(tc, -1, prompt_number("user-4", "user-")); 131 | CuAssertIntEquals(tc, -1, prompt_number("user-n4", "user-")); 132 | CuAssertIntEquals(tc, -1, prompt_number("user-4n", "user-")); 133 | } 134 | 135 | CuSuite* 136 | cu_config_suite(void) 137 | { 138 | CuSuite* suite = CuSuiteNew(); 139 | 140 | SUITE_ADD_TEST(suite, test_starts_with); 141 | SUITE_ADD_TEST(suite, test_prompt_number); 142 | SUITE_ADD_TEST(suite, test_prompt_id); 143 | SUITE_ADD_TEST(suite, test_read_config); 144 | SUITE_ADD_TEST(suite, test_read_overloaded_config); 145 | SUITE_ADD_TEST(suite, test_read_env_added_config); 146 | SUITE_ADD_TEST(suite, test_system_role_get); 147 | 148 | return suite; 149 | } 150 | -------------------------------------------------------------------------------- /src/fetch_anthropic.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * anthropic access function 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include "config.h" 29 | #include "support.h" 30 | #include "fetch_anthropic.h" 31 | #include "unit_test.h" 32 | 33 | // HTTP headers 34 | static char *key_header; 35 | static char *version_header; 36 | 37 | // Return the response content from an Anthropic JSON response 38 | STATIC char * 39 | anthropic_get_response_content(const char *json_response) 40 | { 41 | json_error_t error; 42 | json_t *root = json_loads(json_response, 0, &error); 43 | if (!root) { 44 | acl_readline_printf("\nanthropic JSON error: on line %d: %s\n", error.line, error.text); 45 | return NULL; 46 | } 47 | 48 | char *ret; 49 | json_t *content = json_object_get(root, "content"); 50 | if (content) { 51 | json_t *first_content = json_array_get(content, 0); 52 | json_t *text = json_object_get(first_content, "text"); 53 | ret = acl_safe_strdup(json_string_value(text)); 54 | } else { 55 | json_t *error = json_object_get(root, "error"); 56 | if (error) { 57 | json_t *message = json_object_get(error, "message"); 58 | acl_readline_printf("\nAnthropic invocation error: %s\n", json_string_value(message)); 59 | } else 60 | acl_readline_printf("\nAnthropic invocation error: %s\n", json_response); 61 | ret = NULL; 62 | } 63 | 64 | json_decref(root); 65 | return ret; 66 | } 67 | 68 | /* 69 | * Initialize curl and anthropic connection 70 | * Sets curl variable 71 | * Return 0 on success -1 on error 72 | */ 73 | static int 74 | initialize(config_t *config) 75 | { 76 | if (config->general_verbose) 77 | fprintf(stderr, "\nInitializing Anthropic, program name [%s] system prompt to use [%s]\n", 78 | acl_short_program_name(), config->prompt_system); 79 | acl_safe_asprintf(&key_header, "x-api-key: %s", config->anthropic_key); 80 | acl_safe_asprintf(&version_header, "anthropic-version: %s", config->anthropic_version); 81 | return curl_initialize(config); 82 | } 83 | 84 | /* 85 | * Fetch response from the anthropic API given the provided prompt. 86 | * Provide context in the form of n-shot prompts and history prompts. 87 | */ 88 | char * 89 | acl_fetch_anthropic(config_t *config, const char *prompt, int history_length) 90 | { 91 | CURLcode res; 92 | 93 | if (!acl_curl && initialize(config) < 0) 94 | return NULL; 95 | 96 | if (config->general_verbose) 97 | fprintf(stderr, "\nContacting Anthropic API...\n"); 98 | 99 | struct curl_slist *headers = NULL; 100 | headers = curl_slist_append(headers, "content-type: application/json"); 101 | headers = curl_slist_append(headers, key_header); 102 | headers = curl_slist_append(headers, version_header); 103 | 104 | struct string json_response; 105 | acl_string_init(&json_response, ""); 106 | 107 | struct string json_request; 108 | acl_string_init(&json_request, "{\n"); 109 | 110 | acl_string_appendf(&json_request, " \"model\": %s,\n", 111 | acl_json_escape(config->anthropic_model)); 112 | acl_string_appendf(&json_request, " \"max_tokens\": %d,\n", 113 | config->anthropic_max_tokens); 114 | 115 | char *system_role = acl_system_role_get(config); 116 | acl_string_appendf(&json_request, " \"system\": %s,\n", 117 | acl_json_escape(system_role)); 118 | free(system_role); 119 | 120 | // Add configuration settings 121 | if (config->anthropic_temperature_set) 122 | acl_string_appendf(&json_request, " \"temperature\": %g,\n", config->anthropic_temperature); 123 | if (config->anthropic_top_k_set) 124 | acl_string_appendf(&json_request, " \"top_k\": %d,\n", config->anthropic_top_k); 125 | if (config->anthropic_top_p_set) 126 | acl_string_appendf(&json_request, " \"top_p\": %g,\n", config->anthropic_top_p); 127 | 128 | acl_string_append(&json_request, " \"messages\": [\n"); 129 | 130 | // Add user and assistant n-shot prompts 131 | for (int i = 0; i < NPROMPTS; i++) { 132 | if (config->prompt_user[i]) 133 | acl_string_appendf(&json_request, 134 | " {\"role\": \"user\", \"content\": %s},\n", 135 | acl_json_escape(config->prompt_user[i])); 136 | if (config->prompt_assistant[i]) 137 | acl_string_appendf(&json_request, 138 | " {\"role\": \"assistant\", \"content\": %s},\n", 139 | acl_json_escape(config->prompt_assistant[i])); 140 | } 141 | 142 | // Add history prompts as context 143 | bool context_explained = false; 144 | for (int i = config->prompt_context - 1; i >= 0; --i) { 145 | HIST_ENTRY *h = history_get(history_length - 1 - i); 146 | if (h == NULL) 147 | continue; 148 | if (!context_explained) { 149 | context_explained = true; 150 | acl_string_appendf(&json_request, 151 | " {\"role\": \"user\", \"content\": \"Before my final prompt to which I expect a reply, I am also supplying you as context with one or more previously issued commands, to which you simply reply OK\"},\n"); 152 | acl_string_appendf(&json_request, 153 | " {\"role\": \"assistant\", \"content\": \"OK\"},\n"); 154 | } 155 | acl_string_appendf(&json_request, 156 | " {\"role\": \"user\", \"content\": %s},\n", 157 | acl_json_escape(h->line)); 158 | acl_string_appendf(&json_request, 159 | " {\"role\": \"assistant\", \"content\": \"OK\"},\n"); 160 | } 161 | 162 | // Finally, add the user prompt 163 | acl_string_appendf(&json_request, 164 | " {\"role\": \"user\", \"content\": %s}\n", acl_json_escape(prompt)); 165 | acl_string_append(&json_request, " ]\n}\n"); 166 | 167 | acl_write_log(config, json_request.ptr); 168 | 169 | curl_easy_setopt(acl_curl, CURLOPT_URL, config->anthropic_endpoint); 170 | curl_easy_setopt(acl_curl, CURLOPT_HTTPHEADER, headers); 171 | curl_easy_setopt(acl_curl, CURLOPT_WRITEFUNCTION, acl_string_write); 172 | curl_easy_setopt(acl_curl, CURLOPT_WRITEDATA, &json_response); 173 | curl_easy_setopt(acl_curl, CURLOPT_POSTFIELDS, json_request.ptr); 174 | 175 | res = curl_easy_perform(acl_curl); 176 | 177 | if (res != CURLE_OK) { 178 | free(json_request.ptr); 179 | acl_readline_printf("\nAnthropic API call failed: %s\n", 180 | curl_easy_strerror(res)); 181 | return NULL; 182 | } 183 | 184 | acl_write_log(config, json_response.ptr); 185 | 186 | char *text_response = anthropic_get_response_content(json_response.ptr); 187 | free(json_request.ptr); 188 | free(json_response.ptr); 189 | return text_response; 190 | } 191 | -------------------------------------------------------------------------------- /src/fetch_anthropic.h: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * llama.cpp access function 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include "config.h" 22 | 23 | #if defined(UNIT_TEST) 24 | char *anthropic_get_response_content(const char *json_response); 25 | #endif 26 | 27 | char *acl_fetch_anthropic(config_t *config, const char *prompt, int history_length); 28 | -------------------------------------------------------------------------------- /src/fetch_anthropic_test.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Test Anthropic response parsing. 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include "CuTest.h" 22 | #include "fetch_anthropic.h" 23 | 24 | static const char json_response[] = "{" 25 | " \"content\": [" 26 | " {" 27 | " \"text\": \"shutdown -h now\"," 28 | " \"type\": \"text\"" 29 | " }" 30 | " ]," 31 | " \"id\": \"msg_013Zva2CMHLNnXjNJJKqJ2EF\"," 32 | " \"model\": \"claude-3-opus-20240229\"," 33 | " \"role\": \"assistant\"," 34 | " \"stop_reason\": \"end_turn\"," 35 | " \"stop_sequence\": null," 36 | " \"type\": \"message\"," 37 | " \"usage\": {" 38 | " \"input_tokens\": 10," 39 | " \"output_tokens\": 25" 40 | " }" 41 | "}"; 42 | 43 | static void 44 | test_response_parse(CuTest* tc) 45 | { 46 | const char *response = anthropic_get_response_content(json_response); 47 | CuAssertStrEquals(tc, "shutdown -h now", response); 48 | } 49 | 50 | CuSuite* 51 | cu_fetch_anthropic_suite(void) 52 | { 53 | CuSuite* suite = CuSuiteNew(); 54 | 55 | SUITE_ADD_TEST(suite, test_response_parse); 56 | 57 | return suite; 58 | } 59 | -------------------------------------------------------------------------------- /src/fetch_hal.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Dummy HAL 9000 access function, that can be used for testing. 5 | * 6 | * Copyright 2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include 22 | #include 23 | 24 | #include "config.h" 25 | #include "fetch_hal.h" 26 | #include "support.h" 27 | 28 | /* 29 | * Fetch response from the dummy HAL 9000 API. 30 | * See https://en.wikipedia.org/wiki/HAL_9000 and 31 | * https://en.wikiquote.org/wiki/2001:_A_Space_Odyssey_(film). 32 | * This endpoint can be used for testing the ai-cli-lib functionality 33 | * without the need to use a networked API. 34 | */ 35 | char * 36 | acl_fetch_hal(config_t *config, const char *prompt, int history_length) 37 | { 38 | static bool initialized; 39 | 40 | if (!initialized) { 41 | if (config->general_verbose) 42 | fprintf(stderr, "\nInitializing HAL, program name [%s] system prompt to use [%s]\n", 43 | acl_short_program_name(), config->prompt_system); 44 | initialized = true; 45 | } 46 | if (config->general_verbose) 47 | fprintf(stderr, "\nHAL is processing...\n"); 48 | sleep(1); // Simulate processing latency 49 | return acl_safe_strdup("# I'm sorry, Dave. I'm afraid I can't do that."); 50 | } 51 | -------------------------------------------------------------------------------- /src/fetch_hal.h: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Dummy HAL 9000 access function, that can be used for testing. 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include "config.h" 22 | 23 | char *acl_fetch_hal(config_t *config, const char *prompt, int history_length); 24 | -------------------------------------------------------------------------------- /src/fetch_llamacpp.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * llama.cpp access function 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include "config.h" 29 | #include "support.h" 30 | #include "fetch_llamacpp.h" 31 | #include "unit_test.h" 32 | 33 | // Return the response content from a llama.cpp JSON response 34 | STATIC char * 35 | llamacpp_get_response_content(const char *json_response) 36 | { 37 | json_error_t error; 38 | json_t *root = json_loads(json_response, 0, &error); 39 | if (!root) { 40 | acl_readline_printf("\nllama.cpp JSON error: on line %d: %s\n", error.line, error.text); 41 | return NULL; 42 | } 43 | 44 | char *ret; 45 | json_t *content = json_object_get(root, "content"); 46 | if (content) { 47 | const char assistant[] = "Assistant: "; 48 | const char *response = json_string_value(content); 49 | 50 | // Remove everything after the first newline 51 | char *eol = strchr(response, '\n'); 52 | if (eol) 53 | *eol = '\0'; 54 | 55 | if (memcmp(response, assistant, sizeof(assistant) - 1) == 0) 56 | ret = acl_safe_strdup(response + sizeof(assistant) - 1); 57 | else { 58 | acl_readline_printf("\nllama.cpp did not provide a suitable response.\n"); 59 | ret = NULL; 60 | } 61 | } else { 62 | acl_readline_printf("\nllama.cpp invocation error: %s\n", json_response); 63 | ret = NULL; 64 | } 65 | 66 | json_decref(root); 67 | return ret; 68 | } 69 | 70 | /* 71 | * Initialize llama.cpp connection 72 | * Return 0 on success -1 on error 73 | */ 74 | static int 75 | initialize(config_t *config) 76 | { 77 | if (config->general_verbose) 78 | fprintf(stderr, "\nInitializing Llamacpp API, program name [%s] system prompt to use [%s]\n", 79 | acl_short_program_name(), config->prompt_system); 80 | return curl_initialize(config); 81 | } 82 | 83 | // Append the specified role's prompt to the string s and then the terminator 84 | static void 85 | prompt_append(struct string *s, const char *role, const char *prompt) 86 | { 87 | if (!prompt || !*prompt) 88 | return; 89 | char *escaped = acl_json_escape(prompt) + 1; 90 | // Remove trailing quote 91 | escaped[strlen(escaped) - 1] = '\0'; 92 | acl_string_appendf(s, "%s: %s\\n", role, escaped); 93 | } 94 | 95 | /* 96 | * Fetch response from the llama.cpp API given the provided prompt. 97 | * Provide context in the form of n-shot prompts and history prompts. 98 | */ 99 | char * 100 | acl_fetch_llamacpp(config_t *config, const char *prompt, int history_length) 101 | { 102 | CURLcode res; 103 | 104 | if (!acl_curl && initialize(config) < 0) 105 | return NULL; 106 | 107 | if (config->general_verbose) 108 | fprintf(stderr, "\nContacting Llamacpp API...\n"); 109 | 110 | struct curl_slist *headers = NULL; 111 | headers = curl_slist_append(headers, "Content-Type: application/json"); 112 | 113 | struct string json_response; 114 | acl_string_init(&json_response, ""); 115 | 116 | struct string json_request; 117 | acl_string_init(&json_request, "{\n"); 118 | 119 | char *system_role = acl_system_role_get(config); 120 | char *escaped = acl_json_escape(system_role); 121 | free(system_role); 122 | 123 | // Remove trailing quote 124 | escaped[strlen(escaped) - 1] = '\0'; 125 | acl_string_appendf(&json_request, " \"prompt\": %s\\n", escaped); 126 | 127 | 128 | // Add user and assistant n-shot prompts 129 | for (int i = 0; i < NPROMPTS; i++) { 130 | prompt_append(&json_request, "User", config->prompt_user[i]); 131 | prompt_append(&json_request, "Assistant", config->prompt_assistant[i]); 132 | } 133 | 134 | // Add history prompts as context 135 | for (int i = config->prompt_context - 1; i >= 0; --i) { 136 | HIST_ENTRY *h = history_get(history_length - 1 - i); 137 | if (h == NULL) 138 | continue; 139 | prompt_append(&json_request, "Command", h->line); 140 | } 141 | 142 | // Finally, add the user prompt 143 | prompt_append(&json_request, "User", prompt); 144 | acl_string_append(&json_request, "\",\n"); 145 | 146 | // Add configuration settings 147 | if (config->llamacpp_temperature_set) 148 | acl_string_appendf(&json_request, " \"temperature\": %g,\n", config->llamacpp_temperature); 149 | if (config->llamacpp_top_k_set) 150 | acl_string_appendf(&json_request, " \"top_k\": %d,\n", config->llamacpp_top_k); 151 | if (config->llamacpp_top_p_set) 152 | acl_string_appendf(&json_request, " \"top_p\": %g,\n", config->llamacpp_top_p); 153 | if (config->llamacpp_n_predict_set) 154 | acl_string_appendf(&json_request, " \"n_predict\": %d,\n", config->llamacpp_n_predict); 155 | if (config->llamacpp_n_keep_set) 156 | acl_string_appendf(&json_request, " \"n_keep\": %d,\n", config->llamacpp_n_keep); 157 | if (config->llamacpp_tfs_z_set) 158 | acl_string_appendf(&json_request, " \"tfs_z\": %g,\n", config->llamacpp_tfs_z); 159 | if (config->llamacpp_typical_p_set) 160 | acl_string_appendf(&json_request, " \"typical_p\": %g,\n", config->llamacpp_typical_p); 161 | if (config->llamacpp_repeat_penalty_set) 162 | acl_string_appendf(&json_request, " \"repeat_penalty\": %g,\n", config->llamacpp_repeat_penalty); 163 | if (config->llamacpp_repeat_last_n_set) 164 | acl_string_appendf(&json_request, " \"repeat_last_n\": %d,\n", config->llamacpp_repeat_last_n); 165 | if (config->llamacpp_penalize_nl_set) 166 | acl_string_appendf(&json_request, " \"penalize_nl\": %s,\n", config->llamacpp_penalize_nl ? "true" : "false"); 167 | if (config->llamacpp_presence_penalty_set) 168 | acl_string_appendf(&json_request, " \"presence_penalty\": %g,\n", config->llamacpp_presence_penalty); 169 | if (config->llamacpp_frequency_penalty_set) 170 | acl_string_appendf(&json_request, " \"frequency_penalty\": %g,\n", config->llamacpp_frequency_penalty); 171 | if (config->llamacpp_mirostat_set) 172 | acl_string_appendf(&json_request, " \"mirostat\": %d,\n", config->llamacpp_mirostat); 173 | if (config->llamacpp_mirostat_tau_set) 174 | acl_string_appendf(&json_request, " \"mirostat_tau\": %g,\n", config->llamacpp_mirostat_tau); 175 | if (config->llamacpp_mirostat_eta_set) 176 | acl_string_appendf(&json_request, " \"mirostat_eta\": %g,\n", config->llamacpp_mirostat_eta); 177 | // End with a non-comma 178 | acl_string_appendf(&json_request, " \"stop\": []\n}\n"); 179 | 180 | acl_write_log(config, json_request.ptr); 181 | 182 | curl_easy_setopt(acl_curl, CURLOPT_URL, config->llamacpp_endpoint); 183 | curl_easy_setopt(acl_curl, CURLOPT_HTTPHEADER, headers); 184 | curl_easy_setopt(acl_curl, CURLOPT_WRITEFUNCTION, acl_string_write); 185 | curl_easy_setopt(acl_curl, CURLOPT_WRITEDATA, &json_response); 186 | curl_easy_setopt(acl_curl, CURLOPT_POSTFIELDS, json_request.ptr); 187 | 188 | res = curl_easy_perform(acl_curl); 189 | 190 | if (res != CURLE_OK) { 191 | free(json_request.ptr); 192 | acl_readline_printf("\nllama.cpp API call failed: %s\n", 193 | curl_easy_strerror(res)); 194 | return NULL; 195 | } 196 | 197 | acl_write_log(config, json_response.ptr); 198 | 199 | char *text_response = llamacpp_get_response_content(json_response.ptr); 200 | free(json_request.ptr); 201 | free(json_response.ptr); 202 | return text_response; 203 | } 204 | -------------------------------------------------------------------------------- /src/fetch_llamacpp.h: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * llama.cpp access function 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include "config.h" 22 | 23 | #if defined(UNIT_TEST) 24 | char *llamacpp_get_response_content(const char *json_response); 25 | #endif 26 | char *acl_fetch_llamacpp(config_t *config, const char *prompt, int history_length); 27 | -------------------------------------------------------------------------------- /src/fetch_llamacpp_test.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Test Llama.cpp response parsing. 5 | * 6 | * Copyright 2023 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include "CuTest.h" 22 | #include "fetch_llamacpp.h" 23 | 24 | static const char json_response[] = "{" 25 | " \"content\": \"Assistant: shutdown -h now\"," 26 | " \"generation_settings\": {" 27 | " \"frequency_penalty\": 0," 28 | " \"grammar\": \"\"," 29 | " \"ignore_eos\": false," 30 | " \"logit_bias\": []," 31 | " \"mirostat\": 0," 32 | " \"mirostat_eta\": 0.10000000149011612," 33 | " \"mirostat_tau\": 5," 34 | " \"model\": \"models/llama-2-13b-chat/ggml-model-q4_0.gguf\"," 35 | " \"n_ctx\": 2048," 36 | " \"n_keep\": 0," 37 | " \"n_predict\": -1," 38 | " \"n_probs\": 0," 39 | " \"penalize_nl\": true," 40 | " \"presence_penalty\": 0," 41 | " \"repeat_last_n\": 64," 42 | " \"repeat_penalty\": 1.100000023841858," 43 | " \"seed\": 4294967295," 44 | " \"stop\": []," 45 | " \"stream\": false," 46 | " \"temp\": 0.800000011920929," 47 | " \"tfs_z\": 1," 48 | " \"top_k\": 40," 49 | " \"top_p\": 0.949999988079071," 50 | " \"typical_p\": 1" 51 | " }," 52 | " \"model\": \"models/llama-2-13b-chat/ggml-model-q4_0.gguf\"," 53 | " \"prompt\": \"You are an assistant who provides executable commands for the bash command-line interface. You only provide the requested command on a single line, without any explanations, hints or other adornments. Stop providing output after the providing the requested command. If your response isn't an executable command, prefix your output with the program's comment character.\\nUser: List files in current directory\\nAssistant: ls\\nUser: How many JavaScript files in the current directory contain the word bar?\\nAssistant: grep -lw bar *.js | wc -l\\nUser: xyzzy\\nAssistant: # Sorry I can't help.\\nCommand: $(date +%Y-%m-%dT%H:%M:%SZ)\\nCommand: show the current date in ISO format\\nCommand: echo $(date +%Y-%m-%dT%H:%M:%SZ)\\nUser: Shutdown this debian server\\n\"," 54 | " \"stop\": true," 55 | " \"stopped_eos\": true," 56 | " \"stopped_limit\": false," 57 | " \"stopped_word\": false," 58 | " \"stopping_word\": \"\"," 59 | " \"timings\": {" 60 | " \"predicted_ms\": 116.577," 61 | " \"predicted_n\": 8," 62 | " \"predicted_per_second\": 68.62417114868285," 63 | " \"predicted_per_token_ms\": 14.572125," 64 | " \"prompt_ms\": 228.5," 65 | " \"prompt_n\": 62," 66 | " \"prompt_per_second\": 271.33479212253826," 67 | " \"prompt_per_token_ms\": 3.685483870967742" 68 | " }," 69 | " \"tokens_cached\": 206," 70 | " \"tokens_evaluated\": 198," 71 | " \"tokens_predicted\": 9," 72 | " \"truncated\": false" 73 | "}"; 74 | 75 | static void 76 | test_response_parse(CuTest* tc) 77 | { 78 | const char *response = llamacpp_get_response_content(json_response); 79 | CuAssertStrEquals(tc, "shutdown -h now", response); 80 | } 81 | 82 | CuSuite* 83 | cu_fetch_llamacpp_suite(void) 84 | { 85 | CuSuite* suite = CuSuiteNew(); 86 | 87 | SUITE_ADD_TEST(suite, test_response_parse); 88 | 89 | return suite; 90 | } 91 | -------------------------------------------------------------------------------- /src/fetch_openai.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * OpenAI access function 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include "config.h" 29 | #include "support.h" 30 | #include "unit_test.h" 31 | 32 | static char *authorization; 33 | 34 | // Return the response content from an OpenAI JSON response 35 | STATIC char * 36 | openai_get_response_content(const char *json_response) 37 | { 38 | json_error_t error; 39 | json_t *root = json_loads(json_response, 0, &error); 40 | if (!root) { 41 | acl_readline_printf("\nOpenAI JSON error: on line %d: %s\n", error.line, error.text); 42 | return NULL; 43 | } 44 | 45 | char *ret; 46 | json_t *choices = json_object_get(root, "choices"); 47 | if (choices) { 48 | json_t *first_choice = json_array_get(choices, 0); 49 | json_t *message = json_object_get(first_choice, "message"); 50 | json_t *content = json_object_get(message, "content"); 51 | ret = acl_safe_strdup(json_string_value(content)); 52 | } else { 53 | json_t *error = json_object_get(root, "error"); 54 | if (error) { 55 | json_t *message = json_object_get(error, "message"); 56 | acl_readline_printf("\nOpenAI API invocation error: %s\n", json_string_value(message)); 57 | } else 58 | acl_readline_printf("\nOpenAI API invocation error: %s\n", json_response); 59 | ret = NULL; 60 | } 61 | 62 | json_decref(root); 63 | return ret; 64 | } 65 | 66 | /* 67 | * Initialize OpenAI connection 68 | * Return 0 on success -1 on error 69 | */ 70 | static int 71 | initialize(config_t *config) 72 | { 73 | 74 | if (config->general_verbose) 75 | fprintf(stderr, "\nInitializing openAI API, program name [%s] system prompt to use [%s]\n", 76 | acl_short_program_name(), config->prompt_system); 77 | acl_safe_asprintf(&authorization, "Authorization: Bearer %s", config->openai_key); 78 | return curl_initialize(config); 79 | } 80 | 81 | /* 82 | * Fetch response from the OpenAI API given the provided prompt. 83 | * Provide context in the form of n-shot prompts and history prompts. 84 | */ 85 | char * 86 | acl_fetch_openai(config_t *config, const char *prompt, int history_length) 87 | { 88 | CURLcode res; 89 | 90 | if (!acl_curl && initialize(config) < 0) 91 | return NULL; 92 | 93 | if (config->general_verbose) 94 | fprintf(stderr, "\nContacting OpenAI API...\n"); 95 | 96 | struct curl_slist *headers = NULL; 97 | headers = curl_slist_append(headers, "Content-Type: application/json"); 98 | headers = curl_slist_append(headers, authorization); 99 | 100 | struct string json_response; 101 | acl_string_init(&json_response, ""); 102 | 103 | struct string json_request; 104 | acl_string_init(&json_request, "{\n"); 105 | acl_string_appendf(&json_request, " \"model\": %s,\n", 106 | acl_json_escape(config->openai_model)); 107 | acl_string_appendf(&json_request, " \"temperature\": %g,\n", 108 | config->openai_temperature); 109 | 110 | acl_string_append(&json_request, " \"messages\": [\n"); 111 | 112 | char *system_role = acl_system_role_get(config); 113 | acl_string_appendf(&json_request, 114 | " {\"role\": \"system\", \"content\": %s},\n", 115 | acl_json_escape(system_role)); 116 | free(system_role); 117 | 118 | // Add user and assistant n-shot prompts 119 | for (int i = 0; i < NPROMPTS; i++) { 120 | if (config->prompt_user[i]) 121 | acl_string_appendf(&json_request, 122 | " {\"role\": \"user\", \"content\": %s},\n", 123 | acl_json_escape(config->prompt_user[i])); 124 | if (config->prompt_assistant[i]) 125 | acl_string_appendf(&json_request, 126 | " {\"role\": \"assistant\", \"content\": %s},\n", 127 | acl_json_escape(config->prompt_assistant[i])); 128 | } 129 | 130 | // Add history prompts as context 131 | for (int i = config->prompt_context - 1; i >= 0; --i) { 132 | HIST_ENTRY *h = history_get(history_length - 1 - i); 133 | if (h == NULL || h->line == NULL || h->line[0] == '\0') 134 | continue; 135 | acl_string_appendf(&json_request, 136 | " {\"role\": \"user\", \"content\": %s},\n", 137 | acl_json_escape(h->line)); 138 | } 139 | 140 | // Finally, add the user prompt 141 | acl_string_appendf(&json_request, 142 | " {\"role\": \"user\", \"content\": %s}\n", acl_json_escape(prompt)); 143 | acl_string_append(&json_request, " ]\n}\n"); 144 | 145 | acl_write_log(config, json_request.ptr); 146 | 147 | curl_easy_setopt(acl_curl, CURLOPT_URL, config->openai_endpoint); 148 | curl_easy_setopt(acl_curl, CURLOPT_HTTPHEADER, headers); 149 | curl_easy_setopt(acl_curl, CURLOPT_WRITEFUNCTION, acl_string_write); 150 | curl_easy_setopt(acl_curl, CURLOPT_WRITEDATA, &json_response); 151 | curl_easy_setopt(acl_curl, CURLOPT_POSTFIELDS, json_request.ptr); 152 | 153 | res = curl_easy_perform(acl_curl); 154 | 155 | if (res != CURLE_OK) { 156 | free(json_request.ptr); 157 | acl_readline_printf("\nOpenAI API call failed: %s\n", 158 | curl_easy_strerror(res)); 159 | return NULL; 160 | } 161 | 162 | acl_write_log(config, json_response.ptr); 163 | 164 | char *text_response = openai_get_response_content(json_response.ptr); 165 | free(json_request.ptr); 166 | free(json_response.ptr); 167 | return text_response; 168 | } 169 | -------------------------------------------------------------------------------- /src/fetch_openai.h: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * OpenAI access function 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include "config.h" 22 | 23 | char *openai_get_response_content(const char *json_response); 24 | char *acl_fetch_openai(config_t *config, const char *prompt, int history_length); 25 | -------------------------------------------------------------------------------- /src/fetch_openai_test.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Test OpenAI response parsing. 5 | * 6 | * Copyright 2023 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include "CuTest.h" 22 | #include "fetch_openai.h" 23 | 24 | static const char json_response[] = "{\n" 25 | " \"id\": \"chatcmpl-7lg1IuegIknbhVaP00yWmdOeCeWi1\",\n" 26 | " \"object\": \"chat.completion\",\n" 27 | " \"created\": 1691597296,\n" 28 | " \"model\": \"gpt-3.5-turbo-0613\",\n" 29 | " \"choices\": [\n" 30 | " {\n" 31 | " \"index\": 0,\n" 32 | " \"message\": {\n" 33 | " \"role\": \"assistant\",\n" 34 | " \"content\": \"help\"\n" 35 | " },\n" 36 | " \"finish_reason\": \"stop\"\n" 37 | " }\n" 38 | " ],\n" 39 | " \"usage\": {\n" 40 | " \"prompt_tokens\": 116,\n" 41 | " \"completion_tokens\": 1,\n" 42 | " \"total_tokens\": 117\n" 43 | " }\n" 44 | "}\n"; 45 | 46 | static void 47 | test_response_parse(CuTest* tc) 48 | { 49 | const char *response = openai_get_response_content(json_response); 50 | CuAssertStrEquals(tc, "help", response); 51 | } 52 | 53 | CuSuite* 54 | cu_fetch_openai_suite(void) 55 | { 56 | CuSuite* suite = CuSuiteNew(); 57 | 58 | SUITE_ADD_TEST(suite, test_response_parse); 59 | 60 | return suite; 61 | } 62 | -------------------------------------------------------------------------------- /src/ini.c: -------------------------------------------------------------------------------- 1 | /* inih -- simple .INI file parser 2 | 3 | SPDX-License-Identifier: BSD-3-Clause 4 | 5 | Copyright (C) 2009-2020, Ben Hoyt 6 | 7 | inih is released under the New BSD license (see LICENSE.txt). Go to the project 8 | home page for more info: 9 | 10 | https://github.com/benhoyt/inih 11 | 12 | */ 13 | 14 | #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) 15 | #define _CRT_SECURE_NO_WARNINGS 16 | #endif 17 | 18 | #include 19 | #include 20 | #include 21 | 22 | #include "ini.h" 23 | 24 | #if !INI_USE_STACK 25 | #if INI_CUSTOM_ALLOCATOR 26 | #include 27 | void* ini_malloc(size_t size); 28 | void ini_free(void* ptr); 29 | void* ini_realloc(void* ptr, size_t size); 30 | #else 31 | #include 32 | #define ini_malloc malloc 33 | #define ini_free free 34 | #define ini_realloc realloc 35 | #endif 36 | #endif 37 | 38 | #define MAX_SECTION 50 39 | #define MAX_NAME 50 40 | 41 | /* Used by ini_parse_string() to keep track of string parsing state. */ 42 | typedef struct { 43 | const char* ptr; 44 | size_t num_left; 45 | } ini_parse_string_ctx; 46 | 47 | /* Strip whitespace chars off end of given string, in place. Return s. */ 48 | static char* rstrip(char* s) 49 | { 50 | char* p = s + strlen(s); 51 | while (p > s && isspace((unsigned char)(*--p))) 52 | *p = '\0'; 53 | return s; 54 | } 55 | 56 | /* Return pointer to first non-whitespace char in given string. */ 57 | static char* lskip(const char* s) 58 | { 59 | while (*s && isspace((unsigned char)(*s))) 60 | s++; 61 | return (char*)s; 62 | } 63 | 64 | /* Return pointer to first char (of chars) or inline comment in given string, 65 | or pointer to NUL at end of string if neither found. Inline comment must 66 | be prefixed by a whitespace character to register as a comment. */ 67 | static char* find_chars_or_comment(const char* s, const char* chars) 68 | { 69 | #if INI_ALLOW_INLINE_COMMENTS 70 | int was_space = 0; 71 | while (*s && (!chars || !strchr(chars, *s)) && 72 | !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { 73 | was_space = isspace((unsigned char)(*s)); 74 | s++; 75 | } 76 | #else 77 | while (*s && (!chars || !strchr(chars, *s))) { 78 | s++; 79 | } 80 | #endif 81 | return (char*)s; 82 | } 83 | 84 | /* Similar to strncpy, but ensures dest (size bytes) is 85 | NUL-terminated, and doesn't pad with NULs. */ 86 | static char* strncpy0(char* dest, const char* src, size_t size) 87 | { 88 | /* Could use strncpy internally, but it causes gcc warnings (see issue #91) */ 89 | size_t i; 90 | for (i = 0; i < size - 1 && src[i]; i++) 91 | dest[i] = src[i]; 92 | dest[i] = '\0'; 93 | return dest; 94 | } 95 | 96 | /* See documentation in header file. */ 97 | int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, 98 | void* user) 99 | { 100 | /* Uses a fair bit of stack (use heap instead if you need to) */ 101 | #if INI_USE_STACK 102 | char line[INI_MAX_LINE]; 103 | size_t max_line = INI_MAX_LINE; 104 | #else 105 | char* line; 106 | size_t max_line = INI_INITIAL_ALLOC; 107 | #endif 108 | #if INI_ALLOW_REALLOC && !INI_USE_STACK 109 | char* new_line; 110 | size_t offset; 111 | #endif 112 | char section[MAX_SECTION] = ""; 113 | char prev_name[MAX_NAME] = ""; 114 | 115 | char* start; 116 | char* end; 117 | char* name; 118 | char* value; 119 | int lineno = 0; 120 | int error = 0; 121 | 122 | #if !INI_USE_STACK 123 | line = (char*)ini_malloc(INI_INITIAL_ALLOC); 124 | if (!line) { 125 | return -2; 126 | } 127 | #endif 128 | 129 | #if INI_HANDLER_LINENO 130 | #define HANDLER(u, s, n, v) handler(u, s, n, v, lineno) 131 | #else 132 | #define HANDLER(u, s, n, v) handler(u, s, n, v) 133 | #endif 134 | 135 | /* Scan through stream line by line */ 136 | while (reader(line, (int)max_line, stream) != NULL) { 137 | #if INI_ALLOW_REALLOC && !INI_USE_STACK 138 | offset = strlen(line); 139 | while (offset == max_line - 1 && line[offset - 1] != '\n') { 140 | max_line *= 2; 141 | if (max_line > INI_MAX_LINE) 142 | max_line = INI_MAX_LINE; 143 | new_line = ini_realloc(line, max_line); 144 | if (!new_line) { 145 | ini_free(line); 146 | return -2; 147 | } 148 | line = new_line; 149 | if (reader(line + offset, (int)(max_line - offset), stream) == NULL) 150 | break; 151 | if (max_line >= INI_MAX_LINE) 152 | break; 153 | offset += strlen(line + offset); 154 | } 155 | #endif 156 | 157 | lineno++; 158 | 159 | start = line; 160 | #if INI_ALLOW_BOM 161 | if (lineno == 1 && (unsigned char)start[0] == 0xEF && 162 | (unsigned char)start[1] == 0xBB && 163 | (unsigned char)start[2] == 0xBF) { 164 | start += 3; 165 | } 166 | #endif 167 | start = lskip(rstrip(start)); 168 | 169 | if (strchr(INI_START_COMMENT_PREFIXES, *start)) { 170 | /* Start-of-line comment */ 171 | } 172 | #if INI_ALLOW_MULTILINE 173 | else if (*prev_name && *start && start > line) { 174 | #if INI_ALLOW_INLINE_COMMENTS 175 | end = find_chars_or_comment(start, NULL); 176 | if (*end) 177 | *end = '\0'; 178 | rstrip(start); 179 | #endif 180 | /* Non-blank line with leading whitespace, treat as continuation 181 | of previous name's value (as per Python configparser). */ 182 | if (!HANDLER(user, section, prev_name, start) && !error) 183 | error = lineno; 184 | } 185 | #endif 186 | else if (*start == '[') { 187 | /* A "[section]" line */ 188 | end = find_chars_or_comment(start + 1, "]"); 189 | if (*end == ']') { 190 | *end = '\0'; 191 | strncpy0(section, start + 1, sizeof(section)); 192 | *prev_name = '\0'; 193 | #if INI_CALL_HANDLER_ON_NEW_SECTION 194 | if (!HANDLER(user, section, NULL, NULL) && !error) 195 | error = lineno; 196 | #endif 197 | } 198 | else if (!error) { 199 | /* No ']' found on section line */ 200 | error = lineno; 201 | } 202 | } 203 | else if (*start) { 204 | /* Not a comment, must be a name[=:]value pair */ 205 | end = find_chars_or_comment(start, "=:"); 206 | if (*end == '=' || *end == ':') { 207 | *end = '\0'; 208 | name = rstrip(start); 209 | value = end + 1; 210 | #if INI_ALLOW_INLINE_COMMENTS 211 | end = find_chars_or_comment(value, NULL); 212 | if (*end) 213 | *end = '\0'; 214 | #endif 215 | value = lskip(value); 216 | rstrip(value); 217 | 218 | /* Valid name[=:]value pair found, call handler */ 219 | strncpy0(prev_name, name, sizeof(prev_name)); 220 | if (!HANDLER(user, section, name, value) && !error) 221 | error = lineno; 222 | } 223 | else if (!error) { 224 | /* No '=' or ':' found on name[=:]value line */ 225 | #if INI_ALLOW_NO_VALUE 226 | *end = '\0'; 227 | name = rstrip(start); 228 | if (!HANDLER(user, section, name, NULL) && !error) 229 | error = lineno; 230 | #else 231 | error = lineno; 232 | #endif 233 | } 234 | } 235 | 236 | #if INI_STOP_ON_FIRST_ERROR 237 | if (error) 238 | break; 239 | #endif 240 | } 241 | 242 | #if !INI_USE_STACK 243 | ini_free(line); 244 | #endif 245 | 246 | return error; 247 | } 248 | 249 | /* See documentation in header file. */ 250 | int ini_parse_file(FILE* file, ini_handler handler, void* user) 251 | { 252 | return ini_parse_stream((ini_reader)fgets, file, handler, user); 253 | } 254 | 255 | /* See documentation in header file. */ 256 | int ini_parse(const char* filename, ini_handler handler, void* user) 257 | { 258 | FILE* file; 259 | int error; 260 | 261 | file = fopen(filename, "r"); 262 | if (!file) 263 | return -1; 264 | error = ini_parse_file(file, handler, user); 265 | fclose(file); 266 | return error; 267 | } 268 | 269 | /* An ini_reader function to read the next line from a string buffer. This 270 | is the fgets() equivalent used by ini_parse_string(). */ 271 | static char* ini_reader_string(char* str, int num, void* stream) { 272 | ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream; 273 | const char* ctx_ptr = ctx->ptr; 274 | size_t ctx_num_left = ctx->num_left; 275 | char* strp = str; 276 | char c; 277 | 278 | if (ctx_num_left == 0 || num < 2) 279 | return NULL; 280 | 281 | while (num > 1 && ctx_num_left != 0) { 282 | c = *ctx_ptr++; 283 | ctx_num_left--; 284 | *strp++ = c; 285 | if (c == '\n') 286 | break; 287 | num--; 288 | } 289 | 290 | *strp = '\0'; 291 | ctx->ptr = ctx_ptr; 292 | ctx->num_left = ctx_num_left; 293 | return str; 294 | } 295 | 296 | /* See documentation in header file. */ 297 | int ini_parse_string(const char* string, ini_handler handler, void* user) { 298 | ini_parse_string_ctx ctx; 299 | 300 | ctx.ptr = string; 301 | ctx.num_left = strlen(string); 302 | return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler, 303 | user); 304 | } 305 | -------------------------------------------------------------------------------- /src/ini.h: -------------------------------------------------------------------------------- 1 | /* inih -- simple .INI file parser 2 | 3 | SPDX-License-Identifier: BSD-3-Clause 4 | 5 | Copyright (C) 2009-2020, Ben Hoyt 6 | 7 | inih is released under the New BSD license (see LICENSE.txt). Go to the project 8 | home page for more info: 9 | 10 | https://github.com/benhoyt/inih 11 | 12 | */ 13 | 14 | #ifndef INI_H 15 | #define INI_H 16 | 17 | /* Make this header file easier to include in C++ code */ 18 | #ifdef __cplusplus 19 | extern "C" { 20 | #endif 21 | 22 | #include 23 | 24 | /* Nonzero if ini_handler callback should accept lineno parameter. */ 25 | #ifndef INI_HANDLER_LINENO 26 | #define INI_HANDLER_LINENO 0 27 | #endif 28 | 29 | /* Visibility symbols, required for Windows DLLs */ 30 | #ifndef INI_API 31 | #if defined _WIN32 || defined __CYGWIN__ 32 | # ifdef INI_SHARED_LIB 33 | # ifdef INI_SHARED_LIB_BUILDING 34 | # define INI_API __declspec(dllexport) 35 | # else 36 | # define INI_API __declspec(dllimport) 37 | # endif 38 | # else 39 | # define INI_API 40 | # endif 41 | #else 42 | # if defined(__GNUC__) && __GNUC__ >= 4 43 | # define INI_API __attribute__ ((visibility ("default"))) 44 | # else 45 | # define INI_API 46 | # endif 47 | #endif 48 | #endif 49 | 50 | /* Typedef for prototype of handler function. */ 51 | #if INI_HANDLER_LINENO 52 | typedef int (*ini_handler)(void* user, const char* section, 53 | const char* name, const char* value, 54 | int lineno); 55 | #else 56 | typedef int (*ini_handler)(void* user, const char* section, 57 | const char* name, const char* value); 58 | #endif 59 | 60 | /* Typedef for prototype of fgets-style reader function. */ 61 | typedef char* (*ini_reader)(char* str, int num, void* stream); 62 | 63 | /* Parse given INI-style file. May have [section]s, name=value pairs 64 | (whitespace stripped), and comments starting with ';' (semicolon). Section 65 | is "" if name=value pair parsed before any section heading. name:value 66 | pairs are also supported as a concession to Python's configparser. 67 | 68 | For each name=value pair parsed, call handler function with given user 69 | pointer as well as section, name, and value (data only valid for duration 70 | of handler call). Handler should return nonzero on success, zero on error. 71 | 72 | Returns 0 on success, line number of first error on parse error (doesn't 73 | stop on first error), -1 on file open error, or -2 on memory allocation 74 | error (only when INI_USE_STACK is zero). 75 | */ 76 | INI_API int ini_parse(const char* filename, ini_handler handler, void* user); 77 | 78 | /* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't 79 | close the file when it's finished -- the caller must do that. */ 80 | INI_API int ini_parse_file(FILE* file, ini_handler handler, void* user); 81 | 82 | /* Same as ini_parse(), but takes an ini_reader function pointer instead of 83 | filename. Used for implementing custom or string-based I/O (see also 84 | ini_parse_string). */ 85 | INI_API int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, 86 | void* user); 87 | 88 | /* Same as ini_parse(), but takes a zero-terminated string with the INI data 89 | instead of a file. Useful for parsing INI data from a network socket or 90 | already in memory. */ 91 | INI_API int ini_parse_string(const char* string, ini_handler handler, void* user); 92 | 93 | /* Nonzero to allow multi-line value parsing, in the style of Python's 94 | configparser. If allowed, ini_parse() will call the handler with the same 95 | name for each subsequent line parsed. */ 96 | #ifndef INI_ALLOW_MULTILINE 97 | #define INI_ALLOW_MULTILINE 1 98 | #endif 99 | 100 | /* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of 101 | the file. See https://github.com/benhoyt/inih/issues/21 */ 102 | #ifndef INI_ALLOW_BOM 103 | #define INI_ALLOW_BOM 1 104 | #endif 105 | 106 | /* Chars that begin a start-of-line comment. Per Python configparser, allow 107 | both ; and # comments at the start of a line by default. */ 108 | #ifndef INI_START_COMMENT_PREFIXES 109 | #define INI_START_COMMENT_PREFIXES ";#" 110 | #endif 111 | 112 | /* Nonzero to allow inline comments (with valid inline comment characters 113 | specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match 114 | Python 3.2+ configparser behaviour. */ 115 | #ifndef INI_ALLOW_INLINE_COMMENTS 116 | #define INI_ALLOW_INLINE_COMMENTS 1 117 | #endif 118 | #ifndef INI_INLINE_COMMENT_PREFIXES 119 | #define INI_INLINE_COMMENT_PREFIXES ";" 120 | #endif 121 | 122 | /* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */ 123 | #ifndef INI_USE_STACK 124 | #define INI_USE_STACK 1 125 | #endif 126 | 127 | /* Maximum line length for any line in INI file (stack or heap). Note that 128 | this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */ 129 | #ifndef INI_MAX_LINE 130 | #define INI_MAX_LINE 200 131 | #endif 132 | 133 | /* Nonzero to allow heap line buffer to grow via realloc(), zero for a 134 | fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is 135 | zero. */ 136 | #ifndef INI_ALLOW_REALLOC 137 | #define INI_ALLOW_REALLOC 0 138 | #endif 139 | 140 | /* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK 141 | is zero. */ 142 | #ifndef INI_INITIAL_ALLOC 143 | #define INI_INITIAL_ALLOC 200 144 | #endif 145 | 146 | /* Stop parsing on first error (default is to keep parsing). */ 147 | #ifndef INI_STOP_ON_FIRST_ERROR 148 | #define INI_STOP_ON_FIRST_ERROR 0 149 | #endif 150 | 151 | /* Nonzero to call the handler at the start of each new section (with 152 | name and value NULL). Default is to only call the handler on 153 | each name=value pair. */ 154 | #ifndef INI_CALL_HANDLER_ON_NEW_SECTION 155 | #define INI_CALL_HANDLER_ON_NEW_SECTION 0 156 | #endif 157 | 158 | /* Nonzero to allow a name without a value (no '=' or ':' on the line) and 159 | call the handler with value NULL in this case. Default is to treat 160 | no-value lines as an error. */ 161 | #ifndef INI_ALLOW_NO_VALUE 162 | #define INI_ALLOW_NO_VALUE 0 163 | #endif 164 | 165 | /* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory 166 | allocation functions (INI_USE_STACK must also be 0). These functions must 167 | have the same signatures as malloc/free/realloc and behave in a similar 168 | way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */ 169 | #ifndef INI_CUSTOM_ALLOCATOR 170 | #define INI_CUSTOM_ALLOCATOR 0 171 | #endif 172 | 173 | 174 | #ifdef __cplusplus 175 | } 176 | #endif 177 | 178 | #endif /* INI_H */ 179 | -------------------------------------------------------------------------------- /src/rl_driver.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * rl_driver - Simple readline caller 4 | * 5 | * Copyright 2023 Diomidis Spinellis 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | 28 | int 29 | main(int argc, char *argv[]) 30 | { 31 | char *s; 32 | 33 | using_history(); 34 | while ((s = readline("> ")) != NULL) { 35 | printf("\nRead [%s]\n", s); 36 | if (*s) 37 | add_history(s); 38 | 39 | free(s); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/support.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Safe memory allocation and other support functions. 5 | * The allocation functions exit the program with an error messge 6 | * if allocation fails. 7 | * 8 | * Copyright 2023-2024 Diomidis Spinellis 9 | * 10 | * Licensed under the Apache License, Version 2.0 (the "License"); 11 | * you may not use this file except in compliance with the License. 12 | * You may obtain a copy of the License at 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software 17 | * distributed under the License is distributed on an "AS IS" BASIS, 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | * See the License for the specific language governing permissions and 20 | * limitations under the License. 21 | */ 22 | 23 | #define _GNU_SOURCE 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #include "support.h" 38 | 39 | static FILE *logfile; 40 | CURL *acl_curl; 41 | 42 | // Exit with the specified formatted error message 43 | void 44 | acl_errorf(const char *format, ...) 45 | { 46 | fprintf(stderr, "\nai_cli: "); 47 | va_list args; 48 | va_start(args, format); 49 | vfprintf(stderr, format, args); 50 | va_end(args); 51 | fputc('\n', stderr); 52 | exit(EXIT_FAILURE); 53 | } 54 | 55 | // Exit with a failure if result_ok is false 56 | static void 57 | verify(bool result_ok) 58 | { 59 | if (!result_ok) 60 | acl_errorf("memory allocation failed."); 61 | } 62 | 63 | static void * 64 | safe_malloc(size_t size) 65 | { 66 | void *p = malloc(size); 67 | verify(p != NULL); 68 | return p; 69 | } 70 | 71 | static void * 72 | safe_realloc(void *ptr, size_t size) 73 | { 74 | void *p = realloc(ptr, size); 75 | verify(p != NULL); 76 | return p; 77 | } 78 | 79 | char * 80 | acl_safe_strdup(const char *s) 81 | { 82 | void *p = strdup(s); 83 | verify(p != NULL); 84 | return p; 85 | } 86 | 87 | // Return a dnamically allocated string in the range [begin, end) 88 | char * 89 | acl_range_strdup(const char *begin, const char *end) 90 | { 91 | void *p = strndup(begin, end - begin); 92 | verify(p != NULL); 93 | return p; 94 | } 95 | 96 | // Store the formatted output in the dynamically allocated buffer strp 97 | int 98 | acl_safe_asprintf(char **strp, const char *fmt, ...) 99 | { 100 | int result; 101 | va_list args; 102 | 103 | va_start(args, fmt); 104 | result = vasprintf(strp, fmt, args); 105 | va_end(args); 106 | 107 | verify(result != -1); 108 | return result; 109 | } 110 | 111 | // Return the cardinal number in string or -1 on error 112 | int 113 | acl_strtocard(const char *string) 114 | { 115 | if (*string == '\0') 116 | return -1; 117 | 118 | char *endptr; 119 | errno = 0; 120 | long value = strtol(string, &endptr, 10); 121 | 122 | if (errno != 0 || *endptr != '\0' || value < 0) 123 | return -1; 124 | return (int)value; 125 | } 126 | 127 | // Initialize s as the specified string 128 | void 129 | acl_string_init(string_t *s, const char *value) 130 | { 131 | 132 | s->len = strlen(value); 133 | s->ptr = safe_malloc(s->len + 1); 134 | strcpy(s->ptr, value); 135 | } 136 | 137 | // Write result data into string s 138 | size_t 139 | acl_string_write(void *data, size_t size, size_t nmemb, string_t *s) 140 | { 141 | size_t bytes = size * nmemb; 142 | size_t new_len = s->len + bytes; 143 | s->ptr = safe_realloc(s->ptr, new_len + 1); 144 | memcpy(s->ptr + s->len, data, bytes); 145 | s->ptr[new_len] = '\0'; 146 | s->len = new_len; 147 | 148 | return bytes; 149 | } 150 | 151 | // Append specified data to the string 152 | size_t 153 | acl_string_append(string_t *s, const char *data) 154 | { 155 | size_t bytes = strlen(data); 156 | size_t new_len = s->len + bytes; 157 | s->ptr = safe_realloc(s->ptr, new_len + 1); 158 | memcpy(s->ptr + s->len, data, bytes); 159 | s->ptr[new_len] = '\0'; 160 | s->len = new_len; 161 | 162 | return bytes; 163 | } 164 | 165 | // Append printf(3)-style formatted output to string 166 | int 167 | acl_string_appendf(string_t *s, const char *fmt, ...) 168 | { 169 | int result; 170 | va_list args; 171 | 172 | va_start(args, fmt); 173 | char *strp; 174 | result = vasprintf(&strp, fmt, args); 175 | va_end(args); 176 | 177 | verify(result != -1); 178 | 179 | acl_string_append(s, strp); 180 | free(strp); 181 | 182 | return result; 183 | } 184 | 185 | // Return the short name of the program being used 186 | const char * 187 | acl_short_program_name(void) 188 | { 189 | const char *name; 190 | #ifdef MACOS 191 | name = getprogname(); 192 | #else 193 | // GNU libc-specific; defined in errno.h 194 | name = program_invocation_short_name; 195 | #endif 196 | // Skip leading "-" of login shell (e.g. "-bash") 197 | if (name && name[0] == '-') 198 | return name + 1; 199 | return name; 200 | } 201 | 202 | // Show a message during readline processing 203 | int 204 | acl_readline_printf(const char *fmt, ...) 205 | { 206 | int result; 207 | va_list args; 208 | 209 | va_start(args, fmt); 210 | rl_save_prompt(); 211 | result = vprintf(fmt, args); 212 | va_end(args); 213 | rl_restore_prompt(); 214 | rl_on_new_line(); 215 | rl_redisplay(); 216 | 217 | return result; 218 | } 219 | 220 | /* 221 | * Return the string suitably escaped for JSON. 222 | * Each new call frees the previously allocated values. 223 | */ 224 | char * 225 | acl_json_escape(const char *s) 226 | { 227 | static json_t *string; 228 | static char *result; 229 | 230 | if (string) 231 | json_decref(string); 232 | if (result) 233 | free(result); 234 | 235 | string = json_string(s); 236 | result = json_dumps(string, JSON_ENCODE_ANY); 237 | return result; 238 | } 239 | 240 | // Output an ISO timestamp (with microseconds) to the specified file 241 | static void 242 | timestamp(FILE *f) 243 | { 244 | struct timeval tv; 245 | char buffer[30]; 246 | struct tm *tm_info; 247 | 248 | gettimeofday(&tv, NULL); 249 | tm_info = localtime(&tv.tv_sec); 250 | 251 | strftime(buffer, 26, "%Y-%m-%dT%H:%M:%S", tm_info); 252 | fprintf(f, "{ \"timestamp\": \"%s.%06ld\" }\n", buffer, (long)tv.tv_usec); 253 | } 254 | 255 | /* 256 | * Initialize Curl connections 257 | * Return 0 on success -1 on error 258 | */ 259 | int 260 | curl_initialize(config_t *config) 261 | { 262 | /* 263 | * Under Linux link at runtime (late binding) to minimize linking cost 264 | * (binding will only be performed by programs that use readline) 265 | * and to avoid crashes associated with seccomp filtering. 266 | * 267 | * Under Cygwin link at compile time, because late binding isn't supported. 268 | */ 269 | #if !defined(__CYGWIN__) 270 | if (!dlopen("libcurl." DLL_EXTENSION, RTLD_NOW | RTLD_GLOBAL)) { 271 | acl_readline_printf("\nError loading libcurl: %s\n", dlerror()); 272 | return -1; 273 | } 274 | if (!dlopen("libjansson." DLL_EXTENSION, RTLD_NOW | RTLD_GLOBAL)) { 275 | acl_readline_printf("\nError loading libjansson: %s\n", dlerror()); 276 | return -1; 277 | } 278 | #endif 279 | 280 | if (config->general_logfile) 281 | logfile = fopen(config->general_logfile, "a"); 282 | 283 | curl_global_init(CURL_GLOBAL_DEFAULT); 284 | 285 | acl_curl = curl_easy_init(); 286 | if (!acl_curl) 287 | acl_readline_printf("\nCURL initialization failed.\n"); 288 | return acl_curl ? 0 : -1; 289 | } 290 | 291 | // Write the specified string to the logfile, if enabled 292 | void 293 | acl_write_log(config_t *config, const char *message) 294 | { 295 | if (!logfile) 296 | return; 297 | if (config->general_timestamp) 298 | timestamp(logfile); 299 | fputs(message, logfile); 300 | fflush(logfile); 301 | } 302 | -------------------------------------------------------------------------------- /src/support.h: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Safe memory allocation and other functions. 5 | * The allocation functions exit the program with an error messge 6 | * if allocation fails. 7 | * 8 | * Copyright 2023-2024 Diomidis Spinellis 9 | * 10 | * Licensed under the Apache License, Version 2.0 (the "License"); 11 | * you may not use this file except in compliance with the License. 12 | * You may obtain a copy of the License at 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software 17 | * distributed under the License is distributed on an "AS IS" BASIS, 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | * See the License for the specific language governing permissions and 20 | * limitations under the License. 21 | */ 22 | 23 | #include 24 | #include 25 | 26 | #include "config.h" 27 | 28 | extern CURL *acl_curl; 29 | 30 | int acl_safe_asprintf(char **strp, const char *fmt, ...); 31 | char *acl_safe_strdup(const char *s); 32 | char *acl_range_strdup(const char *begin, const char *end); 33 | const char *acl_short_program_name(void); 34 | 35 | int acl_strtocard(const char *string); 36 | 37 | // Extendable string 38 | typedef struct string { 39 | char *ptr; 40 | size_t len; 41 | } string_t; 42 | 43 | 44 | char *acl_json_escape(const char *s); 45 | int acl_readline_printf(const char *fmt, ...); 46 | void acl_string_init(string_t *s, const char *value); 47 | size_t acl_string_write(void *data, size_t size, size_t nmemb, string_t *s); 48 | size_t acl_string_append(string_t *s, const char *data); 49 | int acl_string_appendf(string_t *s, const char *fmt, ...); 50 | int curl_initialize(config_t *config); 51 | void acl_write_log(config_t *config, const char *message); 52 | void acl_errorf(const char *format, ...); 53 | -------------------------------------------------------------------------------- /src/support_test.c: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * Safe memory allocation and other functions. 5 | * 6 | * Copyright 2023-2024 Diomidis Spinellis 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | #include 22 | 23 | #include "CuTest.h" 24 | #include "support.h" 25 | 26 | void 27 | test_strtocard(CuTest* tc) 28 | { 29 | CuAssertIntEquals(tc, 12, acl_strtocard("12")); 30 | CuAssertIntEquals(tc, -1, acl_strtocard("")); 31 | CuAssertIntEquals(tc, -1, acl_strtocard("x")); 32 | CuAssertIntEquals(tc, -1, acl_strtocard("3x")); 33 | CuAssertIntEquals(tc, -1, acl_strtocard("-3")); 34 | } 35 | 36 | void 37 | test_asprintf(CuTest* tc) 38 | { 39 | char *result; 40 | acl_safe_asprintf(&result, "a=%d", 42); 41 | CuAssertStrEquals(tc, "a=42", result); 42 | free(result); 43 | } 44 | 45 | void 46 | test_range_strdup(CuTest* tc) 47 | { 48 | char string[] = "01234"; 49 | char *result; 50 | result = acl_range_strdup(string + 1, string + 3); 51 | CuAssertStrEquals(tc, "12", result); 52 | free(result); 53 | } 54 | 55 | void 56 | test_string(CuTest* tc) 57 | { 58 | string_t s; 59 | acl_string_init(&s, "hello"); 60 | acl_string_write(", ", 1, 2, &s); 61 | acl_string_append(&s, "world!"); 62 | CuAssertStrEquals(tc, "hello, world!", s.ptr); 63 | 64 | acl_string_appendf(&s, " The answer is %d.", 42); 65 | CuAssertStrEquals(tc, "hello, world! The answer is 42.", s.ptr); 66 | } 67 | 68 | void 69 | test_short_program_name(CuTest* tc) 70 | { 71 | CuAssertStrEquals(tc, "all-tests", acl_short_program_name()); 72 | } 73 | 74 | void 75 | test_json_escape(CuTest* tc) 76 | { 77 | CuAssertStrEquals(tc, "\"a \\\" (quote) and a \\\\ (backslash)\"", acl_json_escape("a \" (quote) and a \\ (backslash)")); 78 | } 79 | 80 | 81 | CuSuite* 82 | cu_support_suite(void) 83 | { 84 | CuSuite* suite = CuSuiteNew(); 85 | 86 | SUITE_ADD_TEST(suite, test_strtocard); 87 | SUITE_ADD_TEST(suite, test_asprintf); 88 | SUITE_ADD_TEST(suite, test_string); 89 | SUITE_ADD_TEST(suite, test_short_program_name); 90 | SUITE_ADD_TEST(suite, test_json_escape); 91 | SUITE_ADD_TEST(suite, test_range_strdup); 92 | 93 | return suite; 94 | } 95 | -------------------------------------------------------------------------------- /src/unit_test.h: -------------------------------------------------------------------------------- 1 | /*- 2 | * 3 | * ai-cli - readline wrapper to obtain a generative AI suggestion 4 | * 5 | * Definition of STATIC to allow unit testing 6 | * 7 | * Copyright 2024 Diomidis Spinellis 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | */ 21 | 22 | // Used for declarations that should be static but aren't for unit testing 23 | #if defined(UNIT_TEST) 24 | #define STATIC 25 | #else 26 | #define STATIC static 27 | #endif 28 | --------------------------------------------------------------------------------