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 --- components/providers/openai/tools.rb | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 components/providers/openai/tools.rb (limited to 'components/providers/openai/tools.rb') diff --git a/components/providers/openai/tools.rb b/components/providers/openai/tools.rb new file mode 100644 index 0000000..ea34ae6 --- /dev/null +++ b/components/providers/openai/tools.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require_relative '../../embedding' + +require 'concurrent' + +module NanoBot + module Components + module Providers + class OpenAI < Base + module Tools + def self.apply(cartridge, tools, feedback) + prepared_tools = NanoBot::Logic::OpenAI::Tools.prepare(cartridge, tools) + + futures = prepared_tools.map do |tool| + Concurrent::Promises.future { process!(tool, feedback) } + end + + results = Concurrent::Promises.zip(*futures).value! + + results.map do |applied_tool| + { + who: 'tool', + message: applied_tool[:output], + meta: { id: applied_tool[:id], name: applied_tool[:name] } + } + end + end + + def self.process!(tool, feedback) + feedback.call( + { should_be_stored: false, + interaction: { who: 'AI', message: nil, meta: { + tool: { action: 'call', id: tool[:id], name: tool[:name], parameters: tool[:parameters] } + } } } + ) + + call = { parameters: %w[parameters], values: [tool[:parameters]], safety: false } + + if %i[fennel lua clojure].count { |key| !tool[:source][key].nil? } > 1 + raise StandardError, 'conflicting tools' + end + + if !tool[:source][:fennel].nil? + call[:source] = tool[:source][:fennel] + tool[:output] = Components::Embedding.fennel(**call) + elsif !tool[:source][:clojure].nil? + call[:source] = tool[:source][:clojure] + tool[:output] = Components::Embedding.clojure(**call) + elsif !tool[:source][:lua].nil? + call[:source] = tool[:source][:lua] + tool[:output] = Components::Embedding.lua(**call) + else + raise 'missing source code' + end + + feedback.call( + { should_be_stored: false, + interaction: { who: 'AI', message: nil, meta: { + tool: { + action: 'response', id: tool[:id], name: tool[:name], + parameters: tool[:parameters], output: tool[:output] + } + } } } + ) + + tool + end + end + end + end + end +end -- cgit v1.2.3 From 89962f27a75183947fc44cd051a1061ce157221d Mon Sep 17 00:00:00 2001 From: icebaker Date: Sat, 18 Nov 2023 20:08:34 -0300 Subject: adding safety sandbox --- components/adapter.rb | 8 ++++++-- components/embedding.rb | 20 +++++++++++++++++--- components/providers/openai.rb | 6 +++--- components/providers/openai/tools.rb | 16 +++++++++++----- controllers/interfaces/tools.rb | 7 ++++--- controllers/session.rb | 6 +++--- logic/cartridge/safety.rb | 18 ++++++++++++++++++ static/cartridges/default.yml | 6 ++++++ 8 files changed, 68 insertions(+), 19 deletions(-) create mode 100644 logic/cartridge/safety.rb (limited to 'components/providers/openai/tools.rb') diff --git a/components/adapter.rb b/components/adapter.rb index a79fee6..32aa169 100644 --- a/components/adapter.rb +++ b/components/adapter.rb @@ -1,16 +1,20 @@ # frozen_string_literal: true require_relative 'embedding' +require_relative '../logic/cartridge/safety' module NanoBot module Components class Adapter - def self.apply(_direction, params) + def self.apply(params, cartridge) content = params[:content] raise StandardError, 'conflicting adapters' if %i[fennel lua clojure].count { |key| !params[key].nil? } > 1 - call = { parameters: %w[content], values: [content], safety: false } + call = { + parameters: %w[content], values: [content], + safety: { sandboxed: Logic::Cartridge::Safety.sandboxed?(cartridge) } + } if params[:fennel] call[:source] = params[:fennel] diff --git a/components/embedding.rb b/components/embedding.rb index c464244..46d0fba 100644 --- a/components/embedding.rb +++ b/components/embedding.rb @@ -9,10 +9,18 @@ require 'tempfile' module NanoBot module Components class Embedding + def self.ensure_safety!(safety) + raise 'missing safety definitions' unless safety.key?(:sandboxed) + end + def self.lua(source:, parameters:, values:, safety:) + ensure_safety!(safety) + + allowed = '' + allowed = ', {math=math,string=string,table=table}' if safety[:sandboxed] + state = SweetMoon::State.new - # code = "_, embedded = pcall(load([[\nreturn function(#{parameters.join(', ')})\nreturn #{source}\nend\n]], nil, 't', {math=math,string=string,table=table}))" - code = "_, embedded = pcall(load([[\nreturn function(#{parameters.join(', ')})\n#{source}\nend\n]], nil, 't'))" + code = "_, embedded = pcall(load([[\nreturn function(#{parameters.join(', ')})\n#{source}\nend\n]], nil, 't'#{allowed}))" state.eval(code) embedded = state.get(:embedded) @@ -20,19 +28,25 @@ module NanoBot end def self.fennel(source:, parameters:, values:, safety:) + ensure_safety!(safety) + path = "#{File.expand_path('../static/fennel', __dir__)}/?.lua" state = SweetMoon::State.new(package_path: path).fennel # TODO: global is deprecated... state.fennel.eval( "(global embedded (fn [#{parameters.join(' ')}] #{source}))", 1, - safety ? { allowedGlobals: %w[math string table] } : nil + safety[:sandboxed] ? { allowedGlobals: %w[math string table] } : nil ) embedded = state.get(:embedded) embedded.call(values) end def self.clojure(source:, parameters:, values:, safety:) + ensure_safety!(safety) + + raise 'TODO: sandboxed Clojure through Babashka not implemented' if safety[:sandboxed] + raise 'invalid Clojure parameter name' if parameters.include?('injected-parameters') key_value = {} diff --git a/components/providers/openai.rb b/components/providers/openai.rb index bf5ae73..996f7f6 100644 --- a/components/providers/openai.rb +++ b/components/providers/openai.rb @@ -45,7 +45,7 @@ module NanoBot provider && interface end - def evaluate(input, &feedback) + def evaluate(input, cartridge, &feedback) messages = input[:history].map do |event| if event[:message].nil? && event[:meta] && event[:meta][:tool_calls] { role: 'assistant', content: nil, tool_calls: event[:meta][:tool_calls] } @@ -122,7 +122,7 @@ module NanoBot needs_another_round: true, interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } } ) - Tools.apply(input[:tools], tools, feedback).each do |interaction| + Tools.apply(cartridge, input[:tools], tools, feedback).each do |interaction| feedback.call({ should_be_stored: true, needs_another_round: true, interaction: }) end end @@ -149,7 +149,7 @@ module NanoBot needs_another_round: true, interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } } ) - Tools.apply(input[:tools], tools, feedback).each do |interaction| + Tools.apply(cartridge, input[:tools], tools, feedback).each do |interaction| feedback.call({ should_be_stored: true, needs_another_round: true, interaction: }) end end diff --git a/components/providers/openai/tools.rb b/components/providers/openai/tools.rb index ea34ae6..50eead9 100644 --- a/components/providers/openai/tools.rb +++ b/components/providers/openai/tools.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative '../../embedding' +require_relative '../../../logic/cartridge/safety' require 'concurrent' @@ -9,11 +10,12 @@ module NanoBot module Providers class OpenAI < Base module Tools - def self.apply(cartridge, tools, feedback) - prepared_tools = NanoBot::Logic::OpenAI::Tools.prepare(cartridge, tools) + def self.apply(cartridge, function_cartridge, tools, feedback) + prepared_tools = NanoBot::Logic::OpenAI::Tools.prepare(function_cartridge, tools) + # TODO: Confirm before starting futures. futures = prepared_tools.map do |tool| - Concurrent::Promises.future { process!(tool, feedback) } + Concurrent::Promises.future { process!(tool, feedback, function_cartridge, cartridge) } end results = Concurrent::Promises.zip(*futures).value! @@ -27,7 +29,7 @@ module NanoBot end end - def self.process!(tool, feedback) + def self.process!(tool, feedback, _function_cartridge, cartridge) feedback.call( { should_be_stored: false, interaction: { who: 'AI', message: nil, meta: { @@ -35,7 +37,11 @@ module NanoBot } } } ) - call = { parameters: %w[parameters], values: [tool[:parameters]], safety: false } + call = { + parameters: %w[parameters], + values: [tool[:parameters]], + safety: { sandboxed: Logic::Cartridge::Safety.sandboxed?(cartridge) } + } if %i[fennel lua clojure].count { |key| !tool[:source][key].nil? } > 1 raise StandardError, 'conflicting tools' diff --git a/controllers/interfaces/tools.rb b/controllers/interfaces/tools.rb index 5105da1..1136600 100644 --- a/controllers/interfaces/tools.rb +++ b/controllers/interfaces/tools.rb @@ -3,13 +3,14 @@ require 'rainbow' require_relative '../../logic/cartridge/tools' +require_relative '../../logic/cartridge/safety' require_relative '../../components/embedding' module NanoBot module Controllers module Interfaces module Tool - def self.adapt(feedback, adapter) + def self.adapt(feedback, adapter, cartridge) call = { parameters: %w[id name parameters parameters-as-json output], values: [ @@ -17,7 +18,7 @@ module NanoBot feedback[:parameters].to_json, feedback[:output] ], - safety: false + safety: { sandboxed: Logic::Cartridge::Safety.sandboxed?(cartridge) } } raise StandardError, 'conflicting adapters' if %i[fennel lua clojure].count { |key| !adapter[key].nil? } > 1 @@ -49,7 +50,7 @@ module NanoBot adapter = Tool.adapter(cartridge, mode, feedback) if %i[fennel lua clojure].any? { |key| !adapter[key].nil? } - message = adapt(feedback, adapter) + message = adapt(feedback, adapter, cartridge) else message = "(#{feedback[:name]} #{feedback[:parameters].to_json})" diff --git a/controllers/session.rb b/controllers/session.rb index 10d0194..546a891 100644 --- a/controllers/session.rb +++ b/controllers/session.rb @@ -72,7 +72,7 @@ module NanoBot mode: mode.to_s, input: message, message: Components::Adapter.apply( - :input, Logic::Cartridge::Interaction.input(@cartridge, mode.to_sym, message) + Logic::Cartridge::Interaction.input(@cartridge, mode.to_sym, message), @cartridge ) } @@ -117,7 +117,7 @@ module NanoBot needs_another_round = false - @provider.evaluate(input) do |feedback| + @provider.evaluate(input, @cartridge) do |feedback| updated_at = Time.now needs_another_round = true if feedback[:needs_another_round] @@ -137,7 +137,7 @@ module NanoBot output = Logic::Cartridge::Interaction.output( @cartridge, mode.to_sym, feedback[:interaction], streaming, feedback[:finished] ) - output[:message] = Components::Adapter.apply(:output, output[:message]) + output[:message] = Components::Adapter.apply(output[:message], @cartridge) event[:output] = (output[:message]).to_s end end diff --git a/logic/cartridge/safety.rb b/logic/cartridge/safety.rb new file mode 100644 index 0000000..84b39d7 --- /dev/null +++ b/logic/cartridge/safety.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require_relative 'fetch' + +module NanoBot + module Logic + module Cartridge + module Safety + def self.sandboxed?(cartridge) + sandboxed = Fetch.cascate(cartridge, [%i[safety functions sandboxed]]) + return true if sandboxed.nil? + + sandboxed + end + end + end + end +end diff --git a/static/cartridges/default.yml b/static/cartridges/default.yml index 167fa83..57eeada 100644 --- a/static/cartridges/default.yml +++ b/static/cartridges/default.yml @@ -1,4 +1,10 @@ --- +safety: + functions: + sandboxed: true + tools: + confirmable: true + interfaces: repl: output: -- cgit v1.2.3 From c470d63b169058d81f44569a5f1c4c1fb222279f Mon Sep 17 00:00:00 2001 From: icebaker Date: Sat, 18 Nov 2023 21:45:16 -0300 Subject: adding support for confirm --- components/embedding.rb | 2 +- components/providers/openai/tools.rb | 26 ++++++++++++- controllers/interfaces/tools.rb | 13 +++++++ controllers/session.rb | 75 ++++++++++++++++++++---------------- logic/cartridge/safety.rb | 21 ++++++++++ 5 files changed, 101 insertions(+), 36 deletions(-) (limited to 'components/providers/openai/tools.rb') diff --git a/components/embedding.rb b/components/embedding.rb index 46d0fba..f08ff3d 100644 --- a/components/embedding.rb +++ b/components/embedding.rb @@ -33,7 +33,7 @@ module NanoBot path = "#{File.expand_path('../static/fennel', __dir__)}/?.lua" state = SweetMoon::State.new(package_path: path).fennel - # TODO: global is deprecated... + # TODO: `global` is deprecated. state.fennel.eval( "(global embedded (fn [#{parameters.join(' ')}] #{source}))", 1, safety[:sandboxed] ? { allowedGlobals: %w[math string table] } : nil diff --git a/components/providers/openai/tools.rb b/components/providers/openai/tools.rb index 50eead9..10c2709 100644 --- a/components/providers/openai/tools.rb +++ b/components/providers/openai/tools.rb @@ -10,12 +10,34 @@ module NanoBot module Providers class OpenAI < Base module Tools + def self.confirm(tool, feedback) + feedback.call( + { should_be_stored: false, + interaction: { who: 'AI', message: nil, meta: { + tool: { action: 'confirm', id: tool[:id], name: tool[:name], parameters: tool[:parameters] } + } } } + ) + end + def self.apply(cartridge, function_cartridge, tools, feedback) prepared_tools = NanoBot::Logic::OpenAI::Tools.prepare(function_cartridge, tools) - # TODO: Confirm before starting futures. + if Logic::Cartridge::Safety.confirmable?(cartridge) + prepared_tools.each { |tool| tool[:allowed] = confirm(tool, feedback) } + else + prepared_tools.each { |tool| tool[:allowed] = true } + end + futures = prepared_tools.map do |tool| - Concurrent::Promises.future { process!(tool, feedback, function_cartridge, cartridge) } + Concurrent::Promises.future do + if tool[:allowed] + process!(tool, feedback, function_cartridge, cartridge) + else + tool[:output] = + "We asked the user you're chatting with for permission, but the user did not allow you to run this tool or function." + tool + end + end end results = Concurrent::Promises.zip(*futures).value! diff --git a/controllers/interfaces/tools.rb b/controllers/interfaces/tools.rb index 1136600..d32afed 100644 --- a/controllers/interfaces/tools.rb +++ b/controllers/interfaces/tools.rb @@ -10,6 +10,17 @@ module NanoBot module Controllers module Interfaces module Tool + def self.confirm(session, cartridge, mode, feedback) + yeses = Logic::Cartridge::Safety.yeses(cartridge) + default_answer = Logic::Cartridge::Safety.default_answer(cartridge) + dispatch_feedback(session, cartridge, mode, feedback) + session.flush + answer = $stdin.gets.chomp.to_s.downcase.strip + answer = default_answer if answer == '' + session.print("\n") + yeses.include?(answer) + end + def self.adapt(feedback, adapter, cartridge) call = { parameters: %w[id name parameters parameters-as-json output], @@ -41,6 +52,8 @@ module NanoBot def self.dispatch_feedback(session, cartridge, mode, feedback) enabled = Logic::Cartridge::Tools.feedback?(cartridge, mode.to_sym, feedback[:action].to_sym) + enabled = true if feedback[:action].to_sym == :confirm + return unless enabled color = Logic::Cartridge::Tools.fetch_from_interface( diff --git a/controllers/session.rb b/controllers/session.rb index 546a891..b3cc7ef 100644 --- a/controllers/session.rb +++ b/controllers/session.rb @@ -6,6 +6,7 @@ require 'fileutils' require 'rainbow' require_relative '../logic/helpers/hash' +require_relative '../logic/cartridge/safety' require_relative '../logic/cartridge/streaming' require_relative '../logic/cartridge/interaction' require_relative '../logic/cartridge/fetch' @@ -102,10 +103,9 @@ module NanoBot prefix = Logic::Cartridge::Affixes.get(@cartridge, mode.to_sym, :output, :prefix) suffix = Logic::Cartridge::Affixes.get(@cartridge, mode.to_sym, :output, :suffix) - color = Logic::Cartridge::Fetch.cascate(@cartridge, [ - [:interfaces, mode.to_sym, :output, :color], - %i[interfaces output color] - ]) + color = Logic::Cartridge::Fetch.cascate( + @cartridge, [[:interfaces, mode.to_sym, :output, :color], %i[interfaces output color]] + ) color = color.to_sym if color @@ -118,44 +118,53 @@ module NanoBot needs_another_round = false @provider.evaluate(input, @cartridge) do |feedback| + needs_another_round = true if feedback[:needs_another_round] + updated_at = Time.now - needs_another_round = true if feedback[:needs_another_round] + if feedback[:interaction] && + feedback.dig(:interaction, :meta, :tool, :action) && + feedback[:interaction][:meta][:tool][:action] == 'confirm' + Interfaces::Tool.confirm(self, @cartridge, mode, feedback[:interaction][:meta][:tool]) + else - if feedback[:interaction] && feedback.dig(:interaction, :meta, :tool, :action) - Interfaces::Tool.dispatch_feedback(self, @cartridge, mode, feedback[:interaction][:meta][:tool]) - end + if feedback[:interaction] && feedback.dig(:interaction, :meta, :tool, :action) + Interfaces::Tool.dispatch_feedback( + self, @cartridge, mode, feedback[:interaction][:meta][:tool] + ) + end - if feedback[:interaction] - event = Marshal.load(Marshal.dump(feedback[:interaction])) - event[:mode] = mode.to_s - event[:output] = nil - - if feedback[:interaction][:who] == 'AI' && feedback[:interaction][:message] - event[:output] = feedback[:interaction][:message] - unless streaming - output = Logic::Cartridge::Interaction.output( - @cartridge, mode.to_sym, feedback[:interaction], streaming, feedback[:finished] - ) - output[:message] = Components::Adapter.apply(output[:message], @cartridge) - event[:output] = (output[:message]).to_s + if feedback[:interaction] + event = Marshal.load(Marshal.dump(feedback[:interaction])) + event[:mode] = mode.to_s + event[:output] = nil + + if feedback[:interaction][:who] == 'AI' && feedback[:interaction][:message] + event[:output] = feedback[:interaction][:message] + unless streaming + output = Logic::Cartridge::Interaction.output( + @cartridge, mode.to_sym, feedback[:interaction], streaming, feedback[:finished] + ) + output[:message] = Components::Adapter.apply(output[:message], @cartridge) + event[:output] = (output[:message]).to_s + end end - end - @state[:history] << event if feedback[:should_be_stored] + @state[:history] << event if feedback[:should_be_stored] - if event[:output] && ((!feedback[:finished] && streaming) || (!streaming && feedback[:finished])) - self.print(color ? Rainbow(event[:output]).send(color) : event[:output]) - end + if event[:output] && ((!feedback[:finished] && streaming) || (!streaming && feedback[:finished])) + self.print(color ? Rainbow(event[:output]).send(color) : event[:output]) + end - # The `print` function already outputs a prefix and a suffix, so - # we should add them afterwards to avoid printing them twice. - event[:output] = "#{prefix}#{event[:output]}#{suffix}" - end + # The `print` function already outputs a prefix and a suffix, so + # we should add them afterwards to avoid printing them twice. + event[:output] = "#{prefix}#{event[:output]}#{suffix}" + end - if feedback[:finished] - flush - ready = true + if feedback[:finished] + flush + ready = true + end end end diff --git a/logic/cartridge/safety.rb b/logic/cartridge/safety.rb index 84b39d7..6414e51 100644 --- a/logic/cartridge/safety.rb +++ b/logic/cartridge/safety.rb @@ -6,6 +6,27 @@ module NanoBot module Logic module Cartridge module Safety + def self.default_answer(cartridge) + default = Fetch.cascate(cartridge, [%i[interfaces tools confirm default]]) + return [] if default.nil? + + default + end + + def self.yeses(cartridge) + yeses_values = Fetch.cascate(cartridge, [%i[interfaces tools confirm yeses]]) + return [] if yeses_values.nil? + + yeses_values + end + + def self.confirmable?(cartridge) + confirmable = Fetch.cascate(cartridge, [%i[safety tools confirmable]]) + return true if confirmable.nil? + + confirmable + end + def self.sandboxed?(cartridge) sandboxed = Fetch.cascate(cartridge, [%i[safety functions sandboxed]]) return true if sandboxed.nil? -- cgit v1.2.3 From e89a1d57c49c94c16c37a8ee3a69b52a9e2b341b Mon Sep 17 00:00:00 2001 From: icebaker Date: Sun, 19 Nov 2023 13:35:54 -0300 Subject: improving tool specifications --- components/providers/openai.rb | 28 ++++++++------- components/providers/openai/tools.rb | 10 +++--- controllers/interfaces/repl.rb | 2 -- controllers/interfaces/tools.rb | 6 ++-- controllers/session.rb | 6 ++-- logic/cartridge/safety.rb | 4 +-- logic/providers/openai/tools.rb | 31 ++++------------- spec/data/cartridges/tools.yml | 39 ++++++++++++++------- spec/logic/cartridge/tools_spec.rb | 58 +++++++++++++++---------------- spec/logic/providers/openai/tools_spec.rb | 24 ++++++++----- static/cartridges/baseline.yml | 2 +- static/cartridges/default.yml | 4 +-- 12 files changed, 110 insertions(+), 104 deletions(-) (limited to 'components/providers/openai/tools.rb') diff --git a/components/providers/openai.rb b/components/providers/openai.rb index 996f7f6..a7e7abe 100644 --- a/components/providers/openai.rb +++ b/components/providers/openai.rb @@ -37,15 +37,7 @@ module NanoBot @client = ::OpenAI::Client.new(uri_base:, access_token: @credentials[:'access-token']) end - def stream(input) - provider = @settings.key?(:stream) ? @settings[:stream] : true - - interface = input[:interface].key?(:stream) ? input[:interface][:stream] : true - - provider && interface - end - - def evaluate(input, cartridge, &feedback) + def evaluate(input, streaming, cartridge, &feedback) messages = input[:history].map do |event| if event[:message].nil? && event[:meta] && event[:meta][:tool_calls] { role: 'assistant', content: nil, tool_calls: event[:meta][:tool_calls] } @@ -76,7 +68,7 @@ module NanoBot payload[:tools] = input[:tools].map { |raw| NanoBot::Logic::OpenAI::Tools.adapt(raw) } if input[:tools] - if stream(input) + if streaming content = '' tools = [] @@ -135,9 +127,21 @@ module NanoBot end end - @client.chat(parameters: payload) + begin + @client.chat(parameters: payload) + rescue StandardError => e + raise e.class, e.response[:body] if e.response && e.response[:body] + + raise e + end else - result = @client.chat(parameters: payload) + begin + result = @client.chat(parameters: payload) + rescue StandardError => e + raise e.class, e.response[:body] if e.response && e.response[:body] + + raise e + end raise StandardError, result['error'] if result['error'] diff --git a/components/providers/openai/tools.rb b/components/providers/openai/tools.rb index 10c2709..cd35e80 100644 --- a/components/providers/openai/tools.rb +++ b/components/providers/openai/tools.rb @@ -10,11 +10,11 @@ module NanoBot module Providers class OpenAI < Base module Tools - def self.confirm(tool, feedback) + def self.confirming(tool, feedback) feedback.call( { should_be_stored: false, interaction: { who: 'AI', message: nil, meta: { - tool: { action: 'confirm', id: tool[:id], name: tool[:name], parameters: tool[:parameters] } + tool: { action: 'confirming', id: tool[:id], name: tool[:name], parameters: tool[:parameters] } } } } ) end @@ -23,7 +23,7 @@ module NanoBot prepared_tools = NanoBot::Logic::OpenAI::Tools.prepare(function_cartridge, tools) if Logic::Cartridge::Safety.confirmable?(cartridge) - prepared_tools.each { |tool| tool[:allowed] = confirm(tool, feedback) } + prepared_tools.each { |tool| tool[:allowed] = confirming(tool, feedback) } else prepared_tools.each { |tool| tool[:allowed] = true } end @@ -55,7 +55,7 @@ module NanoBot feedback.call( { should_be_stored: false, interaction: { who: 'AI', message: nil, meta: { - tool: { action: 'call', id: tool[:id], name: tool[:name], parameters: tool[:parameters] } + tool: { action: 'executing', id: tool[:id], name: tool[:name], parameters: tool[:parameters] } } } } ) @@ -86,7 +86,7 @@ module NanoBot { should_be_stored: false, interaction: { who: 'AI', message: nil, meta: { tool: { - action: 'response', id: tool[:id], name: tool[:name], + action: 'responding', id: tool[:id], name: tool[:name], parameters: tool[:parameters], output: tool[:output] } } } } diff --git a/controllers/interfaces/repl.rb b/controllers/interfaces/repl.rb index d4191b1..fd16ea6 100644 --- a/controllers/interfaces/repl.rb +++ b/controllers/interfaces/repl.rb @@ -37,8 +37,6 @@ module NanoBot [proc { prompt }, proc { 'MISSING INPUT' }] ) - Logic::Cartridge::Streaming.enabled?(cartridge, :repl) - Pry.commands.block_command(/(.*)/, 'handler') do |line| session.print(prefix) unless prefix.nil? session.evaluate_and_print(line, mode: 'repl') diff --git a/controllers/interfaces/tools.rb b/controllers/interfaces/tools.rb index d32afed..aa4fb61 100644 --- a/controllers/interfaces/tools.rb +++ b/controllers/interfaces/tools.rb @@ -10,7 +10,7 @@ module NanoBot module Controllers module Interfaces module Tool - def self.confirm(session, cartridge, mode, feedback) + def self.confirming(session, cartridge, mode, feedback) yeses = Logic::Cartridge::Safety.yeses(cartridge) default_answer = Logic::Cartridge::Safety.default_answer(cartridge) dispatch_feedback(session, cartridge, mode, feedback) @@ -52,7 +52,7 @@ module NanoBot def self.dispatch_feedback(session, cartridge, mode, feedback) enabled = Logic::Cartridge::Tools.feedback?(cartridge, mode.to_sym, feedback[:action].to_sym) - enabled = true if feedback[:action].to_sym == :confirm + enabled = true if feedback[:action].to_sym == :confirming return unless enabled @@ -67,7 +67,7 @@ module NanoBot else message = "(#{feedback[:name]} #{feedback[:parameters].to_json})" - message += " =>\n#{feedback[:output]}" if feedback[:action].to_sym == :response + message += " =>\n#{feedback[:output]}" if feedback[:action].to_sym == :responding end message = "#{adapter[:prefix]}#{message}#{adapter[:suffix]}" diff --git a/controllers/session.rb b/controllers/session.rb index b3cc7ef..378a11d 100644 --- a/controllers/session.rb +++ b/controllers/session.rb @@ -117,15 +117,15 @@ module NanoBot needs_another_round = false - @provider.evaluate(input, @cartridge) do |feedback| + @provider.evaluate(input, streaming, @cartridge) do |feedback| needs_another_round = true if feedback[:needs_another_round] updated_at = Time.now if feedback[:interaction] && feedback.dig(:interaction, :meta, :tool, :action) && - feedback[:interaction][:meta][:tool][:action] == 'confirm' - Interfaces::Tool.confirm(self, @cartridge, mode, feedback[:interaction][:meta][:tool]) + feedback[:interaction][:meta][:tool][:action] == 'confirming' + Interfaces::Tool.confirming(self, @cartridge, mode, feedback[:interaction][:meta][:tool]) else if feedback[:interaction] && feedback.dig(:interaction, :meta, :tool, :action) diff --git a/logic/cartridge/safety.rb b/logic/cartridge/safety.rb index 6414e51..39826a9 100644 --- a/logic/cartridge/safety.rb +++ b/logic/cartridge/safety.rb @@ -7,14 +7,14 @@ module NanoBot module Cartridge module Safety def self.default_answer(cartridge) - default = Fetch.cascate(cartridge, [%i[interfaces tools confirm default]]) + default = Fetch.cascate(cartridge, [%i[interfaces tools confirming default]]) return [] if default.nil? default end def self.yeses(cartridge) - yeses_values = Fetch.cascate(cartridge, [%i[interfaces tools confirm yeses]]) + yeses_values = Fetch.cascate(cartridge, [%i[interfaces tools confirming yeses]]) return [] if yeses_values.nil? yeses_values diff --git a/logic/providers/openai/tools.rb b/logic/providers/openai/tools.rb index 1aa9029..68f2209 100644 --- a/logic/providers/openai/tools.rb +++ b/logic/providers/openai/tools.rb @@ -17,10 +17,7 @@ module NanoBot tool = Helpers::Hash.symbolize_keys(tool) cartridge.each do |candidate| - next unless ( - candidate[:type].nil? || - (candidate[:type] == 'function' && tool[:type] == candidate[:type]) - ) && tool[:function][:name] == candidate[:name] + next unless tool[:function][:name] == candidate[:name] source = {} @@ -31,40 +28,26 @@ module NanoBot applies << { id: tool[:id], name: tool[:function][:name], - type: candidate[:type] || 'function', + type: 'function', parameters: JSON.parse(tool[:function][:arguments]), source: } end end + raise 'missing tool' if applies.size != tools.size + applies end def self.adapt(cartridge) - raise 'unsupported tool' if cartridge[:type] != 'function' && !cartridge[:type].nil? - - adapted = { - type: cartridge[:type] || 'function', + { + type: 'function', function: { name: cartridge[:name], description: cartridge[:description], - parameters: { type: 'object', properties: {} } + parameters: cartridge[:parameters] } } - - properties = adapted[:function][:parameters][:properties] - - adapted[:function][:parameters][:required] = cartridge[:required] if cartridge[:required] - - cartridge[:parameters]&.each do |parameter| - key = parameter[:name].to_sym - properties[key] = {} - properties[key][:type] = parameter[:type] || 'string' - properties[key][:description] = parameter[:description] if parameter[:description] - properties[key][:items] = parameter[:items].slice(:type) if parameter[:items] - end - - adapted end end end diff --git a/spec/data/cartridges/tools.yml b/spec/data/cartridges/tools.yml index 0c2a30b..08164a4 100644 --- a/spec/data/cartridges/tools.yml +++ b/spec/data/cartridges/tools.yml @@ -1,28 +1,41 @@ --- tools: + - name: what-time-is-it + description: Returns the current date and time for a given timezone. + parameters: + type: object + properties: + timezone: + type: string + description: A string representing the timezone that should be used to provide a datetime, following the IANA (Internet Assigned Numbers Authority) Time Zone Database. Examples are "Asia/Tokyo" and "Europe/Paris". + required: + - timezone + fennel: | + (os.date) + - name: get-current-weather - type: function description: Get the current weather in a given location. parameters: - - name: location - - name: unit + type: object + properties: + location: + type: string + unit: + type: string 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. + 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 clojure: | (require '[clojure.java.shell :refer [sh]]) (println (apply sh (get parameters "command"))) diff --git a/spec/logic/cartridge/tools_spec.rb b/spec/logic/cartridge/tools_spec.rb index 42b8c57..913fa62 100644 --- a/spec/logic/cartridge/tools_spec.rb +++ b/spec/logic/cartridge/tools_spec.rb @@ -10,11 +10,11 @@ RSpec.describe NanoBot::Logic::Cartridge::Tools 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, :executing)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :executing)).to be(true) - expect(described_class.feedback?(cartridge, :repl, :response)).to be(false) - expect(described_class.feedback?(cartridge, :eval, :response)).to be(false) + expect(described_class.feedback?(cartridge, :repl, :responding)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :responding)).to be(false) end end @@ -24,11 +24,11 @@ RSpec.describe NanoBot::Logic::Cartridge::Tools do 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, :executing)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :executing)).to be(false) - expect(described_class.feedback?(cartridge, :repl, :response)).to be(false) - expect(described_class.feedback?(cartridge, :eval, :response)).to be(false) + expect(described_class.feedback?(cartridge, :repl, :responding)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :responding)).to be(false) end end @@ -38,59 +38,59 @@ RSpec.describe NanoBot::Logic::Cartridge::Tools do 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, :executing)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :executing)).to be(true) - expect(described_class.feedback?(cartridge, :repl, :response)).to be(true) - expect(described_class.feedback?(cartridge, :eval, :response)).to be(true) + expect(described_class.feedback?(cartridge, :repl, :responding)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :responding)).to be(true) end end context 'top-level-specific overrides' do let(:cartridge) do - { interfaces: { tools: { call: { feedback: false }, response: { feedback: true } } } } + { interfaces: { tools: { executing: { feedback: false }, responding: { 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, :executing)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :executing)).to be(false) - expect(described_class.feedback?(cartridge, :repl, :response)).to be(true) - expect(described_class.feedback?(cartridge, :eval, :response)).to be(true) + expect(described_class.feedback?(cartridge, :repl, :responding)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :responding)).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 } } } + tools: { executing: { feedback: false }, responding: { feedback: true } }, + repl: { tools: { executing: { feedback: true }, responding: { 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, :executing)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :executing)).to be(false) - expect(described_class.feedback?(cartridge, :repl, :response)).to be(false) - expect(described_class.feedback?(cartridge, :eval, :response)).to be(true) + expect(described_class.feedback?(cartridge, :repl, :responding)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :responding)).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 } } } + tools: { executing: { feedback: false }, responding: { feedback: true } }, + eval: { tools: { executing: { feedback: true }, responding: { 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, :executing)).to be(false) + expect(described_class.feedback?(cartridge, :eval, :executing)).to be(true) - expect(described_class.feedback?(cartridge, :repl, :response)).to be(true) - expect(described_class.feedback?(cartridge, :eval, :response)).to be(false) + expect(described_class.feedback?(cartridge, :repl, :responding)).to be(true) + expect(described_class.feedback?(cartridge, :eval, :responding)).to be(false) end end end diff --git a/spec/logic/providers/openai/tools_spec.rb b/spec/logic/providers/openai/tools_spec.rb index c92c374..1758e5f 100644 --- a/spec/logic/providers/openai/tools_spec.rb +++ b/spec/logic/providers/openai/tools_spec.rb @@ -13,14 +13,16 @@ RSpec.describe NanoBot::Logic::OpenAI::Tools 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.', + name: 'what-time-is-it', + description: 'Returns the current date and time for a given timezone.', parameters: { type: 'object', properties: { - location: { type: 'string' }, - unit: { type: 'string' } - } + timezone: { + type: 'string', + description: 'A string representing the timezone that should be used to provide a datetime, following the IANA (Internet Assigned Numbers Authority) Time Zone Database. Examples are "Asia/Tokyo" and "Europe/Paris".' + } + }, required: ['timezone'] } } } ) @@ -28,9 +30,15 @@ RSpec.describe NanoBot::Logic::OpenAI::Tools do 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' } + name: 'get-current-weather', + description: 'Get the current weather in a given location.', + parameters: { + type: 'object', + properties: { + location: { type: 'string' }, + unit: { type: 'string' } + } + } } } ) diff --git a/static/cartridges/baseline.yml b/static/cartridges/baseline.yml index 50c4756..f3aaab1 100644 --- a/static/cartridges/baseline.yml +++ b/static/cartridges/baseline.yml @@ -14,4 +14,4 @@ provider: access-token: ENV/OPENAI_API_KEY settings: user: ENV/NANO_BOTS_END_USER - model: gpt-3.5-turbo + model: gpt-3.5-turbo-1106 diff --git a/static/cartridges/default.yml b/static/cartridges/default.yml index 57eeada..ce2614d 100644 --- a/static/cartridges/default.yml +++ b/static/cartridges/default.yml @@ -19,9 +19,9 @@ interfaces: stream: true suffix: "\n" tools: - call: + executing: feedback: true - response: + responding: feedback: false provider: -- cgit v1.2.3