Skip to content

Commit

Permalink
Add basic tool-assisted integration.
Browse files Browse the repository at this point in the history
  • Loading branch information
ioquatix committed Nov 27, 2024
1 parent a5c4d28 commit 40edec3
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 18 deletions.
68 changes: 50 additions & 18 deletions examples/chatbot/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,69 @@
require "xrb/reference"

require_relative "conversation"
require_relative "toolbox"

class ChatbotView < Live::View
def initialize(...)
super

@conversation = nil
@toolbox ||= Toolbox.default
end

def conversation
@conversation ||= Conversation.find_by(id: @data[:conversation_id])
end

def append_prompt(client, prompt)
previous_context = conversation.context

conversation_message = conversation.conversation_messages.create!(prompt: prompt, response: String.new)

self.append(".conversation .messages") do |builder|
self.render_message(builder, conversation_message)
end

generate = client.generate(prompt, context: previous_context) do |response|
response.body.each do |token|
conversation_message.response += token

self.replace(".message.id#{conversation_message.id}") do |builder|
self.render_message(builder, conversation_message)
end
end
end

conversation_message.response = generate.response
conversation_message.context = generate.context
conversation_message.save!

return conversation_message
end

def update_conversation(prompt)
Console.info(self, "Updating conversation", id: conversation.id, prompt: prompt)

if prompt.start_with? "/explain"
prompt = @toolbox.explain
end

Async::Ollama::Client.open do |client|
previous_context = conversation.context

conversation_message = conversation.conversation_messages.create!(prompt: prompt, response: String.new)
conversation_message = append_prompt(client, prompt)

self.append(".conversation .messages") do |builder|
self.render_message(builder, conversation_message)
end

generate = client.generate(prompt, context: previous_context) do |response|
response.body.each do |token|
conversation_message.response += token

self.replace(".message.id#{conversation_message.id}") do |builder|
self.render_message(builder, conversation_message)
end
while conversation_message
messages = @toolbox.each(conversation_message.response).to_a
break if messages.empty?

results = []

messages.each do |message|
result = @toolbox.call(message)
results << result.to_json
end

conversation_message = append_prompt(client, results.join("\n"))
end

conversation_message.response = generate.response
conversation_message.context = generate.context
conversation_message.save!
end
end

Expand Down
2 changes: 2 additions & 0 deletions examples/chatbot/gems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
gem "markly"

gem "async-ollama"
gem "async-http"

138 changes: 138 additions & 0 deletions examples/chatbot/toolbox.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2024, by Samuel Williams.

require "async/http/internet/instance"

class Tool
def initialize(name, explain, &block)
@name = name
@explain = explain
@block = block
end

attr :name
attr :explain

def as_json
{
"name" => @name,
"explain" => @explain,
}
end

def call(message)
@block.call(message)
end
end

class Toolbox
def self.default
self.new.tap do |toolbox|
toolbox.register("ruby", '{"tool":"ruby", "code": "..."}') do |message|
eval(message[:code])
end

toolbox.register("internet.get", '{"tool":"internet.get", "url": "http://..."}') do |message|
Async::HTTP::Internet.get(message[:url]).read
end

toolbox.register("explain", '{"tool":"explain"}') do |message|
toolbox.as_json
end
end
end

PROMPT = "You have access to the following tools, which you can invoke by replying with a single line of valid JSON:\n\n"

USAGE = <<~EOF
Use these tools to enhance your ability to answer user queries accurately.
When you need to use a tool to answer the user's query, respond **only** with the JSON invocation.
- Example: {"tool":"ruby", "code": "5+5"}
- **Do not** include any explanations, greetings, or additional text when invoking a tool.
- If you are dealing with numbers, ensure you provide them as Integers or Floats, not Strings.
After invoking a tool:
1. You will receive the tool's result as the next input.
2. Use the result to formulate a direct, user-friendly response that answers the original query.
3. Assume the user is unaware of the tool invocation or its result, so clearly summarize the answer without referring to the tool usage or the response it generated.
Continue the conversation naturally after providing the answer. Ensure your responses are concise and user-focused.
## Example Flow:
User: "Why doesn't 5 + 5 equal 11?"
Assistant (invokes tool): {"tool": "ruby", "code": "5+5"}
(Tool Result): 10
Assistant: "The result of 5 + 5 is 10, because addition follows standard arithmetic rules."
EOF

def initialize
@tools = {}
end

def as_json
{
"prompt" => PROMPT,
"tools" => @tools.map(&:as_json),
"usage" => USAGE,
}
end

attr :tools

def register(name, explain, &block)
@tools[name] = Tool.new(name, explain, &block)
end

def tool?(response)
if response.start_with?('{')
begin
return JSON.parse(response, symbolize_names: true)
rescue => error
Console.debug(self, error)
end
end

return false
end

def each(text)
return to_enum(:each, text) unless block_given?

text.each_line do |line|
if message = tool?(line)
yield message
end
end
end

def call(message)
name = message[:tool]

if tool = @tools[name]
result = tool.call(message)

return {result: result}.to_json
else
raise ArgumentError.new("Unknown tool: #{name}")
end
rescue => error
{error: error.message}.to_json
end

def explain
buffer = String.new
buffer << PROMPT

@tools.each do |name, tool|
buffer << tool.explain
end

buffer << "\n" << USAGE << "\n"

return buffer
end
end

0 comments on commit 40edec3

Please sign in to comment.