├── .gitignore ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── bin └── nb ├── components ├── adapter.rb ├── crypto.rb ├── embedding.rb ├── provider.rb ├── providers │ ├── anthropic.rb │ ├── base.rb │ ├── cohere.rb │ ├── google.rb │ ├── maritaca.rb │ ├── mistral.rb │ ├── ollama.rb │ ├── openai.rb │ └── tools.rb ├── storage.rb └── stream.rb ├── controllers ├── cartridges.rb ├── instance.rb ├── interfaces │ ├── cli.rb │ ├── eval.rb │ ├── repl.rb │ └── tools.rb ├── security.rb └── session.rb ├── docker-compose.example.yml ├── logic ├── cartridge │ ├── adapters.rb │ ├── affixes.rb │ ├── default.rb │ ├── fetch.rb │ ├── interaction.rb │ ├── parser.rb │ ├── safety.rb │ ├── streaming.rb │ └── tools.rb ├── helpers │ └── hash.rb └── providers │ ├── anthropic │ └── tokens.rb │ ├── cohere │ └── tokens.rb │ ├── google │ ├── tokens.rb │ └── tools.rb │ ├── maritaca │ └── tokens.rb │ ├── mistral │ └── tokens.rb │ ├── ollama │ └── tokens.rb │ ├── openai.rb │ └── openai │ ├── tokens.rb │ └── tools.rb ├── nano-bots.gemspec ├── ports └── dsl │ ├── nano-bots.rb │ └── nano-bots │ ├── cartridges.rb │ └── cli.rb ├── spec ├── components │ └── storage_spec.rb ├── data │ ├── cartridges │ │ ├── affixes.yml │ │ ├── block.md │ │ ├── markdown.md │ │ ├── meta.md │ │ ├── models │ │ │ ├── anthropic │ │ │ │ ├── claude-3-5-sonnet.yml │ │ │ │ ├── claude-3-haiku.yml │ │ │ │ ├── claude-3-opus.yml │ │ │ │ └── claude-3-sonnet.yml │ │ │ ├── cohere │ │ │ │ ├── command-light.yml │ │ │ │ ├── command-r-plus.yml │ │ │ │ ├── command-r.yml │ │ │ │ └── command.yml │ │ │ ├── google │ │ │ │ ├── gemini-1-0-pro.yml │ │ │ │ ├── gemini-1-5-flash.yml │ │ │ │ ├── gemini-1-5-pro.yml │ │ │ │ └── gemini-pro-gen-lang-api.yml │ │ │ ├── maritaca │ │ │ │ ├── sabia-2-medium.yml │ │ │ │ └── sabia-2-small.yml │ │ │ ├── mistral │ │ │ │ ├── large.yml │ │ │ │ ├── medium.yml │ │ │ │ └── small.yml │ │ │ ├── ollama │ │ │ │ └── phi-3.yml │ │ │ └── openai │ │ │ │ ├── gpt-3-5-turbo.yml │ │ │ │ ├── gpt-4-turbo.yml │ │ │ │ └── gpt-4o.yml │ │ ├── streaming.yml │ │ ├── tools.md │ │ └── tools.yml │ └── providers │ │ ├── google │ │ └── tools.yml │ │ └── openai │ │ └── tools.yml ├── logic │ ├── cartridge │ │ ├── affixes_spec.rb │ │ ├── interaction_spec.rb │ │ ├── parser_spec.rb │ │ ├── streaming_spec.rb │ │ └── tools_spec.rb │ ├── helpers │ │ └── hash_spec.rb │ └── providers │ │ ├── google │ │ └── tools_spec.rb │ │ └── openai │ │ └── tools_spec.rb ├── spec_helper.rb └── tasks │ ├── run-all-models.rb │ └── run-model.rb └── static ├── cartridges ├── baseline.yml └── default.yml ├── fennel ├── LICENSE └── fennel.lua └── gem.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.gem 3 | docker-compose.yml 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 3.1.0 3 | NewCops: enable 4 | 5 | Style/Documentation: 6 | Enabled: false 7 | 8 | require: 9 | - rubocop-rspec 10 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.0 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :test, :development do 8 | gem 'pry-byebug', '~> 3.10', '>= 3.10.1' 9 | gem 'rspec', '~> 3.13' 10 | gem 'rubocop', '~> 1.64', '>= 1.64.1' 11 | gem 'rubocop-rspec', '~> 3.0', '>= 3.0.1' 12 | end 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | nano-bots (3.4.0) 5 | anthropic (~> 0.3.0) 6 | babosa (~> 2.0) 7 | cohere-ai (~> 1.1) 8 | concurrent-ruby (~> 1.3, >= 1.3.3) 9 | dotenv (~> 3.1, >= 3.1.2) 10 | faraday (~> 2.9, >= 2.9.2) 11 | faraday-typhoeus (~> 1.1) 12 | gemini-ai (~> 4.1) 13 | maritaca-ai (~> 1.2) 14 | mistral-ai (~> 1.2) 15 | ollama-ai (~> 1.2, >= 1.2.1) 16 | pry (~> 0.14.2) 17 | rainbow (~> 3.1, >= 3.1.1) 18 | rbnacl (~> 7.1, >= 7.1.1) 19 | redcarpet (~> 3.6) 20 | ruby-openai (~> 7.1) 21 | sweet-moon (~> 1.0) 22 | typhoeus (~> 1.4, >= 1.4.1) 23 | 24 | GEM 25 | remote: https://rubygems.org/ 26 | specs: 27 | addressable (2.8.7) 28 | public_suffix (>= 2.0.2, < 7.0) 29 | anthropic (0.3.0) 30 | event_stream_parser (>= 0.3.0, < 2.0.0) 31 | faraday (>= 1) 32 | faraday-multipart (>= 1) 33 | ast (2.4.2) 34 | babosa (2.0.0) 35 | base64 (0.2.0) 36 | byebug (11.1.3) 37 | coderay (1.1.3) 38 | cohere-ai (1.1.0) 39 | faraday (~> 2.9) 40 | faraday-typhoeus (~> 1.1) 41 | concurrent-ruby (1.3.3) 42 | diff-lcs (1.5.1) 43 | dotenv (3.1.2) 44 | ethon (0.16.0) 45 | ffi (>= 1.15.0) 46 | event_stream_parser (1.0.0) 47 | faraday (2.9.2) 48 | faraday-net_http (>= 2.0, < 3.2) 49 | faraday-multipart (1.0.4) 50 | multipart-post (~> 2) 51 | faraday-net_http (3.1.0) 52 | net-http 53 | faraday-typhoeus (1.1.0) 54 | faraday (~> 2.0) 55 | typhoeus (~> 1.4) 56 | ffi (1.17.0) 57 | gemini-ai (4.1.0) 58 | event_stream_parser (~> 1.0) 59 | faraday (~> 2.9, >= 2.9.2) 60 | faraday-typhoeus (~> 1.1) 61 | googleauth (~> 1.8) 62 | typhoeus (~> 1.4, >= 1.4.1) 63 | google-cloud-env (2.1.1) 64 | faraday (>= 1.0, < 3.a) 65 | googleauth (1.11.0) 66 | faraday (>= 1.0, < 3.a) 67 | google-cloud-env (~> 2.1) 68 | jwt (>= 1.4, < 3.0) 69 | multi_json (~> 1.11) 70 | os (>= 0.9, < 2.0) 71 | signet (>= 0.16, < 2.a) 72 | json (2.7.2) 73 | jwt (2.8.2) 74 | base64 75 | language_server-protocol (3.17.0.3) 76 | maritaca-ai (1.2.0) 77 | event_stream_parser (~> 1.0) 78 | faraday (~> 2.9) 79 | faraday-typhoeus (~> 1.1) 80 | method_source (1.1.0) 81 | mistral-ai (1.2.0) 82 | event_stream_parser (~> 1.0) 83 | faraday (~> 2.9) 84 | faraday-typhoeus (~> 1.1) 85 | multi_json (1.15.0) 86 | multipart-post (2.4.1) 87 | net-http (0.4.1) 88 | uri 89 | ollama-ai (1.2.1) 90 | faraday (~> 2.9) 91 | faraday-typhoeus (~> 1.1) 92 | os (1.1.4) 93 | parallel (1.25.1) 94 | parser (3.3.3.0) 95 | ast (~> 2.4.1) 96 | racc 97 | pry (0.14.2) 98 | coderay (~> 1.1) 99 | method_source (~> 1.0) 100 | pry-byebug (3.10.1) 101 | byebug (~> 11.0) 102 | pry (>= 0.13, < 0.15) 103 | public_suffix (6.0.0) 104 | racc (1.8.0) 105 | rainbow (3.1.1) 106 | rbnacl (7.1.1) 107 | ffi 108 | redcarpet (3.6.0) 109 | regexp_parser (2.9.2) 110 | rexml (3.3.0) 111 | strscan 112 | rspec (3.13.0) 113 | rspec-core (~> 3.13.0) 114 | rspec-expectations (~> 3.13.0) 115 | rspec-mocks (~> 3.13.0) 116 | rspec-core (3.13.0) 117 | rspec-support (~> 3.13.0) 118 | rspec-expectations (3.13.1) 119 | diff-lcs (>= 1.2.0, < 2.0) 120 | rspec-support (~> 3.13.0) 121 | rspec-mocks (3.13.1) 122 | diff-lcs (>= 1.2.0, < 2.0) 123 | rspec-support (~> 3.13.0) 124 | rspec-support (3.13.1) 125 | rubocop (1.64.1) 126 | json (~> 2.3) 127 | language_server-protocol (>= 3.17.0) 128 | parallel (~> 1.10) 129 | parser (>= 3.3.0.2) 130 | rainbow (>= 2.2.2, < 4.0) 131 | regexp_parser (>= 1.8, < 3.0) 132 | rexml (>= 3.2.5, < 4.0) 133 | rubocop-ast (>= 1.31.1, < 2.0) 134 | ruby-progressbar (~> 1.7) 135 | unicode-display_width (>= 2.4.0, < 3.0) 136 | rubocop-ast (1.31.3) 137 | parser (>= 3.3.1.0) 138 | rubocop-rspec (3.0.1) 139 | rubocop (~> 1.61) 140 | ruby-openai (7.1.0) 141 | event_stream_parser (>= 0.3.0, < 2.0.0) 142 | faraday (>= 1) 143 | faraday-multipart (>= 1) 144 | ruby-progressbar (1.13.0) 145 | signet (0.19.0) 146 | addressable (~> 2.8) 147 | faraday (>= 0.17.5, < 3.a) 148 | jwt (>= 1.5, < 3.0) 149 | multi_json (~> 1.10) 150 | strscan (3.1.0) 151 | sweet-moon (1.0.0) 152 | ffi (~> 1.17) 153 | typhoeus (1.4.1) 154 | ethon (>= 0.9.0) 155 | unicode-display_width (2.5.0) 156 | uri (0.13.0) 157 | 158 | PLATFORMS 159 | x86_64-linux 160 | 161 | DEPENDENCIES 162 | nano-bots! 163 | pry-byebug (~> 3.10, >= 3.10.1) 164 | rspec (~> 3.13) 165 | rubocop (~> 1.64, >= 1.64.1) 166 | rubocop-rspec (~> 3.0, >= 3.0.1) 167 | 168 | BUNDLED WITH 169 | 2.4.22 170 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 icebaker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nano Bots 💎 🤖 2 | 3 | An implementation of the [Nano Bots](https://spec.nbots.io) specification with support for [Anthropic Claude](https://www.anthropic.com/claude), [Cohere Command](https://cohere.com), [Google Gemini](https://deepmind.google/technologies/gemini), [Maritaca AI Sabiá](https://www.maritaca.ai), [Mistral AI](https://mistral.ai), [Ollama](https://ollama.ai), [OpenAI ChatGPT](https://openai.com/chatgpt), and others, with support for calling tools (functions). 4 | 5 | ![Ruby Nano Bots](https://raw.githubusercontent.com/icebaker/assets/main/nano-bots/ruby-nano-bots-canvas.png) 6 | 7 | https://user-images.githubusercontent.com/113217272/238141567-c58a240c-7b67-4b3b-864a-0f49bbf6e22f.mp4 8 | 9 | ## TL;DR and Quick Start 10 | 11 | ```sh 12 | gem install nano-bots -v 3.4.0 13 | ``` 14 | 15 | ```bash 16 | nb - - eval "hello" 17 | # => Hello! How may I assist you today? 18 | ``` 19 | 20 | ```bash 21 | nb - - repl 22 | ``` 23 | 24 | ```text 25 | 🤖> Hi, how are you doing? 26 | 27 | As an AI language model, I do not experience emotions but I am functioning 28 | well. How can I assist you? 29 | 30 | 🤖> | 31 | ``` 32 | 33 | ```yaml 34 | --- 35 | meta: 36 | symbol: 🤖 37 | name: Nano Bot Name 38 | author: Your Name 39 | version: 1.0.0 40 | license: CC0-1.0 41 | description: A helpful assistant. 42 | 43 | behaviors: 44 | interaction: 45 | directive: You are a helpful assistant. 46 | 47 | provider: 48 | id: openai 49 | credentials: 50 | access-token: ENV/OPENAI_API_KEY 51 | settings: 52 | user: ENV/NANO_BOTS_END_USER 53 | model: gpt-4o 54 | ``` 55 | 56 | ```bash 57 | nb gpt.yml - eval "hi" 58 | # => Hello! How can I assist you today? 59 | ``` 60 | 61 | ```ruby 62 | gem 'nano-bots', '~> 3.4.0' 63 | ``` 64 | 65 | ```ruby 66 | require 'nano-bots' 67 | 68 | bot = NanoBot.new(cartridge: 'gpt.yml') 69 | 70 | bot.eval('Hi!') do |content, fragment, finished, meta| 71 | print fragment unless fragment.nil? 72 | end 73 | 74 | # => Hello! How can I assist you today? 75 | ``` 76 | 77 | - [TL;DR and Quick Start](#tldr-and-quick-start) 78 | - [Usage](#usage) 79 | - [Command Line](#command-line) 80 | - [Debugging](#debugging) 81 | - [Library](#library) 82 | - [Setup](#setup) 83 | - [Anthropic Claude](#anthropic-claude) 84 | - [Cohere Command](#cohere-command) 85 | - [Maritaca AI MariTalk](#maritaca-ai-maritalk) 86 | - [Mistral AI](#mistral-ai) 87 | - [Ollama](#ollama) 88 | - [OpenAI ChatGPT](#openai-chatgpt) 89 | - [Google Gemini](#google-gemini) 90 | - [Option 1: API Key (Generative Language API)](#option-1-api-key-generative-language-api) 91 | - [Option 2: Service Account Credentials File (Vertex AI API)](#option-2-service-account-credentials-file-vertex-ai-api) 92 | - [Option 3: Application Default Credentials (Vertex AI API)](#option-3-application-default-credentials-vertex-ai-api) 93 | - [Custom Project ID](#custom-project-id) 94 | - [Cartridges](#cartridges) 95 | - [Tools (Functions)](#tools-functions) 96 | - [Experimental Clojure Support](#experimental-clojure-support) 97 | - [Marketplace](#marketplace) 98 | - [Security and Privacy](#security-and-privacy) 99 | - [Cryptography](#cryptography) 100 | - [End-user IDs](#end-user-ids) 101 | - [Decrypting](#decrypting) 102 | - [Supported Providers](#supported-providers) 103 | - [Docker](#docker) 104 | - [Anthropic Claude Container](#anthropic-claude-container) 105 | - [Cohere Command Container](#cohere-command-container) 106 | - [Maritaca AI MariTalk Container](#maritaca-ai-maritalk-container) 107 | - [Mistral AI Container](#mistral-ai-container) 108 | - [Ollama Container](#ollama-container) 109 | - [OpenAI ChatGPT Container](#openai-chatgpt-container) 110 | - [Google Gemini Container](#google-gemini-container) 111 | - [Option 1: API Key (Generative Language API) Config](#option-1-api-key-generative-language-api-config) 112 | - [Option 2: Service Account Credentials File (Vertex AI API) Config](#option-2-service-account-credentials-file-vertex-ai-api-config) 113 | - [Option 3: Application Default Credentials (Vertex AI API) Config](#option-3-application-default-credentials-vertex-ai-api-config) 114 | - [Custom Project ID Config](#custom-project-id-config) 115 | - [Running the Container](#running-the-container) 116 | - [Development](#development) 117 | - [Publish to RubyGems](#publish-to-rubygems) 118 | 119 | ## Usage 120 | 121 | ### Command Line 122 | 123 | After installing the gem, the `nb` binary command will be available for your project or system. 124 | 125 | Examples of usage: 126 | 127 | ```bash 128 | nb - - eval "hello" 129 | # => Hello! How may I assist you today? 130 | 131 | nb to-en-us-translator.yml - eval "Salut, comment ça va?" 132 | # => Hello, how are you doing? 133 | 134 | nb midjourney.yml - eval "happy cyberpunk robot" 135 | # => A cheerful and fun-loving robot is dancing wildly amidst a 136 | # futuristic and lively cityscape. Holographic advertisements 137 | # and vibrant neon colors can be seen in the background. 138 | 139 | nb lisp.yml - eval "(+ 1 2)" 140 | # => 3 141 | 142 | cat article.txt | 143 | nb to-en-us-translator.yml - eval | 144 | nb summarizer.yml - eval 145 | # -> LLM stands for Large Language Model, which refers to an 146 | # artificial intelligence algorithm capable of processing 147 | # and understanding vast amounts of natural language data, 148 | # allowing it to generate human-like responses and perform 149 | # a range of language-related tasks. 150 | ``` 151 | 152 | ```bash 153 | nb - - repl 154 | 155 | nb assistant.yml - repl 156 | ``` 157 | 158 | ```text 159 | 🤖> Hi, how are you doing? 160 | 161 | As an AI language model, I do not experience emotions but I am functioning 162 | well. How can I assist you? 163 | 164 | 🤖> | 165 | ``` 166 | 167 | You can exit the REPL by typing `exit`. 168 | 169 | All of the commands above are stateless. If you want to preserve the history of your interactions, replace the `-` with a state key: 170 | 171 | ```bash 172 | nb assistant.yml your-user eval "Salut, comment ça va?" 173 | nb assistant.yml your-user repl 174 | 175 | nb assistant.yml 6ea6c43c42a1c076b1e3c36fa349ac2c eval "Salut, comment ça va?" 176 | nb assistant.yml 6ea6c43c42a1c076b1e3c36fa349ac2c repl 177 | ``` 178 | 179 | You can use a simple key, such as your username, or a randomly generated one: 180 | 181 | ```ruby 182 | require 'securerandom' 183 | 184 | SecureRandom.hex # => 6ea6c43c42a1c076b1e3c36fa349ac2c 185 | ``` 186 | 187 | ### Debugging 188 | 189 | ```sh 190 | nb - - cartridge 191 | nb cartridge.yml - cartridge 192 | 193 | nb - STATE-KEY state 194 | nb cartridge.yml STATE-KEY state 195 | ``` 196 | 197 | ### Library 198 | 199 | To use it as a library: 200 | 201 | ```ruby 202 | require 'nano-bots/cli' # Equivalent to the `nb` command. 203 | ``` 204 | 205 | ```ruby 206 | require 'nano-bots' 207 | 208 | NanoBot.cli # Equivalent to the `nb` command. 209 | 210 | NanoBot.repl(cartridge: 'cartridge.yml') # Starts a new REPL. 211 | 212 | bot = NanoBot.new(cartridge: 'cartridge.yml') 213 | 214 | bot = NanoBot.new( 215 | cartridge: YAML.safe_load(File.read('cartridge.yml'), permitted_classes: [Symbol]) 216 | ) 217 | 218 | bot = NanoBot.new( 219 | cartridge: { ... } # Parsed Cartridge Hash 220 | ) 221 | 222 | bot.eval('Hello') 223 | 224 | bot.eval('Hello', as: 'eval') 225 | bot.eval('Hello', as: 'repl') 226 | 227 | # When stream is enabled and available: 228 | bot.eval('Hi!') do |content, fragment, finished, meta| 229 | print fragment unless fragment.nil? 230 | end 231 | 232 | bot.repl # Starts a new REPL. 233 | 234 | NanoBot.repl(cartridge: 'cartridge.yml', state: '6ea6c43c42a1c076b1e3c36fa349ac2c') 235 | 236 | bot = NanoBot.new(cartridge: 'cartridge.yml', state: '6ea6c43c42a1c076b1e3c36fa349ac2c') 237 | 238 | bot.prompt # => "🤖\u001b[34m> \u001b[0m" 239 | 240 | bot.boot 241 | 242 | bot.boot(as: 'eval') 243 | bot.boot(as: 'repl') 244 | 245 | bot.boot do |content, fragment, finished, meta| 246 | print fragment unless fragment.nil? 247 | end 248 | ``` 249 | 250 | ## Setup 251 | 252 | To install the CLI on your system: 253 | 254 | ```sh 255 | gem install nano-bots -v 3.4.0 256 | ``` 257 | 258 | To use it in a Ruby project as a library, add to your `Gemfile`: 259 | 260 | ```ruby 261 | gem 'nano-bots', '~> 3.4.0' 262 | ``` 263 | 264 | ```sh 265 | bundle install 266 | ``` 267 | 268 | For credentials and configurations, relevant environment variables can be set in your `.bashrc`, `.zshrc`, or equivalent files, as well as in your Docker Container or System Environment. Example: 269 | 270 | ```sh 271 | export NANO_BOTS_ENCRYPTION_PASSWORD=UNSAFE 272 | export NANO_BOTS_END_USER=your-user 273 | 274 | # export NANO_BOTS_STATE_PATH=/home/user/.local/state/nano-bots 275 | # export NANO_BOTS_CARTRIDGES_PATH=/home/user/.local/share/nano-bots/cartridges 276 | ``` 277 | 278 | Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded: 279 | 280 | ```sh 281 | NANO_BOTS_ENCRYPTION_PASSWORD=UNSAFE 282 | NANO_BOTS_END_USER=your-user 283 | 284 | # NANO_BOTS_STATE_PATH=/home/user/.local/state/nano-bots 285 | # NANO_BOTS_CARTRIDGES_PATH=/home/user/.local/share/nano-bots/cartridges 286 | ``` 287 | 288 | ### Anthropic Claude 289 | 290 | You can obtain your credentials on the [Anthropic Console](https://console.anthropic.com). 291 | 292 | ```sh 293 | export ANTHROPIC_API_KEY=your-api-key 294 | ``` 295 | 296 | Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded: 297 | 298 | ```sh 299 | ANTHROPIC_API_KEY=your-api-key 300 | ``` 301 | 302 | Create a `cartridge.yml` file: 303 | 304 | ```yaml 305 | --- 306 | meta: 307 | symbol: 🤖 308 | name: Nano Bot Name 309 | author: Your Name 310 | version: 1.0.0 311 | license: CC0-1.0 312 | description: A helpful assistant. 313 | 314 | behaviors: 315 | interaction: 316 | directive: You are a helpful assistant. 317 | 318 | provider: 319 | id: anthropic 320 | credentials: 321 | api-key: ENV/ANTHROPIC_API_KEY 322 | settings: 323 | model: claude-3-5-sonnet-20240620 324 | max_tokens: 4096 325 | ``` 326 | 327 | Read the [full specification](https://spec.nbots.io/#/README?id=anthropic-claude) for Anthropic Claude. 328 | 329 | ```bash 330 | nb cartridge.yml - eval "Hello" 331 | 332 | nb cartridge.yml - repl 333 | ``` 334 | 335 | ```ruby 336 | bot = NanoBot.new(cartridge: 'cartridge.yml') 337 | 338 | puts bot.eval('Hello') 339 | ``` 340 | 341 | ### Cohere Command 342 | 343 | You can obtain your credentials on the [Cohere Platform](https://dashboard.cohere.com). 344 | 345 | ```sh 346 | export COHERE_API_KEY=your-api-key 347 | ``` 348 | 349 | Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded: 350 | 351 | ```sh 352 | COHERE_API_KEY=your-api-key 353 | ``` 354 | 355 | Create a `cartridge.yml` file: 356 | 357 | ```yaml 358 | --- 359 | meta: 360 | symbol: 🤖 361 | name: Nano Bot Name 362 | author: Your Name 363 | version: 1.0.0 364 | license: CC0-1.0 365 | description: A helpful assistant. 366 | 367 | behaviors: 368 | interaction: 369 | directive: You are a helpful assistant. 370 | 371 | provider: 372 | id: cohere 373 | credentials: 374 | api-key: ENV/COHERE_API_KEY 375 | settings: 376 | model: command 377 | ``` 378 | 379 | Read the [full specification](https://spec.nbots.io/#/README?id=cohere-command) for Cohere Command. 380 | 381 | ```bash 382 | nb cartridge.yml - eval "Hello" 383 | 384 | nb cartridge.yml - repl 385 | ``` 386 | 387 | ```ruby 388 | bot = NanoBot.new(cartridge: 'cartridge.yml') 389 | 390 | puts bot.eval('Hello') 391 | ``` 392 | 393 | ### Maritaca AI MariTalk 394 | 395 | You can obtain your API key at [MariTalk](https://chat.maritaca.ai). 396 | 397 | Enclose credentials in single quotes when using environment variables to prevent issues with the $ character in the API key: 398 | 399 | ```sh 400 | export MARITACA_API_KEY='123...$a12...' 401 | ``` 402 | 403 | Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded: 404 | 405 | ```sh 406 | MARITACA_API_KEY='123...$a12...' 407 | ``` 408 | 409 | Create a `cartridge.yml` file: 410 | 411 | ```yaml 412 | --- 413 | meta: 414 | symbol: 🤖 415 | name: Nano Bot Name 416 | author: Your Name 417 | version: 1.0.0 418 | license: CC0-1.0 419 | description: A helpful assistant. 420 | 421 | behaviors: 422 | interaction: 423 | directive: You are a helpful assistant. 424 | 425 | provider: 426 | id: maritaca 427 | credentials: 428 | api-key: ENV/MARITACA_API_KEY 429 | settings: 430 | model: sabia-2-medium 431 | ``` 432 | 433 | Read the [full specification](https://spec.nbots.io/#/README?id=mistral-ai) for Mistral AI. 434 | 435 | ```bash 436 | nb cartridge.yml - eval "Hello" 437 | 438 | nb cartridge.yml - repl 439 | ``` 440 | 441 | ```ruby 442 | bot = NanoBot.new(cartridge: 'cartridge.yml') 443 | 444 | puts bot.eval('Hello') 445 | ``` 446 | 447 | ### Mistral AI 448 | 449 | You can obtain your credentials on the [Mistral Platform](https://console.mistral.ai). 450 | 451 | ```sh 452 | export MISTRAL_API_KEY=your-api-key 453 | ``` 454 | 455 | Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded: 456 | 457 | ```sh 458 | MISTRAL_API_KEY=your-api-key 459 | ``` 460 | 461 | Create a `cartridge.yml` file: 462 | 463 | ```yaml 464 | --- 465 | meta: 466 | symbol: 🤖 467 | name: Nano Bot Name 468 | author: Your Name 469 | version: 1.0.0 470 | license: CC0-1.0 471 | description: A helpful assistant. 472 | 473 | behaviors: 474 | interaction: 475 | directive: You are a helpful assistant. 476 | 477 | provider: 478 | id: mistral 479 | credentials: 480 | api-key: ENV/MISTRAL_API_KEY 481 | settings: 482 | model: mistral-medium-latest 483 | ``` 484 | 485 | Read the [full specification](https://spec.nbots.io/#/README?id=mistral-ai) for Mistral AI. 486 | 487 | ```bash 488 | nb cartridge.yml - eval "Hello" 489 | 490 | nb cartridge.yml - repl 491 | ``` 492 | 493 | ```ruby 494 | bot = NanoBot.new(cartridge: 'cartridge.yml') 495 | 496 | puts bot.eval('Hello') 497 | ``` 498 | 499 | ### Ollama 500 | 501 | To install and set up, follow the instructions on the [Ollama](https://ollama.ai) website. 502 | 503 | ```sh 504 | export OLLAMA_API_ADDRESS=http://localhost:11434 505 | ``` 506 | 507 | Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded: 508 | 509 | ```sh 510 | OLLAMA_API_ADDRESS=http://localhost:11434 511 | ``` 512 | 513 | Create a `cartridge.yml` file: 514 | 515 | ```yaml 516 | --- 517 | meta: 518 | symbol: 🤖 519 | name: Nano Bot Name 520 | author: Your Name 521 | version: 1.0.0 522 | license: CC0-1.0 523 | description: A helpful assistant. 524 | 525 | behaviors: 526 | interaction: 527 | directive: You are a helpful assistant. 528 | 529 | provider: 530 | id: ollama 531 | credentials: 532 | address: ENV/OLLAMA_API_ADDRESS 533 | settings: 534 | model: llama3 535 | ``` 536 | 537 | Read the [full specification](https://spec.nbots.io/#/README?id=ollama) for Ollama. 538 | 539 | ```bash 540 | nb cartridge.yml - eval "Hello" 541 | 542 | nb cartridge.yml - repl 543 | ``` 544 | 545 | ```ruby 546 | bot = NanoBot.new(cartridge: 'cartridge.yml') 547 | 548 | puts bot.eval('Hello') 549 | ``` 550 | 551 | ### OpenAI ChatGPT 552 | 553 | You can obtain your credentials on the [OpenAI Platform](https://platform.openai.com). 554 | 555 | ```sh 556 | export OPENAI_API_KEY=your-access-token 557 | ``` 558 | 559 | Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded: 560 | 561 | ```sh 562 | OPENAI_API_KEY=your-access-token 563 | ``` 564 | 565 | Create a `cartridge.yml` file: 566 | 567 | ```yaml 568 | --- 569 | meta: 570 | symbol: 🤖 571 | name: Nano Bot Name 572 | author: Your Name 573 | version: 1.0.0 574 | license: CC0-1.0 575 | description: A helpful assistant. 576 | 577 | behaviors: 578 | interaction: 579 | directive: You are a helpful assistant. 580 | 581 | provider: 582 | id: openai 583 | credentials: 584 | access-token: ENV/OPENAI_API_KEY 585 | settings: 586 | user: ENV/NANO_BOTS_END_USER 587 | model: gpt-4o 588 | ``` 589 | 590 | Read the [full specification](https://spec.nbots.io/#/README?id=openai-chatgpt) for OpenAI ChatGPT. 591 | 592 | ```bash 593 | nb cartridge.yml - eval "Hello" 594 | 595 | nb cartridge.yml - repl 596 | ``` 597 | 598 | ```ruby 599 | bot = NanoBot.new(cartridge: 'cartridge.yml') 600 | 601 | puts bot.eval('Hello') 602 | ``` 603 | 604 | ### Google Gemini 605 | 606 | Click [here](https://github.com/gbaptista/gemini-ai#credentials) to learn how to obtain your credentials. 607 | 608 | #### Option 1: API Key (Generative Language API) 609 | 610 | ```sh 611 | export GOOGLE_API_KEY=your-api-key 612 | ``` 613 | 614 | Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded: 615 | 616 | ```sh 617 | GOOGLE_API_KEY=your-api-key 618 | ``` 619 | 620 | Create a `cartridge.yml` file: 621 | 622 | ```yaml 623 | --- 624 | meta: 625 | symbol: 🤖 626 | name: Nano Bot Name 627 | author: Your Name 628 | version: 1.0.0 629 | license: CC0-1.0 630 | description: A helpful assistant. 631 | 632 | behaviors: 633 | interaction: 634 | directive: You are a helpful assistant. 635 | 636 | provider: 637 | id: google 638 | credentials: 639 | service: generative-language-api 640 | api-key: ENV/GOOGLE_API_KEY 641 | options: 642 | model: gemini-pro 643 | ``` 644 | 645 | Read the [full specification](https://spec.nbots.io/#/README?id=google-gemini) for Google Gemini. 646 | 647 | ```bash 648 | nb cartridge.yml - eval "Hello" 649 | 650 | nb cartridge.yml - repl 651 | ``` 652 | 653 | ```ruby 654 | bot = NanoBot.new(cartridge: 'cartridge.yml') 655 | 656 | puts bot.eval('Hello') 657 | ``` 658 | 659 | #### Option 2: Service Account Credentials File (Vertex AI API) 660 | 661 | ```sh 662 | export GOOGLE_CREDENTIALS_FILE_PATH=google-credentials.json 663 | export GOOGLE_REGION=us-east4 664 | ``` 665 | 666 | Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded: 667 | 668 | ```sh 669 | GOOGLE_CREDENTIALS_FILE_PATH=google-credentials.json 670 | GOOGLE_REGION=us-east4 671 | ``` 672 | 673 | Create a `cartridge.yml` file: 674 | 675 | ```yaml 676 | --- 677 | meta: 678 | symbol: 🤖 679 | name: Nano Bot Name 680 | author: Your Name 681 | version: 1.0.0 682 | license: CC0-1.0 683 | description: A helpful assistant. 684 | 685 | behaviors: 686 | interaction: 687 | directive: You are a helpful assistant. 688 | 689 | provider: 690 | id: google 691 | credentials: 692 | service: vertex-ai-api 693 | file-path: ENV/GOOGLE_CREDENTIALS_FILE_PATH 694 | region: ENV/GOOGLE_REGION 695 | options: 696 | model: gemini-pro 697 | ``` 698 | 699 | Read the [full specification](https://spec.nbots.io/#/README?id=google-gemini) for Google Gemini. 700 | 701 | ```bash 702 | nb cartridge.yml - eval "Hello" 703 | 704 | nb cartridge.yml - repl 705 | ``` 706 | 707 | ```ruby 708 | bot = NanoBot.new(cartridge: 'cartridge.yml') 709 | 710 | puts bot.eval('Hello') 711 | ``` 712 | 713 | #### Option 3: Application Default Credentials (Vertex AI API) 714 | 715 | ```sh 716 | export GOOGLE_REGION=us-east4 717 | ``` 718 | 719 | Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded: 720 | 721 | ```sh 722 | GOOGLE_REGION=us-east4 723 | ``` 724 | 725 | Create a `cartridge.yml` file: 726 | 727 | ```yaml 728 | --- 729 | meta: 730 | symbol: 🤖 731 | name: Nano Bot Name 732 | author: Your Name 733 | version: 1.0.0 734 | license: CC0-1.0 735 | description: A helpful assistant. 736 | 737 | behaviors: 738 | interaction: 739 | directive: You are a helpful assistant. 740 | 741 | provider: 742 | id: google 743 | credentials: 744 | service: vertex-ai-api 745 | region: ENV/GOOGLE_REGION 746 | options: 747 | model: gemini-pro 748 | ``` 749 | 750 | Read the [full specification](https://spec.nbots.io/#/README?id=google-gemini) for Google Gemini. 751 | 752 | ```bash 753 | nb cartridge.yml - eval "Hello" 754 | 755 | nb cartridge.yml - repl 756 | ``` 757 | 758 | ```ruby 759 | bot = NanoBot.new(cartridge: 'cartridge.yml') 760 | 761 | puts bot.eval('Hello') 762 | ``` 763 | 764 | #### Custom Project ID 765 | 766 | If you need to manually set a Google Project ID: 767 | 768 | ```sh 769 | export GOOGLE_PROJECT_ID=your-project-id 770 | ``` 771 | 772 | Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded: 773 | 774 | ```sh 775 | GOOGLE_PROJECT_ID=your-project-id 776 | ``` 777 | 778 | Add to your `cartridge.yml` file: 779 | 780 | ```yaml 781 | --- 782 | provider: 783 | id: google 784 | credentials: 785 | project-id: ENV/GOOGLE_PROJECT_ID 786 | ``` 787 | 788 | ## Cartridges 789 | 790 | Check the Nano Bots specification to learn more about [how to build cartridges](https://spec.nbots.io/#/README?id=cartridges). 791 | 792 | Try the [Nano Bots Clinic (Live Editor)](https://clinic.nbots.io) to learn about creating Cartridges. 793 | 794 | Here's what a Nano Bot Cartridge looks like: 795 | 796 | ```yaml 797 | --- 798 | meta: 799 | symbol: 🤖 800 | name: Nano Bot Name 801 | author: Your Name 802 | version: 1.0.0 803 | license: CC0-1.0 804 | description: A helpful assistant. 805 | 806 | behaviors: 807 | interaction: 808 | directive: You are a helpful assistant. 809 | 810 | provider: 811 | id: openai 812 | credentials: 813 | access-token: ENV/OPENAI_API_KEY 814 | settings: 815 | user: ENV/NANO_BOTS_END_USER 816 | model: gpt-4o 817 | ``` 818 | 819 | ### Tools (Functions) 820 | 821 | Nano Bots can also be powered by _Tools_ (Functions): 822 | 823 | ```yaml 824 | --- 825 | tools: 826 | - name: random-number 827 | description: Generates a random number between 1 and 100. 828 | fennel: | 829 | (math.random 1 100) 830 | ``` 831 | 832 | ``` 833 | 🤖> please generate a random number 834 | 835 | random-number {} [yN] y 836 | 837 | random-number {} 838 | 59 839 | 840 | The randomly generated number is 59. 841 | 842 | 🤖> | 843 | ``` 844 | To successfully use Tools (Functions), you need to specify a provider and a model that supports them. As of the writing of this README, the provider that supports them is [OpenAI](https://platform.openai.com/docs/models), with models `gpt-3.5-turbo-1106` and `gpt-4o`, and [Google](https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/function-calling#supported_models), with the `vertex-ai-api` service and the model `gemini-pro`. Other providers do not yet have support. 845 | 846 | Check the [Nano Bots specification](https://spec.nbots.io/#/README?id=tools-functions-2) to learn more about Tools (Functions). 847 | 848 | #### Experimental Clojure Support 849 | 850 | We are exploring the use of [Clojure](https://clojure.org) through [Babashka](https://babashka.org), powered by [GraalVM](https://www.graalvm.org). 851 | 852 | The experimental support for Clojure would be similar to Lua and Fennel, using the `clojure:` key: 853 | 854 | ```yaml 855 | --- 856 | clojure: | 857 | (-> (java.time.ZonedDateTime/now) 858 | (.format (java.time.format.DateTimeFormatter/ofPattern "yyyy-MM-dd HH:mm")) 859 | (clojure.string/trimr)) 860 | ``` 861 | 862 | Unlike Lua and Fennel, Clojure support is not _embedded_ in this implementation. It relies on having the Babashka binary (`bb`) available in your environment where the Nano Bot is running. 863 | 864 | Here's [how to install Babashka](https://github.com/babashka/babashka#quickstart): 865 | 866 | ```sh 867 | curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | sudo bash 868 | ``` 869 | 870 | This is a quick check to ensure that it is available and working: 871 | ```sh 872 | bb -e '{:hello "world"}' 873 | # => {:hello "world"} 874 | ``` 875 | 876 | We don't have sandbox support for Clojure; this means that you need to disable it to be able to run Clojure code, which you do at your own risk: 877 | 878 | ```yaml 879 | --- 880 | safety: 881 | functions: 882 | sandboxed: false 883 | ``` 884 | 885 | ### Marketplace 886 | 887 | You can explore the Nano Bots [Marketplace](https://nbots.io) to discover new Cartridges that can help you. 888 | 889 | ## Security and Privacy 890 | 891 | Each provider will have its own security and privacy policies (e.g. [OpenAI Policy](https://openai.com/policies/api-data-usage-policies)), so you must consult them to understand their implications. 892 | 893 | ### Cryptography 894 | 895 | By default, all states stored in your local disk are encrypted. 896 | 897 | To ensure that the encryption is secure, you need to define a password through the `NANO_BOTS_ENCRYPTION_PASSWORD` environment variable. Otherwise, although the content will be encrypted, anyone would be able to decrypt it without a password. 898 | 899 | It's important to note that the content shared with providers, despite being transmitted over secure connections (e.g., [HTTPS](https://en.wikipedia.org/wiki/HTTPS)), will be readable by the provider. This is because providers need to operate on the data, which would not be possible if the content was encrypted beyond HTTPS. So, the data stored locally on your system is encrypted, which does not mean that what you share with providers will not be readable by them. 900 | 901 | To ensure that your encryption and password are configured properly, you can run the following command: 902 | ```sh 903 | nb security 904 | ``` 905 | 906 | Which should return: 907 | ```text 908 | ✅ Encryption is enabled and properly working. 909 | This means that your data is stored in an encrypted format on your disk. 910 | 911 | ✅ A password is being used for the encrypted content. 912 | This means that only those who possess the password can decrypt your data. 913 | ``` 914 | 915 | Alternatively, you can check it at runtime with: 916 | ```ruby 917 | require 'nano-bots' 918 | 919 | NanoBot.security.check 920 | # => { encryption: true, password: true } 921 | ``` 922 | 923 | ### End-user IDs 924 | 925 | A common strategy for deploying Nano Bots to multiple users through APIs or automations is to assign a unique [end-user ID](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids) for each user. This can be useful if any of your users violate the provider's policy due to abusive behavior. By providing the end-user ID, you can unravel that even though the activity originated from your API Key, the actions taken were not your own. 926 | 927 | You can define custom end-user identifiers in the following way: 928 | 929 | ```ruby 930 | NanoBot.new(environment: { NANO_BOTS_END_USER: 'custom-user-a' }) 931 | NanoBot.new(environment: { NANO_BOTS_END_USER: 'custom-user-b' }) 932 | ``` 933 | 934 | Consider that you have the following end-user identifier in your environment: 935 | ```sh 936 | NANO_BOTS_END_USER=your-name 937 | ``` 938 | 939 | Or a configuration in your Cartridge: 940 | ```yml 941 | --- 942 | provider: 943 | id: openai 944 | settings: 945 | user: your-name 946 | ``` 947 | 948 | The requests will be performed as follows: 949 | 950 | ```ruby 951 | NanoBot.new(cartridge: '-') 952 | # { user: 'your-name' } 953 | 954 | NanoBot.new(cartridge: '-', environment: { NANO_BOTS_END_USER: 'custom-user-a' }) 955 | # { user: 'custom-user-a' } 956 | 957 | NanoBot.new(cartridge: '-', environment: { NANO_BOTS_END_USER: 'custom-user-b' }) 958 | # { user: 'custom-user-b' } 959 | ``` 960 | 961 | Actually, to enhance privacy, neither your user nor your users' identifiers will be shared in this way. Instead, they will be encrypted before being shared with the provider: 962 | 963 | ```ruby 964 | 'your-name' 965 | # _O7OjYUESagb46YSeUeSfSMzoO1Yg0BZqpsAkPg4j62SeNYlgwq3kn51Ob2wmIehoA== 966 | 967 | 'custom-user-a' 968 | # _O7OjYUESagb46YSeUeSfSMzoO1Yg0BZJgIXHCBHyADW-rn4IQr-s2RvP7vym8u5tnzYMIs= 969 | 970 | 'custom-user-b' 971 | # _O7OjYUESagb46YSeUeSfSMzoO1Yg0BZkjUwCcsh9sVppKvYMhd2qGRvP7vym8u5tnzYMIg= 972 | ``` 973 | 974 | In this manner, you possess identifiers if required, however, their actual content can only be decrypted by you via your secure password (`NANO_BOTS_ENCRYPTION_PASSWORD`). 975 | 976 | ### Decrypting 977 | 978 | To decrypt your encrypted data, once you have properly configured your password, you can simply run: 979 | 980 | ```ruby 981 | require 'nano-bots' 982 | 983 | NanoBot.security.decrypt('_O7OjYUESagb46YSeUeSfSMzoO1Yg0BZqpsAkPg4j62SeNYlgwq3kn51Ob2wmIehoA==') 984 | # your-name 985 | 986 | NanoBot.security.decrypt('_O7OjYUESagb46YSeUeSfSMzoO1Yg0BZJgIXHCBHyADW-rn4IQr-s2RvP7vym8u5tnzYMIs=') 987 | # custom-user-a 988 | 989 | NanoBot.security.decrypt('_O7OjYUESagb46YSeUeSfSMzoO1Yg0BZkjUwCcsh9sVppKvYMhd2qGRvP7vym8u5tnzYMIg=') 990 | # custom-user-b 991 | ``` 992 | 993 | If you lose your password, you lose your data. It is not possible to recover it at all. For real. 994 | 995 | ## Supported Providers 996 | 997 | - [x] [Anthropic Claude](https://www.anthropic.com) 998 | - [x] [Cohere Command](https://cohere.com) 999 | - [x] [Google Gemini](https://deepmind.google/technologies/gemini) 1000 | - [x] [Maritaca AI MariTalk](https://www.maritaca.ai) 1001 | - [x] [Mistral AI](https://mistral.ai) 1002 | - [x] [Ollama](https://ollama.ai) 1003 | - [x] [01.AI Yi](https://01.ai) 1004 | - [x] [LMSYS Vicuna](https://github.com/lm-sys/FastChat) 1005 | - [x] [Meta Llama](https://ai.meta.com/llama/) 1006 | - [x] [WizardLM](https://wizardlm.github.io) 1007 | - [x] [Open AI ChatGPT](https://openai.com/chatgpt) 1008 | 1009 | 01.AI Yi, LMSYS Vicuna, Meta Llama, and WizardLM are open-source models that are supported through [Ollama](https://ollama.ai). 1010 | 1011 | ## Docker 1012 | 1013 | Clone the repository and copy the Docker Compose template: 1014 | 1015 | ``` 1016 | git clone https://github.com/icebaker/ruby-nano-bots.git 1017 | cd ruby-nano-bots 1018 | cp docker-compose.example.yml docker-compose.yml 1019 | ``` 1020 | 1021 | Set your provider credentials and choose your desired path for the cartridges files: 1022 | 1023 | ### Anthropic Claude Container 1024 | 1025 | ```yaml 1026 | --- 1027 | services: 1028 | nano-bots: 1029 | image: ruby:3.3.3-slim-bookworm 1030 | command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev curl && curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | bash && gem install nano-bots -v 3.4.0 && bash" 1031 | environment: 1032 | ANTHROPIC_API_KEY: your-api-key 1033 | NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE 1034 | NANO_BOTS_END_USER: your-user 1035 | volumes: 1036 | - ./your-cartridges:/root/.local/share/nano-bots/cartridges 1037 | - ./your-state-path:/root/.local/state/nano-bots 1038 | ``` 1039 | 1040 | ### Cohere Command Container 1041 | 1042 | ```yaml 1043 | --- 1044 | services: 1045 | nano-bots: 1046 | image: ruby:3.3.3-slim-bookworm 1047 | command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev curl && curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | bash && gem install nano-bots -v 3.4.0 && bash" 1048 | environment: 1049 | COHERE_API_KEY: your-api-key 1050 | NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE 1051 | NANO_BOTS_END_USER: your-user 1052 | volumes: 1053 | - ./your-cartridges:/root/.local/share/nano-bots/cartridges 1054 | - ./your-state-path:/root/.local/state/nano-bots 1055 | ``` 1056 | 1057 | ### Maritaca AI MariTalk Container 1058 | 1059 | ```yaml 1060 | --- 1061 | services: 1062 | nano-bots: 1063 | image: ruby:3.3.3-slim-bookworm 1064 | command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev curl && curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | bash && gem install nano-bots -v 3.4.0 && bash" 1065 | environment: 1066 | MARITACA_API_KEY: your-api-key 1067 | NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE 1068 | NANO_BOTS_END_USER: your-user 1069 | volumes: 1070 | - ./your-cartridges:/root/.local/share/nano-bots/cartridges 1071 | - ./your-state-path:/root/.local/state/nano-bots 1072 | ``` 1073 | 1074 | ### Mistral AI Container 1075 | 1076 | ```yaml 1077 | --- 1078 | services: 1079 | nano-bots: 1080 | image: ruby:3.3.3-slim-bookworm 1081 | command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev curl && curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | bash && gem install nano-bots -v 3.4.0 && bash" 1082 | environment: 1083 | MISTRAL_API_KEY: your-api-key 1084 | NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE 1085 | NANO_BOTS_END_USER: your-user 1086 | volumes: 1087 | - ./your-cartridges:/root/.local/share/nano-bots/cartridges 1088 | - ./your-state-path:/root/.local/state/nano-bots 1089 | ``` 1090 | 1091 | ### Ollama Container 1092 | 1093 | Remember that your `localhost` is by default inaccessible from inside Docker. You need to either establish [inter-container networking](https://docs.docker.com/compose/networking/), use the [host's address](https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host), or use the [host network](https://docs.docker.com/network/network-tutorial-host/), depending on where the Ollama server is running and your preferences. 1094 | 1095 | ```yaml 1096 | --- 1097 | services: 1098 | nano-bots: 1099 | image: ruby:3.3.3-slim-bookworm 1100 | command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev curl && curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | bash && gem install nano-bots -v 3.4.0 && bash" 1101 | environment: 1102 | OLLAMA_API_ADDRESS: http://localhost:11434 1103 | NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE 1104 | NANO_BOTS_END_USER: your-user 1105 | volumes: 1106 | - ./your-cartridges:/root/.local/share/nano-bots/cartridges 1107 | - ./your-state-path:/root/.local/state/nano-bots 1108 | # If you are running the Ollama server on your localhost: 1109 | network_mode: host # WARNING: Be careful, this may be a security risk. 1110 | ``` 1111 | 1112 | ### OpenAI ChatGPT Container 1113 | 1114 | ```yaml 1115 | --- 1116 | services: 1117 | nano-bots: 1118 | image: ruby:3.3.3-slim-bookworm 1119 | command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev curl && curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | bash && gem install nano-bots -v 3.4.0 && bash" 1120 | environment: 1121 | OPENAI_API_KEY: your-access-token 1122 | NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE 1123 | NANO_BOTS_END_USER: your-user 1124 | volumes: 1125 | - ./your-cartridges:/root/.local/share/nano-bots/cartridges 1126 | - ./your-state-path:/root/.local/state/nano-bots 1127 | ``` 1128 | 1129 | ### Google Gemini Container 1130 | 1131 | #### Option 1: API Key (Generative Language API) Config 1132 | 1133 | ```yaml 1134 | --- 1135 | services: 1136 | nano-bots: 1137 | image: ruby:3.3.3-slim-bookworm 1138 | command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev curl && curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | bash && gem install nano-bots -v 3.4.0 && bash" 1139 | environment: 1140 | GOOGLE_API_KEY: your-api-key 1141 | NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE 1142 | NANO_BOTS_END_USER: your-user 1143 | volumes: 1144 | - ./your-cartridges:/root/.local/share/nano-bots/cartridges 1145 | - ./your-state-path:/root/.local/state/nano-bots 1146 | ``` 1147 | 1148 | #### Option 2: Service Account Credentials File (Vertex AI API) Config 1149 | 1150 | ```yaml 1151 | --- 1152 | services: 1153 | nano-bots: 1154 | image: ruby:3.3.3-slim-bookworm 1155 | command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev curl && curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | bash && gem install nano-bots -v 3.4.0 && bash" 1156 | environment: 1157 | GOOGLE_CREDENTIALS_FILE_PATH: /root/.config/google-credentials.json 1158 | GOOGLE_REGION: us-east4 1159 | NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE 1160 | NANO_BOTS_END_USER: your-user 1161 | volumes: 1162 | - ./google-credentials.json:/root/.config/google-credentials.json 1163 | - ./your-cartridges:/root/.local/share/nano-bots/cartridges 1164 | - ./your-state-path:/root/.local/state/nano-bots 1165 | ``` 1166 | 1167 | #### Option 3: Application Default Credentials (Vertex AI API) Config 1168 | 1169 | ```yaml 1170 | --- 1171 | services: 1172 | nano-bots: 1173 | image: ruby:3.3.3-slim-bookworm 1174 | command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev curl && curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | bash && gem install nano-bots -v 3.4.0 && bash" 1175 | environment: 1176 | GOOGLE_REGION: us-east4 1177 | NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE 1178 | NANO_BOTS_END_USER: your-user 1179 | volumes: 1180 | - ./your-cartridges:/root/.local/share/nano-bots/cartridges 1181 | - ./your-state-path:/root/.local/state/nano-bots 1182 | ``` 1183 | 1184 | #### Custom Project ID Config 1185 | If you need to manually set a Google Project ID: 1186 | 1187 | ```yaml 1188 | environment: 1189 | GOOGLE_PROJECT_ID=your-project-id 1190 | ``` 1191 | 1192 | ### Running the Container 1193 | 1194 | Enter the container: 1195 | ```sh 1196 | docker compose run nano-bots 1197 | ``` 1198 | 1199 | Start playing: 1200 | ```sh 1201 | nb - - eval "hello" 1202 | nb - - repl 1203 | 1204 | nb assistant.yml - eval "hello" 1205 | nb assistant.yml - repl 1206 | ``` 1207 | 1208 | You can exit the REPL by typing `exit`. 1209 | 1210 | ## Development 1211 | 1212 | ```bash 1213 | bundle 1214 | rubocop -A 1215 | rspec 1216 | 1217 | bundle exec ruby spec/tasks/run-all-models.rb 1218 | 1219 | bundle exec ruby spec/tasks/run-model.rb spec/data/cartridges/models/openai/gpt-4-turbo.yml 1220 | bundle exec ruby spec/tasks/run-model.rb spec/data/cartridges/models/openai/gpt-4-turbo.yml stream 1221 | ``` 1222 | 1223 | If you face issues upgrading gem versions: 1224 | 1225 | ```sh 1226 | bundle install --full-index 1227 | ``` 1228 | 1229 | ### Publish to RubyGems 1230 | 1231 | ```bash 1232 | gem build nano-bots.gemspec 1233 | 1234 | gem signin 1235 | 1236 | gem push nano-bots-3.4.0.gem 1237 | ``` 1238 | -------------------------------------------------------------------------------- /bin/nb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'nano-bots/cli' 5 | -------------------------------------------------------------------------------- /components/adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'embedding' 4 | require_relative '../logic/cartridge/safety' 5 | 6 | module NanoBot 7 | module Components 8 | class Adapter 9 | def self.apply(params, cartridge) 10 | content = params[:content] 11 | 12 | raise StandardError, 'conflicting adapters' if %i[fennel lua clojure].count { |key| !params[key].nil? } > 1 13 | 14 | call = { 15 | parameters: %w[content], values: [content], 16 | safety: { sandboxed: Logic::Cartridge::Safety.sandboxed?(cartridge) } 17 | } 18 | 19 | if params[:fennel] 20 | call[:source] = params[:fennel] 21 | content = Components::Embedding.fennel(**call) 22 | elsif params[:clojure] 23 | call[:source] = params[:clojure] 24 | content = Components::Embedding.clojure(**call) 25 | elsif params[:lua] 26 | call[:source] = params[:lua] 27 | content = Components::Embedding.lua(**call) 28 | end 29 | 30 | "#{params[:prefix]}#{content}#{params[:suffix]}" 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /components/crypto.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'singleton' 4 | require 'rbnacl' 5 | require 'base64' 6 | 7 | module NanoBot 8 | module Components 9 | class Crypto 10 | include Singleton 11 | 12 | def initialize 13 | password = ENV.fetch('NANO_BOTS_ENCRYPTION_PASSWORD', nil) 14 | 15 | password = 'UNSAFE' unless password && password != '' 16 | 17 | @box = RbNaCl::SecretBox.new(RbNaCl::Hash.sha256(password)) 18 | @fixed_nonce = RbNaCl::Hash.sha256(password)[0...@box.nonce_bytes] 19 | end 20 | 21 | def encrypt(content, soft: false) 22 | nonce = soft ? @fixed_nonce : RbNaCl::Random.random_bytes(@box.nonce_bytes) 23 | Base64.urlsafe_encode64(nonce + @box.encrypt(nonce, content)) 24 | end 25 | 26 | def decrypt(content) 27 | decoded_content = Base64.urlsafe_decode64(content) 28 | nonce = decoded_content[0...@box.nonce_bytes] 29 | cipher_text = decoded_content[@box.nonce_bytes..] 30 | 31 | @box.decrypt(nonce, cipher_text) 32 | end 33 | 34 | def self.encrypt(content, soft: false) 35 | instance.encrypt(content, soft:) 36 | end 37 | 38 | def self.decrypt(content) 39 | instance.decrypt(content) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /components/embedding.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'sweet-moon' 4 | 5 | require 'open3' 6 | require 'json' 7 | require 'tempfile' 8 | 9 | module NanoBot 10 | module Components 11 | class Embedding 12 | def self.ensure_safety!(safety) 13 | raise 'missing safety definitions' unless safety.key?(:sandboxed) 14 | end 15 | 16 | def self.lua(source:, parameters:, values:, safety:) 17 | ensure_safety!(safety) 18 | 19 | allowed = '' 20 | allowed = ', {math=math,string=string,table=table}' if safety[:sandboxed] 21 | 22 | state = SweetMoon::State.new 23 | code = "_, embedded = pcall(load([[\nreturn function(#{parameters.join(', ')})\n#{source}\nend\n]], nil, 't'#{allowed}))" 24 | 25 | state.eval(code) 26 | embedded = state.get(:embedded) 27 | begin 28 | embedded.call(values) 29 | rescue StandardError => e 30 | e.message 31 | end 32 | end 33 | 34 | def self.fennel(source:, parameters:, values:, safety:) 35 | ensure_safety!(safety) 36 | 37 | path = "#{File.expand_path('../static/fennel', __dir__)}/?.lua" 38 | state = SweetMoon::State.new(package_path: path).fennel 39 | 40 | # TODO: `global` is deprecated. 41 | state.fennel.eval( 42 | "(global embedded (fn [#{parameters.join(' ')}] #{source}))", 1, 43 | safety[:sandboxed] ? { allowedGlobals: %w[math string table] } : nil 44 | ) 45 | embedded = state.get(:embedded) 46 | begin 47 | embedded.call(values) 48 | rescue StandardError => e 49 | e.message 50 | end 51 | end 52 | 53 | def self.clojure(source:, parameters:, values:, safety:) 54 | ensure_safety!(safety) 55 | 56 | raise 'Sandboxed Clojure not supported.' if safety[:sandboxed] 57 | 58 | raise 'invalid Clojure parameter name' if parameters.include?('injected-parameters') 59 | 60 | key_value = {} 61 | 62 | parameters.each_with_index { |key, index| key_value[key] = values[index] } 63 | 64 | parameters_json = key_value.to_json 65 | 66 | json_file = Tempfile.new(['nano-bot', '.json']) 67 | clojure_file = Tempfile.new(['nano-bot', '.clj']) 68 | 69 | begin 70 | json_file.write(parameters_json) 71 | json_file.close 72 | 73 | clojure_source = <<~CLOJURE 74 | (require '[cheshire.core :as json]) 75 | (def injected-parameters (json/parse-string (slurp (java.io.FileReader. "#{json_file.path}")))) 76 | 77 | #{parameters.map { |p| "(def #{p} (get injected-parameters \"#{p}\"))" }.join("\n")} 78 | 79 | #{source} 80 | CLOJURE 81 | 82 | clojure_file.write(clojure_source) 83 | clojure_file.close 84 | 85 | bb_command = "bb --prn #{clojure_file.path} | bb -e \"(->> *in* slurp read-string print)\"" 86 | 87 | stdout, stderr, status = Open3.capture3(bb_command) 88 | 89 | status.success? ? stdout : stderr 90 | ensure 91 | json_file&.unlink 92 | clojure_file&.unlink 93 | end 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /components/provider.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'providers/openai' 4 | require_relative 'providers/ollama' 5 | require_relative 'providers/mistral' 6 | require_relative 'providers/google' 7 | require_relative 'providers/anthropic' 8 | require_relative 'providers/cohere' 9 | require_relative 'providers/maritaca' 10 | 11 | module NanoBot 12 | module Components 13 | class Provider 14 | def self.new(provider, environment: {}) 15 | case provider[:id] 16 | when 'openai' 17 | Providers::OpenAI.new(nil, provider[:settings], provider[:credentials], environment:) 18 | when 'ollama' 19 | Providers::Ollama.new(provider[:options], provider[:settings], provider[:credentials], environment:) 20 | when 'mistral' 21 | Providers::Mistral.new(provider[:options], provider[:settings], provider[:credentials], environment:) 22 | when 'google' 23 | Providers::Google.new(provider[:options], provider[:settings], provider[:credentials], environment:) 24 | when 'anthropic' 25 | Providers::Anthropic.new(nil, provider[:settings], provider[:credentials], environment:) 26 | when 'cohere' 27 | Providers::Cohere.new(provider[:options], provider[:settings], provider[:credentials], environment:) 28 | when 'maritaca' 29 | Providers::Maritaca.new(provider[:options], provider[:settings], provider[:credentials], environment:) 30 | else 31 | raise "Unsupported provider \"#{provider[:id]}\"" 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /components/providers/anthropic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'anthropic' 4 | 5 | require_relative 'base' 6 | 7 | require_relative '../../logic/providers/anthropic/tokens' 8 | require_relative '../../logic/helpers/hash' 9 | require_relative '../../logic/cartridge/default' 10 | 11 | module NanoBot 12 | module Components 13 | module Providers 14 | class Anthropic < Base 15 | attr_reader :settings 16 | 17 | CHAT_SETTINGS = %i[ 18 | model stream max_tokens temperature top_k top_p tool_choice 19 | metadata stop_sequences 20 | ].freeze 21 | 22 | def initialize(_options, settings, credentials, _environment) 23 | @settings = settings 24 | 25 | unless @settings.key?(:stream) 26 | @settings = Marshal.load(Marshal.dump(@settings)) 27 | @settings[:stream] = Logic::Helpers::Hash.fetch( 28 | Logic::Cartridge::Default.instance.values, %i[provider settings stream] 29 | ) 30 | end 31 | 32 | @client = ::Anthropic::Client.new( 33 | access_token: credentials[:'api-key'], 34 | anthropic_version: credentials[:'anthropic-version'] 35 | ) 36 | end 37 | 38 | def evaluate(input, streaming, cartridge, &feedback) 39 | messages = input[:history].map do |event| 40 | { role: event[:who] == 'user' ? 'user' : 'assistant', 41 | content: event[:message], 42 | _meta: { at: event[:at] } } 43 | end 44 | 45 | if input[:behavior][:backdrop] 46 | messages.prepend( 47 | { role: 'assistant', 48 | content: 'Ok.', 49 | _meta: { at: Time.now } } 50 | ) 51 | 52 | messages.prepend( 53 | { role: 'user', 54 | content: input[:behavior][:backdrop], 55 | _meta: { at: Time.now } } 56 | ) 57 | end 58 | 59 | payload = { messages: } 60 | 61 | payload[:system] = input[:behavior][:directive] if input[:behavior][:directive] 62 | 63 | CHAT_SETTINGS.each do |key| 64 | payload[key] = @settings[key] unless payload.key?(key) || !@settings.key?(key) 65 | end 66 | 67 | raise 'Anthropic does not support tools.' if input[:tools] 68 | 69 | if streaming 70 | content = '' 71 | 72 | stream_call_back = proc do |event| 73 | partial_content = event.dig('delta', 'text') 74 | 75 | if partial_content && event['type'] == 'content_block_delta' 76 | content += partial_content 77 | feedback.call( 78 | { should_be_stored: false, 79 | interaction: { who: 'AI', message: partial_content } } 80 | ) 81 | end 82 | 83 | if event['type'] == 'content_block_stop' 84 | feedback.call( 85 | { should_be_stored: !(content.nil? || content == ''), 86 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 87 | finished: true } 88 | ) 89 | end 90 | end 91 | 92 | @client.messages( 93 | parameters: Logic::Anthropic::Tokens.apply_policies!( 94 | cartridge, payload 95 | ).merge({ stream: stream_call_back }) 96 | ) 97 | else 98 | result = @client.messages( 99 | parameters: Logic::Anthropic::Tokens.apply_policies!(cartridge, payload) 100 | ) 101 | 102 | content = result['content'].map { |content| content['text'] }.join 103 | 104 | feedback.call( 105 | { should_be_stored: !(content.nil? || content.to_s.strip == ''), 106 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 107 | finished: true } 108 | ) 109 | end 110 | end 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /components/providers/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'openai' 4 | 5 | module NanoBot 6 | module Components 7 | module Providers 8 | class Base 9 | def initialize(_options, _settings, _credentials, _environment: {}) 10 | raise NoMethodError, "The 'initialize' method is not implemented for the current provider." 11 | end 12 | 13 | def evaluate(_payload) 14 | raise NoMethodError, "The 'evaluate' method is not implemented for the current provider." 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /components/providers/cohere.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cohere-ai' 4 | 5 | require_relative 'base' 6 | 7 | require_relative '../../logic/providers/cohere/tokens' 8 | require_relative '../../logic/helpers/hash' 9 | require_relative '../../logic/cartridge/default' 10 | 11 | module NanoBot 12 | module Components 13 | module Providers 14 | class Cohere < Base 15 | attr_reader :settings 16 | 17 | CHAT_SETTINGS = %i[ 18 | model stream prompt_truncation connectors search_queries_only 19 | documents citation_quality temperature max_tokens max_input_tokens 20 | k p seed stop_sequences frequency_penalty presence_penalty 21 | force_single_step 22 | ].freeze 23 | 24 | def initialize(options, settings, credentials, _environment) 25 | @settings = settings 26 | 27 | cohere_options = if options 28 | options.transform_keys { |key| key.to_s.gsub('-', '_').to_sym } 29 | else 30 | {} 31 | end 32 | 33 | unless @settings.key?(:stream) 34 | @settings = Marshal.load(Marshal.dump(@settings)) 35 | @settings[:stream] = Logic::Helpers::Hash.fetch( 36 | Logic::Cartridge::Default.instance.values, %i[provider settings stream] 37 | ) 38 | end 39 | 40 | cohere_options[:server_sent_events] = @settings[:stream] 41 | 42 | @client = ::Cohere.new( 43 | credentials: credentials.transform_keys { |key| key.to_s.gsub('-', '_').to_sym }, 44 | options: cohere_options 45 | ) 46 | end 47 | 48 | def evaluate(input, streaming, cartridge, &feedback) 49 | messages = input[:history].map do |event| 50 | { role: event[:who] == 'user' ? 'USER' : 'CHATBOT', 51 | message: event[:message], 52 | _meta: { at: event[:at] } } 53 | end 54 | 55 | if input[:behavior][:backdrop] 56 | messages.prepend( 57 | { role: 'USER', 58 | message: input[:behavior][:backdrop], 59 | _meta: { at: Time.now } } 60 | ) 61 | end 62 | 63 | payload = { chat_history: messages } 64 | 65 | payload[:message] = payload[:chat_history].pop[:message] 66 | 67 | payload.delete(:chat_history) if payload[:chat_history].empty? 68 | 69 | payload[:preamble_override] = input[:behavior][:directive] if input[:behavior][:directive] 70 | 71 | CHAT_SETTINGS.each do |key| 72 | payload[key] = @settings[key] unless payload.key?(key) || !@settings.key?(key) 73 | end 74 | 75 | raise 'Cohere does not support tools.' if input[:tools] 76 | 77 | if streaming 78 | content = '' 79 | 80 | stream_call_back = proc do |event, _raw| 81 | partial_content = event['text'] 82 | 83 | if partial_content && event['event_type'] == 'text-generation' 84 | content += partial_content 85 | feedback.call( 86 | { should_be_stored: false, 87 | interaction: { who: 'AI', message: partial_content } } 88 | ) 89 | end 90 | 91 | if event['is_finished'] 92 | feedback.call( 93 | { should_be_stored: !(content.nil? || content == ''), 94 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 95 | finished: true } 96 | ) 97 | end 98 | end 99 | 100 | @client.chat( 101 | Logic::Cohere::Tokens.apply_policies!(cartridge, payload), 102 | server_sent_events: true, &stream_call_back 103 | ) 104 | else 105 | result = @client.chat( 106 | Logic::Cohere::Tokens.apply_policies!(cartridge, payload), 107 | server_sent_events: false 108 | ) 109 | 110 | content = result['text'] 111 | 112 | feedback.call( 113 | { should_be_stored: !(content.nil? || content.to_s.strip == ''), 114 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 115 | finished: true } 116 | ) 117 | end 118 | end 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /components/providers/google.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'gemini-ai' 4 | 5 | require_relative 'base' 6 | 7 | require_relative '../../logic/providers/google/tools' 8 | require_relative '../../logic/providers/google/tokens' 9 | require_relative '../../logic/helpers/hash' 10 | require_relative '../../logic/cartridge/default' 11 | 12 | require_relative 'tools' 13 | 14 | module NanoBot 15 | module Components 16 | module Providers 17 | class Google < Base 18 | SAFETY_SETTINGS = %i[category threshold].freeze 19 | 20 | SETTINGS = { 21 | generationConfig: %i[ 22 | temperature maxOutputTokens candidateCount presencePenalty 23 | frequencyPenalty topK topP stopSequences 24 | responseMimeType responseSchema 25 | ].freeze 26 | }.freeze 27 | 28 | attr_reader :settings 29 | 30 | def initialize(options, settings, credentials, _environment) 31 | @settings = settings 32 | 33 | @service = credentials[:service] 34 | 35 | gemini_options = options.transform_keys { |key| key.to_s.gsub('-', '_').to_sym } 36 | 37 | unless gemini_options.key?(:stream) 38 | gemini_options[:stream] = Logic::Helpers::Hash.fetch( 39 | Logic::Cartridge::Default.instance.values, %i[provider settings stream] 40 | ) 41 | end 42 | 43 | gemini_options[:server_sent_events] = gemini_options.delete(:stream) 44 | 45 | @client = Gemini.new( 46 | credentials: credentials.transform_keys { |key| key.to_s.gsub('-', '_').to_sym }, 47 | options: gemini_options 48 | ) 49 | end 50 | 51 | def evaluate(input, streaming, cartridge, &feedback) 52 | messages = input[:history].map do |event| 53 | if event[:message].nil? && event[:meta] && event[:meta][:tool_calls] 54 | { role: 'model', 55 | parts: event[:meta][:tool_calls], 56 | _meta: { at: event[:at] } } 57 | elsif event[:who] == 'tool' 58 | { role: 'function', 59 | parts: [ 60 | { functionResponse: { 61 | name: event[:meta][:name], 62 | response: { name: event[:meta][:name], content: event[:message].to_s } 63 | } } 64 | ], 65 | _meta: { at: event[:at] } } 66 | else 67 | { role: event[:who] == 'user' ? 'user' : 'model', 68 | parts: { text: event[:message] }, 69 | _meta: { at: event[:at] } } 70 | end 71 | end 72 | 73 | if input[:behavior][:backdrop] 74 | messages.prepend( 75 | { role: 'model', 76 | parts: { text: 'Understood.' }, 77 | _meta: { at: Time.now } } 78 | ) 79 | 80 | messages.prepend( 81 | { role: 'user', 82 | parts: { text: input[:behavior][:backdrop] }, 83 | _meta: { at: Time.now } } 84 | ) 85 | end 86 | 87 | payload = { contents: messages, generationConfig: { candidateCount: 1 } } 88 | 89 | if input[:behavior][:directive] && @service == 'vertex-ai-api' 90 | payload[:system_instruction] = { 91 | role: 'user', 92 | parts: { text: input[:behavior][:directive] } 93 | } 94 | elsif input[:behavior][:directive] 95 | # TODO: Will generative-language-api support system instructions? 96 | messages.prepend( 97 | { role: 'user', 98 | parts: { text: input[:behavior][:directive] }, 99 | _meta: { at: Time.now } } 100 | ) 101 | end 102 | 103 | if @settings 104 | SETTINGS.each_key do |key| 105 | SETTINGS[key].each do |sub_key| 106 | if @settings.key?(key) && @settings[key].key?(sub_key) 107 | payload[key] = {} unless payload.key?(key) 108 | payload[key][sub_key] = @settings[key][sub_key] 109 | end 110 | end 111 | end 112 | 113 | if @settings[:safetySettings].is_a?(Array) 114 | payload[:safetySettings] = [] unless payload.key?(:safetySettings) 115 | 116 | @settings[:safetySettings].each do |safety_setting| 117 | setting = {} 118 | SAFETY_SETTINGS.each { |key| setting[key] = safety_setting[key] } 119 | payload[:safetySettings] << setting 120 | end 121 | end 122 | end 123 | 124 | if input[:tools] 125 | payload[:tools] = { 126 | function_declarations: input[:tools].map { |raw| Logic::Google::Tools.adapt(raw) } 127 | } 128 | end 129 | 130 | if streaming 131 | content = '' 132 | tools = [] 133 | 134 | stream_call_back = proc do |event, _parsed, _raw| 135 | # TODO: How to better handle finishReason == 'OTHER'? 136 | return if event.dig('candidates', 0, 'finishReason') == 'OTHER' 137 | 138 | partial_content = event.dig('candidates', 0, 'content', 'parts')&.filter do |part| 139 | part.key?('text') 140 | end&.map { |part| part['text'] }&.join || '' 141 | 142 | partial_tools = event.dig('candidates', 0, 'content', 'parts')&.filter do |part| 143 | part.key?('functionCall') 144 | end || [] 145 | 146 | tools.concat(partial_tools) if partial_tools.size.positive? 147 | 148 | if partial_content 149 | content += partial_content 150 | feedback.call( 151 | { should_be_stored: false, 152 | interaction: { who: 'AI', message: partial_content } } 153 | ) 154 | end 155 | 156 | if event.dig('candidates', 0, 'finishReason') == 'SAFETY' 157 | reasons = event.dig('candidates', 0, 'safetyRatings') 158 | raise StandardError, "Generation stopped for safety reasons: #{reasons}" 159 | elsif event.dig('candidates', 0, 'finishReason') == 'RECITATION' 160 | reasons = event.dig('candidates', 0, 'citationMetadata') 161 | raise StandardError, "Generation stopped for recitation reasons: #{reasons}" 162 | end 163 | end 164 | 165 | @client.stream_generate_content( 166 | Logic::Google::Tokens.apply_policies!(cartridge, payload), 167 | server_sent_events: true, &stream_call_back 168 | ) 169 | 170 | if tools&.size&.positive? 171 | feedback.call( 172 | { should_be_stored: true, 173 | needs_another_round: true, 174 | interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } } 175 | ) 176 | Tools.apply( 177 | cartridge, input[:tools], tools, feedback, Logic::Google::Tools 178 | ).each do |interaction| 179 | feedback.call({ should_be_stored: true, needs_another_round: true, interaction: }) 180 | end 181 | end 182 | 183 | feedback.call( 184 | { should_be_stored: !(content.nil? || content == ''), 185 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 186 | finished: true } 187 | ) 188 | else 189 | result = @client.stream_generate_content( 190 | Logic::Google::Tokens.apply_policies!(cartridge, payload), 191 | server_sent_events: false 192 | ) 193 | 194 | tools = result.dig(0, 'candidates', 0, 'content', 'parts').filter do |part| 195 | part.key?('functionCall') 196 | end 197 | 198 | if tools&.size&.positive? 199 | feedback.call( 200 | { should_be_stored: true, 201 | needs_another_round: true, 202 | interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } } 203 | ) 204 | 205 | Tools.apply( 206 | cartridge, input[:tools], tools, feedback, Logic::Google::Tools 207 | ).each do |interaction| 208 | feedback.call({ should_be_stored: true, needs_another_round: true, interaction: }) 209 | end 210 | end 211 | 212 | content = result.map do |answer| 213 | parts = answer.dig('candidates', 0, 'content', 'parts') 214 | 215 | if parts 216 | parts.filter { |part| part.key?('text') }.map { |part| part['text'] }.join 217 | else 218 | '' 219 | end 220 | end.join 221 | 222 | feedback.call( 223 | { should_be_stored: !(content.nil? || content.to_s.strip == ''), 224 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 225 | finished: true } 226 | ) 227 | end 228 | end 229 | end 230 | end 231 | end 232 | end 233 | -------------------------------------------------------------------------------- /components/providers/maritaca.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'maritaca-ai' 4 | 5 | require_relative 'base' 6 | 7 | require_relative '../../logic/providers/maritaca/tokens' 8 | require_relative '../../logic/helpers/hash' 9 | require_relative '../../logic/cartridge/default' 10 | 11 | module NanoBot 12 | module Components 13 | module Providers 14 | class Maritaca < Base 15 | attr_reader :settings 16 | 17 | CHAT_SETTINGS = %i[ 18 | stream model max_tokens do_sample temperature top_p 19 | repetition_penalty num_tokens_per_message stopping_tokens 20 | ].freeze 21 | 22 | def initialize(options, settings, credentials, _environment) 23 | @settings = settings 24 | 25 | maritaca_options = if options 26 | options.transform_keys { |key| key.to_s.gsub('-', '_').to_sym } 27 | else 28 | {} 29 | end 30 | 31 | unless @settings.key?(:stream) 32 | @settings = Marshal.load(Marshal.dump(@settings)) 33 | @settings[:stream] = Logic::Helpers::Hash.fetch( 34 | Logic::Cartridge::Default.instance.values, %i[provider settings stream] 35 | ) 36 | end 37 | 38 | maritaca_options[:server_sent_events] = @settings[:stream] 39 | 40 | @client = ::Maritaca.new( 41 | credentials: credentials.transform_keys { |key| key.to_s.gsub('-', '_').to_sym }, 42 | options: maritaca_options 43 | ) 44 | end 45 | 46 | def evaluate(input, streaming, cartridge, &feedback) 47 | messages = input[:history].map do |event| 48 | { role: event[:who] == 'user' ? 'user' : 'assistant', 49 | content: event[:message], 50 | _meta: { at: event[:at] } } 51 | end 52 | 53 | # TODO: Does Maritaca have system messages? 54 | %i[backdrop directive].each do |key| 55 | next unless input[:behavior][key] 56 | 57 | messages.prepend( 58 | { role: 'user', 59 | content: input[:behavior][key], 60 | _meta: { at: Time.now } } 61 | ) 62 | end 63 | 64 | payload = { chat_mode: true, messages: } 65 | 66 | CHAT_SETTINGS.each do |key| 67 | payload[key] = @settings[key] unless payload.key?(key) || !@settings.key?(key) 68 | end 69 | 70 | raise 'Maritaca does not support tools.' if input[:tools] 71 | 72 | if streaming 73 | content = '' 74 | 75 | stream_call_back = proc do |event, _raw| 76 | partial_content = event['text'] 77 | 78 | if partial_content 79 | content += partial_content 80 | feedback.call( 81 | { should_be_stored: false, 82 | interaction: { who: 'AI', message: partial_content } } 83 | ) 84 | end 85 | end 86 | 87 | @client.chat_inference( 88 | Logic::Maritaca::Tokens.apply_policies!(cartridge, payload), 89 | server_sent_events: true, &stream_call_back 90 | ) 91 | 92 | feedback.call( 93 | { should_be_stored: !(content.nil? || content == ''), 94 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 95 | finished: true } 96 | ) 97 | else 98 | result = @client.chat_inference( 99 | Logic::Maritaca::Tokens.apply_policies!(cartridge, payload), 100 | server_sent_events: false 101 | ) 102 | 103 | content = result['answer'] 104 | 105 | feedback.call( 106 | { should_be_stored: !(content.nil? || content.to_s.strip == ''), 107 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 108 | finished: true } 109 | ) 110 | end 111 | end 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /components/providers/mistral.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mistral-ai' 4 | 5 | require_relative 'base' 6 | 7 | require_relative '../../logic/providers/mistral/tokens' 8 | require_relative '../../logic/helpers/hash' 9 | require_relative '../../logic/cartridge/default' 10 | 11 | module NanoBot 12 | module Components 13 | module Providers 14 | class Mistral < Base 15 | attr_reader :settings 16 | 17 | CHAT_SETTINGS = %i[ 18 | model temperature top_p max_tokens stream safe_prompt random_seed 19 | safe_mode 20 | ].freeze 21 | 22 | def initialize(options, settings, credentials, _environment) 23 | @settings = settings 24 | 25 | mistral_options = if options 26 | options.transform_keys { |key| key.to_s.gsub('-', '_').to_sym } 27 | else 28 | {} 29 | end 30 | 31 | unless @settings.key?(:stream) 32 | @settings = Marshal.load(Marshal.dump(@settings)) 33 | @settings[:stream] = Logic::Helpers::Hash.fetch( 34 | Logic::Cartridge::Default.instance.values, %i[provider settings stream] 35 | ) 36 | end 37 | 38 | mistral_options[:server_sent_events] = @settings[:stream] 39 | 40 | @client = ::Mistral.new( 41 | credentials: credentials.transform_keys { |key| key.to_s.gsub('-', '_').to_sym }, 42 | options: mistral_options 43 | ) 44 | end 45 | 46 | def evaluate(input, streaming, cartridge, &feedback) 47 | messages = input[:history].map do |event| 48 | { role: event[:who] == 'user' ? 'user' : 'assistant', 49 | content: event[:message], 50 | _meta: { at: event[:at] } } 51 | end 52 | 53 | %i[backdrop directive].each do |key| 54 | next unless input[:behavior][key] 55 | 56 | messages.prepend( 57 | { role: key == :directive ? 'system' : 'user', 58 | content: input[:behavior][key], 59 | _meta: { at: Time.now } } 60 | ) 61 | end 62 | 63 | payload = { messages: } 64 | 65 | CHAT_SETTINGS.each do |key| 66 | payload[key] = @settings[key] unless payload.key?(key) || !@settings.key?(key) 67 | end 68 | 69 | raise 'Mistral does not support tools.' if input[:tools] 70 | 71 | if streaming 72 | content = '' 73 | 74 | stream_call_back = proc do |event, _parsed, _raw| 75 | partial_content = event.dig('choices', 0, 'delta', 'content') 76 | 77 | if partial_content 78 | content += partial_content 79 | feedback.call( 80 | { should_be_stored: false, 81 | interaction: { who: 'AI', message: partial_content } } 82 | ) 83 | end 84 | 85 | if event.dig('choices', 0, 'finish_reason') 86 | feedback.call( 87 | { should_be_stored: !(content.nil? || content == ''), 88 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 89 | finished: true } 90 | ) 91 | end 92 | end 93 | 94 | @client.chat_completions( 95 | Logic::Mistral::Tokens.apply_policies!(cartridge, payload), 96 | server_sent_events: true, &stream_call_back 97 | ) 98 | else 99 | result = @client.chat_completions( 100 | Logic::Mistral::Tokens.apply_policies!(cartridge, payload), 101 | server_sent_events: false 102 | ) 103 | 104 | content = result.dig('choices', 0, 'message', 'content') 105 | 106 | feedback.call( 107 | { should_be_stored: !(content.nil? || content.to_s.strip == ''), 108 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 109 | finished: true } 110 | ) 111 | end 112 | end 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /components/providers/ollama.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ollama-ai' 4 | 5 | require_relative 'base' 6 | 7 | require_relative '../../logic/providers/ollama/tokens' 8 | require_relative '../../logic/helpers/hash' 9 | require_relative '../../logic/cartridge/default' 10 | 11 | module NanoBot 12 | module Components 13 | module Providers 14 | class Ollama < Base 15 | attr_reader :settings 16 | 17 | CHAT_SETTINGS = %i[ 18 | model template stream format raw 19 | ].freeze 20 | 21 | CHAT_OPTIONS = %i[ 22 | num_keep seed num_predict top_k top_p tfs_z typical_p repeat_last_n 23 | temperature repeat_penalty presence_penalty frequency_penalty 24 | mirostat mirostat_tau mirostat_eta penalize_newline numa num_ctx 25 | num_batch num_gpu main_gpu low_vram f16_kv vocab_only use_mmap 26 | use_mlock num_thread stop 27 | ].freeze 28 | 29 | def initialize(options, settings, credentials, _environment) 30 | @settings = settings 31 | 32 | ollama_options = if options 33 | options.transform_keys { |key| key.to_s.gsub('-', '_').to_sym } 34 | else 35 | {} 36 | end 37 | 38 | unless @settings.key?(:stream) 39 | @settings = Marshal.load(Marshal.dump(@settings)) 40 | @settings[:stream] = Logic::Helpers::Hash.fetch( 41 | Logic::Cartridge::Default.instance.values, %i[provider settings stream] 42 | ) 43 | end 44 | 45 | ollama_options[:server_sent_events] = @settings[:stream] 46 | 47 | credentials ||= {} 48 | 49 | @client = ::Ollama.new( 50 | credentials: credentials.transform_keys { |key| key.to_s.gsub('-', '_').to_sym }, 51 | options: ollama_options 52 | ) 53 | end 54 | 55 | def evaluate(input, streaming, cartridge, &feedback) 56 | messages = input[:history].map do |event| 57 | { role: event[:who] == 'user' ? 'user' : 'assistant', 58 | content: event[:message], 59 | _meta: { at: event[:at] } } 60 | end 61 | 62 | %i[backdrop directive].each do |key| 63 | next unless input[:behavior][key] 64 | 65 | messages.prepend( 66 | { role: key == :directive ? 'system' : 'user', 67 | content: input[:behavior][key], 68 | _meta: { at: Time.now } } 69 | ) 70 | end 71 | 72 | payload = { messages: } 73 | 74 | CHAT_SETTINGS.each do |key| 75 | payload[key] = @settings[key] unless payload.key?(key) || !@settings.key?(key) 76 | end 77 | 78 | if @settings.key?(:options) 79 | options = {} 80 | 81 | CHAT_OPTIONS.each do |key| 82 | options[key] = @settings[:options][key] unless options.key?(key) || !@settings[:options].key?(key) 83 | end 84 | 85 | payload[:options] = options unless options.empty? 86 | end 87 | 88 | raise 'Ollama does not support tools.' if input[:tools] 89 | 90 | if streaming 91 | content = '' 92 | 93 | stream_call_back = proc do |event, _raw| 94 | partial_content = event.dig('message', 'content') 95 | 96 | if partial_content 97 | content += partial_content 98 | feedback.call( 99 | { should_be_stored: false, 100 | interaction: { who: 'AI', message: partial_content } } 101 | ) 102 | end 103 | 104 | if event['done'] 105 | feedback.call( 106 | { should_be_stored: !(content.nil? || content == ''), 107 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 108 | finished: true } 109 | ) 110 | end 111 | end 112 | 113 | @client.chat( 114 | Logic::Ollama::Tokens.apply_policies!(cartridge, payload), 115 | server_sent_events: true, &stream_call_back 116 | ) 117 | else 118 | result = @client.chat( 119 | Logic::Ollama::Tokens.apply_policies!(cartridge, payload), 120 | server_sent_events: false 121 | ) 122 | 123 | content = result.map { |event| event.dig('message', 'content') }.join 124 | 125 | feedback.call( 126 | { should_be_stored: !(content.nil? || content.to_s.strip == ''), 127 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 128 | finished: true } 129 | ) 130 | end 131 | end 132 | end 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /components/providers/openai.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'openai' 4 | 5 | require 'faraday/typhoeus' 6 | 7 | require_relative 'base' 8 | require_relative '../crypto' 9 | 10 | require_relative '../../logic/providers/openai/tools' 11 | require_relative '../../logic/providers/openai/tokens' 12 | 13 | require_relative 'tools' 14 | 15 | module NanoBot 16 | module Components 17 | module Providers 18 | class OpenAI < Base 19 | DEFAULT_ADDRESS = 'https://api.openai.com' 20 | 21 | CHAT_SETTINGS = %i[ 22 | model stream frequency_penalty logit_bias logprobs top_logprobs 23 | max_tokens n presence_penalty response_format seed stop temperature 24 | top_p tool_choice 25 | ].freeze 26 | 27 | attr_reader :settings 28 | 29 | def initialize(_options, settings, credentials, environment: {}) 30 | @settings = settings 31 | @credentials = credentials 32 | @environment = environment 33 | 34 | uri_base = if @credentials[:address].nil? || @credentials[:address].to_s.strip.empty? 35 | "#{DEFAULT_ADDRESS}/" 36 | else 37 | "#{@credentials[:address].to_s.sub(%r{/$}, '')}/" 38 | end 39 | 40 | @client = ::OpenAI::Client.new(uri_base:, access_token: @credentials[:'access-token']) do |faraday| 41 | faraday.adapter :typhoeus 42 | end 43 | end 44 | 45 | def evaluate(input, streaming, cartridge, &feedback) 46 | messages = input[:history].map do |event| 47 | if event[:message].nil? && event[:meta] && event[:meta][:tool_calls] 48 | { role: 'assistant', content: nil, 49 | tool_calls: event[:meta][:tool_calls], 50 | _meta: { at: event[:at] } } 51 | elsif event[:who] == 'tool' 52 | { role: event[:who], content: event[:message].to_s, 53 | tool_call_id: event[:meta][:id], 54 | name: event[:meta][:name], 55 | _meta: { at: event[:at] } } 56 | else 57 | { role: event[:who] == 'user' ? 'user' : 'assistant', 58 | content: event[:message], 59 | _meta: { at: event[:at] } } 60 | end 61 | end 62 | 63 | %i[backdrop directive].each do |key| 64 | next unless input[:behavior][key] 65 | 66 | messages.prepend( 67 | { role: key == :directive ? 'system' : 'user', 68 | content: input[:behavior][key], 69 | _meta: { at: Time.now } } 70 | ) 71 | end 72 | 73 | payload = { user: OpenAI.end_user(@settings, @environment), messages: } 74 | 75 | CHAT_SETTINGS.each do |key| 76 | payload[key] = @settings[key] if @settings.key?(key) 77 | end 78 | 79 | payload.delete(:logit_bias) if payload.key?(:logit_bias) && payload[:logit_bias].nil? 80 | 81 | payload[:tools] = input[:tools].map { |raw| Logic::OpenAI::Tools.adapt(raw) } if input[:tools] 82 | 83 | if streaming 84 | content = '' 85 | tools = [] 86 | 87 | payload[:stream] = proc do |chunk, _bytesize| 88 | partial_content = chunk.dig('choices', 0, 'delta', 'content') 89 | partial_tools = chunk.dig('choices', 0, 'delta', 'tool_calls') 90 | 91 | if partial_tools 92 | partial_tools.each do |partial_tool| 93 | tools[partial_tool['index']] = {} if tools[partial_tool['index']].nil? 94 | 95 | partial_tool.keys.reject { |key| ['index'].include?(key) }.each do |key| 96 | target = tools[partial_tool['index']] 97 | 98 | if partial_tool[key].is_a?(Hash) 99 | target[key] = {} if target[key].nil? 100 | partial_tool[key].each_key do |sub_key| 101 | target[key][sub_key] = '' if target[key][sub_key].nil? 102 | 103 | target[key][sub_key] += partial_tool[key][sub_key] 104 | end 105 | else 106 | target[key] = '' if target[key].nil? 107 | 108 | target[key] += partial_tool[key] 109 | end 110 | end 111 | end 112 | end 113 | 114 | if partial_content 115 | content += partial_content 116 | feedback.call( 117 | { should_be_stored: false, 118 | interaction: { who: 'AI', message: partial_content } } 119 | ) 120 | end 121 | 122 | if chunk.dig('choices', 0, 'finish_reason') 123 | if tools&.size&.positive? 124 | feedback.call( 125 | { should_be_stored: true, 126 | needs_another_round: true, 127 | interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } } 128 | ) 129 | Tools.apply( 130 | cartridge, input[:tools], tools, feedback, Logic::OpenAI::Tools 131 | ).each do |interaction| 132 | feedback.call({ should_be_stored: true, needs_another_round: true, interaction: }) 133 | end 134 | end 135 | 136 | feedback.call( 137 | { should_be_stored: !(content.nil? || content.to_s.strip == ''), 138 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 139 | finished: true } 140 | ) 141 | end 142 | end 143 | 144 | begin 145 | @client.chat(parameters: Logic::OpenAI::Tokens.apply_policies!(cartridge, payload)) 146 | rescue StandardError => e 147 | raise e.class, e.response[:body] if e.respond_to?(:response) && e.response && e.response[:body] 148 | 149 | raise e 150 | end 151 | else 152 | begin 153 | result = @client.chat(parameters: Logic::OpenAI::Tokens.apply_policies!(cartridge, payload)) 154 | rescue StandardError => e 155 | raise e.class, e.response[:body] if e.respond_to?(:response) && e.response && e.response[:body] 156 | 157 | raise e 158 | end 159 | 160 | raise StandardError, result['error'] if result['error'] 161 | 162 | tools = result.dig('choices', 0, 'message', 'tool_calls') 163 | 164 | if tools&.size&.positive? 165 | feedback.call( 166 | { should_be_stored: true, 167 | needs_another_round: true, 168 | interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } } 169 | ) 170 | Tools.apply( 171 | cartridge, input[:tools], tools, feedback, Logic::OpenAI::Tools 172 | ).each do |interaction| 173 | feedback.call({ should_be_stored: true, needs_another_round: true, interaction: }) 174 | end 175 | end 176 | 177 | content = result.dig('choices', 0, 'message', 'content') 178 | 179 | feedback.call( 180 | { should_be_stored: !(content.nil? || content == ''), 181 | interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, 182 | finished: true } 183 | ) 184 | end 185 | end 186 | 187 | def self.end_user(settings, environment) 188 | user = ENV.fetch('NANO_BOTS_END_USER', nil) 189 | 190 | user = settings[:user] if !settings[:user].nil? && !settings[:user].to_s.strip.empty? 191 | 192 | candidate = environment && ( 193 | environment['NANO_BOTS_END_USER'] || 194 | environment[:NANO_BOTS_END_USER] 195 | ) 196 | 197 | user = candidate if !candidate.nil? && !candidate.to_s.strip.empty? 198 | 199 | user = if user.nil? || user.to_s.strip.empty? 200 | 'unknown' 201 | else 202 | user.to_s.strip 203 | end 204 | 205 | Crypto.encrypt(user, soft: true) 206 | end 207 | end 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /components/providers/tools.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../embedding' 4 | require_relative '../../logic/cartridge/safety' 5 | 6 | require 'concurrent' 7 | 8 | module NanoBot 9 | module Components 10 | module Providers 11 | module Tools 12 | def self.confirming(tool, feedback) 13 | feedback.call( 14 | { should_be_stored: false, 15 | interaction: { who: 'AI', message: nil, meta: { 16 | tool: { action: 'confirming', id: tool[:id], name: tool[:label], parameters: tool[:parameters] } 17 | } } } 18 | ) 19 | end 20 | 21 | def self.apply(cartridge, function_cartridge, tools, feedback, tools_logic) 22 | prepared_tools = tools_logic.prepare(function_cartridge, tools) 23 | 24 | if Logic::Cartridge::Safety.confirmable?(cartridge) 25 | prepared_tools.each { |tool| tool[:allowed] = confirming(tool, feedback) } 26 | else 27 | prepared_tools.each { |tool| tool[:allowed] = true } 28 | end 29 | 30 | futures = prepared_tools.map do |tool| 31 | Concurrent::Promises.future do 32 | if tool[:allowed] 33 | process!(tool, feedback, function_cartridge, cartridge) 34 | else 35 | tool[:output] = 36 | "We asked the user you're chatting with for permission, but the user did not allow you to run this tool or function." 37 | tool 38 | end 39 | end 40 | end 41 | 42 | results = Concurrent::Promises.zip(*futures).value! 43 | 44 | results.map do |applied_tool| 45 | { 46 | who: 'tool', 47 | message: applied_tool[:output], 48 | meta: { id: applied_tool[:id], name: applied_tool[:name] } 49 | } 50 | end 51 | end 52 | 53 | def self.process!(tool, feedback, _function_cartridge, cartridge) 54 | feedback.call( 55 | { should_be_stored: false, 56 | interaction: { who: 'AI', message: nil, meta: { 57 | tool: { action: 'executing', id: tool[:id], name: tool[:label], parameters: tool[:parameters] } 58 | } } } 59 | ) 60 | 61 | call = { 62 | parameters: %w[parameters], 63 | values: [tool[:parameters]], 64 | safety: { sandboxed: Logic::Cartridge::Safety.sandboxed?(cartridge) } 65 | } 66 | 67 | if %i[fennel lua clojure].count { |key| !tool[:source][key].nil? } > 1 68 | raise StandardError, 'conflicting tools' 69 | end 70 | 71 | if !tool[:source][:fennel].nil? 72 | call[:source] = tool[:source][:fennel] 73 | tool[:output] = Components::Embedding.fennel(**call) 74 | elsif !tool[:source][:clojure].nil? 75 | call[:source] = tool[:source][:clojure] 76 | tool[:output] = Components::Embedding.clojure(**call) 77 | elsif !tool[:source][:lua].nil? 78 | call[:source] = tool[:source][:lua] 79 | tool[:output] = Components::Embedding.lua(**call) 80 | else 81 | raise 'missing source code' 82 | end 83 | 84 | feedback.call( 85 | { should_be_stored: false, 86 | interaction: { who: 'AI', message: nil, meta: { 87 | tool: { 88 | action: 'responding', id: tool[:id], name: tool[:label], 89 | parameters: tool[:parameters], output: tool[:output] 90 | } 91 | } } } 92 | ) 93 | 94 | tool 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /components/storage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'babosa' 4 | 5 | require_relative '../logic/helpers/hash' 6 | require_relative 'crypto' 7 | 8 | module NanoBot 9 | module Components 10 | class Storage 11 | EXTENSIONS = %w[yml yaml markdown mdown mkdn md].freeze 12 | 13 | def self.end_user(cartridge, environment) 14 | user = ENV.fetch('NANO_BOTS_END_USER', nil) 15 | 16 | if cartridge[:provider][:id] == 'openai' && 17 | !cartridge[:provider][:settings][:user].nil? && 18 | !cartridge[:provider][:settings][:user].to_s.strip.empty? 19 | user = cartridge[:provider][:settings][:user] 20 | end 21 | 22 | candidate = environment && ( 23 | environment['NANO_BOTS_END_USER'] || 24 | environment[:NANO_BOTS_END_USER] 25 | ) 26 | 27 | user = candidate if !candidate.nil? && !candidate.to_s.strip.empty? 28 | 29 | user = if user.nil? || user.to_s.strip.empty? 30 | 'unknown' 31 | else 32 | user.to_s.strip 33 | end 34 | 35 | Crypto.encrypt(user, soft: true) 36 | end 37 | 38 | def self.build_path_for_state_file(key, cartridge, environment: {}) 39 | path = [ 40 | Logic::Helpers::Hash.fetch(cartridge, %i[state path]), 41 | Logic::Helpers::Hash.fetch(cartridge, %i[state directory]), 42 | ENV.fetch('NANO_BOTS_STATE_PATH', nil), 43 | ENV.fetch('NANO_BOTS_STATE_DIRECTORY', nil) 44 | ].find do |candidate| 45 | !candidate.nil? && !candidate.empty? 46 | end 47 | 48 | path = "#{user_home!.sub(%r{/$}, '')}/.local/state/nano-bots" if path.nil? 49 | 50 | path = "#{path.sub(%r{/$}, '')}/ruby-nano-bots" 51 | 52 | path = "#{path}/#{cartridge[:meta][:author].to_slug.normalize}" 53 | path = "#{path}/#{cartridge[:meta][:name].to_slug.normalize}" 54 | path = "#{path}/#{cartridge[:meta][:version].to_s.gsub('.', '-').to_slug.normalize}" 55 | path = "#{path}/#{end_user(cartridge, environment)}" 56 | path = "#{path}/#{Crypto.encrypt(key, soft: true)}" 57 | 58 | "#{path}/state.json" 59 | end 60 | 61 | def self.cartridges_path(components: {}) 62 | components[:directory?] = ->(path) { File.directory?(path) } unless components.key?(:directory?) 63 | components[:ENV] = ENV unless components.key?(:ENV) 64 | 65 | default = "#{user_home!(components:).sub(%r{/$}, '')}/.local/share/nano-bots/cartridges" 66 | 67 | from_environment = [ 68 | components[:ENV].fetch('NANO_BOTS_CARTRIDGES_PATH', nil), 69 | components[:ENV].fetch('NANO_BOTS_CARTRIDGES_DIRECTORY', nil) 70 | ].compact 71 | 72 | elected = [ 73 | from_environment.empty? ? nil : from_environment.join(':'), 74 | default 75 | ].compact.uniq.filter do |path| 76 | path.split(':').any? { |candidate| components[:directory?].call(candidate) } 77 | end.compact.first 78 | 79 | return default unless elected 80 | 81 | elected = elected.split(':').filter do |path| 82 | components[:directory?].call(path) 83 | end.compact 84 | 85 | elected.size.positive? ? elected.join(':') : default 86 | end 87 | 88 | def self.cartridge_path(path) 89 | partial = File.join(File.dirname(path), File.basename(path, File.extname(path))) 90 | 91 | candidates = [path] 92 | 93 | EXTENSIONS.each do |extension| 94 | candidates << "#{partial}.#{extension}" 95 | end 96 | 97 | directories = [ 98 | ENV.fetch('NANO_BOTS_CARTRIDGES_PATH', nil), 99 | ENV.fetch('NANO_BOTS_CARTRIDGES_DIRECTORY', nil) 100 | ].compact.map do |directory| 101 | directory.split(':') 102 | end.flatten.map { |directory| directory.sub(%r{/$}, '') } 103 | 104 | directories.each do |directory| 105 | partial = File.join(File.dirname(partial), File.basename(partial, File.extname(partial))) 106 | 107 | partial = partial.sub(%r{^\.?/}, '') 108 | 109 | candidates << "#{directory}/#{partial}" 110 | 111 | EXTENSIONS.each do |extension| 112 | candidates << "#{directory}/#{partial}.#{extension}" 113 | end 114 | end 115 | 116 | directory = "#{user_home!.sub(%r{/$}, '')}/.local/share/nano-bots/cartridges" 117 | 118 | partial = File.join(File.dirname(partial), File.basename(partial, File.extname(partial))) 119 | 120 | partial = partial.sub(%r{^\.?/}, '') 121 | 122 | candidates << "#{directory}/#{partial}" 123 | 124 | EXTENSIONS.each do |extension| 125 | candidates << "#{directory}/#{partial}.#{extension}" 126 | end 127 | 128 | candidates = candidates.uniq 129 | 130 | candidates.find do |candidate| 131 | File.exist?(candidate) && File.file?(candidate) 132 | end 133 | end 134 | 135 | def self.user_home!(components: {}) 136 | return components[:home] if components[:home] 137 | 138 | [Dir.home, `echo ~`.strip, '~'].find do |candidate| 139 | !candidate.nil? && !candidate.empty? 140 | end 141 | end 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /components/stream.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'stringio' 4 | 5 | module NanoBot 6 | module Components 7 | class Stream < StringIO 8 | def write(*args) 9 | if @callback 10 | begin 11 | @accumulated += args.first 12 | rescue StandardError => _e 13 | @accumulated = "#{@accumulated.force_encoding('UTF-8')}#{args.first.force_encoding('UTF-8')}" 14 | end 15 | 16 | if @callback.arity == 3 17 | @callback.call(@accumulated, args.first, false) 18 | else 19 | @callback.call(@accumulated, args.first, false, args[1]) 20 | end 21 | end 22 | super(args.first) 23 | end 24 | 25 | def callback=(block) 26 | @accumulated = '' 27 | @callback = block 28 | end 29 | 30 | def finish 31 | flush 32 | result = string.clone 33 | truncate(0) 34 | rewind 35 | 36 | if @callback 37 | if @callback.arity == 3 38 | @callback.call(@accumulated, nil, true) 39 | else 40 | @callback.call(@accumulated, nil, true, nil) 41 | end 42 | @callback = nil 43 | @accumulated = nil 44 | end 45 | 46 | result 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /controllers/cartridges.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../components/storage' 4 | require_relative '../logic/helpers/hash' 5 | require_relative '../logic/cartridge/default' 6 | require_relative '../logic/cartridge/parser' 7 | 8 | module NanoBot 9 | module Controllers 10 | class Cartridges 11 | def self.load(path) 12 | Logic::Cartridge::Parser.parse(File.read(path), format: File.extname(path)) 13 | end 14 | 15 | def self.all(components: {}) 16 | files = {} 17 | 18 | paths = Components::Storage.cartridges_path(components:) 19 | 20 | paths.split(':').each do |path| 21 | Dir.glob("#{path}/**/*.{yml,yaml,markdown,mdown,mkdn,md}").each do |file| 22 | files[Pathname.new(file).realpath] = { 23 | base: path, 24 | path: Pathname.new(file).realpath 25 | } 26 | end 27 | end 28 | 29 | cartridges = [] 30 | 31 | files.values.uniq.map do |file| 32 | cartridge = load(file[:path]).merge( 33 | { 34 | system: { 35 | id: file[:path].to_s.sub( 36 | /^#{Regexp.escape(file[:base])}/, '' 37 | ).sub(%r{^/}, '').sub(/\.[^.]+\z/, ''), 38 | path: file[:path], 39 | base: file[:base] 40 | } 41 | } 42 | ) 43 | 44 | next if cartridge[:meta][:name].nil? 45 | 46 | cartridges << cartridge 47 | rescue StandardError => _e 48 | end 49 | 50 | cartridges = cartridges.sort_by { |cartridge| cartridge[:meta][:name] } 51 | 52 | cartridges.prepend( 53 | { system: { id: '-' }, meta: { name: 'Default', symbol: '🤖' } } 54 | ) 55 | 56 | cartridges 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /controllers/instance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../logic/helpers/hash' 4 | require_relative '../components/provider' 5 | require_relative '../components/storage' 6 | require_relative '../components/stream' 7 | require_relative 'cartridges' 8 | require_relative 'interfaces/repl' 9 | require_relative 'interfaces/eval' 10 | require_relative 'session' 11 | 12 | module NanoBot 13 | module Controllers 14 | class Instance 15 | def initialize(cartridge_path:, stream:, state: nil, environment: {}) 16 | @stream = stream 17 | 18 | load_cartridge!(cartridge_path) 19 | 20 | provider = Components::Provider.new(@cartridge[:provider], environment:) 21 | 22 | @session = Session.new( 23 | provider:, cartridge: @cartridge, state:, stream: @stream, environment: 24 | ) 25 | end 26 | 27 | def cartridge 28 | @safe_cartridge 29 | end 30 | 31 | def state 32 | @session.state 33 | end 34 | 35 | def boot(as: 'eval', &block) 36 | @stream.callback = block if block && @stream.is_a?(Components::Stream) 37 | 38 | Interfaces::REPL.boot(@cartridge, @session, as:) 39 | 40 | return unless @stream.is_a?(Components::Stream) 41 | 42 | @stream.finish 43 | end 44 | 45 | def prompt 46 | Interfaces::REPL.prompt(@cartridge) 47 | end 48 | 49 | def eval(input, as: 'eval', &block) 50 | @stream.callback = block if block && @stream.is_a?(Components::Stream) 51 | 52 | Interfaces::Eval.evaluate(input, @cartridge, @session, as) 53 | 54 | return unless @stream.is_a?(Components::Stream) 55 | 56 | @stream.finish 57 | end 58 | 59 | def repl 60 | if @stream.is_a?(StringIO) 61 | @stream.flush 62 | @stream = $stdout 63 | @session.stream = @stream 64 | end 65 | Interfaces::REPL.start(@cartridge, @session) 66 | end 67 | 68 | private 69 | 70 | def load_cartridge!(path) 71 | if !path.is_a?(String) && !path.is_a?(Symbol) 72 | @cartridge = path 73 | else 74 | elected_path = if path.strip == '-' 75 | File.expand_path('../static/cartridges/baseline.yml', __dir__) 76 | else 77 | Components::Storage.cartridge_path(path) 78 | end 79 | 80 | if elected_path.nil? 81 | @stream.write("Cartridge file not found: \"#{path}\"\n") 82 | raise StandardError, "Cartridge file not found: \"#{path}\"" 83 | end 84 | 85 | @cartridge = Cartridges.load(elected_path) 86 | end 87 | 88 | @safe_cartridge = Marshal.load(Marshal.dump(@cartridge)) 89 | 90 | inject_environment_variables!(@cartridge) 91 | end 92 | 93 | def inject_environment_variables!(node) 94 | case node 95 | when Hash 96 | node.each do |key, value| 97 | node[key] = inject_environment_variables!(value) 98 | end 99 | when Array 100 | node.each_with_index do |value, index| 101 | node[index] = inject_environment_variables!(value) 102 | end 103 | when String 104 | node.start_with?('ENV') ? ENV.fetch(node.sub(/^ENV./, ''), nil) : node 105 | else 106 | node 107 | end 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /controllers/interfaces/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../instance' 4 | require_relative '../../static/gem' 5 | 6 | module NanoBot 7 | module Controllers 8 | module Interfaces 9 | module CLI 10 | def self.handle! 11 | case ARGV[0] 12 | when 'version' 13 | puts NanoBot::GEM[:version] 14 | exit 15 | when 'specification' 16 | puts NanoBot::GEM[:specification] 17 | exit 18 | when 'security' 19 | result = NanoBot.security.check 20 | 21 | if result[:encryption] 22 | puts "\n✅ Encryption is enabled and properly working." 23 | puts ' This means that your data is stored in an encrypted format on your disk.' 24 | else 25 | puts "\n❌ Encryption is not being utilized to store your content." 26 | puts ' This means that your data can be easily read because it is stored in plaintext.' 27 | end 28 | 29 | if result[:password] 30 | puts "\n✅ A password is being used for the encrypted content." 31 | puts ' This means that only those who possess the password can decrypt your data.' 32 | else 33 | puts "\n❌ No custom password is being used for the encrypted content." 34 | puts ' This means that anyone can easily decrypt your data.' 35 | end 36 | 37 | puts '' 38 | 39 | exit 40 | when 'help', '', nil 41 | puts '' 42 | puts "Nano Bots #{NanoBot::GEM[:version]}" 43 | puts '' 44 | puts ' nb - - eval "hello"' 45 | puts ' nb - - repl' 46 | puts '' 47 | puts ' nb cartridge.yml - eval "hello"' 48 | puts ' nb cartridge.yml - repl' 49 | puts '' 50 | puts ' nb - STATE-KEY eval "hello"' 51 | puts ' nb - STATE-KEY repl' 52 | puts '' 53 | puts ' nb cartridge.yml STATE-KEY eval "hello"' 54 | puts ' nb cartridge.yml STATE-KEY repl' 55 | puts '' 56 | puts ' nb - - cartridge' 57 | puts ' nb cartridge.yml - cartridge' 58 | puts '' 59 | puts ' nb - STATE-KEY state' 60 | puts ' nb cartridge.yml STATE-KEY state' 61 | puts '' 62 | puts ' nb security' 63 | puts ' nb specification' 64 | puts ' nb version' 65 | puts ' nb help' 66 | puts '' 67 | exit 68 | end 69 | 70 | params = { cartridge_path: ARGV[0], state: ARGV[1], command: ARGV[2] } 71 | 72 | bot = Instance.new( 73 | cartridge_path: params[:cartridge_path], state: params[:state], stream: $stdout 74 | ) 75 | 76 | case params[:command] 77 | when 'eval' 78 | params[:input] = ARGV[3..]&.join(' ') 79 | params[:input] = $stdin.read.chomp if params[:input].nil? || params[:input].empty? 80 | bot.eval(params[:input]) 81 | when 'repl' 82 | bot.repl 83 | when 'state' 84 | pp bot.state 85 | when 'cartridge' 86 | puts YAML.dump(bot.cartridge) 87 | else 88 | raise "Command not found: [#{params[:command]}]" 89 | end 90 | end 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /controllers/interfaces/eval.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../logic/cartridge/affixes' 4 | 5 | module NanoBot 6 | module Controllers 7 | module Interfaces 8 | module Eval 9 | def self.evaluate(input, cartridge, session, mode) 10 | prefix = Logic::Cartridge::Affixes.get(cartridge, mode.to_sym, :output, :prefix) 11 | suffix = Logic::Cartridge::Affixes.get(cartridge, mode.to_sym, :output, :suffix) 12 | 13 | session.print(prefix) unless prefix.nil? 14 | 15 | session.evaluate_and_print(input, mode:) 16 | 17 | session.print(suffix) unless suffix.nil? 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /controllers/interfaces/repl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pry' 4 | require 'rainbow' 5 | 6 | require_relative '../../logic/helpers/hash' 7 | require_relative '../../logic/cartridge/affixes' 8 | 9 | module NanoBot 10 | module Controllers 11 | module Interfaces 12 | module REPL 13 | COMMANDS_TO_BE_REMOVED = [ 14 | 'help', 'cd', 'find-method', 'ls', 'pry-backtrace', 'raise-up', 'reset', 'watch', 15 | 'whereami', 'wtf?', '!', 'amend-line', 'edit', 'hist', 'show-input', 'ri', 'show-doc', 16 | 'show-source', 'stat', 'import-set', 'play', '!!!', '!!@', '$', '?', '@', 'file-mode', 17 | 'history', 'quit', 'quit-program', 'reload-method', 'show-method', 'cat', 18 | 'change-inspector', 'change-prompt', 'clear-screen', 'fix-indent', 'list-inspectors', 19 | 'save-file', 'shell-mode', 'pry-version', 'reload-code', 'toggle-color', '!pry', 20 | 'disable-pry', 'jump-to', 'nesting', 'switch-to', 21 | 'pry-theme' 22 | ].freeze 23 | 24 | COMMANDS_TO_KEEP = [ 25 | '/whereami[!?]+/', '.', 'exit', 'exit-all', 'exit-program' 26 | ].freeze 27 | 28 | def self.boot(cartridge, session, prefix = nil, suffix = nil, as: 'repl') 29 | return unless Logic::Helpers::Hash.fetch(cartridge, %i[behaviors boot instruction]) 30 | 31 | prefix ||= Logic::Cartridge::Affixes.get(cartridge, as.to_sym, :output, :prefix) 32 | suffix ||= Logic::Cartridge::Affixes.get(cartridge, as.to_sym, :output, :suffix) 33 | 34 | session.print(prefix) unless prefix.nil? 35 | session.boot(mode: as) 36 | session.print(suffix) unless suffix.nil? 37 | end 38 | 39 | def self.start(cartridge, session) 40 | prefix = Logic::Cartridge::Affixes.get(cartridge, :repl, :output, :prefix) 41 | suffix = Logic::Cartridge::Affixes.get(cartridge, :repl, :output, :suffix) 42 | 43 | boot(cartridge, session, prefix, suffix) 44 | 45 | session.print("\n") if Logic::Helpers::Hash.fetch(cartridge, %i[behaviors boot instruction]) 46 | 47 | prompt = self.prompt(cartridge) 48 | 49 | handler = proc do |line| 50 | session.print(prefix) unless prefix.nil? 51 | session.evaluate_and_print(line, mode: 'repl') 52 | session.print(suffix) unless suffix.nil? 53 | session.print("\n") 54 | session.flush 55 | end 56 | 57 | pry_prompt = Pry::Prompt.new( 58 | 'REPL', 59 | 'REPL Prompt', 60 | [proc { prompt }, proc { 'MISSING INPUT' }] 61 | ) 62 | 63 | pry_instance = Pry.new({ prompt: pry_prompt }) 64 | 65 | pry_instance.config.correct_indent = false 66 | 67 | pry_instance.config.completer = Struct.new(:initialize, :call) do 68 | def initialize(...); end 69 | def call(...); end 70 | end 71 | 72 | first_whereami = true 73 | 74 | pry_instance.config.commands.block_command(/whereami --quiet(.*)/, '/whereami[!?]+/') do |line| 75 | unless first_whereami 76 | handler.call(line.nil? ? 'whereami --quiet' : "whereami --quiet#{line}") 77 | end 78 | first_whereami = false 79 | end 80 | 81 | pry_instance.config.commands.block_command(/\.(.*)/, '.') do |line| 82 | handler.call(line.nil? ? '.' : ".#{line}") 83 | end 84 | 85 | COMMANDS_TO_BE_REMOVED.each do |command| 86 | pry_instance.config.commands.block_command(command, 'handler') do |line| 87 | handler.call(line.nil? ? command : "#{command} #{line}") 88 | end 89 | end 90 | 91 | pry_instance.commands.block_command(/(.*)/, 'handler', &handler) 92 | 93 | Pry::REPL.new(pry_instance).start 94 | end 95 | 96 | def self.prompt(cartridge) 97 | prompt = Logic::Helpers::Hash.fetch(cartridge, %i[interfaces repl prompt]) 98 | result = '' 99 | 100 | if prompt.is_a?(Array) 101 | prompt.each do |partial| 102 | result += if partial[:color] 103 | Rainbow(partial[:text]).send(partial[:color]) 104 | else 105 | partial[:text] 106 | end 107 | end 108 | elsif prompt.is_a?(String) 109 | result = prompt 110 | else 111 | result = "🤖#{Rainbow('> ').blue}" 112 | end 113 | 114 | result 115 | end 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /controllers/interfaces/tools.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rainbow' 4 | 5 | require_relative '../../logic/cartridge/tools' 6 | require_relative '../../logic/cartridge/safety' 7 | require_relative '../../components/embedding' 8 | 9 | module NanoBot 10 | module Controllers 11 | module Interfaces 12 | module Tool 13 | def self.confirming(session, cartridge, mode, feedback) 14 | yeses = Logic::Cartridge::Safety.yeses(cartridge) 15 | default_answer = Logic::Cartridge::Safety.default_answer(cartridge) 16 | dispatch_feedback(session, cartridge, mode, feedback) 17 | session.flush 18 | answer = $stdin.gets.chomp.to_s.downcase.strip 19 | answer = default_answer if answer == '' 20 | session.print("\n") 21 | yeses.include?(answer) 22 | end 23 | 24 | def self.adapt(feedback, adapter, cartridge) 25 | call = { 26 | parameters: %w[id name parameters parameters-as-json output], 27 | values: [ 28 | feedback[:id], feedback[:name], feedback[:parameters], 29 | feedback[:parameters].to_json, 30 | feedback[:output] 31 | ], 32 | safety: { sandboxed: Logic::Cartridge::Safety.sandboxed?(cartridge) } 33 | } 34 | 35 | raise StandardError, 'conflicting adapters' if %i[fennel lua clojure].count { |key| !adapter[key].nil? } > 1 36 | 37 | if adapter[:fennel] 38 | call[:source] = adapter[:fennel] 39 | Components::Embedding.fennel(**call) 40 | elsif adapter[:clojure] 41 | call[:source] = adapter[:clojure] 42 | Components::Embedding.clojure(**call) 43 | elsif adapter[:lua] 44 | call[:parameters] = %w[id name parameters parameters_as_json output] 45 | call[:source] = adapter[:lua] 46 | Components::Embedding.lua(**call) 47 | else 48 | raise 'missing handler for adapter' 49 | end 50 | end 51 | 52 | def self.dispatch_feedback(session, cartridge, mode, feedback) 53 | enabled = Logic::Cartridge::Tools.feedback?(cartridge, mode.to_sym, feedback[:action].to_sym) 54 | 55 | enabled = true if feedback[:action].to_sym == :confirming 56 | 57 | return unless enabled 58 | 59 | color = Logic::Cartridge::Tools.fetch_from_interface( 60 | cartridge, mode.to_sym, feedback[:action].to_sym, [:color] 61 | ) 62 | 63 | adapter = Tool.adapter(cartridge, mode, feedback) 64 | 65 | if %i[fennel lua clojure].any? { |key| !adapter[key].nil? } 66 | message = adapt(feedback, adapter, cartridge) 67 | else 68 | message = "#{feedback[:name]} #{feedback[:parameters].to_json}" 69 | 70 | message += "\n#{feedback[:output]}" if feedback[:action].to_sym == :responding 71 | end 72 | 73 | message = "#{adapter[:prefix]}#{message}#{adapter[:suffix]}" 74 | 75 | session.print( 76 | color.nil? ? message : Rainbow(message).send(color), 77 | { tool: { action: feedback[:action].to_s } } 78 | ) 79 | end 80 | 81 | def self.adapter(cartridge, mode, feedback) 82 | prefix = Logic::Cartridge::Tools.fetch_from_interface( 83 | cartridge, mode.to_sym, feedback[:action].to_sym, [:prefix] 84 | ) 85 | 86 | suffix = Logic::Cartridge::Tools.fetch_from_interface( 87 | cartridge, mode.to_sym, feedback[:action].to_sym, [:suffix] 88 | ) 89 | 90 | fennel = Logic::Cartridge::Tools.fetch_from_interface( 91 | cartridge, mode.to_sym, feedback[:action].to_sym, %i[adapter fennel] 92 | ) 93 | 94 | lua = Logic::Cartridge::Tools.fetch_from_interface( 95 | cartridge, mode.to_sym, feedback[:action].to_sym, %i[adapter lua] 96 | ) 97 | 98 | clojure = Logic::Cartridge::Tools.fetch_from_interface( 99 | cartridge, mode.to_sym, feedback[:action].to_sym, %i[adapter clojure] 100 | ) 101 | 102 | { prefix:, suffix:, fennel:, lua:, clojure: } 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /controllers/security.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../components/crypto' 4 | 5 | module NanoBot 6 | module Controllers 7 | module Security 8 | def self.decrypt(content) 9 | Components::Crypto.decrypt(content) 10 | end 11 | 12 | def self.encrypt(content, soft: false) 13 | Components::Crypto.encrypt(content, soft:) 14 | end 15 | 16 | def self.check 17 | password = ENV.fetch('NANO_BOTS_ENCRYPTION_PASSWORD', nil) 18 | password = 'UNSAFE' unless password && password != '' 19 | 20 | { 21 | encryption: 22 | Components::Crypto.encrypt('SAFE') != 'SAFE' && 23 | Components::Crypto.encrypt('SAFE') != Components::Crypto.encrypt('SAFE') && 24 | Components::Crypto.decrypt(Components::Crypto.encrypt('SAFE')) == 'SAFE', 25 | password: password != 'UNSAFE' 26 | } 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /controllers/session.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'babosa' 4 | 5 | require 'fileutils' 6 | require 'rainbow' 7 | 8 | require_relative '../logic/helpers/hash' 9 | require_relative '../logic/cartridge/safety' 10 | require_relative '../logic/cartridge/streaming' 11 | require_relative '../logic/cartridge/interaction' 12 | require_relative '../logic/cartridge/fetch' 13 | require_relative 'interfaces/tools' 14 | require_relative '../components/stream' 15 | require_relative '../components/storage' 16 | require_relative '../components/adapter' 17 | require_relative '../components/crypto' 18 | 19 | module NanoBot 20 | module Controllers 21 | STREAM_TIMEOUT_IN_SECONDS = 5 22 | INFINITE_LOOP_PREVENTION = 10 23 | 24 | class Session 25 | attr_accessor :stream 26 | 27 | def initialize(provider:, cartridge:, state: nil, stream: $stdout, environment: {}) 28 | @stream = stream 29 | @provider = provider 30 | @cartridge = cartridge 31 | 32 | @stateless = state.nil? || state.strip == '-' || state.strip.empty? 33 | 34 | if @stateless 35 | @state = { history: [] } 36 | else 37 | @state_key = state.strip 38 | 39 | @state_path = Components::Storage.build_path_for_state_file( 40 | state.strip, @cartridge, environment: 41 | ) 42 | 43 | @state = load_state 44 | end 45 | end 46 | 47 | def state 48 | if @state[:history].empty? 49 | nil 50 | else 51 | { state: { path: @state_path, content: @state } } 52 | end 53 | end 54 | 55 | def load_state 56 | return { key: @state_key, history: [] } unless File.exist?(@state_path) 57 | 58 | @state = Logic::Helpers::Hash.symbolize_keys( 59 | JSON.parse(Components::Crypto.decrypt(File.read(@state_path))) 60 | ) 61 | end 62 | 63 | def store_state! 64 | FileUtils.mkdir_p(File.dirname(@state_path)) unless File.exist?(@state_path) 65 | 66 | File.write(@state_path, Components::Crypto.encrypt(JSON.generate(@state))) 67 | end 68 | 69 | def boot(mode:) 70 | instruction = Logic::Helpers::Hash.fetch(@cartridge, %i[behaviors boot instruction]) 71 | return unless instruction 72 | 73 | behavior = Logic::Helpers::Hash.fetch(@cartridge, %i[behaviors boot]) || {} 74 | 75 | @state[:history] << { 76 | at: Time.now, 77 | who: 'user', 78 | mode: mode.to_s, 79 | input: instruction, 80 | message: instruction 81 | } 82 | 83 | input = { behavior:, history: @state[:history] } 84 | 85 | process(input, mode:) 86 | end 87 | 88 | def evaluate_and_print(message, mode:) 89 | behavior = Logic::Helpers::Hash.fetch(@cartridge, %i[behaviors interaction]) || {} 90 | 91 | @state[:history] << { 92 | at: Time.now, 93 | who: 'user', 94 | mode: mode.to_s, 95 | input: message, 96 | message: Components::Adapter.apply( 97 | Logic::Cartridge::Interaction.input(@cartridge, mode.to_sym, message), @cartridge 98 | ) 99 | } 100 | 101 | input = { behavior:, history: @state[:history] } 102 | 103 | process(input, mode:) 104 | end 105 | 106 | def process(input, mode:) 107 | interface = Logic::Helpers::Hash.fetch(@cartridge, [:interfaces, mode.to_sym]) || {} 108 | 109 | input[:interface] = interface 110 | input[:tools] = @cartridge[:tools] 111 | 112 | needs_another_round = true 113 | 114 | rounds = 0 115 | 116 | while needs_another_round 117 | needs_another_round = process_interaction(input, mode:) 118 | rounds += 1 119 | raise StandardError, 'infinite loop prevention' if rounds > INFINITE_LOOP_PREVENTION 120 | end 121 | end 122 | 123 | def process_interaction(input, mode:) 124 | prefix = Logic::Cartridge::Affixes.get(@cartridge, mode.to_sym, :output, :prefix) 125 | suffix = Logic::Cartridge::Affixes.get(@cartridge, mode.to_sym, :output, :suffix) 126 | 127 | color = Logic::Cartridge::Fetch.cascate( 128 | @cartridge, [[:interfaces, mode.to_sym, :output, :color], %i[interfaces output color]] 129 | ) 130 | 131 | color = color.to_sym if color 132 | 133 | streaming = Logic::Cartridge::Streaming.enabled?(@cartridge, mode.to_sym) 134 | 135 | updated_at = Time.now 136 | 137 | ready = false 138 | 139 | needs_another_round = false 140 | 141 | @provider.evaluate(input, streaming, @cartridge) do |feedback| 142 | needs_another_round = true if feedback[:needs_another_round] 143 | 144 | updated_at = Time.now 145 | 146 | if feedback[:interaction] && 147 | feedback.dig(:interaction, :meta, :tool, :action) && 148 | feedback[:interaction][:meta][:tool][:action] == 'confirming' 149 | Interfaces::Tool.confirming(self, @cartridge, mode, feedback[:interaction][:meta][:tool]) 150 | else 151 | if feedback[:interaction] && feedback.dig(:interaction, :meta, :tool, :action) 152 | Interfaces::Tool.dispatch_feedback( 153 | self, @cartridge, mode, feedback[:interaction][:meta][:tool] 154 | ) 155 | end 156 | 157 | if feedback[:interaction] 158 | event = Marshal.load(Marshal.dump(feedback[:interaction])) 159 | event[:mode] = mode.to_s 160 | event[:output] = nil 161 | 162 | if feedback[:interaction][:who] == 'AI' && feedback[:interaction][:message] 163 | event[:output] = feedback[:interaction][:message] 164 | unless streaming 165 | output = Logic::Cartridge::Interaction.output( 166 | @cartridge, mode.to_sym, feedback[:interaction], streaming, feedback[:finished] 167 | ) 168 | output[:message] = Components::Adapter.apply(output[:message], @cartridge) 169 | event[:output] = (output[:message]).to_s 170 | end 171 | end 172 | 173 | if feedback[:should_be_stored] 174 | event[:at] = Time.now 175 | @state[:history] << event 176 | end 177 | 178 | if event[:output] && ((!feedback[:finished] && streaming) || (!streaming && feedback[:finished])) 179 | self.print(color ? Rainbow(event[:output]).send(color) : event[:output]) 180 | end 181 | 182 | # The `print` function already outputs a prefix and a suffix, so 183 | # we should add them afterwards to avoid printing them twice. 184 | event[:output] = "#{prefix}#{event[:output]}#{suffix}" 185 | end 186 | 187 | if feedback[:finished] 188 | flush 189 | ready = true 190 | end 191 | end 192 | end 193 | 194 | until ready 195 | seconds = (Time.now - updated_at).to_i 196 | raise StandardError, 'The stream has become unresponsive.' if seconds >= STREAM_TIMEOUT_IN_SECONDS 197 | end 198 | 199 | store_state! unless @stateless 200 | 201 | needs_another_round 202 | end 203 | 204 | def flush 205 | @stream.flush 206 | end 207 | 208 | def print(content, meta = nil) 209 | if @stream.is_a?(NanoBot::Components::Stream) 210 | @stream.write(content, meta) 211 | else 212 | @stream.write(content) 213 | end 214 | end 215 | end 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /docker-compose.example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | nano-bots: 4 | image: ruby:3.3.3-slim-bookworm 5 | command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev curl && curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | bash && gem install nano-bots -v 3.4.0 && bash" 6 | environment: 7 | ANTHROPIC_API_KEY: your-api-key 8 | 9 | COHERE_API_KEY: your-api-key 10 | 11 | # GOOGLE_API_KEY: your-api-key 12 | 13 | # GOOGLE_CREDENTIALS_FILE_PATH: /root/.config/google-credentials.json 14 | # GOOGLE_CREDENTIALS_FILE_CONTENTS: "contents" 15 | # GOOGLE_PROJECT_ID: your-project-id 16 | GOOGLE_REGION: us-east4 17 | 18 | MARITACA_API_KEY: 'your-api-key' 19 | 20 | MISTRAL_API_KEY: your-api-key 21 | 22 | OLLAMA_API_ADDRESS: http://localhost:11434 23 | 24 | OPENAI_API_KEY: your-access-token 25 | 26 | NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE 27 | NANO_BOTS_END_USER: your-user 28 | 29 | volumes: 30 | - ./google-credentials.json:/root/.config/google-credentials.json 31 | - ./your-cartridges:/root/.local/share/nano-bots/cartridges 32 | - ./your-state-path:/root/.local/state/nano-bots 33 | 34 | # If you are running the Ollama server on your localhost: 35 | # network_mode: host # WARNING: Be careful, this may be a security risk. 36 | -------------------------------------------------------------------------------- /logic/cartridge/adapters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../helpers/hash' 4 | require_relative 'default' 5 | 6 | module NanoBot 7 | module Logic 8 | module Cartridge 9 | module Adapter 10 | def self.expression(cartridge, interface, direction, language) 11 | adapter = [ 12 | { 13 | exists: (Helpers::Hash.fetch(cartridge, [:interfaces, direction, :adapter]) || {}).key?(language), 14 | value: Helpers::Hash.fetch(cartridge, [:interfaces, direction, :adapter, language]) 15 | }, 16 | { 17 | exists: (Helpers::Hash.fetch(cartridge, 18 | [:interfaces, interface, direction, :adapter]) || {}).key?(language), 19 | value: Helpers::Hash.fetch(cartridge, [:interfaces, interface, direction, :adapter, language]) 20 | } 21 | ].filter { |candidate| candidate[:exists] }.last 22 | 23 | return nil if adapter.nil? 24 | 25 | adapter[:value] 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /logic/cartridge/affixes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../helpers/hash' 4 | require_relative 'default' 5 | 6 | module NanoBot 7 | module Logic 8 | module Cartridge 9 | module Affixes 10 | def self.get(cartridge, interface, direction, kind) 11 | affix = [ 12 | { 13 | exists: (Helpers::Hash.fetch(cartridge, [:interfaces, direction]) || {}).key?(kind), 14 | value: Helpers::Hash.fetch(cartridge, [:interfaces, direction, kind]) 15 | }, 16 | { 17 | exists: (Helpers::Hash.fetch(cartridge, [:interfaces, interface, direction]) || {}).key?(kind), 18 | value: Helpers::Hash.fetch(cartridge, [:interfaces, interface, direction, kind]) 19 | } 20 | ].filter { |candidate| candidate[:exists] }.last 21 | 22 | if affix.nil? 23 | return Helpers::Hash.fetch( 24 | Default.instance.values, [:interfaces, interface, direction, kind] 25 | ) 26 | end 27 | 28 | affix[:value] 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /logic/cartridge/default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | require 'singleton' 5 | 6 | require_relative '../helpers/hash' 7 | 8 | module NanoBot 9 | module Logic 10 | module Cartridge 11 | class Default 12 | include Singleton 13 | 14 | def values 15 | return @values if @values 16 | 17 | path = File.expand_path('../../static/cartridges/default.yml', __dir__) 18 | cartridge = YAML.safe_load_file(path, permitted_classes: [Symbol]) 19 | @values = Logic::Helpers::Hash.symbolize_keys(cartridge) 20 | @values 21 | end 22 | 23 | def baseline 24 | return @baseline if @baseline 25 | 26 | path = File.expand_path('../../static/cartridges/baseline.yml', __dir__) 27 | cartridge = YAML.safe_load_file(path, permitted_classes: [Symbol]) 28 | @baseline = Logic::Helpers::Hash.symbolize_keys(cartridge) 29 | @baseline 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /logic/cartridge/fetch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'default' 4 | require_relative '../helpers/hash' 5 | 6 | module NanoBot 7 | module Logic 8 | module Cartridge 9 | module Fetch 10 | def self.cascate(cartridge, paths) 11 | results = paths.map { |path| Helpers::Hash.fetch(cartridge, path) } 12 | result = results.find { |candidate| !candidate.nil? } 13 | return result unless result.nil? 14 | 15 | results = paths.map { |path| Helpers::Hash.fetch(Default.instance.values, path) } 16 | result = results.find { |candidate| !candidate.nil? } 17 | return result unless result.nil? 18 | 19 | nil 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /logic/cartridge/interaction.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'affixes' 4 | require_relative 'adapters' 5 | 6 | module NanoBot 7 | module Logic 8 | module Cartridge 9 | module Interaction 10 | def self.input(cartridge, interface, content) 11 | lua = Adapter.expression(cartridge, interface, :input, :lua) 12 | fennel = Adapter.expression(cartridge, interface, :input, :fennel) 13 | clojure = Adapter.expression(cartridge, interface, :input, :clojure) 14 | 15 | prefix = Affixes.get(cartridge, interface, :input, :prefix) 16 | suffix = Affixes.get(cartridge, interface, :input, :suffix) 17 | 18 | { content:, prefix:, suffix:, lua:, fennel:, clojure: } 19 | end 20 | 21 | def self.output(cartridge, interface, result, streaming, _finished) 22 | if streaming 23 | result[:message] = { content: result[:message], lua: nil, fennel: nil, clojure: nil } 24 | return result 25 | end 26 | 27 | lua = Adapter.expression(cartridge, interface, :output, :lua) 28 | fennel = Adapter.expression(cartridge, interface, :output, :fennel) 29 | clojure = Adapter.expression(cartridge, interface, :output, :clojure) 30 | 31 | result[:message] = { content: result[:message], lua:, fennel:, clojure: } 32 | 33 | result 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /logic/cartridge/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'singleton' 4 | 5 | require 'redcarpet' 6 | require 'redcarpet/render_strip' 7 | 8 | module NanoBot 9 | module Logic 10 | module Cartridge 11 | module Parser 12 | def self.parse(raw, format:) 13 | normalized = format.to_s.downcase.gsub('.', '').strip 14 | 15 | if %w[yml yaml].include?(normalized) 16 | yaml(raw) 17 | elsif %w[markdown mdown mkdn md].include?(normalized) 18 | markdown(raw) 19 | else 20 | raise "Unknown cartridge format: '#{format}'" 21 | end 22 | end 23 | 24 | def self.markdown(raw) 25 | yaml_source = [] 26 | 27 | tools = [] 28 | 29 | blocks = Markdown.new.render(raw).blocks 30 | 31 | previous_block_is_tool = false 32 | 33 | blocks.each do |block| 34 | if block[:language] == 'yaml' 35 | parsed = Logic::Helpers::Hash.symbolize_keys( 36 | YAML.safe_load(block[:source], permitted_classes: [Symbol]) 37 | ) 38 | 39 | if parsed.key?(:tools) && parsed[:tools].is_a?(Array) && !parsed[:tools].empty? 40 | previous_block_is_tool = true 41 | 42 | tools.concat(parsed[:tools]) 43 | 44 | parsed.delete(:tools) 45 | 46 | unless parsed.empty? 47 | yaml_source << YAML.dump( 48 | Logic::Helpers::Hash.stringify_keys(parsed) 49 | ).gsub(/^---/, '') # TODO: Is this safe enough? 50 | end 51 | else 52 | yaml_source << block[:source] 53 | previous_block_is_tool = false 54 | nil 55 | end 56 | elsif previous_block_is_tool 57 | tools.last[block[:language].to_sym] = block[:source] 58 | previous_block_is_tool = false 59 | end 60 | end 61 | 62 | unless tools.empty? 63 | yaml_source << YAML.dump( 64 | Logic::Helpers::Hash.stringify_keys({ tools: }) 65 | ).gsub(/^---/, '') # TODO: Is this safe enough? 66 | end 67 | 68 | cartridge = {} 69 | 70 | yaml_source.each do |source| 71 | cartridge = Logic::Helpers::Hash.deep_merge(cartridge, yaml(source)) 72 | end 73 | 74 | cartridge 75 | end 76 | 77 | def self.yaml(raw) 78 | Logic::Helpers::Hash.symbolize_keys( 79 | YAML.safe_load(raw, permitted_classes: [Symbol]) 80 | ) 81 | end 82 | 83 | class Renderer < Redcarpet::Render::Base 84 | LANGUAGES_MAP = { 85 | 'yml' => 'yaml', 86 | 'yaml' => 'yaml', 87 | 'lua' => 'lua', 88 | 'fnl' => 'fennel', 89 | 'fennel' => 'fennel', 90 | 'clj' => 'clojure', 91 | 'clojure' => 'clojure' 92 | }.freeze 93 | 94 | LANGUAGES = LANGUAGES_MAP.keys.freeze 95 | 96 | def initialize(...) 97 | super 98 | @_nano_bots_blocks = [] 99 | end 100 | 101 | attr_reader :_nano_bots_blocks 102 | 103 | def block_code(code, language) 104 | key = language.to_s.downcase.strip 105 | 106 | return nil unless LANGUAGES.include?(key) 107 | 108 | @_nano_bots_blocks << { language: LANGUAGES_MAP[key], source: code } 109 | 110 | nil 111 | end 112 | end 113 | 114 | class Markdown 115 | attr_reader :markdown 116 | 117 | def initialize 118 | @renderer = Renderer.new 119 | @markdown = Redcarpet::Markdown.new(@renderer, fenced_code_blocks: true) 120 | end 121 | 122 | def blocks 123 | @renderer._nano_bots_blocks 124 | end 125 | 126 | def render(raw) 127 | @markdown.render(raw.gsub(/```\w/, "\n\n\\0")) 128 | self 129 | end 130 | end 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /logic/cartridge/safety.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'fetch' 4 | 5 | module NanoBot 6 | module Logic 7 | module Cartridge 8 | module Safety 9 | def self.default_answer(cartridge) 10 | default = Fetch.cascate(cartridge, [%i[interfaces tools confirming default]]) 11 | return [] if default.nil? 12 | 13 | default 14 | end 15 | 16 | def self.yeses(cartridge) 17 | yeses_values = Fetch.cascate(cartridge, [%i[interfaces tools confirming yeses]]) 18 | return [] if yeses_values.nil? 19 | 20 | yeses_values 21 | end 22 | 23 | def self.confirmable?(cartridge) 24 | confirmable = Fetch.cascate(cartridge, [%i[safety tools confirmable]]) 25 | return true if confirmable.nil? 26 | 27 | confirmable 28 | end 29 | 30 | def self.sandboxed?(cartridge) 31 | sandboxed = Fetch.cascate(cartridge, [%i[safety functions sandboxed]]) 32 | return true if sandboxed.nil? 33 | 34 | sandboxed 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /logic/cartridge/streaming.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../helpers/hash' 4 | 5 | module NanoBot 6 | module Logic 7 | module Cartridge 8 | module Streaming 9 | def self.enabled?(cartridge, interface) 10 | provider_stream = case Helpers::Hash.fetch(cartridge, %i[provider id]) 11 | when 'openai', 'mistral', 'anthropic', 'cohere', 'ollama' 12 | Helpers::Hash.fetch(cartridge, %i[provider settings stream]) 13 | when 'google', 'maritaca' 14 | Helpers::Hash.fetch(cartridge, %i[provider options stream]) 15 | end 16 | 17 | return false if provider_stream == false 18 | 19 | specific_interface = Helpers::Hash.fetch(cartridge, [:interfaces, interface, :output, :stream]) 20 | 21 | return specific_interface unless specific_interface.nil? 22 | 23 | interface = Helpers::Hash.fetch(cartridge, %i[interfaces output stream]) 24 | 25 | return interface unless interface.nil? 26 | 27 | true 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /logic/cartridge/tools.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'fetch' 4 | require_relative 'affixes' 5 | require_relative 'adapters' 6 | 7 | module NanoBot 8 | module Logic 9 | module Cartridge 10 | module Tools 11 | def self.fetch_from_interface(cartridge, interface, action, path) 12 | Fetch.cascate(cartridge, [ 13 | [:interfaces, interface, :tools, action].concat(path), 14 | [:interfaces, :tools, action].concat(path), 15 | %i[interfaces tools].concat(path) 16 | ]) 17 | end 18 | 19 | def self.feedback?(cartridge, interface, action) 20 | Fetch.cascate(cartridge, [ 21 | [:interfaces, interface, :tools, action, :feedback], 22 | [:interfaces, :tools, action, :feedback], 23 | %i[interfaces tools feedback] 24 | ]) 25 | end 26 | 27 | def self.input(cartridge, interface, content) 28 | lua = Adapter.expression(cartridge, interface, :input, :lua) 29 | fennel = Adapter.expression(cartridge, interface, :input, :fennel) 30 | 31 | prefix = Affixes.get(cartridge, interface, :input, :prefix) 32 | suffix = Affixes.get(cartridge, interface, :input, :suffix) 33 | 34 | { content:, prefix:, suffix:, lua:, fennel: } 35 | end 36 | 37 | def self.output(cartridge, interface, result, streaming, _finished) 38 | if streaming 39 | result[:message] = { content: result[:message], lua: nil, fennel: nil } 40 | return result 41 | end 42 | 43 | lua = Adapter.expression(cartridge, interface, :output, :lua) 44 | fennel = Adapter.expression(cartridge, interface, :output, :fennel) 45 | 46 | result[:message] = { content: result[:message], lua:, fennel: } 47 | 48 | result 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /logic/helpers/hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NanoBot 4 | module Logic 5 | module Helpers 6 | module Hash 7 | def self.deep_merge(hash1, hash2) 8 | hash1.merge(hash2) do |_key, old_val, new_val| 9 | if old_val.is_a?(::Hash) && new_val.is_a?(::Hash) 10 | deep_merge(old_val, new_val) 11 | else 12 | new_val 13 | end 14 | end 15 | end 16 | 17 | def self.symbolize_keys(object) 18 | case object 19 | when ::Hash 20 | object.each_with_object({}) do |(key, value), result| 21 | result[key.to_sym] = symbolize_keys(value) 22 | end 23 | when Array 24 | object.map { |e| symbolize_keys(e) } 25 | else 26 | object 27 | end 28 | end 29 | 30 | def self.stringify_keys(object) 31 | case object 32 | when ::Hash 33 | object.each_with_object({}) do |(key, value), result| 34 | result[key.to_s] = stringify_keys(value) 35 | end 36 | when Array 37 | object.map { |e| stringify_keys(e) } 38 | else 39 | object 40 | end 41 | end 42 | 43 | def self.fetch(object, path) 44 | node = object 45 | 46 | return nil unless node 47 | 48 | path.each do |key| 49 | unless node.is_a?(::Hash) 50 | node = nil 51 | break 52 | end 53 | node = node[key] 54 | break if node.nil? 55 | end 56 | 57 | node 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /logic/providers/anthropic/tokens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NanoBot 4 | module Logic 5 | module Anthropic 6 | module Tokens 7 | def self.apply_policies!(_cartridge, payload) 8 | payload[:messages] = payload[:messages].map { |message| message.except(:_meta) } 9 | payload 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /logic/providers/cohere/tokens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NanoBot 4 | module Logic 5 | module Cohere 6 | module Tokens 7 | def self.apply_policies!(_cartridge, payload) 8 | if payload[:chat_history] 9 | payload[:chat_history] = payload[:chat_history].map { |message| message.except(:_meta) } 10 | end 11 | 12 | payload 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /logic/providers/google/tokens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NanoBot 4 | module Logic 5 | module Google 6 | module Tokens 7 | def self.apply_policies!(_cartridge, payload) 8 | payload[:contents] = payload[:contents].map { |message| message.except(:_meta) } 9 | payload 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /logic/providers/google/tools.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'babosa' 5 | 6 | require_relative '../../helpers/hash' 7 | 8 | module NanoBot 9 | module Logic 10 | module Google 11 | module Tools 12 | def self.prepare(cartridge, tools) 13 | applies = [] 14 | 15 | tools = Marshal.load(Marshal.dump(tools)) 16 | 17 | tools.each do |tool| 18 | tool = Helpers::Hash.symbolize_keys(tool) 19 | 20 | cartridge.each do |candidate| 21 | candidate_key = candidate[:name].to_slug.normalize.gsub('-', '_') 22 | tool_key = tool[:functionCall][:name].to_slug.normalize.gsub('-', '_') 23 | 24 | next unless candidate_key == tool_key 25 | 26 | source = {} 27 | 28 | source[:clojure] = candidate[:clojure] if candidate[:clojure] 29 | source[:fennel] = candidate[:fennel] if candidate[:fennel] 30 | source[:lua] = candidate[:lua] if candidate[:lua] 31 | 32 | applies << { 33 | label: candidate[:name], 34 | name: tool[:functionCall][:name], 35 | type: 'function', 36 | parameters: tool[:functionCall][:args], 37 | source: 38 | } 39 | end 40 | end 41 | 42 | raise 'missing tool' if applies.size != tools.size 43 | 44 | applies 45 | end 46 | 47 | def self.adapt(cartridge) 48 | output = { 49 | name: cartridge[:name], 50 | description: cartridge[:description] 51 | } 52 | 53 | output[:parameters] = (cartridge[:parameters] || { type: 'object', properties: {} }) 54 | 55 | output 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /logic/providers/maritaca/tokens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NanoBot 4 | module Logic 5 | module Maritaca 6 | module Tokens 7 | def self.apply_policies!(_cartridge, payload) 8 | payload[:messages] = payload[:messages].map { |message| message.except(:_meta) } 9 | payload 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /logic/providers/mistral/tokens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NanoBot 4 | module Logic 5 | module Mistral 6 | module Tokens 7 | def self.apply_policies!(_cartridge, payload) 8 | payload[:messages] = payload[:messages].map { |message| message.except(:_meta) } 9 | payload 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /logic/providers/ollama/tokens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NanoBot 4 | module Logic 5 | module Ollama 6 | module Tokens 7 | def self.apply_policies!(_cartridge, payload) 8 | payload[:messages] = payload[:messages].map { |message| message.except(:_meta) } 9 | payload 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /logic/providers/openai.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | 5 | module NanoBot 6 | module Logic 7 | module OpenAI 8 | def self.prepare_tools(cartridge, tools) 9 | applies = [] 10 | tools.each do |tool| 11 | cartridge.each do |candidate| 12 | next unless candidate[:type] == 'function' && 13 | tool[:type] == candidate[:type] && 14 | tool[:function][:name] == candidate[:name] 15 | 16 | source = {} 17 | 18 | source[:fennel] = candidate[:fennel] if candidate[:fennel] 19 | source[:lua] = candidate[:lua] if candidate[:lua] 20 | 21 | applies << { 22 | name: tool[:function][:name], 23 | type: candidate[:type], 24 | parameters: JSON.parse(tool[:function][:arguments]), 25 | source: 26 | } 27 | end 28 | end 29 | 30 | applies 31 | end 32 | 33 | def self.adapt_tool(cartridge) 34 | raise 'unsupported tool' if cartridge[:type] != 'function' 35 | 36 | adapted = { 37 | type: 'function', 38 | function: { 39 | name: cartridge[:name], description: cartridge[:description], 40 | parameters: { type: 'object', properties: {} } 41 | } 42 | } 43 | 44 | properties = adapted[:function][:parameters][:properties] 45 | 46 | cartridge[:parameters].each do |parameter| 47 | key = parameter[:name].to_sym 48 | properties[key] = {} 49 | properties[key][:type] = parameter[:type] || 'string' 50 | properties[key][:description] = parameter[:description] if parameter[:description] 51 | end 52 | 53 | adapted 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /logic/providers/openai/tokens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NanoBot 4 | module Logic 5 | module OpenAI 6 | module Tokens 7 | def self.apply_policies!(_cartridge, payload) 8 | payload[:messages] = payload[:messages].map { |message| message.except(:_meta) } 9 | payload 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /logic/providers/openai/tools.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'babosa' 5 | 6 | require_relative '../../helpers/hash' 7 | 8 | module NanoBot 9 | module Logic 10 | module OpenAI 11 | module Tools 12 | def self.prepare(cartridge, tools) 13 | applies = [] 14 | 15 | tools = Marshal.load(Marshal.dump(tools)) 16 | 17 | tools.each do |tool| 18 | tool = Helpers::Hash.symbolize_keys(tool) 19 | 20 | cartridge.each do |candidate| 21 | candidate_key = candidate[:name].to_slug.normalize.gsub('-', '_') 22 | tool_key = tool[:function][:name].to_slug.normalize.gsub('-', '_') 23 | 24 | next unless candidate_key == tool_key 25 | 26 | source = {} 27 | 28 | source[:clojure] = candidate[:clojure] if candidate[:clojure] 29 | source[:fennel] = candidate[:fennel] if candidate[:fennel] 30 | source[:lua] = candidate[:lua] if candidate[:lua] 31 | 32 | applies << { 33 | id: tool[:id], 34 | label: candidate[:name], 35 | name: tool[:function][:name], 36 | type: 'function', 37 | parameters: JSON.parse(tool[:function][:arguments]), 38 | source: 39 | } 40 | end 41 | end 42 | 43 | raise 'missing tool' if applies.size != tools.size 44 | 45 | applies 46 | end 47 | 48 | def self.adapt(cartridge) 49 | output = { 50 | type: 'function', 51 | function: { 52 | name: cartridge[:name], description: cartridge[:description] 53 | } 54 | } 55 | 56 | output[:function][:parameters] = (cartridge[:parameters] || { type: 'object', properties: {} }) 57 | 58 | output 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /nano-bots.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'static/gem' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = NanoBot::GEM[:name] 7 | spec.version = NanoBot::GEM[:version] 8 | spec.authors = [NanoBot::GEM[:author]] 9 | 10 | spec.summary = NanoBot::GEM[:summary] 11 | spec.description = NanoBot::GEM[:description] 12 | 13 | spec.homepage = NanoBot::GEM[:github] 14 | 15 | spec.license = NanoBot::GEM[:license] 16 | 17 | spec.required_ruby_version = Gem::Requirement.new(">= #{NanoBot::GEM[:ruby]}") 18 | 19 | spec.metadata['allowed_push_host'] = NanoBot::GEM[:gem_server] 20 | 21 | spec.metadata['homepage_uri'] = spec.homepage 22 | spec.metadata['source_code_uri'] = NanoBot::GEM[:github] 23 | 24 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 25 | `git ls-files -z`.split("\x0").reject do |f| 26 | f.match(%r{\A(?:test|spec|features)/}) 27 | end 28 | end 29 | 30 | spec.require_paths = ['ports/dsl'] 31 | 32 | spec.executables = ['nb'] 33 | 34 | spec.add_dependency 'babosa', '~> 2.0' 35 | spec.add_dependency 'concurrent-ruby', '~> 1.3', '>= 1.3.3' 36 | spec.add_dependency 'dotenv', '~> 3.1', '>= 3.1.2' 37 | spec.add_dependency 'pry', '~> 0.14.2' 38 | spec.add_dependency 'rainbow', '~> 3.1', '>= 3.1.1' 39 | spec.add_dependency 'rbnacl', '~> 7.1', '>= 7.1.1' 40 | spec.add_dependency 'redcarpet', '~> 3.6' 41 | spec.add_dependency 'sweet-moon', '~> 1.0' 42 | 43 | spec.add_dependency 'anthropic', '~> 0.3.0' 44 | spec.add_dependency 'cohere-ai', '~> 1.1' 45 | spec.add_dependency 'gemini-ai', '~> 4.1' 46 | spec.add_dependency 'maritaca-ai', '~> 1.2' 47 | spec.add_dependency 'mistral-ai', '~> 1.2' 48 | spec.add_dependency 'ollama-ai', '~> 1.2', '>= 1.2.1' 49 | spec.add_dependency 'ruby-openai', '~> 7.1' 50 | 51 | spec.add_dependency 'faraday', '~> 2.9', '>= 2.9.2' 52 | spec.add_dependency 'faraday-typhoeus', '~> 1.1' 53 | spec.add_dependency 'typhoeus', '~> 1.4', '>= 1.4.1' 54 | 55 | spec.metadata['rubygems_mfa_required'] = 'true' 56 | end 57 | -------------------------------------------------------------------------------- /ports/dsl/nano-bots.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dotenv/load' 4 | 5 | require_relative '../../static/gem' 6 | require_relative '../../controllers/cartridges' 7 | require_relative '../../controllers/instance' 8 | require_relative '../../controllers/security' 9 | require_relative '../../controllers/interfaces/cli' 10 | require_relative '../../components/stream' 11 | require_relative 'nano-bots/cartridges' 12 | 13 | module NanoBot 14 | def self.new(cartridge: '-', state: '-', environment: {}) 15 | Controllers::Instance.new( 16 | cartridge_path: cartridge, 17 | state:, 18 | stream: Components::Stream.new, 19 | environment: 20 | ) 21 | end 22 | 23 | def self.security 24 | Controllers::Security 25 | end 26 | 27 | def self.cartridges 28 | Cartridges 29 | end 30 | 31 | def self.cli 32 | Controllers::Interfaces::CLI.handle! 33 | end 34 | 35 | def self.repl(cartridge: '-', state: '-', environment: {}) 36 | Controllers::Instance.new( 37 | cartridge_path: cartridge, state:, stream: $stdout, environment: 38 | ).repl 39 | end 40 | 41 | def self.version 42 | NanoBot::GEM[:version] 43 | end 44 | 45 | def self.specification 46 | NanoBot::GEM[:specification] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /ports/dsl/nano-bots/cartridges.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../../controllers/cartridges' 4 | 5 | module NanoBot 6 | module Cartridges 7 | def self.all(components: {}) 8 | Controllers::Cartridges.all(components:) 9 | end 10 | 11 | def self.load(path) 12 | Controllers::Cartridges.load(path) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /ports/dsl/nano-bots/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'nano-bots' 4 | 5 | NanoBot.cli 6 | -------------------------------------------------------------------------------- /spec/components/storage_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../components/storage' 4 | 5 | RSpec.describe NanoBot::Components::Storage do 6 | it 'symbolizes keys' do 7 | expect( 8 | described_class.cartridges_path( 9 | components: { home: '/home/aqua', ENV: {}, directory?: ->(_) { true } } 10 | ) 11 | ).to eq('/home/aqua/.local/share/nano-bots/cartridges') 12 | 13 | expect( 14 | described_class.cartridges_path( 15 | components: { 16 | home: '/home/aqua', 17 | ENV: { 'NANO_BOTS_CARTRIDGES_DIRECTORY' => '/home/aqua/my-cartridges' }, 18 | directory?: ->(_) { true } 19 | } 20 | ) 21 | ).to eq('/home/aqua/my-cartridges') 22 | 23 | expect( 24 | described_class.cartridges_path( 25 | components: { 26 | home: '/home/aqua', 27 | ENV: { 28 | 'NANO_BOTS_CARTRIDGES_DIRECTORY' => '/home/aqua/my-cartridges', 29 | 'NANO_BOTS_CARTRIDGES_PATH' => '/home/aqua/lime/my-cartridges' 30 | }, 31 | directory?: ->(_) { true } 32 | } 33 | ) 34 | ).to eq('/home/aqua/lime/my-cartridges:/home/aqua/my-cartridges') 35 | 36 | expect( 37 | described_class.cartridges_path( 38 | components: { 39 | home: '/home/aqua', 40 | ENV: { 41 | 'NANO_BOTS_CARTRIDGES_DIRECTORY' => '/home/aqua/my-cartridges', 42 | 'NANO_BOTS_CARTRIDGES_PATH' => '/home/aqua/lime/my-cartridges:/home/aqua/ivory/my-cartridges' 43 | }, 44 | directory?: lambda do |path| 45 | { '/home/aqua/my-cartridges' => true, 46 | '/home/aqua/lime/my-cartridge' => false, 47 | '/home/aqua/ivory/my-cartridges' => true }[path] 48 | end 49 | } 50 | ) 51 | ).to eq('/home/aqua/ivory/my-cartridges:/home/aqua/my-cartridges') 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/data/cartridges/affixes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | interfaces: 3 | input: 4 | prefix: A 5 | suffix: B 6 | output: 7 | prefix: C 8 | suffix: D 9 | repl: 10 | input: 11 | prefix: E 12 | suffix: F 13 | output: 14 | prefix: G 15 | suffix: H 16 | eval: 17 | input: 18 | prefix: I 19 | suffix: J 20 | output: 21 | prefix: K 22 | suffix: L 23 | -------------------------------------------------------------------------------- /spec/data/cartridges/block.md: -------------------------------------------------------------------------------- 1 | First, we need to add some important details: 2 | ```yaml 3 | safety: 4 | functions: 5 | sandboxed: false 6 | ``` 7 | Hi! 8 | -------------------------------------------------------------------------------- /spec/data/cartridges/markdown.md: -------------------------------------------------------------------------------- 1 | A cartridge is a YAML file with human-readable data that outlines the bot's goals, expected behaviors, and settings for authentication and provider utilization. 2 | 3 | We begin with the meta section, which provides information about what this cartridge is designed for: 4 | 5 | ```yaml 6 | meta: 7 | symbol: 🤖 8 | name: ChatGPT 4o 9 | author: icebaker 10 | version: 0.0.1 11 | license: CC0-1.0 12 | description: A helpful assistant. 13 | ``` 14 | 15 | It includes details like versioning and license. 16 | 17 | Next, we add a behavior section that will provide the bot with a directive on how it should behave: 18 | 19 | ```yaml 20 | behaviors: 21 | interaction: 22 | directive: You are a helpful assistant. 23 | ``` 24 | 25 | Now, we need to provide instructions on how this Nano Bot should connect with a provider, which credentials to use, and what specific configurations for the LLM are required: 26 | 27 | ```yaml 28 | provider: 29 | id: openai 30 | credentials: 31 | access-token: ENV/OPENAI_API_KEY 32 | settings: 33 | user: ENV/NANO_BOTS_END_USER 34 | model: gpt-4o 35 | ``` 36 | 37 | In my API, I have set the environment variables `OPENAI_API_KEY` and `NANO_BOTS_END_USER`, which is where the values for these will come from. 38 | -------------------------------------------------------------------------------- /spec/data/cartridges/meta.md: -------------------------------------------------------------------------------- 1 | Start by defining a meta section: 2 | 3 | ```yaml 4 | meta: 5 | symbol: 🤖 6 | name: Nano Bot Name 7 | author: Your Name 8 | description: A helpful assistant. 9 | ``` 10 | 11 | You can also add version and license information: 12 | 13 | ```yaml 14 | meta: 15 | version: 1.0.0 16 | license: CC0-1.0 17 | ``` 18 | 19 | Then, add a behavior section: 20 | 21 | ```yaml 22 | behaviors: 23 | interaction: 24 | directive: You are a helpful assistant. 25 | ``` 26 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/anthropic/claude-3-5-sonnet.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟤 4 | name: Anthropic Claude 3.5 Sonnet 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: anthropic 9 | credentials: 10 | api-key: ENV/ANTHROPIC_API_KEY 11 | settings: 12 | model: claude-3-5-sonnet-20240620 13 | max_tokens: 4096 14 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/anthropic/claude-3-haiku.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟤 4 | name: Anthropic Claude 3 Haiku 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: anthropic 9 | credentials: 10 | api-key: ENV/ANTHROPIC_API_KEY 11 | settings: 12 | model: claude-3-haiku-20240307 13 | max_tokens: 4096 14 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/anthropic/claude-3-opus.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟤 4 | name: Anthropic Claude 3 Opus 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: anthropic 9 | credentials: 10 | api-key: ENV/ANTHROPIC_API_KEY 11 | settings: 12 | model: claude-3-opus-20240229 13 | max_tokens: 4096 14 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/anthropic/claude-3-sonnet.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟤 4 | name: Anthropic Claude 3 Sonnet 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: anthropic 9 | credentials: 10 | api-key: ENV/ANTHROPIC_API_KEY 11 | settings: 12 | model: claude-3-sonnet-20240229 13 | max_tokens: 4096 14 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/cohere/command-light.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟣 4 | name: Cohere Command Light 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: cohere 9 | credentials: 10 | api-key: ENV/COHERE_API_KEY 11 | settings: 12 | model: command-light 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/cohere/command-r-plus.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟣 4 | name: Cohere Command R+ 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: cohere 9 | credentials: 10 | api-key: ENV/COHERE_API_KEY 11 | settings: 12 | model: command-r-plus 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/cohere/command-r.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟣 4 | name: Cohere Command R 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: cohere 9 | credentials: 10 | api-key: ENV/COHERE_API_KEY 11 | settings: 12 | model: command-r 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/cohere/command.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟣 4 | name: Cohere Command 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: cohere 9 | credentials: 10 | api-key: ENV/COHERE_API_KEY 11 | settings: 12 | model: command 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/google/gemini-1-0-pro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🔵 4 | name: Google Gemini 1.0 Pro 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: google 9 | credentials: 10 | service: vertex-ai-api 11 | region: us-east4 12 | options: 13 | model: gemini-1.0-pro 14 | settings: 15 | safetySettings: 16 | - category: HARM_CATEGORY_UNSPECIFIED 17 | threshold: BLOCK_ONLY_HIGH 18 | - category: HARM_CATEGORY_HARASSMENT 19 | threshold: BLOCK_ONLY_HIGH 20 | - category: HARM_CATEGORY_HATE_SPEECH 21 | threshold: BLOCK_ONLY_HIGH 22 | - category: HARM_CATEGORY_SEXUALLY_EXPLICIT 23 | threshold: BLOCK_ONLY_HIGH 24 | - category: HARM_CATEGORY_DANGEROUS_CONTENT 25 | threshold: BLOCK_ONLY_HIGH 26 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/google/gemini-1-5-flash.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🔵 4 | name: Google Gemini 1.5 Flash 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: google 9 | credentials: 10 | service: vertex-ai-api 11 | region: us-east4 12 | options: 13 | model: gemini-1.5-flash 14 | settings: 15 | safetySettings: 16 | - category: HARM_CATEGORY_UNSPECIFIED 17 | threshold: BLOCK_ONLY_HIGH 18 | - category: HARM_CATEGORY_HARASSMENT 19 | threshold: BLOCK_ONLY_HIGH 20 | - category: HARM_CATEGORY_HATE_SPEECH 21 | threshold: BLOCK_ONLY_HIGH 22 | - category: HARM_CATEGORY_SEXUALLY_EXPLICIT 23 | threshold: BLOCK_ONLY_HIGH 24 | - category: HARM_CATEGORY_DANGEROUS_CONTENT 25 | threshold: BLOCK_ONLY_HIGH 26 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/google/gemini-1-5-pro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🔵 4 | name: Google Gemini 1.5 Pro 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: google 9 | credentials: 10 | service: vertex-ai-api 11 | region: us-east4 12 | options: 13 | model: gemini-1.5-pro 14 | settings: 15 | safetySettings: 16 | - category: HARM_CATEGORY_UNSPECIFIED 17 | threshold: BLOCK_ONLY_HIGH 18 | - category: HARM_CATEGORY_HARASSMENT 19 | threshold: BLOCK_ONLY_HIGH 20 | - category: HARM_CATEGORY_HATE_SPEECH 21 | threshold: BLOCK_ONLY_HIGH 22 | - category: HARM_CATEGORY_SEXUALLY_EXPLICIT 23 | threshold: BLOCK_ONLY_HIGH 24 | - category: HARM_CATEGORY_DANGEROUS_CONTENT 25 | threshold: BLOCK_ONLY_HIGH 26 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/google/gemini-pro-gen-lang-api.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🔵 4 | name: Google Gemini 1.0 Pro (Generative Language API) 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: google 9 | credentials: 10 | service: generative-language-api 11 | api-key: ENV/GOOGLE_API_KEY 12 | options: 13 | model: gemini-pro 14 | settings: 15 | safetySettings: 16 | - category: HARM_CATEGORY_HARASSMENT 17 | threshold: BLOCK_ONLY_HIGH 18 | - category: HARM_CATEGORY_HATE_SPEECH 19 | threshold: BLOCK_ONLY_HIGH 20 | - category: HARM_CATEGORY_SEXUALLY_EXPLICIT 21 | threshold: BLOCK_ONLY_HIGH 22 | - category: HARM_CATEGORY_DANGEROUS_CONTENT 23 | threshold: BLOCK_ONLY_HIGH 24 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/maritaca/sabia-2-medium.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🦜 4 | name: Maritaca Sabiá 2 Medium 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: maritaca 9 | credentials: 10 | api-key: ENV/MARITACA_API_KEY 11 | settings: 12 | model: sabia-2-medium 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/maritaca/sabia-2-small.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🦜 4 | name: Maritaca Sabiá 2 Small 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: maritaca 9 | credentials: 10 | api-key: ENV/MARITACA_API_KEY 11 | settings: 12 | model: sabia-2-small 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/mistral/large.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟠 4 | name: Mistral Large 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: mistral 9 | credentials: 10 | api-key: ENV/MISTRAL_API_KEY 11 | settings: 12 | model: mistral-large-latest 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/mistral/medium.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟠 4 | name: Mistral Medium 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: mistral 9 | credentials: 10 | api-key: ENV/MISTRAL_API_KEY 11 | settings: 12 | model: mistral-medium-latest 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/mistral/small.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟠 4 | name: Mistral Small 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: mistral 9 | credentials: 10 | api-key: ENV/MISTRAL_API_KEY 11 | settings: 12 | model: mistral-small-latest 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/ollama/phi-3.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🦙 4 | name: Phi-3 through Ollama 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: ollama 9 | settings: 10 | model: phi3 11 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/openai/gpt-3-5-turbo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟢 4 | name: OpenAI GPT 3.5 Turbo 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: openai 9 | credentials: 10 | access-token: ENV/OPENAI_API_KEY 11 | settings: 12 | model: gpt-3.5-turbo 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/openai/gpt-4-turbo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟢 4 | name: OpenAI GPT 4 Turbo 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: openai 9 | credentials: 10 | access-token: ENV/OPENAI_API_KEY 11 | settings: 12 | model: gpt-4-turbo 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/models/openai/gpt-4o.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🟢 4 | name: OpenAI GPT 4o 5 | license: CC0-1.0 6 | 7 | provider: 8 | id: openai 9 | credentials: 10 | access-token: ENV/OPENAI_API_KEY 11 | settings: 12 | model: gpt-4o 13 | -------------------------------------------------------------------------------- /spec/data/cartridges/streaming.yml: -------------------------------------------------------------------------------- 1 | --- 2 | interfaces: 3 | output: 4 | stream: true 5 | repl: 6 | output: 7 | stream: true 8 | eval: 9 | output: 10 | stream: true 11 | 12 | provider: 13 | id: openai 14 | settings: 15 | stream: true 16 | -------------------------------------------------------------------------------- /spec/data/cartridges/tools.md: -------------------------------------------------------------------------------- 1 | A cartridge is a YAML file with human-readable data that outlines the bot's goals, expected behaviors, and settings for authentication and provider utilization. 2 | 3 | We begin with the meta section, which provides information about what this cartridge is designed for: 4 | 5 | ```yaml 6 | meta: 7 | symbol: 🕛 8 | name: Date and Time 9 | author: icebaker 10 | version: 0.0.1 11 | license: CC0-1.0 12 | description: A helpful assistant. 13 | ``` 14 | 15 | It includes details like versioning and license. 16 | 17 | Next, we add a behavior section that will provide the bot with a directive on how it should behave: 18 | 19 | ```yaml 20 | behaviors: 21 | interaction: 22 | directive: You are a helpful assistant. 23 | ``` 24 | 25 | Now, we need to provide instructions on how this Nano Bot should connect with a provider, which credentials to use, and what specific configurations for the LLM are required: 26 | 27 | ```yaml 28 | provider: 29 | id: openai 30 | credentials: 31 | access-token: ENV/OPENAI_API_KEY 32 | settings: 33 | user: ENV/NANO_BOTS_END_USER 34 | model: gpt-4o 35 | ``` 36 | 37 | In my API, I have set the environment variables `OPENAI_API_KEY` and `NANO_BOTS_END_USER`, which is where the values for these will come from. 38 | 39 | Nano Bot ready; let's start adding some extra power to it. 40 | 41 | ## Random Numbers 42 | 43 | ```yml 44 | tools: 45 | - name: random-number 46 | description: Generates a random number within a given range. 47 | parameters: 48 | type: object 49 | properties: 50 | from: 51 | type: integer 52 | description: The minimum expected number for random generation. 53 | to: 54 | type: integer 55 | description: The maximum expected number for random generation. 56 | required: 57 | - from 58 | - to 59 | ``` 60 | 61 | ```clj 62 | (let [{:strs [from to]} parameters] 63 | (+ from (rand-int (+ 1 (- to from))))) 64 | ``` 65 | 66 | ## Date and Time 67 | 68 | ```yaml 69 | tools: 70 | - name: date-and-time 71 | description: Returns the current date and time. 72 | ``` 73 | 74 | ```fnl 75 | (os.date) 76 | ``` 77 | -------------------------------------------------------------------------------- /spec/data/cartridges/tools.yml: -------------------------------------------------------------------------------- 1 | --- 2 | tools: 3 | - name: what-time-is-it 4 | description: Returns the current date and time for a given timezone. 5 | parameters: 6 | type: object 7 | properties: 8 | timezone: 9 | type: string 10 | description: A string representing the timezone that should be used to provide a datetime, following the IANA (Internet Assigned Numbers Authority) Time Zone Database. Examples are "Asia/Tokyo" and "Europe/Paris". 11 | required: 12 | - timezone 13 | fennel: | 14 | (os.date) 15 | 16 | - name: get-current-weather 17 | description: Get the current weather in a given location. 18 | parameters: 19 | type: object 20 | properties: 21 | location: 22 | type: string 23 | unit: 24 | type: string 25 | fennel: | 26 | (let [{:location location :unit unit} parameters] 27 | (.. "Here is the weather in " location ", in " unit ": 35.8°C.")) 28 | 29 | - name: sh 30 | description: It has access to computer users' data and can be used to run shell commands, similar to those in a Linux terminal, to extract information. Please be mindful and careful to avoid running dangerous commands on users' computers. 31 | parameters: 32 | type: object 33 | properties: 34 | command: 35 | type: array 36 | description: An array of strings that represents a shell command along with its arguments or options. For instance, `["df", "-h"]` executes the `df -h` command, where each array element specifies either the command itself or an associated argument/option. 37 | items: 38 | type: string 39 | clojure: | 40 | (require '[clojure.java.shell :refer [sh]]) 41 | (println (apply sh (get parameters "command"))) 42 | 43 | - name: clock 44 | description: Returns the current date and time. 45 | fennel: | 46 | (os.date) 47 | -------------------------------------------------------------------------------- /spec/data/providers/google/tools.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - functionCall: 3 | name: get_current_weather 4 | args: 5 | location: Tokyo, Japan 6 | - functionCall: 7 | name: what_time_is_it 8 | args: 9 | timezone: local 10 | -------------------------------------------------------------------------------- /spec/data/providers/openai/tools.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - id: call_XYZ 3 | type: function 4 | function: 5 | name: get-current-weather 6 | arguments: '{"location":"Tokyo, Japan"}' 7 | - id: call_ZYX 8 | type: function 9 | function: 10 | name: what-time-is-it 11 | arguments: "{}" 12 | -------------------------------------------------------------------------------- /spec/logic/cartridge/affixes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | require_relative '../../../logic/cartridge/affixes' 6 | 7 | RSpec.describe NanoBot::Logic::Cartridge::Affixes do 8 | context 'interfaces' do 9 | let(:cartridge) { load_symbolized('cartridges/affixes.yml') } 10 | 11 | it 'gets the expected affixes' do 12 | expect(described_class.get(cartridge, :repl, :input, :prefix)).to eq('E') 13 | expect(described_class.get(cartridge, :repl, :input, :suffix)).to eq('F') 14 | expect(described_class.get(cartridge, :repl, :output, :prefix)).to eq('G') 15 | expect(described_class.get(cartridge, :repl, :output, :suffix)).to eq('H') 16 | 17 | expect(described_class.get(cartridge, :eval, :input, :prefix)).to eq('I') 18 | expect(described_class.get(cartridge, :eval, :input, :suffix)).to eq('J') 19 | expect(described_class.get(cartridge, :eval, :output, :prefix)).to eq('K') 20 | expect(described_class.get(cartridge, :eval, :output, :suffix)).to eq('L') 21 | end 22 | end 23 | 24 | context 'interfaces fallback' do 25 | let(:cartridge) { load_symbolized('cartridges/affixes.yml') } 26 | 27 | it 'gets the expected affixes' do 28 | cartridge[:interfaces][:repl][:input].delete(:prefix) 29 | cartridge[:interfaces][:repl][:input].delete(:suffix) 30 | cartridge[:interfaces][:eval][:input].delete(:prefix) 31 | cartridge[:interfaces][:eval][:input].delete(:suffix) 32 | 33 | cartridge[:interfaces][:repl][:output].delete(:prefix) 34 | cartridge[:interfaces][:repl][:output].delete(:suffix) 35 | cartridge[:interfaces][:eval][:output].delete(:prefix) 36 | cartridge[:interfaces][:eval][:output].delete(:suffix) 37 | 38 | expect(described_class.get(cartridge, :repl, :input, :prefix)).to eq('A') 39 | expect(described_class.get(cartridge, :repl, :input, :suffix)).to eq('B') 40 | expect(described_class.get(cartridge, :repl, :output, :prefix)).to eq('C') 41 | expect(described_class.get(cartridge, :repl, :output, :suffix)).to eq('D') 42 | 43 | expect(described_class.get(cartridge, :eval, :input, :prefix)).to eq('A') 44 | expect(described_class.get(cartridge, :eval, :input, :suffix)).to eq('B') 45 | expect(described_class.get(cartridge, :eval, :output, :prefix)).to eq('C') 46 | expect(described_class.get(cartridge, :eval, :output, :suffix)).to eq('D') 47 | end 48 | end 49 | 50 | context 'interfaces nil' do 51 | let(:cartridge) { load_symbolized('cartridges/affixes.yml') } 52 | 53 | it 'gets the expected affixes' do 54 | cartridge[:interfaces][:repl][:input][:prefix] = nil 55 | cartridge[:interfaces][:repl][:input][:suffix] = nil 56 | cartridge[:interfaces][:eval][:input][:prefix] = nil 57 | cartridge[:interfaces][:eval][:input][:suffix] = nil 58 | 59 | cartridge[:interfaces][:repl][:output][:prefix] = nil 60 | cartridge[:interfaces][:repl][:output][:suffix] = nil 61 | cartridge[:interfaces][:eval][:output][:prefix] = nil 62 | cartridge[:interfaces][:eval][:output][:suffix] = nil 63 | 64 | expect(described_class.get(cartridge, :repl, :input, :prefix)).to be_nil 65 | expect(described_class.get(cartridge, :repl, :input, :suffix)).to be_nil 66 | expect(described_class.get(cartridge, :repl, :output, :prefix)).to be_nil 67 | expect(described_class.get(cartridge, :repl, :output, :suffix)).to be_nil 68 | 69 | expect(described_class.get(cartridge, :eval, :input, :prefix)).to be_nil 70 | expect(described_class.get(cartridge, :eval, :input, :suffix)).to be_nil 71 | expect(described_class.get(cartridge, :eval, :output, :prefix)).to be_nil 72 | expect(described_class.get(cartridge, :eval, :output, :suffix)).to be_nil 73 | end 74 | end 75 | 76 | context 'default' do 77 | let(:cartridge) { {} } 78 | 79 | it 'gets the expected affixes' do 80 | expect(described_class.get(cartridge, :repl, :input, :prefix)).to be_nil 81 | expect(described_class.get(cartridge, :repl, :input, :suffix)).to be_nil 82 | expect(described_class.get(cartridge, :repl, :output, :prefix)).to eq("\n") 83 | expect(described_class.get(cartridge, :repl, :output, :suffix)).to eq("\n") 84 | 85 | expect(described_class.get(cartridge, :eval, :input, :prefix)).to be_nil 86 | expect(described_class.get(cartridge, :eval, :input, :suffix)).to be_nil 87 | expect(described_class.get(cartridge, :eval, :output, :prefix)).to be_nil 88 | expect(described_class.get(cartridge, :eval, :output, :suffix)).to eq("\n") 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/logic/cartridge/interaction_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | require_relative '../../../logic/cartridge/interaction' 6 | 7 | RSpec.describe NanoBot::Logic::Cartridge::Interaction do 8 | context 'input' do 9 | let(:cartridge) { load_symbolized('cartridges/affixes.yml') } 10 | 11 | it 'prepares the input' do 12 | expect(described_class.input(cartridge, :repl, 'hello')).to eq( 13 | { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: 'E', suffix: 'F' } 14 | ) 15 | 16 | expect(described_class.input({}, :repl, 'hello')).to eq( 17 | { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: nil, suffix: nil } 18 | ) 19 | 20 | expect(described_class.input(cartridge, :eval, 'hello')).to eq( 21 | { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: 'I', suffix: 'J' } 22 | ) 23 | 24 | expect(described_class.input({}, :eval, 'hello')).to eq( 25 | { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: nil, suffix: nil } 26 | ) 27 | end 28 | 29 | it 'prepares the non-streaming output' do 30 | expect(described_class.output(cartridge, :repl, { message: 'hello' }, false, true)).to eq( 31 | { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } } 32 | ) 33 | 34 | expect(described_class.output({}, :repl, { message: 'hello' }, false, true)).to eq( 35 | { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } } 36 | ) 37 | 38 | expect(described_class.output(cartridge, :eval, { message: 'hello' }, false, true)).to eq( 39 | { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } } 40 | ) 41 | 42 | expect(described_class.output({}, :eval, { message: 'hello' }, false, true)).to eq( 43 | { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } } 44 | ) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/logic/cartridge/parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../../logic/cartridge/parser' 4 | 5 | RSpec.describe NanoBot::Logic::Cartridge::Parser do 6 | context 'markdown' do 7 | context 'default' do 8 | let(:raw) { File.read('spec/data/cartridges/markdown.md') } 9 | 10 | it 'parses markdown cartridge' do 11 | expect(described_class.parse(raw, format: 'md')).to eq( 12 | { meta: { 13 | symbol: '🤖', 14 | name: 'ChatGPT 4o', 15 | author: 'icebaker', 16 | version: '0.0.1', 17 | license: 'CC0-1.0', 18 | description: 'A helpful assistant.' 19 | }, 20 | behaviors: { interaction: { directive: 'You are a helpful assistant.' } }, 21 | provider: { 22 | id: 'openai', 23 | credentials: { 'access-token': 'ENV/OPENAI_API_KEY' }, 24 | settings: { 25 | user: 'ENV/NANO_BOTS_END_USER', 26 | model: 'gpt-4o' 27 | } 28 | } } 29 | ) 30 | end 31 | end 32 | 33 | context 'meta' do 34 | let(:raw) { File.read('spec/data/cartridges/meta.md') } 35 | 36 | it 'parses markdown cartridge' do 37 | expect(described_class.parse(raw, format: 'md')).to eq( 38 | { 39 | meta: { 40 | symbol: '🤖', 41 | name: 'Nano Bot Name', 42 | author: 'Your Name', 43 | description: 'A helpful assistant.', 44 | version: '1.0.0', 45 | license: 'CC0-1.0' 46 | }, 47 | behaviors: { 48 | interaction: { 49 | directive: 'You are a helpful assistant.' 50 | } 51 | } 52 | } 53 | ) 54 | end 55 | end 56 | 57 | context 'tools' do 58 | let(:raw) { File.read('spec/data/cartridges/tools.md') } 59 | 60 | it 'parses markdown cartridge' do 61 | expect(described_class.parse(raw, format: 'md')).to eq( 62 | { meta: { 63 | symbol: '🕛', 64 | name: 'Date and Time', 65 | author: 'icebaker', 66 | version: '0.0.1', 67 | license: 'CC0-1.0', 68 | description: 'A helpful assistant.' 69 | }, 70 | behaviors: { 71 | interaction: { 72 | directive: 'You are a helpful assistant.' 73 | } 74 | }, 75 | provider: { 76 | id: 'openai', 77 | credentials: { 'access-token': 'ENV/OPENAI_API_KEY' }, 78 | settings: { 79 | user: 'ENV/NANO_BOTS_END_USER', 80 | model: 'gpt-4o' 81 | } 82 | }, 83 | tools: [ 84 | { name: 'random-number', 85 | description: 'Generates a random number within a given range.', 86 | parameters: { 87 | type: 'object', 88 | properties: { 89 | from: { 90 | type: 'integer', 91 | description: 'The minimum expected number for random generation.' 92 | }, 93 | to: { 94 | type: 'integer', 95 | description: 'The maximum expected number for random generation.' 96 | } 97 | }, 98 | required: %w[from to] 99 | }, 100 | clojure: "(let [{:strs [from to]} parameters]\n (+ from (rand-int (+ 1 (- to from)))))\n" }, 101 | { name: 'date-and-time', 102 | description: 'Returns the current date and time.', 103 | fennel: "(os.date)\n" } 104 | ] } 105 | ) 106 | end 107 | end 108 | 109 | context 'block' do 110 | let(:raw) { File.read('spec/data/cartridges/block.md') } 111 | 112 | it 'parses markdown cartridge' do 113 | expect(described_class.parse(raw, format: 'md')).to eq( 114 | { safety: { functions: { sandboxed: false } } } 115 | ) 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/logic/cartridge/streaming_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | require_relative '../../../logic/cartridge/streaming' 6 | 7 | RSpec.describe NanoBot::Logic::Cartridge::Streaming do 8 | context 'interfaces override' do 9 | context 'defaults' do 10 | context 'openai' do 11 | let(:cartridge) { { provider: { id: 'openai' } } } 12 | 13 | it 'uses default values when appropriate' do 14 | expect(described_class.enabled?(cartridge, :repl)).to be(true) 15 | expect(described_class.enabled?(cartridge, :eval)).to be(true) 16 | end 17 | end 18 | 19 | context 'google' do 20 | let(:cartridge) { { provider: { id: 'google' } } } 21 | 22 | it 'uses default values when appropriate' do 23 | expect(described_class.enabled?(cartridge, :repl)).to be(true) 24 | expect(described_class.enabled?(cartridge, :eval)).to be(true) 25 | end 26 | end 27 | end 28 | 29 | context 'top-level overrides' do 30 | let(:cartridge) { { interfaces: { output: { stream: false } } } } 31 | 32 | it 'overrides default values when appropriate' do 33 | expect(described_class.enabled?(cartridge, :repl)).to be(false) 34 | expect(described_class.enabled?(cartridge, :eval)).to be(false) 35 | end 36 | end 37 | end 38 | 39 | context 'provider' do 40 | let(:cartridge) { load_symbolized('cartridges/streaming.yml') } 41 | 42 | it 'checks if stream is enabled' do 43 | cartridge[:provider][:settings][:stream] = false 44 | expect(described_class.enabled?(cartridge, :repl)).to be(false) 45 | end 46 | end 47 | 48 | context 'repl' do 49 | let(:cartridge) { load_symbolized('cartridges/streaming.yml') } 50 | 51 | it 'checks if stream is enabled' do 52 | cartridge[:interfaces][:repl][:output][:stream] = false 53 | expect(described_class.enabled?(cartridge, :repl)).to be(false) 54 | end 55 | end 56 | 57 | context 'interface + repl' do 58 | let(:cartridge) { load_symbolized('cartridges/streaming.yml') } 59 | 60 | it 'checks if stream is enabled' do 61 | cartridge[:interfaces][:output][:stream] = false 62 | cartridge[:interfaces][:repl][:output][:stream] = true 63 | expect(described_class.enabled?(cartridge, :repl)).to be(true) 64 | end 65 | end 66 | 67 | context 'interface' do 68 | let(:cartridge) { load_symbolized('cartridges/streaming.yml') } 69 | 70 | it 'checks if stream is enabled' do 71 | cartridge[:interfaces][:output][:stream] = false 72 | cartridge[:interfaces][:repl][:output].delete(:stream) 73 | expect(described_class.enabled?(cartridge, :repl)).to be(false) 74 | end 75 | end 76 | 77 | context '- repl' do 78 | let(:cartridge) { load_symbolized('cartridges/streaming.yml') } 79 | 80 | it 'checks if stream is enabled' do 81 | cartridge[:interfaces][:repl][:output].delete(:stream) 82 | expect(described_class.enabled?(cartridge, :repl)).to be(true) 83 | end 84 | end 85 | 86 | context '- interface' do 87 | let(:cartridge) { load_symbolized('cartridges/streaming.yml') } 88 | 89 | it 'checks if stream is enabled' do 90 | cartridge[:interfaces][:output].delete(:stream) 91 | cartridge[:interfaces][:repl][:output].delete(:stream) 92 | expect(described_class.enabled?(cartridge, :repl)).to be(true) 93 | end 94 | end 95 | 96 | context '- provider' do 97 | let(:cartridge) { load_symbolized('cartridges/streaming.yml') } 98 | 99 | it 'checks if stream is enabled' do 100 | cartridge[:provider][:settings].delete(:stream) 101 | cartridge[:interfaces][:output].delete(:stream) 102 | cartridge[:interfaces][:repl][:output].delete(:stream) 103 | expect(described_class.enabled?(cartridge, :repl)).to be(true) 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/logic/cartridge/tools_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | require_relative '../../../logic/cartridge/tools' 6 | 7 | RSpec.describe NanoBot::Logic::Cartridge::Tools do 8 | context 'interfaces override' do 9 | context 'defaults' do 10 | let(:cartridge) { {} } 11 | 12 | it 'uses default values when appropriate' do 13 | expect(described_class.feedback?(cartridge, :repl, :executing)).to be(false) 14 | expect(described_class.feedback?(cartridge, :eval, :executing)).to be(false) 15 | 16 | expect(described_class.feedback?(cartridge, :repl, :responding)).to be(true) 17 | expect(described_class.feedback?(cartridge, :eval, :responding)).to be(true) 18 | end 19 | end 20 | 21 | context 'top-level overrides' do 22 | let(:cartridge) do 23 | { interfaces: { tools: { feedback: false } } } 24 | end 25 | 26 | it 'overrides default values when appropriate' do 27 | expect(described_class.feedback?(cartridge, :repl, :executing)).to be(false) 28 | expect(described_class.feedback?(cartridge, :eval, :executing)).to be(false) 29 | 30 | expect(described_class.feedback?(cartridge, :repl, :responding)).to be(false) 31 | expect(described_class.feedback?(cartridge, :eval, :responding)).to be(false) 32 | end 33 | end 34 | 35 | context 'top-level overrides' do 36 | let(:cartridge) do 37 | { interfaces: { tools: { feedback: true } } } 38 | end 39 | 40 | it 'overrides default values when appropriate' do 41 | expect(described_class.feedback?(cartridge, :repl, :executing)).to be(true) 42 | expect(described_class.feedback?(cartridge, :eval, :executing)).to be(true) 43 | 44 | expect(described_class.feedback?(cartridge, :repl, :responding)).to be(true) 45 | expect(described_class.feedback?(cartridge, :eval, :responding)).to be(true) 46 | end 47 | end 48 | 49 | context 'top-level-specific overrides' do 50 | let(:cartridge) do 51 | { interfaces: { tools: { executing: { feedback: false }, responding: { feedback: true } } } } 52 | end 53 | 54 | it 'overrides default values when appropriate' do 55 | expect(described_class.feedback?(cartridge, :repl, :executing)).to be(false) 56 | expect(described_class.feedback?(cartridge, :eval, :executing)).to be(false) 57 | 58 | expect(described_class.feedback?(cartridge, :repl, :responding)).to be(true) 59 | expect(described_class.feedback?(cartridge, :eval, :responding)).to be(true) 60 | end 61 | end 62 | 63 | context 'repl interface overrides' do 64 | let(:cartridge) do 65 | { interfaces: { 66 | tools: { executing: { feedback: false }, responding: { feedback: true } }, 67 | repl: { tools: { executing: { feedback: true }, responding: { feedback: false } } } 68 | } } 69 | end 70 | 71 | it 'overrides default values when appropriate' do 72 | expect(described_class.feedback?(cartridge, :repl, :executing)).to be(true) 73 | expect(described_class.feedback?(cartridge, :eval, :executing)).to be(false) 74 | 75 | expect(described_class.feedback?(cartridge, :repl, :responding)).to be(false) 76 | expect(described_class.feedback?(cartridge, :eval, :responding)).to be(true) 77 | end 78 | end 79 | 80 | context 'eval interface overrides' do 81 | let(:cartridge) do 82 | { interfaces: { 83 | tools: { executing: { feedback: false }, responding: { feedback: true } }, 84 | eval: { tools: { executing: { feedback: true }, responding: { feedback: false } } } 85 | } } 86 | end 87 | 88 | it 'overrides default values when appropriate' do 89 | expect(described_class.feedback?(cartridge, :repl, :executing)).to be(false) 90 | expect(described_class.feedback?(cartridge, :eval, :executing)).to be(true) 91 | 92 | expect(described_class.feedback?(cartridge, :repl, :responding)).to be(true) 93 | expect(described_class.feedback?(cartridge, :eval, :responding)).to be(false) 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/logic/helpers/hash_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../../logic/helpers/hash' 4 | 5 | RSpec.describe NanoBot::Logic::Helpers::Hash do 6 | it 'symbolizes keys' do 7 | expect(described_class.symbolize_keys({ 'a' => 'b', 'c' => { 'd' => ['e'] } })).to eq( 8 | { a: 'b', c: { d: ['e'] } } 9 | ) 10 | end 11 | 12 | it 'deep merges' do 13 | expect(described_class.deep_merge( 14 | { a: { x: 1, y: 2 }, b: 3 }, 15 | { a: { y: 99, z: 4 }, c: 5 } 16 | )).to eq( 17 | { a: { x: 1, y: 99, z: 4 }, b: 3, c: 5 } 18 | ) 19 | end 20 | 21 | it 'stringify keys' do 22 | expect(described_class.stringify_keys({ a: 'b', c: { d: [:e] } })).to eq( 23 | { 'a' => 'b', 'c' => { 'd' => [:e] } } 24 | ) 25 | end 26 | 27 | it 'fetch a path of keys' do 28 | expect(described_class.fetch({ a: 'b', c: { d: ['e'] } }, %i[c d])).to eq( 29 | ['e'] 30 | ) 31 | 32 | expect(described_class.fetch({ a: 'b', c: { d: ['e'] } }, %i[c e])).to be_nil 33 | 34 | expect(described_class.fetch({ a: 'b', c: { d: ['e'] } }, %i[a b])).to be_nil 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/logic/providers/google/tools_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | require_relative '../../../../logic/providers/google/tools' 6 | 7 | RSpec.describe NanoBot::Logic::Google::Tools do 8 | context 'tools' do 9 | let(:cartridge) { load_symbolized('cartridges/tools.yml') } 10 | 11 | context 'adapt' do 12 | it 'adapts to Google expected format' do 13 | expect(described_class.adapt(cartridge[:tools][0])).to eq( 14 | { name: 'what-time-is-it', 15 | description: 'Returns the current date and time for a given timezone.', 16 | parameters: { 17 | type: 'object', 18 | properties: { 19 | timezone: { 20 | type: 'string', 21 | description: 'A string representing the timezone that should be used to provide a datetime, following the IANA (Internet Assigned Numbers Authority) Time Zone Database. Examples are "Asia/Tokyo" and "Europe/Paris".' 22 | } 23 | }, 24 | required: ['timezone'] 25 | } } 26 | ) 27 | 28 | expect(described_class.adapt(cartridge[:tools][1])).to eq( 29 | { name: 'get-current-weather', 30 | description: 'Get the current weather in a given location.', 31 | parameters: { 32 | type: 'object', 33 | properties: { location: { type: 'string' }, unit: { type: 'string' } } 34 | } } 35 | ) 36 | 37 | expect(described_class.adapt(cartridge[:tools][2])).to eq( 38 | { name: 'sh', 39 | description: "It has access to computer users' data and can be used to run shell commands, similar to those in a Linux terminal, to extract information. Please be mindful and careful to avoid running dangerous commands on users' computers.", 40 | parameters: { 41 | type: 'object', 42 | properties: { 43 | command: { 44 | type: 'array', 45 | description: 'An array of strings that represents a shell command along with its arguments or options. For instance, `["df", "-h"]` executes the `df -h` command, where each array element specifies either the command itself or an associated argument/option.', 46 | items: { type: 'string' } 47 | } 48 | } 49 | } } 50 | ) 51 | 52 | expect(described_class.adapt(cartridge[:tools][3])).to eq( 53 | { name: 'clock', 54 | description: 'Returns the current date and time.', 55 | parameters: { type: 'object', properties: {} } } 56 | ) 57 | end 58 | end 59 | 60 | context 'prepare' do 61 | let(:tools) { load_symbolized('providers/google/tools.yml') } 62 | 63 | it 'prepare tools to be executed' do 64 | expect(described_class.prepare(cartridge[:tools], tools)).to eq( 65 | [{ name: 'get_current_weather', 66 | label: 'get-current-weather', 67 | type: 'function', 68 | parameters: { location: 'Tokyo, Japan' }, 69 | source: { fennel: "(let [{:location location :unit unit} parameters]\n (.. \"Here is the weather in \" location \", in \" unit \": 35.8°C.\"))\n" } }, 70 | { name: 'what_time_is_it', 71 | label: 'what-time-is-it', 72 | type: 'function', parameters: { timezone: 'local' }, 73 | source: { fennel: "(os.date)\n" } }] 74 | ) 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/logic/providers/openai/tools_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | require_relative '../../../../logic/providers/openai/tools' 6 | 7 | RSpec.describe NanoBot::Logic::OpenAI::Tools do 8 | context 'tools' do 9 | let(:cartridge) { load_symbolized('cartridges/tools.yml') } 10 | 11 | context 'adapt' do 12 | it 'adapts to OpenAI expected format' do 13 | expect(described_class.adapt(cartridge[:tools][0])).to eq( 14 | { type: 'function', 15 | function: { 16 | name: 'what-time-is-it', 17 | description: 'Returns the current date and time for a given timezone.', 18 | parameters: { 19 | type: 'object', 20 | properties: { 21 | timezone: { 22 | type: 'string', 23 | description: 'A string representing the timezone that should be used to provide a datetime, following the IANA (Internet Assigned Numbers Authority) Time Zone Database. Examples are "Asia/Tokyo" and "Europe/Paris".' 24 | } 25 | }, required: ['timezone'] 26 | } 27 | } } 28 | ) 29 | 30 | expect(described_class.adapt(cartridge[:tools][1])).to eq( 31 | { type: 'function', 32 | function: { 33 | name: 'get-current-weather', 34 | description: 'Get the current weather in a given location.', 35 | parameters: { 36 | type: 'object', 37 | properties: { 38 | location: { type: 'string' }, 39 | unit: { type: 'string' } 40 | } 41 | } 42 | } } 43 | ) 44 | 45 | expect(described_class.adapt(cartridge[:tools][2])).to eq( 46 | { type: 'function', 47 | function: { 48 | name: 'sh', 49 | description: "It has access to computer users' data and can be used to run shell commands, similar to those in a Linux terminal, to extract information. Please be mindful and careful to avoid running dangerous commands on users' computers.", 50 | parameters: { 51 | type: 'object', 52 | properties: { 53 | command: { 54 | type: 'array', 55 | description: 'An array of strings that represents a shell command along with its arguments or options. For instance, `["df", "-h"]` executes the `df -h` command, where each array element specifies either the command itself or an associated argument/option.', 56 | items: { type: 'string' } 57 | } 58 | } 59 | } 60 | } } 61 | ) 62 | 63 | expect(described_class.adapt(cartridge[:tools][3])).to eq( 64 | { type: 'function', 65 | function: { 66 | name: 'clock', 67 | description: 'Returns the current date and time.', 68 | parameters: { type: 'object', properties: {} } 69 | } } 70 | ) 71 | end 72 | end 73 | 74 | context 'prepare' do 75 | let(:tools) { load_symbolized('providers/openai/tools.yml') } 76 | 77 | it 'prepare tools to be executed' do 78 | expect(described_class.prepare(cartridge[:tools], tools)).to eq( 79 | [{ id: 'call_XYZ', 80 | name: 'get-current-weather', 81 | label: 'get-current-weather', 82 | type: 'function', 83 | parameters: { 'location' => 'Tokyo, Japan' }, 84 | source: { fennel: "(let [{:location location :unit unit} parameters]\n (.. \"Here is the weather in \" location \", in \" unit \": 35.8°C.\"))\n" } }, 85 | { id: 'call_ZYX', 86 | name: 'what-time-is-it', 87 | label: 'what-time-is-it', 88 | type: 'function', 89 | parameters: {}, 90 | source: { fennel: "(os.date)\n" } }] 91 | ) 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | require_relative '../logic/helpers/hash' 6 | 7 | RSpec.configure do |config| 8 | config.expect_with :rspec do |expectations| 9 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 10 | end 11 | 12 | config.mock_with :rspec do |mocks| 13 | mocks.verify_partial_doubles = true 14 | end 15 | 16 | config.shared_context_metadata_behavior = :apply_to_host_groups 17 | end 18 | 19 | def load_symbolized(path) 20 | cartridge = YAML.safe_load_file("spec/data/#{path}", permitted_classes: [Symbol]) 21 | 22 | NanoBot::Logic::Helpers::Hash.symbolize_keys(cartridge) 23 | end 24 | -------------------------------------------------------------------------------- /spec/tasks/run-all-models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dotenv/load' 4 | 5 | require 'yaml' 6 | 7 | require_relative '../../ports/dsl/nano-bots' 8 | require_relative '../../logic/helpers/hash' 9 | 10 | def run_model!(cartridge, stream = true) 11 | if stream == false 12 | cartridge[:provider][:options] = {} unless cartridge[:provider].key?(:options) 13 | cartridge[:provider][:options][:stream] = false 14 | 15 | cartridge[:provider][:settings] = {} unless cartridge[:provider].key?(:settings) 16 | cartridge[:provider][:settings][:stream] = false 17 | end 18 | 19 | puts "\n#{cartridge[:meta][:symbol]} #{cartridge[:meta][:name]}\n\n" 20 | 21 | bot = NanoBot.new(cartridge:) 22 | 23 | output = bot.eval('Hi!') do |_content, fragment, _finished, _meta| 24 | print fragment unless fragment.nil? 25 | end 26 | puts '' 27 | puts '-' * 20 28 | puts '' 29 | puts output 30 | puts '' 31 | puts '*' * 20 32 | end 33 | 34 | puts '[NO STREAM]' 35 | 36 | Dir['spec/data/cartridges/models/*/*.yml'].each do |path| 37 | run_model!( 38 | NanoBot::Logic::Helpers::Hash.symbolize_keys( 39 | YAML.safe_load_file(path, permitted_classes: [Symbol]) 40 | ), 41 | false 42 | ) 43 | end 44 | 45 | puts "\n[STREAM]" 46 | 47 | Dir['spec/data/cartridges/models/*/*.yml'].each do |path| 48 | run_model!( 49 | NanoBot::Logic::Helpers::Hash.symbolize_keys( 50 | YAML.safe_load_file(path, permitted_classes: [Symbol]) 51 | ) 52 | ) 53 | end 54 | -------------------------------------------------------------------------------- /spec/tasks/run-model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dotenv/load' 4 | 5 | require 'yaml' 6 | 7 | require_relative '../../ports/dsl/nano-bots' 8 | require_relative '../../logic/helpers/hash' 9 | 10 | def run_model!(cartridge, stream = true) 11 | if stream == false 12 | cartridge[:provider][:options] = {} unless cartridge[:provider].key?(:options) 13 | cartridge[:provider][:options][:stream] = false 14 | 15 | cartridge[:provider][:settings] = {} unless cartridge[:provider].key?(:settings) 16 | cartridge[:provider][:settings][:stream] = false 17 | end 18 | 19 | puts "\n#{cartridge[:meta][:symbol]} #{cartridge[:meta][:name]}\n\n" 20 | 21 | bot = NanoBot.new(cartridge:) 22 | 23 | output = bot.eval('Hi!') do |_content, fragment, _finished, _meta| 24 | print fragment unless fragment.nil? 25 | end 26 | puts '' 27 | puts '-' * 20 28 | puts '' 29 | puts output 30 | puts '' 31 | puts '*' * 20 32 | end 33 | 34 | run_model!( 35 | NanoBot::Logic::Helpers::Hash.symbolize_keys( 36 | YAML.safe_load_file(ARGV[0].to_s.strip, permitted_classes: [Symbol]) 37 | ), 38 | ARGV[1].to_s.strip == 'stream' 39 | ) 40 | -------------------------------------------------------------------------------- /static/cartridges/baseline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | symbol: 🤖 4 | name: Unknown 5 | author: None 6 | version: 0.0.0 7 | license: CC0-1.0 8 | description: Unknown 9 | 10 | provider: 11 | id: openai 12 | credentials: 13 | address: ENV/OPENAI_API_ADDRESS 14 | access-token: ENV/OPENAI_API_KEY 15 | settings: 16 | user: ENV/NANO_BOTS_END_USER 17 | model: gpt-3.5-turbo 18 | -------------------------------------------------------------------------------- /static/cartridges/default.yml: -------------------------------------------------------------------------------- 1 | --- 2 | safety: 3 | functions: 4 | sandboxed: true 5 | tools: 6 | confirmable: true 7 | 8 | interfaces: 9 | repl: 10 | output: 11 | stream: true 12 | prefix: "\n" 13 | suffix: "\n" 14 | prompt: 15 | - text: '🤖' 16 | - text: '> ' 17 | eval: 18 | output: 19 | stream: true 20 | suffix: "\n" 21 | tools: 22 | confirming: 23 | suffix: ' [yN] ' 24 | default: 'n' 25 | yeses: ['y', 'yes'] 26 | executing: 27 | feedback: false 28 | responding: 29 | suffix: "\n\n" 30 | feedback: true 31 | 32 | provider: 33 | options: 34 | stream: true 35 | settings: 36 | stream: true 37 | -------------------------------------------------------------------------------- /static/fennel/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /static/gem.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NanoBot 4 | GEM = { 5 | name: 'nano-bots', 6 | version: '3.4.0', 7 | specification: '3.2.0', 8 | author: 'icebaker', 9 | summary: 'Ruby Implementation of Nano Bots: small, AI-powered bots for OpenAI ChatGPT, Ollama, Mistral AI, Anthropic Claude, Cohere Command, Maritaca AI MariTalk, and Google Gemini.', 10 | description: 'Ruby Implementation of Nano Bots: small, AI-powered bots that can be easily shared as a single file, designed to support multiple providers such as OpenAI ChatGPT, Ollama, Mistral AI, Anthropic Claude, Cohere Command, Maritaca AI MariTalk, and Google Gemini, with support for calling Tools (Functions).', 11 | github: 'https://github.com/icebaker/ruby-nano-bots', 12 | gem_server: 'https://rubygems.org', 13 | license: 'MIT', 14 | ruby: '3.1.0' 15 | }.freeze 16 | end 17 | --------------------------------------------------------------------------------