diff options
author | icebaker <icebaker@proton.me> | 2023-05-11 19:24:50 -0300 |
---|---|---|
committer | icebaker <icebaker@proton.me> | 2023-05-11 19:24:50 -0300 |
commit | ec5e25547a401141586c87621266f9cd68c59e3c (patch) | |
tree | 547b3c7fa04c9e695785b9beeda0be5a4a77b006 |
first commit
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .rspec | 1 | ||||
-rw-r--r-- | .rubocop.yml | 9 | ||||
-rw-r--r-- | Gemfile | 11 | ||||
-rw-r--r-- | Gemfile.lock | 88 | ||||
-rw-r--r-- | LICENSE | 21 | ||||
-rw-r--r-- | README.md | 166 | ||||
-rwxr-xr-x | bin/rnb | 4 | ||||
-rw-r--r-- | components/provider.rb | 20 | ||||
-rw-r--r-- | components/providers/base.rb | 15 | ||||
-rw-r--r-- | components/providers/openai.rb | 79 | ||||
-rw-r--r-- | controllers/instance.rb | 61 | ||||
-rw-r--r-- | controllers/interfaces/cli.rb | 30 | ||||
-rw-r--r-- | controllers/interfaces/repl.rb | 67 | ||||
-rw-r--r-- | controllers/session.rb | 133 | ||||
-rw-r--r-- | logic/helpers/hash.rb | 35 | ||||
-rw-r--r-- | nano-bots.gemspec | 42 | ||||
-rw-r--r-- | ports/dsl/nano-bots.rb | 25 | ||||
-rw-r--r-- | ports/dsl/nano-bots/cli.rb | 5 | ||||
-rw-r--r-- | spec/spec_helper.rb | 98 | ||||
-rw-r--r-- | static/gem.rb | 15 |
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 @@ -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 @@ -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 @@ -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 +``` @@ -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 |