summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoricebaker <113217272+icebaker@users.noreply.github.com>2024-01-10 22:02:42 -0300
committerGitHub <noreply@github.com>2024-01-10 22:02:42 -0300
commit50677c9b2ab6ce8f6fe232834f139f1e16f0e079 (patch)
tree866218debc763547818de0099098bc5fe2ae29c7
parent819381e7bd3e3ca5d310ad0a29b6925dcfa26720 (diff)
parentaabf3d9b711f66fe4195a8c850856826c7ad5580 (diff)
Merge pull request #15 from icebaker/ib-markdown
Adding support to Markdown Cartridges
-rw-r--r--Gemfile.lock2
-rw-r--r--README.md10
-rw-r--r--components/embedding.rb2
-rw-r--r--components/providers/openai.rb4
-rw-r--r--components/storage.rb69
-rw-r--r--controllers/cartridges.rb42
-rw-r--r--controllers/instance.rb7
-rw-r--r--logic/cartridge/parser.rb134
-rw-r--r--logic/helpers/hash.rb23
-rw-r--r--nano-bots.gemspec1
-rw-r--r--ports/dsl/nano-bots.rb3
-rw-r--r--ports/dsl/nano-bots/cartridges.rb15
-rw-r--r--spec/components/storage_spec.rb53
-rw-r--r--spec/data/cartridges/block.md7
-rw-r--r--spec/data/cartridges/markdown.md37
-rw-r--r--spec/data/cartridges/meta.md25
-rw-r--r--spec/data/cartridges/models/ollama/phi-2.yml (renamed from spec/data/cartridges/models/ollama/llama2.yml)4
-rw-r--r--spec/data/cartridges/tools.md76
-rw-r--r--spec/logic/cartridge/parser_spec.rb119
-rw-r--r--spec/logic/helpers/hash_spec.rb17
20 files changed, 600 insertions, 50 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 6b3cc60..fc0da2e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,6 +13,7 @@ PATH
pry (~> 0.14.2)
rainbow (~> 3.1, >= 3.1.1)
rbnacl (~> 7.1, >= 7.1.1)
+ redcarpet (~> 3.6)
ruby-openai (~> 6.3, >= 6.3.1)
sweet-moon (~> 0.0.7)
@@ -82,6 +83,7 @@ GEM
rainbow (3.1.1)
rbnacl (7.1.1)
ffi
+ redcarpet (3.6.0)
regexp_parser (2.8.3)
rexml (3.2.6)
rspec (3.12.0)
diff --git a/README.md b/README.md
index 7134b28..61a35b7 100644
--- a/README.md
+++ b/README.md
@@ -269,8 +269,8 @@ For credentials and configurations, relevant environment variables can be set in
export NANO_BOTS_ENCRYPTION_PASSWORD=UNSAFE
export NANO_BOTS_END_USER=your-user
-# export NANO_BOTS_STATE_DIRECTORY=/home/user/.local/state/nano-bots
-# export NANO_BOTS_CARTRIDGES_DIRECTORY=/home/user/.local/share/nano-bots/cartridges
+# export NANO_BOTS_STATE_PATH=/home/user/.local/state/nano-bots
+# export NANO_BOTS_CARTRIDGES_PATH=/home/user/.local/share/nano-bots/cartridges
```
Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded:
@@ -279,8 +279,8 @@ Alternatively, if your current directory has a `.env` file with the environment
NANO_BOTS_ENCRYPTION_PASSWORD=UNSAFE
NANO_BOTS_END_USER=your-user
-# NANO_BOTS_STATE_DIRECTORY=/home/user/.local/state/nano-bots
-# NANO_BOTS_CARTRIDGES_DIRECTORY=/home/user/.local/share/nano-bots/cartridges
+# NANO_BOTS_STATE_PATH=/home/user/.local/state/nano-bots
+# NANO_BOTS_CARTRIDGES_PATH=/home/user/.local/share/nano-bots/cartridges
```
### Cohere Command
@@ -963,7 +963,7 @@ cd ruby-nano-bots
cp docker-compose.example.yml docker-compose.yml
```
-Set your provider credentials and choose your desired directory for the cartridges files:
+Set your provider credentials and choose your desired path for the cartridges files:
### Cohere Command Container
diff --git a/components/embedding.rb b/components/embedding.rb
index f08ff3d..4dd2e6c 100644
--- a/components/embedding.rb
+++ b/components/embedding.rb
@@ -45,7 +45,7 @@ module NanoBot
def self.clojure(source:, parameters:, values:, safety:)
ensure_safety!(safety)
- raise 'TODO: sandboxed Clojure through Babashka not implemented' if safety[:sandboxed]
+ raise 'Sandboxed Clojure not supported.' if safety[:sandboxed]
raise 'invalid Clojure parameter name' if parameters.include?('injected-parameters')
diff --git a/components/providers/openai.rb b/components/providers/openai.rb
index e71f143..79b935e 100644
--- a/components/providers/openai.rb
+++ b/components/providers/openai.rb
@@ -140,7 +140,7 @@ module NanoBot
begin
@client.chat(parameters: Logic::OpenAI::Tokens.apply_policies!(cartridge, payload))
rescue StandardError => e
- raise e.class, e.response[:body] if e.response && e.response[:body]
+ raise e.class, e.response[:body] if e.respond_to?(:response) && e.response && e.response[:body]
raise e
end
@@ -148,7 +148,7 @@ module NanoBot
begin
result = @client.chat(parameters: Logic::OpenAI::Tokens.apply_policies!(cartridge, payload))
rescue StandardError => e
- raise e.class, e.response[:body] if e.response && e.response[:body]
+ raise e.class, e.response[:body] if e.respond_to?(:response) && e.response && e.response[:body]
raise e
end
diff --git a/components/storage.rb b/components/storage.rb
index 6a3fe13..10ea335 100644
--- a/components/storage.rb
+++ b/components/storage.rb
@@ -8,6 +8,8 @@ require_relative 'crypto'
module NanoBot
module Components
class Storage
+ EXTENSIONS = %w[yml yaml markdown mdown mkdn md].freeze
+
def self.end_user(cartridge, environment)
user = ENV.fetch('NANO_BOTS_END_USER', nil)
@@ -35,7 +37,9 @@ module NanoBot
def self.build_path_and_ensure_state_file!(key, cartridge, environment: {})
path = [
+ Logic::Helpers::Hash.fetch(cartridge, %i[state path]),
Logic::Helpers::Hash.fetch(cartridge, %i[state directory]),
+ ENV.fetch('NANO_BOTS_STATE_PATH', nil),
ENV.fetch('NANO_BOTS_STATE_DIRECTORY', nil)
].find do |candidate|
!candidate.nil? && !candidate.empty?
@@ -64,32 +68,59 @@ module NanoBot
path
end
- def self.cartridges_path
- [
- ENV.fetch('NANO_BOTS_CARTRIDGES_DIRECTORY', nil),
- "#{user_home!.sub(%r{/$}, '')}/.local/share/nano-bots/cartridges"
- ].compact.uniq.filter { |path| File.directory?(path) }.compact.first
+ def self.cartridges_path(components: {})
+ components[:directory?] = ->(path) { File.directory?(path) } unless components.key?(:directory?)
+ components[:ENV] = ENV unless components.key?(:ENV)
+
+ default = "#{user_home!(components:).sub(%r{/$}, '')}/.local/share/nano-bots/cartridges"
+
+ from_environment = [
+ components[:ENV].fetch('NANO_BOTS_CARTRIDGES_PATH', nil),
+ components[:ENV].fetch('NANO_BOTS_CARTRIDGES_DIRECTORY', nil)
+ ].compact
+
+ elected = [
+ from_environment.empty? ? nil : from_environment.join(':'),
+ default
+ ].compact.uniq.filter do |path|
+ path.split(':').any? { |candidate| components[:directory?].call(candidate) }
+ end.compact.first
+
+ return default unless elected
+
+ elected = elected.split(':').filter do |path|
+ components[:directory?].call(path)
+ end.compact
+
+ elected.size.positive? ? elected.join(':') : default
end
def self.cartridge_path(path)
partial = File.join(File.dirname(path), File.basename(path, File.extname(path)))
- candidates = [
- path,
- "#{partial}.yml",
- "#{partial}.yaml"
- ]
+ candidates = [path]
- unless ENV.fetch('NANO_BOTS_CARTRIDGES_DIRECTORY', nil).nil?
- directory = ENV.fetch('NANO_BOTS_CARTRIDGES_DIRECTORY').sub(%r{/$}, '')
+ EXTENSIONS.each do |extension|
+ candidates << "#{partial}.#{extension}"
+ end
+
+ directories = [
+ ENV.fetch('NANO_BOTS_CARTRIDGES_PATH', nil),
+ ENV.fetch('NANO_BOTS_CARTRIDGES_DIRECTORY', nil)
+ ].compact.map do |directory|
+ directory.split(':')
+ end.flatten.map { |directory| directory.sub(%r{/$}, '') }
+ directories.each do |directory|
partial = File.join(File.dirname(partial), File.basename(partial, File.extname(partial)))
partial = partial.sub(%r{^\.?/}, '')
candidates << "#{directory}/#{partial}"
- candidates << "#{directory}/#{partial}.yml"
- candidates << "#{directory}/#{partial}.yaml"
+
+ EXTENSIONS.each do |extension|
+ candidates << "#{directory}/#{partial}.#{extension}"
+ end
end
directory = "#{user_home!.sub(%r{/$}, '')}/.local/share/nano-bots/cartridges"
@@ -99,8 +130,10 @@ module NanoBot
partial = partial.sub(%r{^\.?/}, '')
candidates << "#{directory}/#{partial}"
- candidates << "#{directory}/#{partial}.yml"
- candidates << "#{directory}/#{partial}.yaml"
+
+ EXTENSIONS.each do |extension|
+ candidates << "#{directory}/#{partial}.#{extension}"
+ end
candidates = candidates.uniq
@@ -109,7 +142,9 @@ module NanoBot
end
end
- def self.user_home!
+ def self.user_home!(components: {})
+ return components[:home] if components[:home]
+
[Dir.home, `echo ~`.strip, '~'].find do |candidate|
!candidate.nil? && !candidate.empty?
end
diff --git a/controllers/cartridges.rb b/controllers/cartridges.rb
index df474a9..3be8d53 100644
--- a/controllers/cartridges.rb
+++ b/controllers/cartridges.rb
@@ -3,35 +3,43 @@
require_relative '../components/storage'
require_relative '../logic/helpers/hash'
require_relative '../logic/cartridge/default'
+require_relative '../logic/cartridge/parser'
module NanoBot
module Controllers
class Cartridges
- def self.all
+ def self.load(path)
+ Logic::Cartridge::Parser.parse(File.read(path), format: File.extname(path))
+ end
+
+ def self.all(components: {})
files = {}
- path = Components::Storage.cartridges_path
+ paths = Components::Storage.cartridges_path(components:)
- Dir.glob("#{path}/**/*.{yml,yaml}").each do |file|
- files[Pathname.new(file).realpath] = {
- base: path,
- path: Pathname.new(file).realpath
- }
+ paths.split(':').each do |path|
+ Dir.glob("#{path}/**/*.{yml,yaml,markdown,mdown,mkdn,md}").each do |file|
+ files[Pathname.new(file).realpath] = {
+ base: path,
+ path: Pathname.new(file).realpath
+ }
+ end
end
cartridges = []
files.values.uniq.map do |file|
- cartridge = Logic::Helpers::Hash.symbolize_keys(
- YAML.safe_load_file(file[:path], permitted_classes: [Symbol])
- ).merge({
- system: {
- id: file[:path].to_s.sub(/^#{Regexp.escape(file[:base])}/, '').sub(%r{^/}, '').sub(/\.[^.]+\z/,
- ''),
- path: file[:path],
- base: file[:base]
- }
- })
+ cartridge = load(file[:path]).merge(
+ {
+ system: {
+ id: file[:path].to_s.sub(
+ /^#{Regexp.escape(file[:base])}/, ''
+ ).sub(%r{^/}, '').sub(/\.[^.]+\z/, ''),
+ path: file[:path],
+ base: file[:base]
+ }
+ }
+ )
next if cartridge[:meta][:name].nil?
diff --git a/controllers/instance.rb b/controllers/instance.rb
index 259a548..85b97ec 100644
--- a/controllers/instance.rb
+++ b/controllers/instance.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true
-require 'yaml'
-
require_relative '../logic/helpers/hash'
require_relative '../components/provider'
require_relative '../components/storage'
require_relative '../components/stream'
+require_relative 'cartridges'
require_relative 'interfaces/repl'
require_relative 'interfaces/eval'
require_relative 'session'
@@ -83,13 +82,11 @@ module NanoBot
raise StandardError, "Cartridge file not found: \"#{path}\""
end
- @cartridge = YAML.safe_load_file(elected_path, permitted_classes: [Symbol])
+ @cartridge = Cartridges.load(elected_path)
end
@safe_cartridge = Marshal.load(Marshal.dump(@cartridge))
- @cartridge = Logic::Helpers::Hash.symbolize_keys(@cartridge)
-
inject_environment_variables!(@cartridge)
end
diff --git a/logic/cartridge/parser.rb b/logic/cartridge/parser.rb
new file mode 100644
index 0000000..50b3dc5
--- /dev/null
+++ b/logic/cartridge/parser.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+require 'singleton'
+
+require 'redcarpet'
+require 'redcarpet/render_strip'
+
+module NanoBot
+ module Logic
+ module Cartridge
+ module Parser
+ def self.parse(raw, format:)
+ normalized = format.to_s.downcase.gsub('.', '').strip
+
+ if %w[yml yaml].include?(normalized)
+ yaml(raw)
+ elsif %w[markdown mdown mkdn md].include?(normalized)
+ markdown(raw)
+ else
+ raise "Unknown cartridge format: '#{format}'"
+ end
+ end
+
+ def self.markdown(raw)
+ yaml_source = []
+
+ tools = []
+
+ blocks = Markdown.new.render(raw).blocks
+
+ previous_block_is_tool = false
+
+ blocks.each do |block|
+ if block[:language] == 'yaml'
+ parsed = Logic::Helpers::Hash.symbolize_keys(
+ YAML.safe_load(block[:source], permitted_classes: [Symbol])
+ )
+
+ if parsed.key?(:tools) && parsed[:tools].is_a?(Array) && !parsed[:tools].empty?
+ previous_block_is_tool = true
+
+ tools.concat(parsed[:tools])
+
+ parsed.delete(:tools)
+
+ unless parsed.empty?
+ yaml_source << YAML.dump(
+ Logic::Helpers::Hash.stringify_keys(parsed)
+ ).gsub(/^---/, '') # TODO: Is this safe enough?
+ end
+ else
+ yaml_source << block[:source]
+ previous_block_is_tool = false
+ nil
+ end
+ elsif previous_block_is_tool
+ tools.last[block[:language].to_sym] = block[:source]
+ previous_block_is_tool = false
+ end
+ end
+
+ unless tools.empty?
+ yaml_source << YAML.dump(
+ Logic::Helpers::Hash.stringify_keys({ tools: })
+ ).gsub(/^---/, '') # TODO: Is this safe enough?
+ end
+
+ cartridge = {}
+
+ yaml_source.each do |source|
+ cartridge = Logic::Helpers::Hash.deep_merge(cartridge, yaml(source))
+ end
+
+ cartridge
+ end
+
+ def self.yaml(raw)
+ Logic::Helpers::Hash.symbolize_keys(
+ YAML.safe_load(raw, permitted_classes: [Symbol])
+ )
+ end
+
+ class Renderer < Redcarpet::Render::Base
+ LANGUAGES_MAP = {
+ 'yml' => 'yaml',
+ 'yaml' => 'yaml',
+ 'lua' => 'lua',
+ 'fnl' => 'fennel',
+ 'fennel' => 'fennel',
+ 'clj' => 'clojure',
+ 'clojure' => 'clojure'
+ }.freeze
+
+ LANGUAGES = LANGUAGES_MAP.keys.freeze
+
+ def initialize(...)
+ super(...)
+ @_nano_bots_blocks = []
+ end
+
+ attr_reader :_nano_bots_blocks
+
+ def block_code(code, language)
+ key = language.to_s.downcase.strip
+
+ return nil unless LANGUAGES.include?(key)
+
+ @_nano_bots_blocks << { language: LANGUAGES_MAP[key], source: code }
+
+ nil
+ end
+ end
+
+ class Markdown
+ attr_reader :markdown
+
+ def initialize
+ @renderer = Renderer.new
+ @markdown = Redcarpet::Markdown.new(@renderer, fenced_code_blocks: true)
+ end
+
+ def blocks
+ @renderer._nano_bots_blocks
+ end
+
+ def render(raw)
+ @markdown.render(raw.gsub(/```\w/, "\n\n\\0"))
+ self
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/logic/helpers/hash.rb b/logic/helpers/hash.rb
index 90432b5..66b6742 100644
--- a/logic/helpers/hash.rb
+++ b/logic/helpers/hash.rb
@@ -4,6 +4,16 @@ module NanoBot
module Logic
module Helpers
module Hash
+ def self.deep_merge(hash1, hash2)
+ hash1.merge(hash2) do |_key, old_val, new_val|
+ if old_val.is_a?(::Hash) && new_val.is_a?(::Hash)
+ deep_merge(old_val, new_val)
+ else
+ new_val
+ end
+ end
+ end
+
def self.symbolize_keys(object)
case object
when ::Hash
@@ -17,6 +27,19 @@ module NanoBot
end
end
+ def self.stringify_keys(object)
+ case object
+ when ::Hash
+ object.each_with_object({}) do |(key, value), result|
+ result[key.to_s] = stringify_keys(value)
+ end
+ when Array
+ object.map { |e| stringify_keys(e) }
+ else
+ object
+ end
+ end
+
def self.fetch(object, path)
node = object
diff --git a/nano-bots.gemspec b/nano-bots.gemspec
index f843f37..87c4f48 100644
--- a/nano-bots.gemspec
+++ b/nano-bots.gemspec
@@ -37,6 +37,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'pry', '~> 0.14.2'
spec.add_dependency 'rainbow', '~> 3.1', '>= 3.1.1'
spec.add_dependency 'rbnacl', '~> 7.1', '>= 7.1.1'
+ spec.add_dependency 'redcarpet', '~> 3.6'
spec.add_dependency 'sweet-moon', '~> 0.0.7'
spec.add_dependency 'cohere-ai', '~> 1.0', '>= 1.0.1'
diff --git a/ports/dsl/nano-bots.rb b/ports/dsl/nano-bots.rb
index e01b2c4..20d8f14 100644
--- a/ports/dsl/nano-bots.rb
+++ b/ports/dsl/nano-bots.rb
@@ -8,6 +8,7 @@ require_relative '../../controllers/instance'
require_relative '../../controllers/security'
require_relative '../../controllers/interfaces/cli'
require_relative '../../components/stream'
+require_relative 'nano-bots/cartridges'
module NanoBot
def self.new(cartridge: '-', state: '-', environment: {})
@@ -24,7 +25,7 @@ module NanoBot
end
def self.cartridges
- Controllers::Cartridges.all
+ Cartridges
end
def self.cli
diff --git a/ports/dsl/nano-bots/cartridges.rb b/ports/dsl/nano-bots/cartridges.rb
new file mode 100644
index 0000000..fb23c39
--- /dev/null
+++ b/ports/dsl/nano-bots/cartridges.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative '../../../controllers/cartridges'
+
+module NanoBot
+ module Cartridges
+ def self.all(components: {})
+ Controllers::Cartridges.all(components:)
+ end
+
+ def self.load(path)
+ Controllers::Cartridges.load(path)
+ end
+ end
+end
diff --git a/spec/components/storage_spec.rb b/spec/components/storage_spec.rb
new file mode 100644
index 0000000..99131dd
--- /dev/null
+++ b/spec/components/storage_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require_relative '../../components/storage'
+
+RSpec.describe NanoBot::Components::Storage do
+ it 'symbolizes keys' do
+ expect(
+ described_class.cartridges_path(
+ components: { home: '/home/aqua', ENV: {}, directory?: ->(_) { true } }
+ )
+ ).to eq('/home/aqua/.local/share/nano-bots/cartridges')
+
+ expect(
+ described_class.cartridges_path(
+ components: {
+ home: '/home/aqua',
+ ENV: { 'NANO_BOTS_CARTRIDGES_DIRECTORY' => '/home/aqua/my-cartridges' },
+ directory?: ->(_) { true }
+ }
+ )
+ ).to eq('/home/aqua/my-cartridges')
+
+ expect(
+ described_class.cartridges_path(
+ components: {
+ home: '/home/aqua',
+ ENV: {
+ 'NANO_BOTS_CARTRIDGES_DIRECTORY' => '/home/aqua/my-cartridges',
+ 'NANO_BOTS_CARTRIDGES_PATH' => '/home/aqua/lime/my-cartridges'
+ },
+ directory?: ->(_) { true }
+ }
+ )
+ ).to eq('/home/aqua/lime/my-cartridges:/home/aqua/my-cartridges')
+
+ expect(
+ described_class.cartridges_path(
+ components: {
+ home: '/home/aqua',
+ ENV: {
+ 'NANO_BOTS_CARTRIDGES_DIRECTORY' => '/home/aqua/my-cartridges',
+ 'NANO_BOTS_CARTRIDGES_PATH' => '/home/aqua/lime/my-cartridges:/home/aqua/ivory/my-cartridges'
+ },
+ directory?: lambda do |path|
+ { '/home/aqua/my-cartridges' => true,
+ '/home/aqua/lime/my-cartridge' => false,
+ '/home/aqua/ivory/my-cartridges' => true }[path]
+ end
+ }
+ )
+ ).to eq('/home/aqua/ivory/my-cartridges:/home/aqua/my-cartridges')
+ end
+end
diff --git a/spec/data/cartridges/block.md b/spec/data/cartridges/block.md
new file mode 100644
index 0000000..ef8588d
--- /dev/null
+++ b/spec/data/cartridges/block.md
@@ -0,0 +1,7 @@
+First, we need to add some important details:
+```yaml
+safety:
+ functions:
+ sandboxed: false
+```
+Hi!
diff --git a/spec/data/cartridges/markdown.md b/spec/data/cartridges/markdown.md
new file mode 100644
index 0000000..cd50b7b
--- /dev/null
+++ b/spec/data/cartridges/markdown.md
@@ -0,0 +1,37 @@
+A cartridge is a YAML file with human-readable data that outlines the bot's goals, expected behaviors, and settings for authentication and provider utilization.
+
+We begin with the meta section, which provides information about what this cartridge is designed for:
+
+```yaml
+meta:
+ symbol: 🤖
+ name: ChatGPT 4 Turbo
+ author: icebaker
+ version: 0.0.1
+ license: CC0-1.0
+ description: A helpful assistant.
+```
+
+It includes details like versioning and license.
+
+Next, we add a behavior section that will provide the bot with a directive on how it should behave:
+
+```yaml
+behaviors:
+ interaction:
+ directive: You are a helpful assistant.
+```
+
+Now, we need to provide instructions on how this Nano Bot should connect with a provider, which credentials to use, and what specific configurations for the LLM are required:
+
+```yaml
+provider:
+ id: openai
+ credentials:
+ access-token: ENV/OPENAI_API_KEY
+ settings:
+ user: ENV/NANO_BOTS_END_USER
+ model: gpt-4-1106-preview
+```
+
+In my API, I have set the environment variables `OPENAI_API_KEY` and `NANO_BOTS_END_USER`, which is where the values for these will come from.
diff --git a/spec/data/cartridges/meta.md b/spec/data/cartridges/meta.md
new file mode 100644
index 0000000..68a0cbd
--- /dev/null
+++ b/spec/data/cartridges/meta.md
@@ -0,0 +1,25 @@
+Start by defining a meta section:
+
+```yaml
+meta:
+ symbol: 🤖
+ name: Nano Bot Name
+ author: Your Name
+ description: A helpful assistant.
+```
+
+You can also add version and license information:
+
+```yaml
+meta:
+ version: 1.0.0
+ license: CC0-1.0
+```
+
+Then, add a behavior section:
+
+```yaml
+behaviors:
+ interaction:
+ directive: You are a helpful assistant.
+```
diff --git a/spec/data/cartridges/models/ollama/llama2.yml b/spec/data/cartridges/models/ollama/phi-2.yml
index 7f20753..5c8e131 100644
--- a/spec/data/cartridges/models/ollama/llama2.yml
+++ b/spec/data/cartridges/models/ollama/phi-2.yml
@@ -1,10 +1,10 @@
---
meta:
symbol: 🦙
- name: Llama 2 through Ollama
+ name: Phi-2 through Ollama
license: CC0-1.0
provider:
id: ollama
settings:
- model: llama2
+ model: phi
diff --git a/spec/data/cartridges/tools.md b/spec/data/cartridges/tools.md
new file mode 100644
index 0000000..5d2da5a
--- /dev/null
+++ b/spec/data/cartridges/tools.md
@@ -0,0 +1,76 @@
+A cartridge is a YAML file with human-readable data that outlines the bot's goals, expected behaviors, and settings for authentication and provider utilization.
+
+We begin with the meta section, which provides information about what this cartridge is designed for:
+
+```yaml
+meta:
+ symbol: 🕛
+ name: Date and Time
+ author: icebaker
+ version: 0.0.1
+ license: CC0-1.0
+ description: A helpful assistant.
+```
+
+It includes details like versioning and license.
+
+Next, we add a behavior section that will provide the bot with a directive on how it should behave:
+
+```yaml
+behaviors:
+ interaction:
+ directive: You are a helpful assistant.
+```
+
+Now, we need to provide instructions on how this Nano Bot should connect with a provider, which credentials to use, and what specific configurations for the LLM are required:
+
+```yaml
+provider:
+ id: openai
+ credentials:
+ access-token: ENV/OPENAI_API_KEY
+ settings:
+ user: ENV/NANO_BOTS_END_USER
+ model: gpt-4-1106-preview
+```
+
+In my API, I have set the environment variables `OPENAI_API_KEY` and `NANO_BOTS_END_USER`, which is where the values for these will come from.
+
+Nano Bot ready; let's start adding some extra power to it.
+
+## Random Numbers
+
+```yml
+tools:
+- name: random-number
+ description: Generates a random number within a given range.
+ parameters:
+ type: object
+ properties:
+ from:
+ type: integer
+ description: The minimum expected number for random generation.
+ to:
+ type: integer
+ description: The maximum expected number for random generation.
+ required:
+ - from
+ - to
+```
+
+```clj
+(let [{:strs [from to]} parameters]
+ (+ from (rand-int (+ 1 (- to from)))))
+```
+
+## Date and Time
+
+```yaml
+tools:
+- name: date-and-time
+ description: Returns the current date and time.
+```
+
+```fnl
+(os.date)
+```
diff --git a/spec/logic/cartridge/parser_spec.rb b/spec/logic/cartridge/parser_spec.rb
new file mode 100644
index 0000000..8297baa
--- /dev/null
+++ b/spec/logic/cartridge/parser_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require_relative '../../../logic/cartridge/parser'
+
+RSpec.describe NanoBot::Logic::Cartridge::Parser do
+ context 'markdown' do
+ context 'default' do
+ let(:raw) { File.read('spec/data/cartridges/markdown.md') }
+
+ it 'parses markdown cartridge' do
+ expect(described_class.parse(raw, format: 'md')).to eq(
+ { meta: {
+ symbol: '🤖',
+ name: 'ChatGPT 4 Turbo',
+ author: 'icebaker',
+ version: '0.0.1',
+ license: 'CC0-1.0',
+ description: 'A helpful assistant.'
+ },
+ behaviors: { interaction: { directive: 'You are a helpful assistant.' } },
+ provider: {
+ id: 'openai',
+ credentials: { 'access-token': 'ENV/OPENAI_API_KEY' },
+ settings: {
+ user: 'ENV/NANO_BOTS_END_USER',
+ model: 'gpt-4-1106-preview'
+ }
+ } }
+ )
+ end
+ end
+
+ context 'meta' do
+ let(:raw) { File.read('spec/data/cartridges/meta.md') }
+
+ it 'parses markdown cartridge' do
+ expect(described_class.parse(raw, format: 'md')).to eq(
+ {
+ meta: {
+ symbol: '🤖',
+ name: 'Nano Bot Name',
+ author: 'Your Name',
+ description: 'A helpful assistant.',
+ version: '1.0.0',
+ license: 'CC0-1.0'
+ },
+ behaviors: {
+ interaction: {
+ directive: 'You are a helpful assistant.'
+ }
+ }
+ }
+ )
+ end
+ end
+
+ context 'tools' do
+ let(:raw) { File.read('spec/data/cartridges/tools.md') }
+
+ it 'parses markdown cartridge' do
+ expect(described_class.parse(raw, format: 'md')).to eq(
+ { meta: {
+ symbol: '🕛',
+ name: 'Date and Time',
+ author: 'icebaker',
+ version: '0.0.1',
+ license: 'CC0-1.0',
+ description: 'A helpful assistant.'
+ },
+ behaviors: {
+ interaction: {
+ directive: 'You are a helpful assistant.'
+ }
+ },
+ provider: {
+ id: 'openai',
+ credentials: { 'access-token': 'ENV/OPENAI_API_KEY' },
+ settings: {
+ user: 'ENV/NANO_BOTS_END_USER',
+ model: 'gpt-4-1106-preview'
+ }
+ },
+ tools: [
+ { name: 'random-number',
+ description: 'Generates a random number within a given range.',
+ parameters: {
+ type: 'object',
+ properties: {
+ from: {
+ type: 'integer',
+ description: 'The minimum expected number for random generation.'
+ },
+ to: {
+ type: 'integer',
+ description: 'The maximum expected number for random generation.'
+ }
+ },
+ required: %w[from to]
+ },
+ clojure: "(let [{:strs [from to]} parameters]\n (+ from (rand-int (+ 1 (- to from)))))\n" },
+ { name: 'date-and-time',
+ description: 'Returns the current date and time.',
+ fennel: "(os.date)\n" }
+ ] }
+ )
+ end
+ end
+
+ context 'block' do
+ let(:raw) { File.read('spec/data/cartridges/block.md') }
+
+ it 'parses markdown cartridge' do
+ expect(described_class.parse(raw, format: 'md')).to eq(
+ { safety: { functions: { sandboxed: false } } }
+ )
+ end
+ end
+ end
+end
diff --git a/spec/logic/helpers/hash_spec.rb b/spec/logic/helpers/hash_spec.rb
index 09012c8..0da92fb 100644
--- a/spec/logic/helpers/hash_spec.rb
+++ b/spec/logic/helpers/hash_spec.rb
@@ -7,7 +7,24 @@ RSpec.describe NanoBot::Logic::Helpers::Hash do
expect(described_class.symbolize_keys({ 'a' => 'b', 'c' => { 'd' => ['e'] } })).to eq(
{ a: 'b', c: { d: ['e'] } }
)
+ end
+
+ it 'deep merges' do
+ expect(described_class.deep_merge(
+ { a: { x: 1, y: 2 }, b: 3 },
+ { a: { y: 99, z: 4 }, c: 5 }
+ )).to eq(
+ { a: { x: 1, y: 99, z: 4 }, b: 3, c: 5 }
+ )
+ end
+
+ it 'stringify keys' do
+ expect(described_class.stringify_keys({ a: 'b', c: { d: [:e] } })).to eq(
+ { 'a' => 'b', 'c' => { 'd' => [:e] } }
+ )
+ end
+ it 'fetch a path of keys' do
expect(described_class.fetch({ a: 'b', c: { d: ['e'] } }, %i[c d])).to eq(
['e']
)