nano-bots 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: eb32dfa429c178b7af6c3f9a4c8046846ab9ae894c94bbf2038644ceef9c2368
4
+ data.tar.gz: 21e37c25c085c27f0f0357f8bb878cba2a366641f9d4649dc25536bcd51afc28
5
+ SHA512:
6
+ metadata.gz: 0a4a1f1a7f33eb6da088b8418cd0d4da61d8e6890ee8b6a0276330217164966b7266c032eabf3e3454e7879bd5e525b47cc99eff40259b7a1a9f9cf86a72db4f
7
+ data.tar.gz: 2098e8e5da98da2499ea4267e70be61ba9a06feb5c3fb6375d8ff05dbd01ae72134d34e5205884af80f3c4029c32dc09fec3c91b744ee6f61b05db8a5309b8ec
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .env
2
+ *.gem
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,9 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1.4
3
+ NewCops: enable
4
+
5
+ Style/Documentation:
6
+ Enabled: false
7
+
8
+ require:
9
+ - rubocop-rspec
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :test, :development do
8
+ gem 'rspec', '~> 3.12'
9
+ gem 'rubocop', '~> 1.47'
10
+ gem 'rubocop-rspec', '~> 2.22'
11
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,88 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ nano-bots (0.0.1)
5
+ babosa (~> 2.0)
6
+ dotenv (~> 2.8, >= 2.8.1)
7
+ faraday (~> 2.7, >= 2.7.4)
8
+ pry (~> 0.14.2)
9
+ rainbow (~> 3.1, >= 3.1.1)
10
+ ruby-openai (~> 4.0)
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ ast (2.4.2)
16
+ babosa (2.0.0)
17
+ coderay (1.1.3)
18
+ diff-lcs (1.5.0)
19
+ dotenv (2.8.1)
20
+ faraday (2.7.4)
21
+ faraday-net_http (>= 2.0, < 3.1)
22
+ ruby2_keywords (>= 0.0.4)
23
+ faraday-multipart (1.0.4)
24
+ multipart-post (~> 2)
25
+ faraday-net_http (3.0.2)
26
+ json (2.6.3)
27
+ method_source (1.0.0)
28
+ multipart-post (2.3.0)
29
+ parallel (1.23.0)
30
+ parser (3.2.2.1)
31
+ ast (~> 2.4.1)
32
+ pry (0.14.2)
33
+ coderay (~> 1.1)
34
+ method_source (~> 1.0)
35
+ rainbow (3.1.1)
36
+ regexp_parser (2.8.0)
37
+ rexml (3.2.5)
38
+ rspec (3.12.0)
39
+ rspec-core (~> 3.12.0)
40
+ rspec-expectations (~> 3.12.0)
41
+ rspec-mocks (~> 3.12.0)
42
+ rspec-core (3.12.2)
43
+ rspec-support (~> 3.12.0)
44
+ rspec-expectations (3.12.3)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.12.0)
47
+ rspec-mocks (3.12.5)
48
+ diff-lcs (>= 1.2.0, < 2.0)
49
+ rspec-support (~> 3.12.0)
50
+ rspec-support (3.12.0)
51
+ rubocop (1.50.2)
52
+ json (~> 2.3)
53
+ parallel (~> 1.10)
54
+ parser (>= 3.2.0.0)
55
+ rainbow (>= 2.2.2, < 4.0)
56
+ regexp_parser (>= 1.8, < 3.0)
57
+ rexml (>= 3.2.5, < 4.0)
58
+ rubocop-ast (>= 1.28.0, < 2.0)
59
+ ruby-progressbar (~> 1.7)
60
+ unicode-display_width (>= 2.4.0, < 3.0)
61
+ rubocop-ast (1.28.1)
62
+ parser (>= 3.2.1.0)
63
+ rubocop-capybara (2.18.0)
64
+ rubocop (~> 1.41)
65
+ rubocop-factory_bot (2.22.0)
66
+ rubocop (~> 1.33)
67
+ rubocop-rspec (2.22.0)
68
+ rubocop (~> 1.33)
69
+ rubocop-capybara (~> 2.17)
70
+ rubocop-factory_bot (~> 2.22)
71
+ ruby-openai (4.0.0)
72
+ faraday (>= 1)
73
+ faraday-multipart (>= 1)
74
+ ruby-progressbar (1.13.0)
75
+ ruby2_keywords (0.0.5)
76
+ unicode-display_width (2.4.2)
77
+
78
+ PLATFORMS
79
+ x86_64-linux
80
+
81
+ DEPENDENCIES
82
+ nano-bots!
83
+ rspec (~> 3.12)
84
+ rubocop (~> 1.47)
85
+ rubocop-rspec (~> 2.22)
86
+
87
+ BUNDLED WITH
88
+ 2.4.13
data/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # Nano Bots 💎 🤖
2
+
3
+ A Ruby implementation of the [Nano Bots](https://github.com/icebaker/nano-bots) specification.
4
+
5
+ ![Ruby Nano Bots](https://user-images.githubusercontent.com/113217272/237839690-7880915a-b287-4484-a75e-0b96284b8a32.png)
6
+ _Image artificially created by Midjourney through a prompt generated by a Nano Bot specialized in Midjourney._
7
+
8
+ https://user-images.githubusercontent.com/113217272/237840989-1e29a5cc-6644-48d0-87b4-62798dc6ebd3.mp4
9
+
10
+ - [Setup](#setup)
11
+ - [Usage](#usage)
12
+ - [Command Line](#command-line)
13
+ - [Library](#library)
14
+ - [Cartridges](#cartridges)
15
+ - [Development](#development)
16
+ - [Publish to RubyGems](#publish-to-rubygems)
17
+
18
+ ## Setup
19
+
20
+ For a system usage:
21
+
22
+ ```sh
23
+ gem install nano-bots -v 0.0.1
24
+ ```
25
+
26
+ To use it in a project, add it to your `Gemfile`:
27
+
28
+ ```ruby
29
+ gem 'nano-bots', '~> 0.0.1'
30
+ ```
31
+
32
+ ```sh
33
+ bundle install
34
+ ```
35
+
36
+ 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:
37
+
38
+ ```sh
39
+ export OPENAI_API_ADDRESS=https://api.openai.com
40
+ export OPENAI_API_ACCESS_TOKEN=your-token
41
+ export OPENAI_API_USER_IDENTIFIER=your-user
42
+
43
+ export NANO_BOTS_STATE_DIRECTORY=/home/user/.local/state/nano-bots
44
+ export NANO_BOTS_CARTRIDGES_DIRECTORY=/home/user/.local/share/nano-bots/cartridges
45
+ ```
46
+
47
+ Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded.
48
+
49
+ ## Usage
50
+
51
+ ### Command Line
52
+
53
+ After installing the gem, the `rnb` binary command will be available for your project or system.
54
+
55
+ Examples of usage:
56
+
57
+ ```bash
58
+ rnb to-en-us-translator.yml - eval "Salut, comment ça va?"
59
+ # => Hello, how are you doing?
60
+
61
+ rnb midjourney.yml - eval "happy and friendly cyberpunk robot"
62
+ # => The robot exploring a bustling city, surrounded by neon lights
63
+ # and high-rise buildings. The prompt should include colorful
64
+ # lighting and a sense of excitement in the facial expression.
65
+
66
+ rnb lisp.yml - eval "(+ 1 2)"
67
+ # => 3
68
+
69
+ cat article.txt |
70
+ rnb to-en-us-translator.yml - eval |
71
+ rnb summarizer.yml - eval
72
+ # -> LLM stands for Large Language Model, which refers to an
73
+ # artificial intelligence algorithm capable of processing
74
+ # and understanding vast amounts of natural language data,
75
+ # allowing it to generate human-like responses and perform
76
+ # a range of language-related tasks.
77
+ ```
78
+
79
+ ```bash
80
+ rnb assistant.yml - repl
81
+ ```
82
+
83
+ 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:
84
+
85
+ ```ruby
86
+ require 'securerandom'
87
+
88
+ SecureRandom.hex # => 6ea6c43c42a1c076b1e3c36fa349ac2c
89
+ ```
90
+
91
+ ```bash
92
+ rnb assistant.yml your-user eval "Salut, comment ça va?"
93
+ rnb assistant.yml your-user repl
94
+
95
+ rnb assistant.yml 6ea6c43c42a1c076b1e3c36fa349ac2c eval "Salut, comment ça va?"
96
+ rnb assistant.yml 6ea6c43c42a1c076b1e3c36fa349ac2c repl
97
+ ```
98
+
99
+ ### Library
100
+
101
+ To use it as a library:
102
+
103
+ ```ruby
104
+ require 'nano-bots/cli' # Equivalent to the `rnb` command.
105
+ ```
106
+
107
+ ```ruby
108
+ require 'nano-bots'
109
+
110
+ NanoBot.cli # Equivalent to the `rnb` command.
111
+
112
+ NanoBot.repl(cartridge: 'cartridge.yml') # Starts a new REPL.
113
+
114
+ bot = NanoBot.new(cartridge: 'cartridge.yml')
115
+
116
+ bot.eval('Hello')
117
+
118
+ bot.repl # Starts a new REPL.
119
+
120
+ NanoBot.repl(cartridge: 'cartridge.yml', state: '6ea6c43c42a1c076b1e3c36fa349ac2c')
121
+
122
+ bot = NanoBot.new(cartridge: 'cartridge.yml', state: '6ea6c43c42a1c076b1e3c36fa349ac2c')
123
+ ```
124
+
125
+ ## Cartridges
126
+
127
+ Here's what a Nano Bot Cartridge looks like:
128
+
129
+ ```yaml
130
+ ---
131
+ name: Assistant
132
+ version: 0.0.1
133
+
134
+ behaviors:
135
+ interaction:
136
+ directive: You are a helpful assistant.
137
+
138
+ interfaces:
139
+ repl:
140
+ prompt:
141
+ - text: '🤖'
142
+ - text: '> '
143
+ color: blue
144
+
145
+ provider:
146
+ name: openai
147
+ settings:
148
+ model: gpt-3.5-turbo
149
+ credentials:
150
+ address: ENV/OPENAI_API_ADDRESS
151
+ access-token: ENV/OPENAI_API_ACCESS_TOKEN
152
+ user-identifier: ENV/OPENAI_API_USER_IDENTIFIER
153
+ ```
154
+
155
+ Check the Nano Bots specification to learn more about [how to build cartridges](https://icebaker.github.io/nano-bots/#/README?id=cartridges).
156
+
157
+ ## Development
158
+
159
+ ```bash
160
+ bundle
161
+ rubocop -A
162
+ rspec
163
+ ```
164
+
165
+ ### Publish to RubyGems
166
+
167
+ ```bash
168
+ gem build nano-bots.gemspec
169
+
170
+ gem signin
171
+
172
+ gem push nano-bots-0.0.1.gem
173
+ ```
data/bin/rnb ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'nano-bots/cli'
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openai'
4
+
5
+ require_relative './providers/openai'
6
+
7
+ module NanoBot
8
+ module Components
9
+ class Provider
10
+ def self.new(provider)
11
+ case provider[:name]
12
+ when 'openai'
13
+ Providers::OpenAI.new(provider[:settings])
14
+ else
15
+ raise "Unsupported provider #{provider[:name]}"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openai'
4
+
5
+ module NanoBot
6
+ module Components
7
+ module Providers
8
+ class Base
9
+ def evaluate(_payload)
10
+ raise NoMethodError, "The 'evaluate' method is not implemented for the current provider."
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openai'
4
+
5
+ require_relative './base'
6
+
7
+ module NanoBot
8
+ module Components
9
+ module Providers
10
+ class OpenAI < Base
11
+ CHAT_SETTINGS = %i[
12
+ model stream temperature top_p n stop max_tokens
13
+ presence_penalty frequency_penalty logit_bias
14
+ ].freeze
15
+
16
+ attr_reader :settings
17
+
18
+ def initialize(settings)
19
+ @settings = settings
20
+
21
+ @client = ::OpenAI::Client.new(
22
+ uri_base: "#{@settings[:credentials][:address].sub(%r{/$}, '')}/",
23
+ access_token: @settings[:credentials][:'access-token']
24
+ )
25
+ end
26
+
27
+ def evaluate(input, &block)
28
+ messages = input[:history].map do |event|
29
+ { role: event[:who] == 'user' ? 'user' : 'assistant',
30
+ content: event[:message] }
31
+ end
32
+
33
+ %i[instruction backdrop directive].each do |key|
34
+ next unless input[:behavior][key]
35
+
36
+ messages.prepend(
37
+ { role: key == :directive ? 'system' : 'user',
38
+ content: input[:behavior][key] }
39
+ )
40
+ end
41
+
42
+ payload = {
43
+ model: @settings[:model],
44
+ user: @settings[:credentials][:'user-identifier'],
45
+ messages:
46
+ }
47
+
48
+ CHAT_SETTINGS.each do |key|
49
+ payload[key] = @settings[key] if @settings.key?(key)
50
+ end
51
+
52
+ payload.delete(:logit_bias) if payload.key?(:logit_bias) && payload[:logit_bias].nil?
53
+
54
+ if @settings[:stream] && input[:interface][:stream]
55
+ content = ''
56
+
57
+ payload[:stream] = proc do |chunk, _bytesize|
58
+ partial = chunk.dig('choices', 0, 'delta', 'content')
59
+ if partial
60
+ content += partial
61
+ block.call({ who: 'AI', message: partial }, false)
62
+ end
63
+
64
+ block.call({ who: 'AI', message: content }, true) if chunk.dig('choices', 0, 'finish_reason')
65
+ end
66
+
67
+ @client.chat(parameters: payload)
68
+ else
69
+ result = @client.chat(parameters: payload)
70
+
71
+ raise StandardError, result['error'] if result['error']
72
+
73
+ block.call({ who: 'AI', message: result.dig('choices', 0, 'message', 'content') }, true)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babosa'
4
+
5
+ require_relative '../logic/helpers/hash'
6
+
7
+ module NanoBot
8
+ module Components
9
+ class Storage
10
+ def self.build_path_and_ensure_state_file!(key, cartridge)
11
+ path = [
12
+ Logic::Helpers::Hash.fetch(cartridge, %i[state directory]),
13
+ ENV.fetch('NANO_BOTS_STATE_DIRECTORY', nil)
14
+ ].find do |candidate|
15
+ !candidate.nil? && !candidate.empty?
16
+ end
17
+
18
+ path = "#{user_home!.sub(%r{/$}, '')}/.local/state/nano-bots" if path.nil?
19
+
20
+ path = "#{path.sub(%r{/$}, '')}/nano-bots-rb/#{cartridge[:name].to_slug.normalize}"
21
+ path = "#{path}/#{cartridge[:version].to_slug.normalize}/#{key.to_slug.normalize}"
22
+ path = "#{path}/state.json"
23
+
24
+ FileUtils.mkdir_p(File.dirname(path))
25
+
26
+ File.write(path, JSON.generate({ key:, history: [] })) unless File.exist?(path)
27
+
28
+ path
29
+ end
30
+
31
+ def self.cartridge_path(path)
32
+ partial = File.join(File.dirname(path), File.basename(path, File.extname(path)))
33
+
34
+ candidates = [
35
+ path,
36
+ "#{partial}.yml",
37
+ "#{partial}.yaml"
38
+ ]
39
+
40
+ unless ENV.fetch('NANO_BOTS_CARTRIDGES_DIRECTORY', nil).nil?
41
+ directory = ENV.fetch('NANO_BOTS_CARTRIDGES_DIRECTORY').sub(%r{/$}, '')
42
+
43
+ partial = File.join(File.dirname(partial), File.basename(partial, File.extname(partial)))
44
+
45
+ partial = path.sub(%r{^\.?/}, '')
46
+
47
+ candidates << "#{directory}/#{partial}"
48
+ candidates << "#{directory}/#{partial}.yml"
49
+ candidates << "#{directory}/#{partial}.yaml"
50
+ end
51
+
52
+ directory = "#{user_home!.sub(%r{/$}, '')}/.local/share/nano-bots/cartridges"
53
+
54
+ partial = File.join(File.dirname(partial), File.basename(partial, File.extname(partial)))
55
+
56
+ partial = path.sub(%r{^\.?/}, '')
57
+
58
+ candidates << "#{directory}/#{partial}"
59
+ candidates << "#{directory}/#{partial}.yml"
60
+ candidates << "#{directory}/#{partial}.yaml"
61
+
62
+ candidates = candidates.uniq
63
+
64
+ candidates.find do |candidate|
65
+ File.exist?(candidate) && File.file?(candidate)
66
+ end
67
+ end
68
+
69
+ def self.user_home!
70
+ [Dir.home, `echo ~`.strip, '~'].find do |candidate|
71
+ !candidate.nil? && !candidate.empty?
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ require_relative '../logic/helpers/hash'
6
+ require_relative '../components/provider'
7
+ require_relative '../components/storage'
8
+ require_relative './interfaces/repl'
9
+ require_relative './session'
10
+
11
+ module NanoBot
12
+ module Controllers
13
+ class Instance
14
+ def initialize(cartridge_path:, state: nil)
15
+ load_cartridge!(cartridge_path)
16
+
17
+ provider = Components::Provider.new(@cartridge[:provider])
18
+
19
+ @session = Session.new(provider:, cartridge: @cartridge, state:)
20
+ end
21
+
22
+ def debug
23
+ @session.debug
24
+ end
25
+
26
+ def eval(input)
27
+ @session.evaluate_and_print(input, mode: 'eval')
28
+ end
29
+
30
+ def repl
31
+ Interfaces::REPL.start(@cartridge, @session)
32
+ end
33
+
34
+ private
35
+
36
+ def load_cartridge!(path)
37
+ @cartridge = Logic::Helpers::Hash.symbolize_keys(
38
+ YAML.safe_load(
39
+ File.read(Components::Storage.cartridge_path(path)),
40
+ permitted_classes: [Symbol]
41
+ )
42
+ )
43
+
44
+ inject_environment_variables!(@cartridge)
45
+ end
46
+
47
+ def inject_environment_variables!(node)
48
+ case node
49
+ when Hash
50
+ node.each do |key, value|
51
+ node[key] = inject_environment_variables!(value)
52
+ end
53
+ when Array
54
+ node.each_with_index do |value, index|
55
+ node[index] = inject_environment_variables!(value)
56
+ end
57
+ when String
58
+ node.start_with?('ENV') ? ENV.fetch(node.sub(/^ENV./, ''), nil) : node
59
+ else
60
+ node
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,47 @@
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 'help', '', nil
16
+ puts "Ruby Nano Bots #{NanoBot::GEM[:version]}"
17
+ puts ' rnb cartridge.yml - eval "Hello"'
18
+ puts ' rnb cartridge.yml - repl'
19
+ puts ' rnb cartridge.yml - debug'
20
+ puts ' rnb cartridge.yml STATE-KEY eval "Hello"'
21
+ puts ' rnb cartridge.yml STATE-KEY repl'
22
+ puts ' rnb cartridge.yml STATE-KEY debug'
23
+ puts ' rnb version'
24
+ exit
25
+ end
26
+
27
+ params = { cartridge_path: ARGV[0], state: ARGV[1], command: ARGV[2] }
28
+
29
+ bot = Instance.new(cartridge_path: params[:cartridge_path], state: params[:state])
30
+
31
+ case params[:command]
32
+ when 'eval'
33
+ params[:input] = ARGV[3..]&.join(' ')
34
+ params[:input] = $stdin.read.chomp if params[:input].nil? || params[:input].empty?
35
+ bot.eval(params[:input])
36
+ when 'repl'
37
+ bot.repl
38
+ when 'debug'
39
+ bot.debug
40
+ else
41
+ raise "TODO: [#{params[:command]}]"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pry'
4
+ require 'rainbow'
5
+
6
+ require_relative '../../logic/helpers/hash'
7
+
8
+ module NanoBot
9
+ module Controllers
10
+ module Interfaces
11
+ module REPL
12
+ def self.start(cartridge, session)
13
+ if Logic::Helpers::Hash.fetch(
14
+ cartridge, %i[interfaces repl prefix]
15
+ )
16
+ session.print(Logic::Helpers::Hash.fetch(cartridge,
17
+ %i[interfaces repl prefix]))
18
+ end
19
+
20
+ session.boot(mode: 'repl')
21
+
22
+ session.print(Logic::Helpers::Hash.fetch(cartridge, %i[interfaces repl postfix]) || "\n")
23
+
24
+ session.flush
25
+
26
+ prompt = build_prompt(cartridge[:interfaces][:repl][:prompt])
27
+
28
+ Pry.config.prompt = Pry::Prompt.new(
29
+ 'REPL',
30
+ 'REPL Prompt',
31
+ [proc { prompt }, proc { 'MISSING INPUT' }]
32
+ )
33
+
34
+ Pry.commands.block_command(/(.*)/, 'handler') do |line|
35
+ if Logic::Helpers::Hash.fetch(
36
+ cartridge, %i[interfaces repl prefix]
37
+ )
38
+ session.print(Logic::Helpers::Hash.fetch(
39
+ cartridge, %i[interfaces repl prefix]
40
+ ))
41
+ end
42
+
43
+ session.evaluate_and_print(line, mode: 'repl')
44
+ session.print(Logic::Helpers::Hash.fetch(cartridge, %i[interfaces repl postfix]) || "\n")
45
+ session.flush
46
+ end
47
+
48
+ Pry.start
49
+ end
50
+
51
+ def self.build_prompt(prompt)
52
+ result = ''
53
+
54
+ prompt.each do |partial|
55
+ result += if partial[:color]
56
+ Rainbow(partial[:text]).send(partial[:color])
57
+ else
58
+ partial[:text]
59
+ end
60
+ end
61
+
62
+ result
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babosa'
4
+
5
+ require 'fileutils'
6
+
7
+ require_relative '../logic/helpers/hash'
8
+ require_relative '../components/storage'
9
+
10
+ module NanoBot
11
+ module Controllers
12
+ STREAM_TIMEOUT_IN_SECONDS = 5
13
+
14
+ class Session
15
+ def initialize(provider:, cartridge:, state: nil)
16
+ @provider = provider
17
+ @cartridge = cartridge
18
+
19
+ @output = $stdout
20
+
21
+ @stateless = state.nil? || state.strip == '-' || state.strip.empty?
22
+
23
+ if @stateless
24
+ @state = { history: [] }
25
+ else
26
+ @state_path = Components::Storage.build_path_and_ensure_state_file!(
27
+ state.strip, @cartridge
28
+ )
29
+ @state = load_state
30
+ end
31
+ end
32
+
33
+ def debug
34
+ pp({
35
+ state: {
36
+ path: @state_path,
37
+ content: @state
38
+ }
39
+ })
40
+ end
41
+
42
+ def load_state
43
+ @state = Logic::Helpers::Hash.symbolize_keys(JSON.parse(File.read(@state_path)))
44
+ end
45
+
46
+ def store_state!
47
+ File.write(@state_path, JSON.generate(@state))
48
+ end
49
+
50
+ def boot(mode:)
51
+ return unless Logic::Helpers::Hash.fetch(@cartridge, %i[behaviors boot instruction])
52
+
53
+ behavior = Logic::Helpers::Hash.fetch(@cartridge, %i[behaviors boot]) || {}
54
+
55
+ input = { behavior:, history: [] }
56
+
57
+ process(input, mode:)
58
+ end
59
+
60
+ def evaluate_and_print(message, mode:)
61
+ behavior = Logic::Helpers::Hash.fetch(@cartridge, %i[behaviors interaction]) || {}
62
+
63
+ @state[:history] << ({ who: 'user', message: })
64
+
65
+ input = { behavior:, history: @state[:history] }
66
+
67
+ process(input, mode:)
68
+ end
69
+
70
+ def process(input, mode:)
71
+ streaming = @provider.settings[:stream] && Logic::Helpers::Hash.fetch(
72
+ @cartridge, [:interfaces, mode.to_sym, :stream]
73
+ )
74
+
75
+ interface = Logic::Helpers::Hash.fetch(@cartridge, [:interfaces, mode.to_sym]) || {}
76
+
77
+ input[:interface] = interface
78
+
79
+ updated_at = Time.now
80
+
81
+ ready = false
82
+ @provider.evaluate(input) do |output, finished|
83
+ updated_at = Time.now
84
+ if finished
85
+ @state[:history] << output
86
+ self.print(output[:message]) unless streaming
87
+ unless Logic::Helpers::Hash.fetch(@cartridge, [:interfaces, mode.to_sym, :postfix]).nil?
88
+ self.print(Logic::Helpers::Hash.fetch(@cartridge, [:interfaces, mode.to_sym, :postfix]))
89
+ end
90
+ ready = true
91
+ flush
92
+ elsif streaming
93
+ self.print(output[:message])
94
+ end
95
+ end
96
+
97
+ until ready
98
+ seconds = (Time.now - updated_at).to_i
99
+ raise StandardError, 'The stream has become unresponsive.' if seconds >= STREAM_TIMEOUT_IN_SECONDS
100
+ end
101
+
102
+ store_state! unless @stateless
103
+ end
104
+
105
+ def flush
106
+ @output.flush
107
+ end
108
+
109
+ def print(content)
110
+ @output.write(content)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NanoBot
4
+ module Logic
5
+ module Helpers
6
+ module Hash
7
+ def self.symbolize_keys(object)
8
+ case object
9
+ when ::Hash
10
+ object.each_with_object({}) do |(key, value), result|
11
+ result[key.to_sym] = symbolize_keys(value)
12
+ end
13
+ when Array
14
+ object.map { |e| symbolize_keys(e) }
15
+ else
16
+ object
17
+ end
18
+ end
19
+
20
+ def self.fetch(object, path)
21
+ node = object
22
+
23
+ return nil unless node
24
+
25
+ path.each do |key|
26
+ node = node[key]
27
+ break if node.nil?
28
+ end
29
+
30
+ node
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
data/nano-bots.gemspec ADDED
@@ -0,0 +1,42 @@
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 = ['rnb']
33
+
34
+ spec.add_dependency 'babosa', '~> 2.0'
35
+ spec.add_dependency 'dotenv', '~> 2.8', '>= 2.8.1'
36
+ spec.add_dependency 'faraday', '~> 2.7', '>= 2.7.4'
37
+ spec.add_dependency 'pry', '~> 0.14.2'
38
+ spec.add_dependency 'rainbow', '~> 3.1', '>= 3.1.1'
39
+ spec.add_dependency 'ruby-openai', '~> 4.0'
40
+
41
+ spec.metadata['rubygems_mfa_required'] = 'true'
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nano-bots'
4
+
5
+ NanoBot.cli
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotenv/load'
4
+
5
+ require_relative '../../static/gem'
6
+ require_relative '../../controllers/instance'
7
+ require_relative '../../controllers/interfaces/cli'
8
+
9
+ module NanoBot
10
+ def self.new(cartridge:, state: '-')
11
+ Controllers::Instance.new(cartridge_path: cartridge, state:)
12
+ end
13
+
14
+ def self.cli
15
+ Controllers::Interfaces::CLI.handle!
16
+ end
17
+
18
+ def self.repl(cartridge:, state: '-')
19
+ Controllers::Instance.new(cartridge_path: cartridge, state:).repl
20
+ end
21
+
22
+ def self.version
23
+ NanoBot::GEM[:version]
24
+ end
25
+ end
data/static/gem.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NanoBot
4
+ GEM = {
5
+ name: 'nano-bots',
6
+ version: '0.0.1',
7
+ author: 'icebaker',
8
+ summary: 'Ruby Implementation of Nano Bots: small, AI-powered bots',
9
+ description: 'Ruby Implementation of Nano Bots: small, AI-powered bots easily shared as a single file, designed to support multiple providers such as Vicuna, OpenAI ChatGPT, Google PaLM, Alpaca, and LLaMA.',
10
+ github: 'https://github.com/icebaker/ruby-nano-bots',
11
+ gem_server: 'https://rubygems.org',
12
+ license: 'MIT',
13
+ ruby: '3.1.4'
14
+ }.freeze
15
+ end
metadata ADDED
@@ -0,0 +1,172 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nano-bots
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - icebaker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-05-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: babosa
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dotenv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.8'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 2.8.1
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '2.8'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 2.8.1
47
+ - !ruby/object:Gem::Dependency
48
+ name: faraday
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.7'
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 2.7.4
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '2.7'
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 2.7.4
67
+ - !ruby/object:Gem::Dependency
68
+ name: pry
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: 0.14.2
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: 0.14.2
81
+ - !ruby/object:Gem::Dependency
82
+ name: rainbow
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '3.1'
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 3.1.1
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '3.1'
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 3.1.1
101
+ - !ruby/object:Gem::Dependency
102
+ name: ruby-openai
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '4.0'
108
+ type: :runtime
109
+ prerelease: false
110
+ version_requirements: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '4.0'
115
+ description: 'Ruby Implementation of Nano Bots: small, AI-powered bots easily shared
116
+ as a single file, designed to support multiple providers such as Vicuna, OpenAI
117
+ ChatGPT, Google PaLM, Alpaca, and LLaMA.'
118
+ email:
119
+ executables:
120
+ - rnb
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".gitignore"
125
+ - ".rspec"
126
+ - ".rubocop.yml"
127
+ - Gemfile
128
+ - Gemfile.lock
129
+ - LICENSE
130
+ - README.md
131
+ - bin/rnb
132
+ - components/provider.rb
133
+ - components/providers/base.rb
134
+ - components/providers/openai.rb
135
+ - components/storage.rb
136
+ - controllers/instance.rb
137
+ - controllers/interfaces/cli.rb
138
+ - controllers/interfaces/repl.rb
139
+ - controllers/session.rb
140
+ - logic/helpers/hash.rb
141
+ - nano-bots.gemspec
142
+ - ports/dsl/nano-bots.rb
143
+ - ports/dsl/nano-bots/cli.rb
144
+ - static/gem.rb
145
+ homepage: https://github.com/icebaker/ruby-nano-bots
146
+ licenses:
147
+ - MIT
148
+ metadata:
149
+ allowed_push_host: https://rubygems.org
150
+ homepage_uri: https://github.com/icebaker/ruby-nano-bots
151
+ source_code_uri: https://github.com/icebaker/ruby-nano-bots
152
+ rubygems_mfa_required: 'true'
153
+ post_install_message:
154
+ rdoc_options: []
155
+ require_paths:
156
+ - ports/dsl
157
+ required_ruby_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: 3.1.4
162
+ required_rubygems_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ requirements: []
168
+ rubygems_version: 3.4.10
169
+ signing_key:
170
+ specification_version: 4
171
+ summary: 'Ruby Implementation of Nano Bots: small, AI-powered bots'
172
+ test_files: []