From 8ae78b954350755a47a13133668dba93bac15f37 Mon Sep 17 00:00:00 2001 From: icebaker Date: Sat, 18 Nov 2023 19:07:10 -0300 Subject: adding support for tools --- spec/data/cartridges/tools.yml | 28 +++++++++ spec/data/providers/openai/tools.yml | 11 ++++ spec/logic/cartridge/affixes_spec.rb | 6 +- spec/logic/cartridge/interaction_spec.rb | 18 +++--- spec/logic/cartridge/streaming_spec.rb | 34 ++++++++--- spec/logic/cartridge/tools_spec.rb | 97 +++++++++++++++++++++++++++++++ spec/logic/providers/openai/tools_spec.rb | 73 +++++++++++++++++++++++ spec/spec_helper.rb | 4 +- 8 files changed, 250 insertions(+), 21 deletions(-) create mode 100644 spec/data/cartridges/tools.yml create mode 100644 spec/data/providers/openai/tools.yml create mode 100644 spec/logic/cartridge/tools_spec.rb create mode 100644 spec/logic/providers/openai/tools_spec.rb (limited to 'spec') diff --git a/spec/data/cartridges/tools.yml b/spec/data/cartridges/tools.yml new file mode 100644 index 0000000..0c2a30b --- /dev/null +++ b/spec/data/cartridges/tools.yml @@ -0,0 +1,28 @@ +--- +tools: + - name: get-current-weather + type: function + description: Get the current weather in a given location. + parameters: + - name: location + - name: unit + fennel: | + (let [{:location location :unit unit} parameters] + (.. "Here is the weather in " location ", in " unit ": 35.8°C.")) + + - name: what-time-is-it + description: Returns the current date and time. + fennel: | + (os.date) + + - name: sh + description: It has access to computer users' data and can be used to run shell commands, similar to those in a Linux terminal, to extract information. Please be mindful and careful to avoid running dangerous commands on users' computers. + parameters: + - name: command + type: array + items: + type: string + description: An array of strings that represents a shell command along with its arguments or options. For instance, `["df", "-h"]` executes the `df -h` command, where each array element specifies either the command itself or an associated argument/option. + clojure: | + (require '[clojure.java.shell :refer [sh]]) + (println (apply sh (get parameters "command"))) diff --git a/spec/data/providers/openai/tools.yml b/spec/data/providers/openai/tools.yml new file mode 100644 index 0000000..681fb2c --- /dev/null +++ b/spec/data/providers/openai/tools.yml @@ -0,0 +1,11 @@ +--- +- id: call_XYZ + type: function + function: + name: get-current-weather + arguments: '{"location":"Tokyo, Japan"}' +- id: call_ZYX + type: function + function: + name: what-time-is-it + arguments: "{}" diff --git a/spec/logic/cartridge/affixes_spec.rb b/spec/logic/cartridge/affixes_spec.rb index 8f08e1d..7263008 100644 --- a/spec/logic/cartridge/affixes_spec.rb +++ b/spec/logic/cartridge/affixes_spec.rb @@ -6,7 +6,7 @@ require_relative '../../../logic/cartridge/affixes' RSpec.describe NanoBot::Logic::Cartridge::Affixes do context 'interfaces' do - let(:cartridge) { load_cartridge('affixes.yml') } + let(:cartridge) { load_symbolized('cartridges/affixes.yml') } it 'gets the expected affixes' do expect(described_class.get(cartridge, :repl, :input, :prefix)).to eq('E') @@ -22,7 +22,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Affixes do end context 'interfaces fallback' do - let(:cartridge) { load_cartridge('affixes.yml') } + let(:cartridge) { load_symbolized('cartridges/affixes.yml') } it 'gets the expected affixes' do cartridge[:interfaces][:repl][:input].delete(:prefix) @@ -48,7 +48,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Affixes do end context 'interfaces nil' do - let(:cartridge) { load_cartridge('affixes.yml') } + let(:cartridge) { load_symbolized('cartridges/affixes.yml') } it 'gets the expected affixes' do cartridge[:interfaces][:repl][:input][:prefix] = nil diff --git a/spec/logic/cartridge/interaction_spec.rb b/spec/logic/cartridge/interaction_spec.rb index 347ac45..f3ba46e 100644 --- a/spec/logic/cartridge/interaction_spec.rb +++ b/spec/logic/cartridge/interaction_spec.rb @@ -6,41 +6,41 @@ require_relative '../../../logic/cartridge/interaction' RSpec.describe NanoBot::Logic::Cartridge::Interaction do context 'input' do - let(:cartridge) { load_cartridge('affixes.yml') } + let(:cartridge) { load_symbolized('cartridges/affixes.yml') } it 'prepares the input' do expect(described_class.input(cartridge, :repl, 'hello')).to eq( - { content: 'hello', fennel: nil, lua: nil, prefix: 'E', suffix: 'F' } + { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: 'E', suffix: 'F' } ) expect(described_class.input({}, :repl, 'hello')).to eq( - { content: 'hello', fennel: nil, lua: nil, prefix: nil, suffix: nil } + { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: nil, suffix: nil } ) expect(described_class.input(cartridge, :eval, 'hello')).to eq( - { content: 'hello', fennel: nil, lua: nil, prefix: 'I', suffix: 'J' } + { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: 'I', suffix: 'J' } ) expect(described_class.input({}, :eval, 'hello')).to eq( - { content: 'hello', fennel: nil, lua: nil, prefix: nil, suffix: nil } + { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: nil, suffix: nil } ) end it 'prepares the non-streamming output' do expect(described_class.output(cartridge, :repl, { message: 'hello' }, false, true)).to eq( - { message: { content: 'hello', fennel: nil, lua: nil } } + { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } } ) expect(described_class.output({}, :repl, { message: 'hello' }, false, true)).to eq( - { message: { content: 'hello', fennel: nil, lua: nil } } + { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } } ) expect(described_class.output(cartridge, :eval, { message: 'hello' }, false, true)).to eq( - { message: { content: 'hello', fennel: nil, lua: nil } } + { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } } ) expect(described_class.output({}, :eval, { message: 'hello' }, false, true)).to eq( - { message: { content: 'hello', fennel: nil, lua: nil } } + { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } } ) end end diff --git a/spec/logic/cartridge/streaming_spec.rb b/spec/logic/cartridge/streaming_spec.rb index e5ad012..466dd0b 100644 --- a/spec/logic/cartridge/streaming_spec.rb +++ b/spec/logic/cartridge/streaming_spec.rb @@ -5,8 +5,28 @@ require 'yaml' require_relative '../../../logic/cartridge/streaming' RSpec.describe NanoBot::Logic::Cartridge::Streaming do + context 'interfaces override' do + context 'defaults' do + let(:cartridge) { {} } + + it 'uses default values when appropriate' do + expect(described_class.enabled?(cartridge, :repl)).to be(true) + expect(described_class.enabled?(cartridge, :eval)).to be(true) + end + end + + context 'top-level overrides' do + let(:cartridge) { { interfaces: { output: { stream: false } } } } + + it 'overrides default values when appropriate' do + expect(described_class.enabled?(cartridge, :repl)).to be(false) + expect(described_class.enabled?(cartridge, :eval)).to be(false) + end + end + end + context 'provider' do - let(:cartridge) { load_cartridge('streaming.yml') } + let(:cartridge) { load_symbolized('cartridges/streaming.yml') } it 'checks if stream is enabled' do cartridge[:provider][:settings][:stream] = false @@ -15,7 +35,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do end context 'repl' do - let(:cartridge) { load_cartridge('streaming.yml') } + let(:cartridge) { load_symbolized('cartridges/streaming.yml') } it 'checks if stream is enabled' do cartridge[:interfaces][:repl][:output][:stream] = false @@ -24,7 +44,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do end context 'interface + repl' do - let(:cartridge) { load_cartridge('streaming.yml') } + let(:cartridge) { load_symbolized('cartridges/streaming.yml') } it 'checks if stream is enabled' do cartridge[:interfaces][:output][:stream] = false @@ -34,7 +54,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do end context 'interface' do - let(:cartridge) { load_cartridge('streaming.yml') } + let(:cartridge) { load_symbolized('cartridges/streaming.yml') } it 'checks if stream is enabled' do cartridge[:interfaces][:output][:stream] = false @@ -44,7 +64,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do end context '- repl' do - let(:cartridge) { load_cartridge('streaming.yml') } + let(:cartridge) { load_symbolized('cartridges/streaming.yml') } it 'checks if stream is enabled' do cartridge[:interfaces][:repl][:output].delete(:stream) @@ -53,7 +73,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do end context '- interface' do - let(:cartridge) { load_cartridge('streaming.yml') } + let(:cartridge) { load_symbolized('cartridges/streaming.yml') } it 'checks if stream is enabled' do cartridge[:interfaces][:output].delete(:stream) @@ -63,7 +83,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do end context '- provider' do - let(:cartridge) { load_cartridge('streaming.yml') } + let(:cartridge) { load_symbolized('cartridges/streaming.yml') } it 'checks if stream is enabled' do cartridge[:provider][:settings].delete(:stream) diff --git a/spec/logic/cartridge/tools_spec.rb b/spec/logic/cartridge/tools_spec.rb new file mode 100644 index 0000000..42b8c57 --- /dev/null +++ b/spec/logic/cartridge/tools_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'yaml' + +require_relative '../../../logic/cartridge/tools' + +RSpec.describe NanoBot::Logic::Cartridge::Tools do + context 'interfaces override' do + context 'defaults' do + let(:cartridge) { {} } + + it 'uses default values when appropriate' do + expect(described_class.feedback?(cartridge, :repl, :call)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :call)).to be(true) + + expect(described_class.feedback?(cartridge, :repl, :response)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :response)).to be(false) + end + end + + context 'top-level overrides' do + let(:cartridge) do + { interfaces: { tools: { feedback: false } } } + end + + it 'overrides default values when appropriate' do + expect(described_class.feedback?(cartridge, :repl, :call)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :call)).to be(false) + + expect(described_class.feedback?(cartridge, :repl, :response)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :response)).to be(false) + end + end + + context 'top-level overrides' do + let(:cartridge) do + { interfaces: { tools: { feedback: true } } } + end + + it 'overrides default values when appropriate' do + expect(described_class.feedback?(cartridge, :repl, :call)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :call)).to be(true) + + expect(described_class.feedback?(cartridge, :repl, :response)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :response)).to be(true) + end + end + + context 'top-level-specific overrides' do + let(:cartridge) do + { interfaces: { tools: { call: { feedback: false }, response: { feedback: true } } } } + end + + it 'overrides default values when appropriate' do + expect(described_class.feedback?(cartridge, :repl, :call)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :call)).to be(false) + + expect(described_class.feedback?(cartridge, :repl, :response)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :response)).to be(true) + end + end + + context 'repl interface overrides' do + let(:cartridge) do + { interfaces: { + tools: { call: { feedback: false }, response: { feedback: true } }, + repl: { tools: { call: { feedback: true }, response: { feedback: false } } } + } } + end + + it 'overrides default values when appropriate' do + expect(described_class.feedback?(cartridge, :repl, :call)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :call)).to be(false) + + expect(described_class.feedback?(cartridge, :repl, :response)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :response)).to be(true) + end + end + + context 'eval interface overrides' do + let(:cartridge) do + { interfaces: { + tools: { call: { feedback: false }, response: { feedback: true } }, + eval: { tools: { call: { feedback: true }, response: { feedback: false } } } + } } + end + + it 'overrides default values when appropriate' do + expect(described_class.feedback?(cartridge, :repl, :call)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :call)).to be(true) + + expect(described_class.feedback?(cartridge, :repl, :response)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :response)).to be(false) + end + end + end +end diff --git a/spec/logic/providers/openai/tools_spec.rb b/spec/logic/providers/openai/tools_spec.rb new file mode 100644 index 0000000..c92c374 --- /dev/null +++ b/spec/logic/providers/openai/tools_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'yaml' + +require_relative '../../../../logic/providers/openai/tools' + +RSpec.describe NanoBot::Logic::OpenAI::Tools do + context 'tools' do + let(:cartridge) { load_symbolized('cartridges/tools.yml') } + + context 'adapt' do + it 'adapts to OpenAI expected format' do + expect(described_class.adapt(cartridge[:tools][0])).to eq( + { type: 'function', + function: { + name: 'get-current-weather', + description: 'Get the current weather in a given location.', + parameters: { + type: 'object', + properties: { + location: { type: 'string' }, + unit: { type: 'string' } + } + } + } } + ) + + expect(described_class.adapt(cartridge[:tools][1])).to eq( + { type: 'function', + function: { + name: 'what-time-is-it', + description: 'Returns the current date and time.', + parameters: { properties: {}, type: 'object' } + } } + ) + + expect(described_class.adapt(cartridge[:tools][2])).to eq( + { type: 'function', + function: { + name: 'sh', + description: "It has access to computer users' data and can be used to run shell commands, similar to those in a Linux terminal, to extract information. Please be mindful and careful to avoid running dangerous commands on users' computers.", + parameters: { + type: 'object', + properties: { + command: { + type: 'array', + description: 'An array of strings that represents a shell command along with its arguments or options. For instance, `["df", "-h"]` executes the `df -h` command, where each array element specifies either the command itself or an associated argument/option.', + items: { type: 'string' } + } + } + } + } } + ) + end + end + + context 'prepare' do + let(:tools) { load_symbolized('providers/openai/tools.yml') } + + it 'prepare tools to be executed' do + expect(described_class.prepare(cartridge[:tools], tools)).to eq( + [{ id: 'call_XYZ', + name: 'get-current-weather', + type: 'function', + parameters: { 'location' => 'Tokyo, Japan' }, + source: { fennel: "(let [{:location location :unit unit} parameters]\n (.. \"Here is the weather in \" location \", in \" unit \": 35.8°C.\"))\n" } }, + { id: 'call_ZYX', name: 'what-time-is-it', type: 'function', parameters: {}, + source: { fennel: "(os.date)\n" } }] + ) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ad9038d..cb46554 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,8 +16,8 @@ RSpec.configure do |config| config.shared_context_metadata_behavior = :apply_to_host_groups end -def load_cartridge(path) - cartridge = YAML.safe_load(File.read("spec/data/cartridges/#{path}"), permitted_classes: [Symbol]) +def load_symbolized(path) + cartridge = YAML.safe_load_file("spec/data/#{path}", permitted_classes: [Symbol]) NanoBot::Logic::Helpers::Hash.symbolize_keys(cartridge) end -- cgit v1.2.3