Skip to content

Commit a97c7f6

Browse files
authored
Expose RubyLLM tool classes via MetaToolService (#12)
* Expose RubyLLM tool classes via MetaToolService * Fix hook preservation in ruby_llm_tools
1 parent 967b35f commit a97c7f6

File tree

3 files changed

+61
-0
lines changed

3 files changed

+61
-0
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ Tools::MetaToolService.new.register_tool(
9999

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

102+
## Using Tools in Host Application
103+
104+
You can easily fetch the generated RubyLLM tool classes for use in your host application (e.g., when calling an LLM API):
105+
106+
```ruby
107+
# Fetch specific tool classes by name
108+
tool_classes = Tools::MetaToolService.ruby_llm_tools(['book_meeting', 'calculator'])
109+
110+
# Use them with RubyLLM
111+
response = RubyLLM.chat(
112+
messages: messages,
113+
tools: tool_classes,
114+
model: 'gpt-4o'
115+
)
116+
```
117+
118+
102119
## Development
103120

104121
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rails test` to run the tests.

app/services/tools/meta_tool_service.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ def register_tool(class_name, before_call: nil, after_call: nil)
6565
{ error: "Could not find #{class_name}: #{e.message}" }
6666
end
6767

68+
sig { params(tool_names: T::Array[String]).returns(T::Array[T.class_of(Object)]) }
69+
def self.ruby_llm_tools(tool_names)
70+
ToolMeta.registry.filter_map do |service_class|
71+
schema = ToolSchema::Builder.build(service_class)
72+
next unless tool_names.include?(schema[:name])
73+
74+
tool_class_name = ToolSchema::RubyLlmFactory.tool_class_name(service_class)
75+
Tools.const_get(tool_class_name) if Tools.const_defined?(tool_class_name)
76+
end
77+
end
78+
6879
private
6980

7081
sig { params(query: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }

test/meta_tool_service_test.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,39 @@ def test_register_tool_with_hooks_ruby_llm
102102
assert after_called, 'After hook should be called for RubyLLM tool'
103103
end
104104

105+
def test_ruby_llm_tools_returns_correct_tool_classes
106+
# Register a tool first
107+
meta_service.register_tool('Tools::SampleService')
108+
109+
tools = Tools::MetaToolService.ruby_llm_tools(['sample'])
110+
assert_equal 1, tools.size
111+
assert_equal Tools::Sample, tools.first
112+
assert_includes tools.first.ancestors, RubyLLM::Tool
113+
114+
# Test with non-existent tool
115+
tools = Tools::MetaToolService.ruby_llm_tools(['non_existent'])
116+
assert_empty tools
117+
end
118+
119+
def test_ruby_llm_tools_preserves_hooks
120+
ToolMeta.clear_registry
121+
before_called = false
122+
123+
meta_service.register_tool(
124+
'Tools::SampleService',
125+
before_call: ->(_args) { before_called = true }
126+
)
127+
128+
# Fetch tool using the public API
129+
tools = Tools::MetaToolService.ruby_llm_tools(['sample'])
130+
assert_equal 1, tools.size
131+
132+
# Execute to check if hook persists
133+
tools.first.new.execute(name: 'Hook Check')
134+
135+
assert before_called, 'Before hook should be preserved when fetching via ruby_llm_tools'
136+
end
137+
105138
private
106139

107140
def meta_service

0 commit comments

Comments
 (0)