Commit 94a0329
Changed files (3)
bin/test
@@ -5,4 +5,4 @@ set -e
cd "$(dirname "$0")/.."
-bundle exec rake spec
+bundle exec rspec "$@"
lib/elelem/toolbox.rb
@@ -36,16 +36,12 @@ module Elelem
{ bytes_written: full_path.write(args["content"]) }
end
- EVAL_TOOL = Tool.build("eval", "Evaluates the Ruby expression(s) in the elelem coding agent allowing an LLM to create its own tools", { ruby: { type: "string" }, }, ["ruby"]) do |args|
- { result: eval(args["ruby"]) }
- end
-
attr_reader :tools
def initialize
@tools_by_name = {}
@tools = { read: [], write: [], execute: [] }
- add_tool(EVAL_TOOL, :execute)
+ add_tool(eval_tool(binding), :execute)
add_tool(EXEC_TOOL, :execute)
add_tool(GREP_TOOL, :read)
add_tool(LIST_TOOL, :read)
@@ -59,15 +55,30 @@ module Elelem
@tools_by_name[tool.name] = tool
end
+ def register_tool(name, description, properties = {}, required = [], mode: :execute, &block)
+ add_tool(Tool.build(name, description, properties, required, &block), mode)
+ end
+
def tools_for(modes)
- modes.map { |mode| tools[mode].map(&:to_h) }.flatten
+ Array(modes).map { |mode| tools[mode].map(&:to_h) }.flatten
end
def run_tool(name, args)
@tools_by_name[name]&.call(args) || { error: "Unknown tool", name: name, args: args }
rescue => error
- puts error.inspect
- { error: error.message, name: name, args: args }
+ { error: error.message, name: name, args: args, backtrace: error.backtrace.first(5) }
+ end
+
+ def tool_schema(name)
+ @tools_by_name[name]&.to_h
+ end
+
+ private
+
+ def eval_tool(target_binding)
+ Tool.build("eval", "Evaluates Ruby code with full access to register new tools via the `register_tool(name, desc, properties, required, mode: :execute) { |args| ... }` method.", { ruby: { type: "string" } }, ["ruby"]) do |args|
+ { result: target_binding.eval(args["ruby"]) }
+ end
end
end
end
spec/elelem/toolbox_spec.rb
@@ -48,4 +48,59 @@ RSpec.describe Elelem::Toolbox do
expect(tool_names).not_to include("execute")
end
end
+
+ describe "meta-programming with eval tool" do
+ it "allows LLM to register new tools dynamically" do
+ subject.run_tool("eval", {
+ "ruby" => <<~RUBY
+ register_tool("hello", "Says hello to a name", { name: { type: "string" } }, ["name"]) do |args|
+ { greeting: "Hello, " + args['name']+ "!" }
+ end
+ RUBY
+ })
+
+ expect(subject.tools_for(:execute)).to include(hash_including({
+ type: "function",
+ function: {
+ name: "hello",
+ description: "Says hello to a name",
+ parameters: {
+ type: "object",
+ properties: { name: { type: "string" } },
+ required: ["name"]
+ }
+ }
+ }))
+ end
+
+ it "allows LLM to call dynamically created tools" do
+ subject.run_tool("eval", {
+ "ruby" => <<~RUBY
+ register_tool("add", "Adds two numbers", { a: { type: "number" }, b: { type: "number" } }, ["a", "b"]) do |args|
+ { sum: args["a"] + args["b"] }
+ end
+ RUBY
+ })
+
+ result = subject.run_tool("add", { "a" => 5, "b" => 3 })
+ expect(result[:sum]).to eq(8)
+ end
+
+ it "allows LLM to inspect tool schemas" do
+ result = subject.run_tool("eval", { "ruby" => "tool_schema('read')" })
+ expect(result[:result]).to be_a(Hash)
+ expect(result[:result].dig(:function, :name)).to eq("read")
+ end
+
+ it "executes arbitrary Ruby code" do
+ result = subject.run_tool("eval", { "ruby" => "2 + 2" })
+ expect(result[:result]).to eq(4)
+ end
+
+ it "handles errors gracefully" do
+ result = subject.run_tool("eval", { "ruby" => "undefined_variable" })
+ expect(result[:error]).to include("undefined")
+ expect(result[:backtrace]).to be_an(Array)
+ end
+ end
end