Facade Pattern in SharedTools¶
Complex tools in SharedTools use the Facade pattern to provide a simple, unified interface while delegating to specialized sub-tools. This guide explains the architecture, benefits, and implementation patterns.
What is the Facade Pattern?¶
The Facade pattern provides a simplified interface to a complex subsystem. In SharedTools:
- Facade Tool: A high-level tool (e.g., BrowserTool) that LLMs interact with
- Sub-tools: Specialized tools (e.g., VisitTool, ClickTool) that do the actual work
- Driver: The underlying implementation (e.g., WatirDriver) that interfaces with external systems
┌─────────────────────────────────────────────────┐
│ BrowserTool (Facade) │
│ - Unified execute(action:, **params) interface │
│ - Parameter validation │
│ - Action routing │
└────────┬────────────────────────────────────────┘
│
├─────────────┬──────────────┬─────────────┐
│ │ │ │
┌────▼─────┐ ┌───▼──────┐ ┌───▼──────┐ ┌──▼──────┐
│VisitTool │ │ClickTool │ │InspectToo│ │Screenshot│
│ │ │ │ │l │ │Tool │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
└─────────────┴──────────────┴─────────────┘
│
┌──────▼──────┐
│WatirDriver │
│(Browser API)│
└─────────────┘
Why Use the Facade Pattern?¶
Benefits for LLMs¶
- Single Interface: LLMs interact with one tool instead of many
- Consistent API: All actions use the same
executemethod - Action-based: Natural verb-based interface (visit, click, inspect)
- Parameter Validation: Centralized validation before delegation
Benefits for Developers¶
- Separation of Concerns: Each sub-tool handles one responsibility
- Testability: Test sub-tools independently
- Maintainability: Changes to one action don't affect others
- Extensibility: Add new actions by adding sub-tools
Benefits for Users¶
- Simplicity: One tool to learn instead of many
- Discoverability: All actions documented in one place
- Consistency: Similar patterns across all tools
Facade Architecture¶
The Facade Tool¶
The facade tool provides the public interface:
module SharedTools
module Tools
class BrowserTool < ::RubyLLM::Tool
def self.name = 'browser_tool'
# Define action constants
module Action
VISIT = "visit"
CLICK = "click"
PAGE_INSPECT = "page_inspect"
SCREENSHOT = "screenshot"
end
# Comprehensive description
description <<~TEXT
Automates a web browser to perform various actions...
1. `visit` - Navigate to a website
2. `click` - Click an element
3. `page_inspect` - Get page HTML
4. `screenshot` - Take a screenshot
TEXT
# Define all possible parameters
param :action, desc: "The action to perform"
param :url, desc: "URL (required for visit)"
param :selector, desc: "CSS selector (required for click)"
param :full_html, desc: "Get full HTML vs summary"
def initialize(driver: nil, logger: nil)
@driver = driver || default_driver
@logger = logger || RubyLLM.logger
end
# Central routing method
def execute(action:, **params)
case action.to_s.downcase
when Action::VISIT
require_param!(:url, params[:url])
visit_tool.execute(url: params[:url])
when Action::CLICK
require_param!(:selector, params[:selector])
click_tool.execute(selector: params[:selector])
when Action::PAGE_INSPECT
page_inspect_tool.execute(full_html: params[:full_html] || false)
when Action::SCREENSHOT
screenshot_tool.execute
else
raise ArgumentError, "Unknown action: #{action}"
end
end
private
# Lazy-load sub-tools
def visit_tool
@visit_tool ||= Browser::VisitTool.new(
driver: @driver,
logger: @logger
)
end
def click_tool
@click_tool ||= Browser::ClickTool.new(
driver: @driver,
logger: @logger
)
end
def page_inspect_tool
@page_inspect_tool ||= Browser::PageInspectTool.new(
driver: @driver,
logger: @logger
)
end
def screenshot_tool
@screenshot_tool ||= Browser::PageScreenshotTool.new(
driver: @driver,
logger: @logger
)
end
def require_param!(name, value)
raise ArgumentError, "#{name} required for this action" if value.nil?
end
def default_driver
if defined?(Watir)
Browser::WatirDriver.new(logger: @logger)
else
raise LoadError, "BrowserTool requires watir gem or a driver"
end
end
end
end
end
Sub-tools¶
Sub-tools handle specific operations:
module SharedTools
module Tools
module Browser
# Sub-tool for visiting URLs
class VisitTool
def initialize(driver:, logger:)
@driver = driver
@logger = logger
end
def execute(url:)
@logger.info("Visiting #{url}")
@driver.goto(url: url)
"Navigated to #{url}"
end
end
# Sub-tool for clicking elements
class ClickTool
def initialize(driver:, logger:)
@driver = driver
@logger = logger
end
def execute(selector:)
@logger.info("Clicking #{selector}")
@driver.click(selector: selector)
"Clicked element: #{selector}"
end
end
# Sub-tool for page inspection
class PageInspectTool
def initialize(driver:, logger:)
@driver = driver
@logger = logger
end
def execute(full_html: false, summarize: false)
@logger.info("Inspecting page (full: #{full_html}, summary: #{summarize})")
if full_html
@driver.html
elsif summarize
HTMLSummarizer.new(@driver.html).summarize
else
@driver.html
end
end
end
end
end
end
Sub-tool Guidelines¶
- Single Responsibility: Each sub-tool does one thing
- No RubyLLM::Tool: Sub-tools are plain Ruby classes
- Shared Dependencies: Driver and logger passed from facade
- Simple Interface: Usually one
executemethod - Return Values: Return meaningful results
Real-World Examples¶
DiskTool Facade¶
class DiskTool < ::RubyLLM::Tool
module Action
FILE_READ = "file_read"
FILE_WRITE = "file_write"
FILE_CREATE = "file_create"
FILE_DELETE = "file_delete"
FILE_MOVE = "file_move"
FILE_REPLACE = "file_replace"
DIRECTORY_CREATE = "directory_create"
DIRECTORY_DELETE = "directory_delete"
DIRECTORY_MOVE = "directory_move"
DIRECTORY_LIST = "directory_list"
end
def execute(action:, path:, **params)
case action
when Action::FILE_READ
@driver.file_read(path: path)
when Action::FILE_WRITE
@driver.file_write(path: path, text: params[:text])
when Action::FILE_CREATE
@driver.file_create(path: path)
when Action::FILE_DELETE
@driver.file_delete(path: path)
when Action::FILE_MOVE
@driver.file_move(path: path, destination: params[:destination])
when Action::FILE_REPLACE
@driver.file_replace(
path: path,
old_text: params[:old_text],
new_text: params[:new_text]
)
when Action::DIRECTORY_CREATE
@driver.directory_create(path: path)
when Action::DIRECTORY_DELETE
@driver.directory_delete(path: path)
when Action::DIRECTORY_MOVE
@driver.directory_move(path: path, destination: params[:destination])
when Action::DIRECTORY_LIST
@driver.directory_list(path: path)
end
end
end
Note: DiskTool delegates directly to driver methods rather than sub-tools. This is valid when operations are simple enough not to require separate classes.
DatabaseTool Facade¶
class DatabaseTool < ::RubyLLM::Tool
def execute(statements:)
results = []
statements.each do |statement|
result = perform(statement: statement)
results << result.merge(statement: statement)
break unless result[:status] == :ok
end
results
end
private
def perform(statement:)
@logger&.info("Executing: #{statement}")
@driver.perform(statement: statement)
end
end
DatabaseTool uses a simpler pattern with statement execution loop.
Implementation Patterns¶
Pattern 1: Sub-tool Delegation¶
Best for complex operations requiring multiple steps:
def execute(action:, **params)
case action
when "complex_action"
complex_action_tool.execute(**params)
end
end
private
def complex_action_tool
@complex_action_tool ||= ComplexActionTool.new(
driver: @driver,
logger: @logger,
config: @config
)
end
Pattern 2: Direct Driver Delegation¶
Best for simple operations:
def execute(action:, path:)
case action
when "simple_action"
@driver.simple_action(path: path)
end
end
Pattern 3: Mixed Approach¶
Use sub-tools for complex operations, direct calls for simple ones:
def execute(action:, **params)
case action
when "complex_action"
complex_tool.execute(**params) # Delegate to sub-tool
when "simple_action"
@driver.simple_action(**params) # Direct driver call
end
end
Parameter Handling¶
Extracting Required Parameters¶
def execute(action:, **params)
case action
when Action::VISIT
url = params.fetch(:url) { raise ArgumentError, "url required" }
visit_tool.execute(url: url)
when Action::CLICK
selector = params.fetch(:selector) { raise "selector required" }
click_tool.execute(selector: selector)
end
end
Optional Parameters with Defaults¶
def execute(action:, **params)
case action
when Action::PAGE_INSPECT
full_html = params.fetch(:full_html, false)
context_size = params.fetch(:context_size, 2)
page_inspect_tool.execute(
full_html: full_html,
context_size: context_size
)
end
end
Parameter Validation Helper¶
private
def require_param!(name, value)
raise ArgumentError, "#{name} is required for this action" if value.nil?
end
def require_params!(params, *names)
names.each do |name|
require_param!(name, params[name])
end
end
# Usage
def execute(action:, **params)
case action
when Action::TEXT_FIELD_SET
require_params!(params, :selector, :value)
text_field_tool.execute(
selector: params[:selector],
value: params[:value]
)
end
end
Testing Facade Tools¶
Test the Facade¶
RSpec.describe SharedTools::Tools::BrowserTool do
let(:driver) { instance_double(Browser::WatirDriver) }
let(:tool) { described_class.new(driver: driver) }
describe "action routing" do
it "routes visit action to driver" do
expect(driver).to receive(:goto).with(url: "https://test.com")
tool.execute(action: "visit", url: "https://test.com")
end
it "routes click action to driver" do
expect(driver).to receive(:click).with(selector: ".button")
tool.execute(action: "click", selector: ".button")
end
it "raises error for invalid action" do
expect {
tool.execute(action: "invalid")
}.to raise_error(ArgumentError, /Unknown action/)
end
end
describe "parameter validation" do
it "requires url for visit action" do
expect {
tool.execute(action: "visit")
}.to raise_error(ArgumentError, /url.*required/)
end
it "requires selector for click action" do
expect {
tool.execute(action: "click")
}.to raise_error(ArgumentError, /selector.*required/)
end
end
end
Test Sub-tools Independently¶
RSpec.describe SharedTools::Tools::Browser::VisitTool do
let(:driver) { instance_double(Browser::WatirDriver) }
let(:logger) { instance_double(Logger) }
let(:tool) { described_class.new(driver: driver, logger: logger) }
it "navigates to URL" do
expect(driver).to receive(:goto).with(url: "https://test.com")
expect(logger).to receive(:info).with(/Visiting/)
result = tool.execute(url: "https://test.com")
expect(result).to include("Navigated")
end
end
Advanced Patterns¶
Action Registry¶
For tools with many actions:
class BrowserTool < ::RubyLLM::Tool
ACTIONS = {
"visit" => :handle_visit,
"click" => :handle_click,
"inspect" => :handle_inspect
}.freeze
def execute(action:, **params)
handler = ACTIONS[action.to_s.downcase]
raise ArgumentError, "Unknown action: #{action}" unless handler
send(handler, **params)
end
private
def handle_visit(url:, **_)
visit_tool.execute(url: url)
end
def handle_click(selector:, **_)
click_tool.execute(selector: selector)
end
def handle_inspect(full_html: false, **_)
inspect_tool.execute(full_html: full_html)
end
end
Middleware Pattern¶
Add cross-cutting concerns:
class BrowserTool < ::RubyLLM::Tool
def execute(action:, **params)
with_authorization(action, params) do
with_timing(action) do
with_error_handling(action) do
route_action(action, **params)
end
end
end
end
private
def with_authorization(action, params)
return yield if SharedTools.auto_execute
if SharedTools.execute?(tool: self.class.name, stuff: describe_action(action, params))
yield
end
end
def with_timing(action)
start = Time.now
result = yield
@logger.info("#{action} took #{Time.now - start}s")
result
end
def with_error_handling(action)
yield
rescue => e
@logger.error("#{action} failed: #{e.message}")
raise
end
def route_action(action, **params)
case action
when "visit" then visit_tool.execute(**params)
when "click" then click_tool.execute(**params)
end
end
end
When to Use Facades¶
Use Facades When:¶
- Tool has multiple related operations (visit, click, inspect)
- Operations share common resources (driver, logger)
- LLM needs simple, action-based interface
- Sub-operations are complex enough to warrant separate classes
Don't Use Facades When:¶
- Tool has only one operation
- Operations are too simple to warrant sub-tools
- No shared state or resources
- Direct driver delegation is clearer
Next Steps¶
- Learn about Driver Interfaces
- Review the RubyLLM::Tool Base Class
- Explore BrowserTool implementation
- See Example Workflows using facades