summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoricebaker <icebaker@proton.me>2023-05-11 19:24:50 -0300
committericebaker <icebaker@proton.me>2023-05-11 19:24:50 -0300
commitec5e25547a401141586c87621266f9cd68c59e3c (patch)
tree547b3c7fa04c9e695785b9beeda0be5a4a77b006
first commit
-rw-r--r--.gitignore2
-rw-r--r--.rspec1
-rw-r--r--.rubocop.yml9
-rw-r--r--Gemfile11
-rw-r--r--Gemfile.lock88
-rw-r--r--LICENSE21
-rw-r--r--README.md166
-rwxr-xr-xbin/rnb4
-rw-r--r--components/provider.rb20
-rw-r--r--components/providers/base.rb15
-rw-r--r--components/providers/openai.rb79
-rw-r--r--controllers/instance.rb61
-rw-r--r--controllers/interfaces/cli.rb30
-rw-r--r--controllers/interfaces/repl.rb67
-rw-r--r--controllers/session.rb133
-rw-r--r--logic/helpers/hash.rb35
-rw-r--r--nano-bots.gemspec42
-rw-r--r--ports/dsl/nano-bots.rb25
-rw-r--r--ports/dsl/nano-bots/cli.rb5
-rw-r--r--spec/spec_helper.rb98
-rw-r--r--static/gem.rb15
21 files changed, 927 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..24ad4c4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.env
+*.gem
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..c99d2e7
--- /dev/null
+++ b/.rspec
@@ -0,0 +1 @@
+--require spec_helper
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..14382e5
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,9 @@
+AllCops:
+ TargetRubyVersion: 3.1.4
+ NewCops: enable
+
+Style/Documentation:
+ Enabled: false
+
+require:
+ - rubocop-rspec
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..5691eef
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
+
+gemspec
+
+group :test, :development do
+ gem 'rspec', '~> 3.12'
+ gem 'rubocop', '~> 1.47'
+ gem 'rubocop-rspec', '~> 2.22'
+end
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..bd2525f
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,88 @@
+PATH
+ remote: .
+ specs:
+ nano-bots (0.0.1)
+ babosa (~> 2.0)
+ dotenv (~> 2.8, >= 2.8.1)
+ faraday (~> 2.7, >= 2.7.4)
+ pry (~> 0.14.2)
+ rainbow (~> 3.1, >= 3.1.1)
+ ruby-openai (~> 4.0)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ ast (2.4.2)
+ babosa (2.0.0)
+ coderay (1.1.3)
+ diff-lcs (1.5.0)
+ dotenv (2.8.1)
+ faraday (2.7.4)
+ faraday-net_http (>= 2.0, < 3.1)
+ ruby2_keywords (>= 0.0.4)
+ faraday-multipart (1.0.4)
+ multipart-post (~> 2)
+ faraday-net_http (3.0.2)
+ json (2.6.3)
+ method_source (1.0.0)
+ multipart-post (2.3.0)
+ parallel (1.23.0)
+ parser (3.2.2.1)
+ ast (~> 2.4.1)
+ pry (0.14.2)
+ coderay (~> 1.1)
+ method_source (~> 1.0)
+ rainbow (3.1.1)
+ regexp_parser (2.8.0)
+ rexml (3.2.5)
+ rspec (3.12.0)
+ rspec-core (~> 3.12.0)
+ rspec-expectations (~> 3.12.0)
+ rspec-mocks (~> 3.12.0)
+ rspec-core (3.12.2)
+ rspec-support (~> 3.12.0)
+ rspec-expectations (3.12.3)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-mocks (3.12.5)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-support (3.12.0)
+ rubocop (1.50.2)
+ json (~> 2.3)
+ parallel (~> 1.10)
+ parser (>= 3.2.0.0)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.28.0, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.28.1)
+ parser (>= 3.2.1.0)
+ rubocop-capybara (2.18.0)
+ rubocop (~> 1.41)
+ rubocop-factory_bot (2.22.0)
+ rubocop (~> 1.33)
+ rubocop-rspec (2.22.0)
+ rubocop (~> 1.33)
+ rubocop-capybara (~> 2.17)
+ rubocop-factory_bot (~> 2.22)
+ ruby-openai (4.0.0)
+ faraday (>= 1)
+ faraday-multipart (>= 1)
+ ruby-progressbar (1.13.0)
+ ruby2_keywords (0.0.5)
+ unicode-display_width (2.4.2)
+
+PLATFORMS
+ x86_64-linux
+
+DEPENDENCIES
+ nano-bots!
+ rspec (~> 3.12)
+ rubocop (~> 1.47)
+ rubocop-rspec (~> 2.22)
+
+BUNDLED WITH
+ 2.4.13
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d6576a1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 icebaker
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ef15d10
--- /dev/null
+++ b/README.md
@@ -0,0 +1,166 @@
+# Nano Bots 💎 🤖
+
+A Ruby implementation of the [Nano Bots](https://github.com/icebaker/nano-bots) specification.
+
+- [Setup](#setup)
+- [Usage](#usage)
+ - [Command Line](#command-line)
+ - [Library](#library)
+- [Cartridges](#cartridges)
+- [Development](#development)
+ - [Publish to RubyGems](#publish-to-rubygems)
+
+## Setup
+
+For a system usage:
+
+```sh
+gem install nano-bots -v 0.0.1
+```
+
+To use it in a project, add it to your `Gemfile`:
+
+```ruby
+gem 'nano-bots', '~> 0.0.1'
+```
+
+```sh
+bundle install
+```
+
+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:
+
+```sh
+OPENAI_API_ADDRESS=https://api.openai.com
+OPENAI_API_ACCESS_TOKEN=your-token
+OPENAI_API_USER_IDENTIFIER=your-user
+NANO_BOTS_STATE_DIRECTORY=/home/your-user/.local/share/.nano-bots
+```
+
+Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded.
+
+## Usage
+
+### Command Line
+
+After installing the gem, the `rnb` binary command will be available for your project or system.
+
+Examples of usage:
+
+```bash
+rnb to-en-us-translator.yml - eval "Salut, comment ça va?"
+# => Hello, how are you doing?
+
+rnb midjourney.yml - eval "happy and friendly cyberpunk robot"
+# => The robot exploring a bustling city, surrounded by neon lights
+# and high-rise buildings. The prompt should include colorful
+# lighting and a sense of excitement in the facial expression.
+
+rnb lisp.yml - eval "(+ 1 2)"
+# => 3
+
+cat article.txt |
+ rnb to-en-us-translator.yml - eval |
+ rnb summarizer.yml - eval
+# -> LLM stands for Large Language Model, which refers to an
+# artificial intelligence algorithm capable of processing
+# and understanding vast amounts of natural language data,
+# allowing it to generate human-like responses and perform
+# a range of language-related tasks.
+```
+
+```bash
+rnb assistant.yml - repl
+```
+
+All of the commands above are stateless. If you want to preserve the history of your interactions, replace the `-` with a state key. You can use a simple key, such as your username, or a randomly generated one:
+
+```ruby
+require 'securerandom'
+
+SecureRandom.hex # => 6ea6c43c42a1c076b1e3c36fa349ac2c
+```
+
+```bash
+rnb assistant.yml your-user eval "Salut, comment ça va?"
+rnb assistant.yml your-user repl
+
+rnb assistant.yml 6ea6c43c42a1c076b1e3c36fa349ac2c eval "Salut, comment ça va?"
+rnb assistant.yml 6ea6c43c42a1c076b1e3c36fa349ac2c repl
+```
+
+### Library
+
+To use it as a library:
+
+```ruby
+require 'nano-bots/cli' # Equivalent to the `rnb` command.
+```
+
+```ruby
+require 'nano-bots'
+
+NanoBot.cli # Equivalent to the `rnb` command.
+
+NanoBot.repl(cartridge: 'cartridge.yml') # Starts a new REPL.
+
+bot = NanoBot.new(cartridge: 'cartridge.yml')
+
+bot.eval('Hello')
+
+bot.repl # Starts a new REPL.
+
+NanoBot.repl(cartridge: 'cartridge.yml', state: '6ea6c43c42a1c076b1e3c36fa349ac2c')
+
+bot = NanoBot.new(cartridge: 'cartridge.yml', state: '6ea6c43c42a1c076b1e3c36fa349ac2c')
+```
+
+## Cartridges
+
+Here's what a Nano Bot Cartridge looks like:
+
+```yaml
+---
+name: Assistant
+version: 0.0.1
+
+behaviors:
+ interaction:
+ directive: You are a helpful assistant.
+
+interfaces:
+ repl:
+ prompt:
+ - text: '🤖'
+ - text: '> '
+ color: blue
+
+provider:
+ name: openai
+ settings:
+ model: gpt-3.5-turbo
+ credentials:
+ address: ENV/OPENAI_API_ADDRESS
+ access-token: ENV/OPENAI_API_ACCESS_TOKEN
+ user-identifier: ENV/OPENAI_API_USER_IDENTIFIER
+```
+
+Check the Nano Bots specification to learn more about [how to build cartridges](https://icebaker.github.io/nano-bots/#/README?id=cartridges).
+
+## Development
+
+```bash
+bundle
+rubocop -A
+rspec
+```
+
+### Publish to RubyGems
+
+```bash
+gem build nano-bots.gemspec
+
+gem signin
+
+gem push nano-bots-0.0.1.gem
+```
diff --git a/bin/rnb b/bin/rnb
new file mode 100755
index 0000000..b674232
--- /dev/null
+++ b/bin/rnb
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require 'nano-bots/cli'
diff --git a/components/provider.rb b/components/provider.rb
new file mode 100644
index 0000000..dbfc8bd
--- /dev/null
+++ b/components/provider.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'openai'
+
+require_relative './providers/openai'
+
+module NanoBot
+ module Components
+ class Provider
+ def self.new(provider)
+ case provider[:name]
+ when 'openai'
+ Providers::OpenAI.new(provider[:settings])
+ else
+ raise "Unsupported provider #{provider[:name]}"
+ end
+ end
+ end
+ end
+end
diff --git a/components/providers/base.rb b/components/providers/base.rb
new file mode 100644
index 0000000..011c5dd
--- /dev/null
+++ b/components/providers/base.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'openai'
+
+module NanoBot
+ module Components
+ module Providers
+ class Base
+ def evaluate(_payload)
+ raise NoMethodError, "The 'evaluate' method is not implemented for the current provider."
+ end
+ end
+ end
+ end
+end
diff --git a/components/providers/openai.rb b/components/providers/openai.rb
new file mode 100644
index 0000000..e163573
--- /dev/null
+++ b/components/providers/openai.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'openai'
+
+require_relative './base'
+
+module NanoBot
+ module Components
+ module Providers
+ class OpenAI < Base
+ CHAT_SETTINGS = %i[
+ model stream temperature top_p n stop max_tokens
+ presence_penalty frequency_penalty logit_bias
+ ].freeze
+
+ attr_reader :settings
+
+ def initialize(settings)
+ @settings = settings
+
+ @client = ::OpenAI::Client.new(
+ uri_base: "#{@settings[:credentials][:address].sub(%r{/$}, '')}/",
+ access_token: @settings[:credentials][:'access-token']
+ )
+ end
+
+ def evaluate(input, &block)
+ messages = input[:history].map do |event|
+ { role: event[:who] == 'user' ? 'user' : 'assistant',
+ content: event[:message] }
+ end
+
+ %i[instruction backdrop directive].each do |key|
+ next unless input[:behavior][key]
+
+ messages.prepend(
+ { role: key == :directive ? 'system' : 'user',
+ content: input[:behavior][key] }
+ )
+ end
+
+ payload = {
+ model: @settings[:model],
+ user: @settings[:credentials][:'user-identifier'],
+ messages:
+ }
+
+ CHAT_SETTINGS.each do |key|
+ payload[key] = @settings[key] if @settings.key?(key)
+ end
+
+ payload.delete(:logit_bias) if payload.key?(:logit_bias) && payload[:logit_bias].nil?
+
+ if @settings[:stream] && input[:interface][:stream]
+ content = ''
+
+ payload[:stream] = proc do |chunk, _bytesize|
+ partial = chunk.dig('choices', 0, 'delta', 'content')
+ if partial
+ content += partial
+ block.call({ who: 'AI', message: partial }, false)
+ end
+
+ block.call({ who: 'AI', message: content }, true) if chunk.dig('choices', 0, 'finish_reason')
+ end
+
+ @client.chat(parameters: payload)
+ else
+ result = @client.chat(parameters: payload)
+
+ raise StandardError, result['error'] if result['error']
+
+ block.call({ who: 'AI', message: result.dig('choices', 0, 'message', 'content') }, true)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/controllers/instance.rb b/controllers/instance.rb
new file mode 100644
index 0000000..5635658
--- /dev/null
+++ b/controllers/instance.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'yaml'
+
+require_relative '../logic/helpers/hash'
+require_relative '../components/provider'
+require_relative './interfaces/repl'
+require_relative './session'
+
+module NanoBot
+ module Controllers
+ class Instance
+ def initialize(cartridge_path:, state: nil)
+ load_cartridge!(cartridge_path)
+
+ provider = Components::Provider.new(@cartridge[:provider])
+
+ @session = Session.new(provider:, cartridge: @cartridge, state:)
+ end
+
+ def debug
+ @session.debug
+ end
+
+ def eval(input)
+ @session.evaluate_and_print(input, mode: 'eval')
+ end
+
+ def repl
+ Interfaces::REPL.start(@cartridge, @session)
+ end
+
+ private
+
+ def load_cartridge!(path)
+ @cartridge = Logic::Helpers::Hash.symbolize_keys(
+ YAML.safe_load(File.read(path), permitted_classes: [Symbol])
+ )
+
+ inject_environment_variables!(@cartridge)
+ end
+
+ def inject_environment_variables!(node)
+ case node
+ when Hash
+ node.each do |key, value|
+ node[key] = inject_environment_variables!(value)
+ end
+ when Array
+ node.each_with_index do |value, index|
+ node[index] = inject_environment_variables!(value)
+ end
+ when String
+ node.start_with?('ENV') ? ENV.fetch(node.sub(/^ENV./, '')) : node
+ else
+ node
+ end
+ end
+ end
+ end
+end
diff --git a/controllers/interfaces/cli.rb b/controllers/interfaces/cli.rb
new file mode 100644
index 0000000..473ab7b
--- /dev/null
+++ b/controllers/interfaces/cli.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require_relative '../instance'
+
+module NanoBot
+ module Controllers
+ module Interfaces
+ module CLI
+ def self.handle!
+ params = { cartridge_path: ARGV[0], state: ARGV[1], command: ARGV[2] }
+
+ bot = Instance.new(cartridge_path: params[:cartridge_path], state: params[:state])
+
+ case params[:command]
+ when 'eval'
+ params[:input] = ARGV[3..]&.join(' ')
+ params[:input] = $stdin.read.chomp if params[:input].nil? || params[:input].empty?
+ bot.eval(params[:input])
+ when 'repl'
+ bot.repl
+ when 'debug'
+ bot.debug
+ else
+ raise "TODO: [#{params[:command]}]"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/controllers/interfaces/repl.rb b/controllers/interfaces/repl.rb
new file mode 100644
index 0000000..7b53eb2
--- /dev/null
+++ b/controllers/interfaces/repl.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'pry'
+require 'rainbow'
+
+require_relative '../../logic/helpers/hash'
+
+module NanoBot
+ module Controllers
+ module Interfaces
+ module REPL
+ def self.start(cartridge, session)
+ if Logic::Helpers::Hash.fetch(
+ cartridge, %i[interfaces repl prefix]
+ )
+ session.print(Logic::Helpers::Hash.fetch(cartridge,
+ %i[interfaces repl prefix]))
+ end
+
+ session.boot(mode: 'repl')
+
+ session.print(Logic::Helpers::Hash.fetch(cartridge, %i[interfaces repl postfix]) || "\n")
+
+ session.flush
+
+ prompt = build_prompt(cartridge[:interfaces][:repl][:prompt])
+
+ Pry.config.prompt = Pry::Prompt.new(
+ 'REPL',
+ 'REPL Prompt',
+ [proc { prompt }, proc { 'MISSING INPUT' }]
+ )
+
+ Pry.commands.block_command(/(.*)/, 'handler') do |line|
+ if Logic::Helpers::Hash.fetch(
+ cartridge, %i[interfaces repl prefix]
+ )
+ session.print(Logic::Helpers::Hash.fetch(
+ cartridge, %i[interfaces repl prefix]
+ ))
+ end
+
+ session.evaluate_and_print(line, mode: 'repl')
+ session.print(Logic::Helpers::Hash.fetch(cartridge, %i[interfaces repl postfix]) || "\n")
+ session.flush
+ end
+
+ Pry.start
+ end
+
+ def self.build_prompt(prompt)
+ result = ''
+
+ prompt.each do |partial|
+ result += if partial[:color]
+ Rainbow(partial[:text]).send(partial[:color])
+ else
+ partial[:text]
+ end
+ end
+
+ result
+ end
+ end
+ end
+ end
+end
diff --git a/controllers/session.rb b/controllers/session.rb
new file mode 100644
index 0000000..fadb21b
--- /dev/null
+++ b/controllers/session.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'babosa'
+
+require 'fileutils'
+
+require_relative '../logic/helpers/hash'
+
+module NanoBot
+ module Controllers
+ STREAM_TIMEOUT_IN_SECONDS = 5
+
+ class Session
+ def initialize(provider:, cartridge:, state: nil)
+ @provider = provider
+ @cartridge = cartridge
+
+ @output = $stdout
+
+ @stateless = state.nil? || state.strip == '-' || state.strip.empty?
+
+ if @stateless
+ @state = { history: [] }
+ else
+ build_path_and_ensure_state_file!(state.strip)
+ @state = load_state
+ end
+ end
+
+ def debug
+ pp({
+ state: {
+ path: @state_path,
+ content: @state
+ }
+ })
+ end
+
+ def load_state
+ @state = Logic::Helpers::Hash.symbolize_keys(JSON.parse(File.read(@state_path)))
+ end
+
+ def store_state!
+ File.write(@state_path, JSON.generate(@state))
+ end
+
+ def build_path_and_ensure_state_file!(key)
+ path = Logic::Helpers::Hash.fetch(@cartridge, %i[state directory])
+
+ path = "#{user_home!.sub(%r{/$}, '')}/.local/share/.nano-bots" if path.nil? || path.empty?
+
+ path = "#{path.sub(%r{/$}, '')}/nano-bots-rb/#{@cartridge[:name].to_slug.normalize}"
+ path = "#{path}/#{@cartridge[:version].to_slug.normalize}/#{key.to_slug.normalize}"
+ path = "#{path}/state.json"
+
+ @state_path = path
+
+ FileUtils.mkdir_p(File.dirname(@state_path))
+
+ File.write(@state_path, JSON.generate({ key:, history: [] })) unless File.exist?(@state_path)
+ end
+
+ def user_home!
+ [Dir.home, `echo ~`.strip, '~'].find do |candidate|
+ !candidate.nil? && !candidate.empty?
+ end
+ end
+
+ def boot(mode:)
+ return unless Logic::Helpers::Hash.fetch(@cartridge, %i[behaviors boot instruction])
+
+ behavior = Logic::Helpers::Hash.fetch(@cartridge, %i[behaviors boot]) || {}
+
+ input = { behavior:, history: [] }
+
+ process(input, mode:)
+ end
+
+ def evaluate_and_print(message, mode:)
+ behavior = Logic::Helpers::Hash.fetch(@cartridge, %i[behaviors interaction]) || {}
+
+ @state[:history] << ({ who: 'user', message: })
+
+ input = { behavior:, history: @state[:history] }
+
+ process(input, mode:)
+ end
+
+ def process(input, mode:)
+ streaming = @provider.settings[:stream] && Logic::Helpers::Hash.fetch(
+ @cartridge, [:interfaces, mode.to_sym, :stream]
+ )
+
+ interface = Logic::Helpers::Hash.fetch(@cartridge, [:interfaces, mode.to_sym]) || {}
+
+ input[:interface] = interface
+
+ updated_at = Time.now
+
+ ready = false
+ @provider.evaluate(input) do |output, finished|
+ updated_at = Time.now
+ if finished
+ @state[:history] << output
+ self.print(output[:message]) unless streaming
+ unless Logic::Helpers::Hash.fetch(@cartridge, [:interfaces, mode.to_sym, :postfix]).nil?
+ self.print(Logic::Helpers::Hash.fetch(@cartridge, [:interfaces, mode.to_sym, :postfix]))
+ end
+ ready = true
+ flush
+ elsif streaming
+ self.print(output[:message])
+ end
+ end
+
+ until ready
+ seconds = (Time.now - updated_at).to_i
+ raise StandardError, 'The stream has become unresponsive.' if seconds >= STREAM_TIMEOUT_IN_SECONDS
+ end
+
+ store_state! unless @stateless
+ end
+
+ def flush
+ @output.flush
+ end
+
+ def print(content)
+ @output.write(content)
+ end
+ end
+ end
+end
diff --git a/logic/helpers/hash.rb b/logic/helpers/hash.rb
new file mode 100644
index 0000000..52bd8d4
--- /dev/null
+++ b/logic/helpers/hash.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module NanoBot
+ module Logic
+ module Helpers
+ module Hash
+ def self.symbolize_keys(object)
+ case object
+ when ::Hash
+ object.each_with_object({}) do |(key, value), result|
+ result[key.to_sym] = symbolize_keys(value)
+ end
+ when Array
+ object.map { |e| symbolize_keys(e) }
+ else
+ object
+ end
+ end
+
+ def self.fetch(object, path)
+ node = object
+
+ return nil unless node
+
+ path.each do |key|
+ node = node[key]
+ break if node.nil?
+ end
+
+ node
+ end
+ end
+ end
+ end
+end
diff --git a/nano-bots.gemspec b/nano-bots.gemspec
new file mode 100644
index 0000000..88a1270
--- /dev/null
+++ b/nano-bots.gemspec
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require_relative 'static/gem'
+
+Gem::Specification.new do |spec|
+ spec.name = NanoBot::GEM[:name]
+ spec.version = NanoBot::GEM[:version]
+ spec.authors = [NanoBot::GEM[:author]]
+
+ spec.summary = NanoBot::GEM[:summary]
+ spec.description = NanoBot::GEM[:description]
+
+ spec.homepage = NanoBot::GEM[:github]
+
+ spec.license = NanoBot::GEM[:license]
+
+ spec.required_ruby_version = Gem::Requirement.new(">= #{NanoBot::GEM[:ruby]}")
+
+ spec.metadata['allowed_push_host'] = NanoBot::GEM[:gem_server]
+
+ spec.metadata['homepage_uri'] = spec.homepage
+ spec.metadata['source_code_uri'] = NanoBot::GEM[:github]
+
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
+ `git ls-files -z`.split("\x0").reject do |f|
+ f.match(%r{\A(?:test|spec|features)/})
+ end
+ end
+
+ spec.require_paths = ['ports/dsl']
+
+ spec.executables = ['rnb']
+
+ spec.add_dependency 'babosa', '~> 2.0'
+ spec.add_dependency 'dotenv', '~> 2.8', '>= 2.8.1'
+ spec.add_dependency 'faraday', '~> 2.7', '>= 2.7.4'
+ spec.add_dependency 'pry', '~> 0.14.2'
+ spec.add_dependency 'rainbow', '~> 3.1', '>= 3.1.1'
+ spec.add_dependency 'ruby-openai', '~> 4.0'
+
+ spec.metadata['rubygems_mfa_required'] = 'true'
+end
diff --git a/ports/dsl/nano-bots.rb b/ports/dsl/nano-bots.rb
new file mode 100644
index 0000000..bce169c
--- /dev/null
+++ b/ports/dsl/nano-bots.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'dotenv/load'
+
+require_relative '../../static/gem'
+require_relative '../../controllers/instance'
+require_relative '../../controllers/interfaces/cli'
+
+module NanoBot
+ def self.new(cartridge:, state: '-')
+ Controllers::Instance.new(cartridge_path: cartridge, state:)
+ end
+
+ def self.cli
+ Controllers::Interfaces::CLI.handle!
+ end
+
+ def self.repl(cartridge:, state: '-')
+ Controllers::Instance.new(cartridge_path: cartridge, state:).repl
+ end
+
+ def self.version
+ NanoBot::GEM[:version]
+ end
+end
diff --git a/ports/dsl/nano-bots/cli.rb b/ports/dsl/nano-bots/cli.rb
new file mode 100644
index 0000000..3212db6
--- /dev/null
+++ b/ports/dsl/nano-bots/cli.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+require 'nano-bots'
+
+NanoBot.cli
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..4f8c8d9
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+# This file was generated by the `rspec --init` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause
+# this file to always be loaded, without a need to explicitly require it in any
+# files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need
+# it.
+#
+# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ # rspec-expectations config goes here. You can use an alternate
+ # assertion/expectation library such as wrong or the stdlib/minitest
+ # assertions if you prefer.
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`, e.g.:
+ # be_bigger_than(2).and_smaller_than(4).description
+ # # => "be bigger than 2 and smaller than 4"
+ # ...rather than:
+ # # => "be bigger than 2"
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ # rspec-mocks config goes here. You can use an alternate test double
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
+ # have no way to turn it off -- the option exists only for backwards
+ # compatibility in RSpec 3). It causes shared context metadata to be
+ # inherited by the metadata hash of host groups and examples, rather than
+ # triggering implicit auto-inclusion in groups with matching metadata.
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+
+ # The settings below are suggested to provide a good initial experience
+ # with RSpec, but feel free to customize to your heart's content.
+ # # This allows you to limit a spec run to individual examples or groups
+ # # you care about by tagging them with `:focus` metadata. When nothing
+ # # is tagged with `:focus`, all examples get run. RSpec also provides
+ # # aliases for `it`, `describe`, and `context` that include `:focus`
+ # # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+ # config.filter_run_when_matching :focus
+ #
+ # # Allows RSpec to persist some state between runs in order to support
+ # # the `--only-failures` and `--next-failure` CLI options. We recommend
+ # # you configure your source control system to ignore this file.
+ # config.example_status_persistence_file_path = "spec/examples.txt"
+ #
+ # # Limits the available syntax to the non-monkey patched syntax that is
+ # # recommended. For more details, see:
+ # # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
+ # config.disable_monkey_patching!
+ #
+ # # This setting enables warnings. It's recommended, but in some cases may
+ # # be too noisy due to issues in dependencies.
+ # config.warnings = true
+ #
+ # # Many RSpec users commonly either run the entire suite or an individual
+ # # file, and it's useful to allow more verbose output when running an
+ # # individual spec file.
+ # if config.files_to_run.one?
+ # # Use the documentation formatter for detailed output,
+ # # unless a formatter has already been configured
+ # # (e.g. via a command-line flag).
+ # config.default_formatter = "doc"
+ # end
+ #
+ # # Print the 10 slowest examples and example groups at the
+ # # end of the spec run, to help surface which specs are running
+ # # particularly slow.
+ # config.profile_examples = 10
+ #
+ # # Run specs in random order to surface order dependencies. If you find an
+ # # order dependency and want to debug it, you can fix the order by providing
+ # # the seed, which is printed after each run.
+ # # --seed 1234
+ # config.order = :random
+ #
+ # # Seed global randomization in this process using the `--seed` CLI option.
+ # # Setting this allows you to use `--seed` to deterministically reproduce
+ # # test failures related to randomization by passing the same `--seed` value
+ # # as the one that triggered the failure.
+ # Kernel.srand config.seed
+end
diff --git a/static/gem.rb b/static/gem.rb
new file mode 100644
index 0000000..afcc9f1
--- /dev/null
+++ b/static/gem.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module NanoBot
+ GEM = {
+ name: 'nano-bots',
+ version: '0.0.1',
+ author: 'icebaker',
+ summary: 'Iris REPL',
+ description: 'Intelligent Ruby Interface System Read–eval–print loop',
+ github: 'https://github.com/icebaker/nano-bot',
+ gem_server: 'https://rubygems.org',
+ license: 'MIT',
+ ruby: '3.1.4'
+ }.freeze
+end