diff --git a/README.md b/README.md index 41d3ee6..cc920ea 100644 --- a/README.md +++ b/README.md @@ -134,60 +134,58 @@ class MyProductsIndexer < AiAgent::Embeddings::Indexers::BaseIndexer end ``` -# External tools usage(DEVIN) +# External tools usage(DEVIN like) ```ruby class EvaluateRubyCodeAgent < AiAgent::Agents::BasicAgentWithTools config do |c| c.who_am_i = <<~WHO_AM_I - I am a ruby programming agent agent, i use my tools to evaluate ruby code + I have multiple roles, here are some of them: + - #{AiAgent::Tools::Ruby::EvaluateRubyTool.who_am_i_description} WHO_AM_I c.ollama_model = 'llama3.1' # you need a model with tools end def tools - @tools ||= { + @tools ||= [ + evaluate_ruby_tool.tool_definition, + ].inject({}) do |h, tool| + h.merge(tool) + end.deep_merge({ evaluate_ruby: { - execute: ->(params) do - code = params['code'] - output = evaluate_ruby_tool.evaluate(code) - AiAgent::Conversation::Message.new(:tool, output) # you need to return a message - end, - description: 'Evaluates Ruby code', - parameters: { - type: 'string', - description: 'The Ruby code to evaluate' - }, - required: ['code'] - }, - ask_user: { - execute: ->(params) do - question = params['question'] - puts question - answer = gets.chomp - stop if answer == 'exit' - answer - end, - description: 'Asks the user a question', - parameters: { - type: 'string', - description: 'The question to ask the user' - }, - required: ['question'] + before_execute: ->(params) do + puts params['code'].colorize(:green) + end } - } + }) end private def evaluate_ruby_tool @evaluate_ruby_tool ||= AiAgent::Tools::Ruby::EvaluateRubyTool.new(self) end + def show_normal_response(content) + puts content + end + + def ask_for_user_input + print '> ' + response = gets.chomp + exit if response == 'exit' + AiAgent::Conversation::Message.new(:user, response) + end + + def show_tool_call_explanation(tool_call) + puts 'Ai: '.colorize(:magenta) + tool_call.explanation + end end agent = EvaluateRubyCodeAgent.new([]) # [] is the history of messages agent.run_in_loop ``` +Check for more tools in `lib/ai_agent/tools` + # Rails Integration #### Example 1) Standalone Chat Agent ```ruby @@ -242,10 +240,6 @@ class AiConversationsController < ApplicationController end ``` -# 🔮 Future Plans - -- 🛠️ Ability for agents to execute tasks independently - # 🛠️ Development After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run bundle exec rake install. diff --git a/lib/ai_agent/agents/basic_agent_with_tools.rb b/lib/ai_agent/agents/basic_agent_with_tools.rb index 30cab84..ba8c0b0 100644 --- a/lib/ai_agent/agents/basic_agent_with_tools.rb +++ b/lib/ai_agent/agents/basic_agent_with_tools.rb @@ -48,6 +48,10 @@ def serialize_tools type: :string, description: 'Explain the reason for the tool call' } + + t_options[:parameters][:required].tap do |required| + required << '__explain' if required && required.include?('__explain') + end end { diff --git a/lib/ai_agent/tools/os/execute_command.rb b/lib/ai_agent/tools/os/execute_command.rb index 0877129..a6d1e42 100644 --- a/lib/ai_agent/tools/os/execute_command.rb +++ b/lib/ai_agent/tools/os/execute_command.rb @@ -8,6 +8,12 @@ def self.who_am_i_description "I have access to the system's shell and can execute commands." end + def initialize(agent, sudo: false, root: false) + super(agent) + @sudo = sudo + @root = root + end + def execute_command(command, path) command = "cd #{path} && #{command}" result = Open3.capture3(command) @@ -36,7 +42,7 @@ def tool_definition rescue RuntimeError => e AiAgent::Conversation::Message.new(:tool, "Error executing command: #{e.inspect}") end, - description: "Executes a command in the system's shell.", + description: "Executes a command in the system's shell. Use this tool to execute commands in the system's shell. Sudo access: #{@sudo}, root access: #{@root}.", parameters: { type: 'object', properties: { @@ -58,7 +64,7 @@ def tool_definition path = params['path'] || Dir.pwd AiAgent::Conversation::Message.new(:tool, execute_command_in_background(command, path)) end, - description: "Executes a command in the system's shell in the background.", + description: "Executes a command in the system's shell in the background. Use this tool to execute commands in the system's shell in the background. Sudo access: #{@sudo}, root access: #{@root}", parameters: { type: 'object', properties: { diff --git a/lib/ai_agent/tools/os/file_system.rb b/lib/ai_agent/tools/os/file_system.rb index 0bddc22..f72f309 100644 --- a/lib/ai_agent/tools/os/file_system.rb +++ b/lib/ai_agent/tools/os/file_system.rb @@ -132,7 +132,7 @@ def tool_definition content = read_file(params['path']) AiAgent::Conversation::Message.new(:tool, content) end, - description: 'Reads the content of a file.', + description: 'Reads the content of a file. Use this tool to read the content of a file.', parameters: { type: 'object', properties: { @@ -150,7 +150,7 @@ def tool_definition write_file(path, params['content']) AiAgent::Conversation::Message.new(:tool, "Wrote to #{path}") end, - description: 'Writes content to a file.', + description: 'Writes content to a file. Use this tool to write content to a file.', parameters: { type: 'object', properties: { @@ -171,8 +171,10 @@ def tool_definition path = params['path'] write_file_binary(path, params['content']) AiAgent::Conversation::Message.new(:tool, "Wrote binary to #{path}") + rescue Errno::ENOENT + AiAgent::Conversation::Message.new(:tool, "File not found #{path}") end, - description: 'Writes binary content to a file.', + description: 'Writes binary content to a file. Use this tool to write binary content to a file.', parameters: { type: 'object', properties: { @@ -194,8 +196,10 @@ def tool_definition path = params['path'] append_to_file(path, params['content']) AiAgent::Conversation::Message.new(:tool, "Appended to #{path}") + rescue Errno::ENOENT + AiAgent::Conversation::Message.new(:tool, "File not found #{path}") end, - description: 'Appends content to a file.', + description: 'Appends content to a file. Use this tool to append content to a file.', parameters: { type: 'object', properties: { @@ -217,7 +221,7 @@ def tool_definition replace_in_file(path, params['search'], params['replace']) AiAgent::Conversation::Message.new(:tool, "Replaced in #{path}") end, - description: 'Replaces content in a file.', + description: 'Replaces content in a file. Use this tool to replace content in a file.', parameters: { type: 'object', properties: { @@ -242,7 +246,7 @@ def tool_definition delete_file(params['path']) AiAgent::Conversation::Message.new(:tool, "Deleted #{params['path']}") end, - description: 'Deletes a file.', + description: 'Deletes a file. Use this tool to delete a file.', parameters: { type: 'object', properties: { @@ -259,7 +263,7 @@ def tool_definition exists = file_exists?(params['path']) AiAgent::Conversation::Message.new(:tool, "File exists: #{exists}") end, - description: 'Checks if a file exists.', + description: 'Checks if a file exists. Use this tool to check if a file exists.', parameters: { type: 'object', properties: { @@ -276,7 +280,7 @@ def tool_definition exists = directory_exists?(params['path']) AiAgent::Conversation::Message.new(:tool, "Directory exists: #{exists}") end, - description: 'Checks if a directory exists.', + description: 'Checks if a directory exists. Use this tool to check if a directory exists.', parameters: { type: 'object', properties: { @@ -293,7 +297,7 @@ def tool_definition create_directory(params['path']) AiAgent::Conversation::Message.new(:tool, "Created #{params['path']}") end, - description: 'Creates a directory.', + description: 'Creates a directory. Use this tool to create a directory.', parameters: { type: 'object', properties: { @@ -310,7 +314,7 @@ def tool_definition delete_directory(params['path']) AiAgent::Conversation::Message.new(:tool, "Deleted #{params['path']}") end, - description: 'Deletes a directory.', + description: 'Deletes a directory. Use this tool to delete a directory.', parameters: { type: 'object', properties: { @@ -327,7 +331,7 @@ def tool_definition copy_file(params['source'], params['destination']) AiAgent::Conversation::Message.new(:tool, "Copied #{params['source']} to #{params['destination']}") end, - description: 'Copies a file.', + description: 'Copies a file. Use this tool to copy a file.', parameters: { type: 'object', properties: { @@ -348,7 +352,7 @@ def tool_definition move_file(params['source'], params['destination']) AiAgent::Conversation::Message.new(:tool, "Moved #{params['source']} to #{params['destination']}") end, - description: 'Moves a file.', + description: 'Moves a file. Use this tool to move a file.', parameters: { type: 'object', properties: { @@ -369,7 +373,7 @@ def tool_definition copy_directory(params['source'], params['destination']) AiAgent::Conversation::Message.new(:tool, "Copied #{params['source']} to #{params['destination']}") end, - description: 'Copies a directory.', + description: 'Copies a directory. Use this tool to copy a directory.', parameters: { type: 'object', properties: { @@ -390,7 +394,7 @@ def tool_definition move_directory(params['source'], params['destination']) AiAgent::Conversation::Message.new(:tool, "Moved directory #{params['source']} to #{params['destination']}") end, - description: 'Moves a directory.', + description: 'Moves a directory. Use this tool to move a directory.', parameters: { type: 'object', properties: { @@ -411,7 +415,7 @@ def tool_definition files = list_files(params['path']) AiAgent::Conversation::Message.new(:tool, files.join("\n")) end, - description: 'Lists files in a directory.', + description: 'Lists files in a directory. Use this tool to list files in a directory.', parameters: { type: 'object', properties: { @@ -428,7 +432,7 @@ def tool_definition directories = list_directories(params['path']) AiAgent::Conversation::Message.new(:tool, directories.join("\n")) end, - description: 'Lists directories in a directory.', + description: 'Lists directories in a directory. Use this tool to list subdirectories in a directory.', parameters: { type: 'object', properties: { @@ -445,7 +449,7 @@ def tool_definition items = list_files_and_directories(params['path']) AiAgent::Conversation::Message.new(:tool, items.join("\n")) end, - description: 'Lists both files and directories in a directory.', + description: 'Lists both files and directories in a directory. Use this tool to list both files and directories in a directory.', parameters: { type: 'object', properties: { @@ -462,7 +466,7 @@ def tool_definition files = list_files_recursive(params['path']) AiAgent::Conversation::Message.new(:tool, files.join("\n")) end, - description: 'Lists files recursively in a directory.', + description: 'Lists files recursively in a directory. Use this tool to list files from a directory recursively.', parameters: { type: 'object', properties: { @@ -479,7 +483,7 @@ def tool_definition directories = list_directories_recursive(params['path']) AiAgent::Conversation::Message.new(:tool, directories.join("\n")) end, - description: 'Lists directories recursively in a directory.', + description: 'Lists directories recursively in a directory. Use this tool to list subdirectories from a directory recursively.', parameters: { type: 'object', properties: { @@ -496,7 +500,7 @@ def tool_definition items = list_files_and_directories_recursive(params['path']) AiAgent::Conversation::Message.new(:tool, items.join("\n")) end, - description: 'Lists both files and directories recursively in a directory.', + description: 'Lists both files and directories recursively in a directory. Use this tool to list both files and directories from a directory recursively.', parameters: { type: 'object', properties: { @@ -512,8 +516,10 @@ def tool_definition execute: ->(params) do size = file_size(params['path']) AiAgent::Conversation::Message.new(:tool, size.to_s) + rescue Errno::ENOENT + AiAgent::Conversation::Message.new(:tool, "File not found #{params['path']}") end, - description: 'Gets the size of a file in bytes.', + description: 'Gets the size of a file in bytes. Use this tool to get the size of a file in bytes.', parameters: { type: 'object', properties: { @@ -529,8 +535,10 @@ def tool_definition execute: ->(params) do size = file_size_human(params['path']) AiAgent::Conversation::Message.new(:tool, size) + rescue Errno::ENOENT + AiAgent::Conversation::Message.new(:tool, "File not found #{params['path']}") end, - description: 'Gets the size of a file in human-readable format.', + description: 'Gets the size of a file in human-readable format. Use this tool to get the size of a file in human-readable format.', parameters: { type: 'object', properties: { @@ -547,7 +555,7 @@ def tool_definition type = file_type(params['path']) AiAgent::Conversation::Message.new(:tool, type) end, - description: 'Gets the MIME type of a file.', + description: 'Gets the MIME type of a file. Use this tool to get the MIME type of a file.', parameters: { type: 'object', properties: { @@ -564,7 +572,7 @@ def tool_definition mime_type = file_mime_type(params['path']) AiAgent::Conversation::Message.new(:tool, mime_type) end, - description: 'Gets the MIME type of a file.', + description: 'Gets the MIME type of a file. Use this tool to get the MIME type of a file.', parameters: { type: 'object', properties: { @@ -581,7 +589,7 @@ def tool_definition extension = file_extension(params['path']) AiAgent::Conversation::Message.new(:tool, extension) end, - description: 'Gets the extension of a file.', + description: 'Gets the extension of a file. Use this tool to get the extension of a file.', parameters: { type: 'object', properties: { @@ -598,7 +606,7 @@ def tool_definition name = file_name(params['path']) AiAgent::Conversation::Message.new(:tool, name) end, - description: 'Gets the name of a file.', + description: 'Gets the name of a file. Use this tool to get the name of a file.', parameters: { type: 'object', properties: { @@ -615,7 +623,7 @@ def tool_definition name = file_name_without_extension(params['path']) AiAgent::Conversation::Message.new(:tool, name) end, - description: 'Gets the name of a file without its extension.', + description: 'Gets the name of a file without its extension. Use this tool to get the name of a file without its extension.', parameters: { type: 'object', properties: { @@ -632,7 +640,7 @@ def tool_definition path = file_path(params['path']) AiAgent::Conversation::Message.new(:tool, path) end, - description: 'Gets the directory path of a file.', + description: 'Gets the directory path of a file. Use this tool to get the directory path of a file.', parameters: { type: 'object', properties: { diff --git a/lib/ai_agent/tools/ruby/evaluate_ruby_tool.rb b/lib/ai_agent/tools/ruby/evaluate_ruby_tool.rb index 232b66d..1e583e7 100644 --- a/lib/ai_agent/tools/ruby/evaluate_ruby_tool.rb +++ b/lib/ai_agent/tools/ruby/evaluate_ruby_tool.rb @@ -29,7 +29,7 @@ def tool_definition AiAgent::Conversation::Message.new(:tool, 'Error evaluating Ruby code', metadata: { error: e.inspect, backtrace: e.backtrace }) end end, - description: "Evaluates Ruby code, it's running in a binding, so you can define variables and use them in the next calls", + description: "Evaluates Ruby code, it's running in a binding, so you can define variables/methods/classes and use them in the next calls", parameters: { type: 'object', properties: { @@ -37,9 +37,13 @@ def tool_definition type: 'string', description: 'The Ruby code to evaluate' }, + __explain: { + type: :string, + description: 'Human readable explanation of the tool code' + } }, + required: %w[code __explain] }, - required: ['code'] } } end diff --git a/playground/ruby_evaluate_agent.rb b/playground/evaluate_ruby_code_agent.rb similarity index 66% rename from playground/ruby_evaluate_agent.rb rename to playground/evaluate_ruby_code_agent.rb index 3fb65c0..70c822b 100644 --- a/playground/ruby_evaluate_agent.rb +++ b/playground/evaluate_ruby_code_agent.rb @@ -8,9 +8,6 @@ class EvaluateRubyCodeAgent < AiAgent::Agents::BasicAgentWithTools c.who_am_i = <<~WHO_AM_I I have multiple roles, here are some of them: - #{AiAgent::Tools::Ruby::EvaluateRubyTool.who_am_i_description} - - #{AiAgent::Tools::Os::ExecuteCommand.who_am_i_description} - - Prioritize using ruby tools, only use execute_command if you need to run a system command.#{' '} WHO_AM_I c.ollama_model = 'llama3.1' # you need a model with tools @@ -19,26 +16,21 @@ class EvaluateRubyCodeAgent < AiAgent::Agents::BasicAgentWithTools def tools @tools ||= [ evaluate_ruby_tool.tool_definition, - execute_command_tool.tool_definition, - # file_system_tool.tool_definition ].inject({}) do |h, tool| h.merge(tool) - end.deep_merge({}) + end.deep_merge({ + evaluate_ruby: { + before_execute: ->(params) do + puts params['code'].colorize(:green) + end + } + }) end private def evaluate_ruby_tool @evaluate_ruby_tool ||= AiAgent::Tools::Ruby::EvaluateRubyTool.new(self) end - - def file_system_tool - @file_system_tool ||= AiAgent::Tools::Os::FileSystem.new(self) - end - - def execute_command_tool - @execute_command_tool ||= AiAgent::Tools::Os::ExecuteCommand.new(self) - end - def show_normal_response(content) puts content end @@ -56,6 +48,6 @@ def show_tool_call_explanation(tool_call) end EvaluateRubyCodeAgent.new([]).tap do |agent| - agent.initial_message = 'What ruby version I have?' + agent.initial_message = 'What ruby version I have? Use "evaluate" tool to find out.' agent.run_in_loop end