summaryrefslogtreecommitdiff
path: root/controllers
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 /controllers
first commit
Diffstat (limited to 'controllers')
-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
4 files changed, 291 insertions, 0 deletions
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