SharedTools Architecture¶
This document provides a comprehensive overview of SharedTools' architecture, design patterns, and implementation details.
High-Level Architecture¶
┌─────────────────────────────────────────────────────────┐
│ LLM Application │
└───────────────────────┬─────────────────────────────────┘
│
│ uses
▼
┌─────────────────────────────────────────────────────────┐
│ SharedTools Module │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Authorization System │ │
│ │ - Human-in-the-loop (@auto_execute) │ │
│ │ - execute? method for confirmations │ │
│ └────────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────┘
│
│ provides
▼
┌─────────────────────────────────────────────────────────┐
│ Tool Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐│
│ │ Browser │ │ Disk │ │ Database │ │Computer ││
│ │ Tool │ │ Tool │ │ Tool │ │ Tool ││
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬────┘│
│ │ │ │ │ │
│ │ delegates │ delegates │ delegates │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐│
│ │Sub-tools│ │ Driver │ │ Driver │ │ Driver ││
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘│
└───────┼─────────────┼─────────────┼────────────┼──────┘
│ │ │ │
│ uses │ uses │ uses │ uses
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────┐
│ External Systems │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐│
│ │ Watir │ │FileSystem│ │ Database │ │ OS ││
│ │(Browser) │ │ │ │ (SQLite) │ │ API ││
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘│
└─────────────────────────────────────────────────────────┘
Module Organization¶
Core Module (lib/shared_tools.rb)¶
The main SharedTools module provides:
-
Authorization System:
module SharedTools @auto_execute ||= false def self.auto_execute(wildwest=true) @auto_execute = wildwest end def self.execute?(tool: 'unknown', stuff: '') return true if @auto_execute == true # Prompt user for confirmation puts "The AI (tool: #{tool}) wants to do the following ..." puts stuff print "Is it okay to proceed? (y/N" STDIN.getch == "y" end end -
Zeitwerk Autoloading:
Tool Hierarchy¶
All tools follow this inheritance structure:
::RubyLLM::Tool (from ruby_llm gem)
│
├── SharedTools::Tools::BrowserTool
├── SharedTools::Tools::DiskTool
├── SharedTools::Tools::DatabaseTool
├── SharedTools::Tools::ComputerTool
├── SharedTools::Tools::EvalTool
└── SharedTools::Tools::DocTool
Design Patterns¶
1. Facade Pattern¶
Used by: BrowserTool, ComputerTool
Purpose: Provide a simplified interface to complex subsystems
Structure:
BrowserTool (Facade)
├── Browser::VisitTool
├── Browser::ClickTool
├── Browser::InspectTool
├── Browser::PageInspectTool
├── Browser::SelectorInspectTool
├── Browser::TextFieldAreaSetTool
└── Browser::PageScreenshotTool
Benefits: - Single unified interface for LLMs - Action-based routing - Easier testing of individual operations - Clear separation of concerns
2. Strategy Pattern (Driver Interface)¶
Used by: All tools that interact with external systems
Purpose: Make the implementation swappable
Structure:
Tool
└── uses Driver (via strategy pattern)
├── WatirDriver (production)
├── SeleniumDriver (alternative)
└── MockDriver (testing)
Benefits: - Testability with mock drivers - Platform independence - Easy to add new implementations
3. Template Method Pattern¶
Used by: BaseDriver classes
Purpose: Define algorithm structure, let subclasses implement steps
Example:
class BaseDriver
def perform_action
validate_input # concrete
execute_action # abstract - subclass implements
log_result # concrete
end
def execute_action
raise NotImplementedError
end
end
4. Command Pattern¶
Used by: Action-based tools
Purpose: Encapsulate actions as objects
Structure:
module Action
VISIT = "visit"
CLICK = "click"
end
def execute(action:, **params)
case action
when Action::VISIT then execute_visit(**params)
when Action::CLICK then execute_click(**params)
end
end
Component Details¶
Authorization System¶
Design Goals¶
- Safety First: Default to requiring confirmation
- Flexibility: Allow automation when needed
- Clarity: Show exactly what will be executed
- Non-blocking: Works with both interactive and automated contexts
Implementation¶
module SharedTools
class << self
def auto_execute(wildwest=true)
@auto_execute = wildwest
end
def execute?(tool: 'unknown', stuff: '')
return true if @auto_execute == true
# Present action to user
puts "\n\nThe AI (tool: #{tool}) wants to do the following ..."
puts "=" * 42
puts(stuff.empty? ? "unknown strange and mysterious things" : stuff)
puts "=" * 42
sleep 0.2 if defined?(AIA) # Allow CLI spinner to recycle
print "\nIs it okay to proceed? (y/N"
STDIN.getch == "y"
end
end
end
Usage in Tools¶
def execute(action:, path:)
return unless SharedTools.execute?(
tool: self.class.name,
stuff: "#{action} on #{path}"
)
perform_action(action, path)
end
Zeitwerk Autoloading¶
Configuration¶
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
# Ignore aggregate loader files
loader.ignore("#{__dir__}/shared_tools/ruby_llm.rb")
loader.setup
Benefits¶
- Automatic code loading on first use
- Convention over configuration (file names match class names)
- Faster startup times
- Reloading support in development
File Naming Convention¶
lib/shared_tools/tools/browser_tool.rb
└─▶ SharedTools::Tools::BrowserTool
lib/shared_tools/tools/browser/visit_tool.rb
└─▶ SharedTools::Tools::Browser::VisitTool
Tool Base Class Integration¶
All tools extend RubyLLM::Tool:
class MyTool < ::RubyLLM::Tool
# Class method for tool name
def self.name = 'my_tool'
# DSL for description
description "What this tool does"
# DSL for parameters
param :action, desc: "Action to perform"
param :path, desc: "Path parameter"
# Initialize with optional dependencies
def initialize(driver: nil, logger: nil)
@driver = driver
@logger = logger || RubyLLM.logger
end
# Execute method with keyword args
def execute(action:, path:)
# Implementation
end
end
Data Flow¶
Simple Tool Execution¶
1. LLM decides to use tool
└─▶ tool_name: "disk_tool"
parameters: {action: "file_read", path: "./file.txt"}
2. RubyLLM routes to SharedTools::Tools::DiskTool
3. Authorization check
└─▶ SharedTools.execute?
├─▶ auto_execute=true? → proceed
└─▶ auto_execute=false? → ask user
4. Tool execution
└─▶ execute(action: "file_read", path: "./file.txt")
5. Driver delegation
└─▶ @driver.file_read(path: "./file.txt")
6. Return result to LLM
└─▶ "File contents: ..."
Complex Tool Execution (Facade)¶
1. LLM: BrowserTool with action="visit"
2. BrowserTool.execute(action: "visit", url: "...")
├─▶ Authorization check
├─▶ Parameter validation
└─▶ Route to sub-tool
3. visit_tool.execute(url: "...")
└─▶ Sub-tool logic
4. Driver call
└─▶ @driver.goto(url: "...")
5. Watir/browser operation
└─▶ Actual browser navigation
6. Result flows back
└─▶ Sub-tool → Facade → RubyLLM → LLM
Error Handling Architecture¶
Error Propagation¶
External System Error
└─▶ Driver catches and wraps
└─▶ Tool handles or propagates
└─▶ RubyLLM catches
└─▶ LLM receives error message
Error Types¶
-
ArgumentError: Invalid parameters
-
LoadError: Missing dependencies
-
NotImplementedError: Unimplemented driver methods
-
RuntimeError: Operational errors
Error Responses (DatabaseTool pattern)¶
def perform(statement:)
result = execute(statement)
{ status: :ok, result: result }
rescue DatabaseError => e
{ status: :error, result: e.message }
end
Testing Architecture¶
Test Organization¶
spec/
├── shared_tools/
│ └── tools/
│ ├── browser_tool_spec.rb # Tool tests
│ ├── browser/
│ │ ├── visit_tool_spec.rb # Sub-tool tests
│ │ └── watir_driver_spec.rb # Driver tests
│ └── disk_tool_spec.rb
└── spec_helper.rb
Test Doubles Strategy¶
-
Mock Drivers: For tool testing
-
Real Drivers: For integration testing
-
Fixture Data: For consistent test data
Performance Considerations¶
Lazy Loading¶
Sub-tools and drivers are loaded only when needed:
Connection Pooling¶
For database tools:
Caching¶
Expensive operations can be cached:
Security Architecture¶
Input Validation¶
All user inputs are validated:
Authorization Layer¶
Safe Defaults¶
@auto_execute = falseby default- Read-only operations don't require confirmation
- Destructive operations always show preview
Extensibility Points¶
Adding New Tools¶
- Create class extending
RubyLLM::Tool - Implement
self.name,description,params - Implement
executemethod - Add tests
- Zeitwerk handles loading
Adding New Drivers¶
- Extend appropriate
BaseDriver - Implement required methods
- Register in tool's
default_driveror pass explicitly
Adding New Actions¶
For facade tools:
- Add action constant
- Add to description
- Add to
executecase statement - Create sub-tool if complex
- Add tests
Future Architecture Goals¶
Planned Improvements¶
- Plugin System: Load tools from external gems
- Middleware: Chain operations (logging, caching, retry)
- Event System: Hook into tool lifecycle
- Configuration: Centralized tool configuration
- Metrics: Built-in performance monitoring
Diagrams¶
Component Interaction¶
┌─────────┐ uses ┌──────────────┐ delegates ┌────────────┐
│ LLM │──────▶│ BrowserTool │──────────▶│ Sub-tool │
└─────────┘ └──────────────┘ └────────────┘
│ │
│ uses │ uses
▼ ▼
┌─────────────┐ ┌───────────┐
│ Authorization│ │ Driver │
└─────────────┘ └───────────┘
│
│ uses
▼
┌────────────┐
│ External │
│ System │
└────────────┘
Next Steps¶
- Review Contributing Guidelines
- Learn about Testing Strategies
- Explore Tool Implementation Examples