diff options
Diffstat (limited to 'controllers')
-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 |
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 |