summaryrefslogtreecommitdiff
path: root/components/providers
diff options
context:
space:
mode:
authoricebaker <113217272+icebaker@users.noreply.github.com>2023-11-29 07:53:19 -0300
committerGitHub <noreply@github.com>2023-11-29 07:53:19 -0300
commit9f79a161905f5af8e331930cc77c7be10703596f (patch)
treeb2ab75665e42de79f22cca82bf03cfc49759f485 /components/providers
parente1ab6853262b83f483060961f17bf895989a19c0 (diff)
parent154aa68caf50a18af5c0dff1d368fc639314e0ba (diff)
Merge pull request #5 from icebaker/ib-tools
Adding support for Spec 1.0.0: Tools (Functions)
Diffstat (limited to 'components/providers')
-rw-r--r--components/providers/openai.rb123
-rw-r--r--components/providers/openai/tools.rb101
2 files changed, 204 insertions, 20 deletions
diff --git a/components/providers/openai.rb b/components/providers/openai.rb
index ce6fb33..87f2bc5 100644
--- a/components/providers/openai.rb
+++ b/components/providers/openai.rb
@@ -2,9 +2,14 @@
require 'openai'
-require_relative './base'
+require_relative 'base'
require_relative '../crypto'
+require_relative '../../logic/providers/openai/tools'
+require_relative '../../controllers/interfaces/tools'
+
+require_relative 'openai/tools'
+
module NanoBot
module Components
module Providers
@@ -32,17 +37,16 @@ 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, &block)
+ def evaluate(input, streaming, cartridge, &feedback)
messages = input[:history].map do |event|
- { role: event[:who] == 'user' ? 'user' : 'assistant',
- content: event[:message] }
+ if event[:message].nil? && event[:meta] && event[:meta][:tool_calls]
+ { role: 'assistant', content: nil, tool_calls: event[:meta][:tool_calls] }
+ elsif event[:who] == 'tool'
+ { role: event[:who], content: event[:message].to_s,
+ tool_call_id: event[:meta][:id], name: event[:meta][:name] }
+ else
+ { role: event[:who] == 'user' ? 'user' : 'assistant', content: event[:message] }
+ end
end
%i[instruction backdrop directive].each do |key|
@@ -62,26 +66,105 @@ module NanoBot
payload.delete(:logit_bias) if payload.key?(:logit_bias) && payload[:logit_bias].nil?
- if stream(input)
+ payload[:tools] = input[:tools].map { |raw| NanoBot::Logic::OpenAI::Tools.adapt(raw) } if input[:tools]
+
+ if streaming
content = ''
+ tools = []
payload[:stream] = proc do |chunk, _bytesize|
- partial = chunk.dig('choices', 0, 'delta', 'content')
- if partial
- content += partial
- block.call({ who: 'AI', message: partial }, false)
+ partial_content = chunk.dig('choices', 0, 'delta', 'content')
+ partial_tools = chunk.dig('choices', 0, 'delta', 'tool_calls')
+
+ if partial_tools
+ partial_tools.each do |partial_tool|
+ tools[partial_tool['index']] = {} if tools[partial_tool['index']].nil?
+
+ partial_tool.keys.reject { |key| ['index'].include?(key) }.each do |key|
+ target = tools[partial_tool['index']]
+
+ if partial_tool[key].is_a?(Hash)
+ target[key] = {} if target[key].nil?
+ partial_tool[key].each_key do |sub_key|
+ target[key][sub_key] = '' if target[key][sub_key].nil?
+
+ target[key][sub_key] += partial_tool[key][sub_key]
+ end
+ else
+ target[key] = '' if target[key].nil?
+
+ target[key] += partial_tool[key]
+ end
+ end
+ end
end
- block.call({ who: 'AI', message: content }, true) if chunk.dig('choices', 0, 'finish_reason')
+ if partial_content
+ content += partial_content
+ feedback.call(
+ { should_be_stored: false,
+ interaction: { who: 'AI', message: partial_content } }
+ )
+ end
+
+ if chunk.dig('choices', 0, 'finish_reason')
+ if tools&.size&.positive?
+ feedback.call(
+ { should_be_stored: true,
+ needs_another_round: true,
+ interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } }
+ )
+ Tools.apply(cartridge, input[:tools], tools, feedback).each do |interaction|
+ feedback.call({ should_be_stored: true, needs_another_round: true, interaction: })
+ end
+ end
+
+ feedback.call(
+ { should_be_stored: !(content.nil? || content == ''),
+ interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content },
+ finished: true }
+ )
+ 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']
- block.call({ who: 'AI', message: result.dig('choices', 0, 'message', 'content') }, true)
+ tools = result.dig('choices', 0, 'message', 'tool_calls')
+
+ if tools&.size&.positive?
+ feedback.call(
+ { should_be_stored: true,
+ needs_another_round: true,
+ interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } }
+ )
+ Tools.apply(cartridge, input[:tools], tools, feedback).each do |interaction|
+ feedback.call({ should_be_stored: true, needs_another_round: true, interaction: })
+ end
+ end
+
+ content = result.dig('choices', 0, 'message', 'content')
+
+ feedback.call(
+ { should_be_stored: !(content.nil? || content == ''),
+ interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content },
+ finished: true }
+ )
end
end
diff --git a/components/providers/openai/tools.rb b/components/providers/openai/tools.rb
new file mode 100644
index 0000000..cd35e80
--- /dev/null
+++ b/components/providers/openai/tools.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require_relative '../../embedding'
+require_relative '../../../logic/cartridge/safety'
+
+require 'concurrent'
+
+module NanoBot
+ module Components
+ module Providers
+ class OpenAI < Base
+ module Tools
+ def self.confirming(tool, feedback)
+ feedback.call(
+ { should_be_stored: false,
+ interaction: { who: 'AI', message: nil, meta: {
+ tool: { action: 'confirming', 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)
+
+ if Logic::Cartridge::Safety.confirmable?(cartridge)
+ prepared_tools.each { |tool| tool[:allowed] = confirming(tool, feedback) }
+ else
+ prepared_tools.each { |tool| tool[:allowed] = true }
+ end
+
+ futures = prepared_tools.map do |tool|
+ 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!
+
+ 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, _function_cartridge, cartridge)
+ feedback.call(
+ { should_be_stored: false,
+ interaction: { who: 'AI', message: nil, meta: {
+ tool: { action: 'executing', id: tool[:id], name: tool[:name], parameters: tool[:parameters] }
+ } } }
+ )
+
+ 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'
+ 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: 'responding', id: tool[:id], name: tool[:name],
+ parameters: tool[:parameters], output: tool[:output]
+ }
+ } } }
+ )
+
+ tool
+ end
+ end
+ end
+ end
+ end
+end