Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,23 @@ Tools::MetaToolService.new.register_tool(

These hooks are executed around the tool's entrypoint method for both RubyLLM and FastMCP wrappers.

## Using Tools in Host Application

You can easily fetch the generated RubyLLM tool classes for use in your host application (e.g., when calling an LLM API):

```ruby
# Fetch specific tool classes by name
tool_classes = Tools::MetaToolService.ruby_llm_tools(['book_meeting', 'calculator'])

# Use them with RubyLLM
response = RubyLLM.chat(
messages: messages,
tools: tool_classes,
model: 'gpt-4o'
)
```


## Development

After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rails test` to run the tests.
Expand Down
11 changes: 11 additions & 0 deletions app/services/tools/meta_tool_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ def register_tool(class_name, before_call: nil, after_call: nil)
{ error: "Could not find #{class_name}: #{e.message}" }
end

sig { params(tool_names: T::Array[String]).returns(T::Array[T.class_of(Object)]) }
def self.ruby_llm_tools(tool_names)
ToolMeta.registry.filter_map do |service_class|
schema = ToolSchema::Builder.build(service_class)
next unless tool_names.include?(schema[:name])

tool_class_name = ToolSchema::RubyLlmFactory.tool_class_name(service_class)
Tools.const_get(tool_class_name) if Tools.const_defined?(tool_class_name)
end
end

private

sig { params(query: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
Expand Down
33 changes: 33 additions & 0 deletions test/meta_tool_service_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,39 @@ def test_register_tool_with_hooks_ruby_llm
assert after_called, 'After hook should be called for RubyLLM tool'
end

def test_ruby_llm_tools_returns_correct_tool_classes
# Register a tool first
meta_service.register_tool('Tools::SampleService')

tools = Tools::MetaToolService.ruby_llm_tools(['sample'])
assert_equal 1, tools.size
assert_equal Tools::Sample, tools.first
assert_includes tools.first.ancestors, RubyLLM::Tool

# Test with non-existent tool
tools = Tools::MetaToolService.ruby_llm_tools(['non_existent'])
assert_empty tools
end

def test_ruby_llm_tools_preserves_hooks
ToolMeta.clear_registry
before_called = false

meta_service.register_tool(
'Tools::SampleService',
before_call: ->(_args) { before_called = true }
)

# Fetch tool using the public API
tools = Tools::MetaToolService.ruby_llm_tools(['sample'])
assert_equal 1, tools.size

# Execute to check if hook persists
tools.first.new.execute(name: 'Hook Check')

assert before_called, 'Before hook should be preserved when fetching via ruby_llm_tools'
end

private

def meta_service
Expand Down