Toolsets
DSPy::Tools::Toolset groups related Ruby methods and exports selected methods as individual tools. Use this page for the supported DSL, export behavior, built-in names, and runtime boundaries. For an application-focused extension recipe, see Custom Toolsets.
When to Use Toolsets
Use a toolset when several operations belong to one capability and should share naming or implementation logic. A standalone DSPy::Tools::Base subclass is simpler for one operation.
Export only operations that an agent may call. A tool declaration is an interface, not permission to perform the operation.
Define and Export a Toolset
Direct-require the Toolset feature when a file does not otherwise load dspy:
require "dspy/tools/toolset"
class TextStatsToolset < DSPy::Tools::Toolset
extend T::Sig
toolset_name "stats"
tool :word_count, description: "Count whitespace-separated words"
tool :line_count, description: "Count lines"
sig { params(text: String).returns(Integer) }
def word_count(text:)
text.split(/\s+/).reject(&:empty?).length
end
sig { params(text: String).returns(Integer) }
def line_count(text:)
text.lines.length
end
end
toolset = TextStatsToolset.new
puts toolset.word_count(text: "one two")
tools = TextStatsToolset.to_tools
puts tools.map(&:name)
to_tools is a class method. It calls new with no arguments once, then gives every exported ToolProxy that same instance. Current Toolsets intended for to_tools therefore need a zero-argument constructor. Do not create a configured instance and expect instance.class.to_tools to preserve that instance or its constructor arguments.
Pass the returned proxies to a DSPy::ReAct agent with an explicit loop bound:
agent = DSPy::ReAct.new(
AnalyzeText,
tools: TextStatsToolset.to_tools,
max_iterations: 5
)
Text Processing Toolset Example
The core gem ships DSPy::Tools::TextProcessingToolset, but require "dspy" does not load that optional feature constant. This complete example uses only its portable Ruby word-count operation; it needs no API key, network access, or external search binary.
require "dspy/tools/text_processing_toolset"
toolset = DSPy::Tools::TextProcessingToolset.new
tools = toolset.class.to_tools
puts tools.map(&:name).join(",")
word_count = tools.to_h { |tool| [tool.name, tool] }.fetch("text_wc")
puts word_count.call(text: "one two\nthree")
The output begins with the eight exported names listed under Text Processing Operations, followed by:
Lines: 2, Words: 3, Characters: 13
How Toolset Export Works
- The
toolDSL records an exposed Ruby method, generated name, and description. .to_toolsconstructs one zero-argument Toolset instance.- Each
ToolProxydelegates to one exposed method on that shared instance. - Sorbet method signatures provide the parameter schema and runtime coercion used by
dynamic_call. DSPy::ReActpresents the proxies to the model and controls the model-directed loop.
DSL Methods
toolset_name(name)
Sets the prefix for names generated by tool:
class DatabaseToolset < DSPy::Tools::Toolset
toolset_name "db"
tool :query # exported as "db_query"
def query
"ok"
end
end
Without an explicit name, DSPy.rb removes the Toolset suffix from the class name and lowercases the remainder.
tool(method_name, options)
Exposes one instance method. tool_name: overrides the generated <toolset_name>_<method_name> name.
tool :search,
tool_name: "catalog_lookup",
description: "Look up an item in the product catalog"
Descriptions should state the operation and its relevant scope. They do not enforce that scope.
Type Safety
Sorbet signatures drive JSON Schema generation and conversion for proxy calls. Whether a keyword is required comes from the Ruby method signature: T.nilable permits nil, while a Ruby default permits omission.
class Priority < T::Enum
enums do
Low = new("low")
High = new("high")
end
end
class TaskToolset < DSPy::Tools::Toolset
extend T::Sig
toolset_name "task"
tool :assign, description: "Assign a task priority"
sig do
params(
task_id: String,
priority: Priority,
note: T.nilable(String)
).returns(String)
end
def assign(task_id:, priority:, note: nil)
"#{task_id}: #{priority.serialize}#{": #{note}" if note}"
end
end
Basic Types
Toolset schemas support String, Integer, Float, Numeric, and T::Boolean.
Enums
T::Enum values are represented as their serialized strings and converted back to enum instances at the tool boundary.
Structs
T::Struct parameters become object schemas and are converted to struct instances before the method runs.
Collections
Typed arrays and hashes preserve their declared element or value types during schema generation and conversion.
Nullable and Optional Parameters
T.nilable(String) allows a string or nil. A method default such as note: nil makes the keyword omittable. These are separate properties.
Union Types
T.any(...) becomes a schema union. Prefer unions whose variants can be distinguished reliably from their JSON representation.
Supported Sorbet Types Reference
| Sorbet type | Schema shape | Runtime conversion |
|---|---|---|
String |
string | yes |
Integer |
integer | yes |
Float, Numeric |
number | yes |
T::Boolean |
boolean | yes |
T::Enum |
string enum | yes |
T::Struct |
object | yes |
T::Array[Type] |
array | element-wise |
T::Hash[K, V] |
object | key/value conversion |
T.nilable(Type) |
type or null | yes |
T.any(T1, T2) |
union | yes, when distinguishable |
Schema Generation Examples
Inspect a proxy’s schema rather than maintaining a second hand-written schema:
assign = TaskToolset.to_tools.to_h { |tool| [tool.name, tool] }.fetch("task_assign")
puts assign.schema
The schema describes the callable name, description, and parameters. It does not describe authorization, sandboxing, input trust, timeouts, or transaction behavior.
Text Processing Operations
DSPy::Tools::TextProcessingToolset exports exactly these names:
| Exported tool | Ruby method | Notes |
|---|---|---|
text_grep |
grep |
invokes the system grep command through a temporary file |
text_wc |
word_count |
pure Ruby line, word, and character counts |
text_rg |
ripgrep |
invokes the external rg command through a temporary file |
text_extract_lines |
extract_lines |
selects a line or line range |
text_filter_lines |
filter_lines |
applies a Ruby regular expression |
text_unique_lines |
unique_lines |
removes duplicate lines |
text_sort_lines |
sort_lines |
sorts lexically or numerically |
text_summarize_text |
summarize_text |
returns deterministic text statistics |
The grep and ripgrep helpers interpolate pattern values into shell command strings and use temporary files. Quotes and option-like patterns can change command interpretation. The implementation suppresses command stderr, has no elapsed-time or output cap, and unlinks its temporary file only after the normal command path. These are convenience utilities for trusted inputs in a controlled runtime, not a shell-isolation boundary. Verify binary availability, constrain pattern and input size, apply a process timeout and cleanup strategy where needed, and isolate untrusted workloads outside the application process.
text_filter_lines compiles its pattern as a Ruby regular expression. Constrain pattern complexity and input size when patterns are not application-owned; type conversion does not prevent expensive regular-expression evaluation.
The core gem also ships DSPy::Tools::GitHubCLIToolset at dspy/tools/github_cli_toolset. Its operations depend on the gh executable and the caller’s GitHub authentication and repository permissions.
LLM Usage
An agent selects the exported name, not the Ruby method name. This example uses the pure-Ruby text_wc operation; it does not expose the shell-backed text_grep or text_rg proxies to model-supplied arguments:
{
"action": "text_wc",
"action_input": {
"text": "one two\nthree"
}
}
Treat model-produced text as untrusted input and cap its byte size before processing it. A pure-Ruby operation avoids this Toolset’s shell boundary, but its schema still does not impose resource limits or an elapsed-time deadline. Pass only the reviewed proxies an agent needs, and keep max_iterations and operation-level limits explicit.
Testing
Test both the Ruby operation and the exported proxy contract:
RSpec.describe TextStatsToolset do
it "counts words and exports stable names" do
expect(described_class.new.word_count(text: "one two")).to eq(2)
tools = described_class.to_tools
expect(tools.map(&:name)).to eq(%w[stats_word_count stats_line_count])
expect(tools.first.dynamic_call("text" => "one two")).to eq(2)
end
end
For packaged built-ins, test in a clean subprocess against files listed by the gemspec. A normal spec process may already have loaded dspy, masking a missing direct-load dependency.
Limitations
.to_toolscallsnewwithout arguments; constructor-required Toolsets cannot use this export path.- Tool parameters should be keyword arguments so schema requiredness and coercion are represented correctly.
- Each exposed method becomes a separate proxy; method chaining is not exposed.
- A direct proxy
calldelegates failures from the method.dynamic_callconverts parsing, coercion, and method exceptions to an"Error: ..."string. Define and test the failure contract your application consumes. - Tool schemas do not cap input size, execution time, retries, or agent iterations.
Runtime Boundaries
A Toolset schema describes call shape and performs supported type conversion. It does not grant authorization, sanitize values for a shell or query, provide a sandbox, impose a timeout, isolate side effects, or make an operation idempotent. The application must enforce permissions and input policy, bound resource use and the agent loop, handle partial failures, and define retry and transaction behavior. max_iterations limits model-directed steps; it does not cancel a slow subprocess, network request, or tool method.
Keep those controls in deterministic Ruby around or inside the tool. Use OS or container isolation when an operation can execute untrusted code or commands; a Sorbet schema is not that isolation.
Design Decisions
The tool DSL exposes only declared methods. This keeps the model-facing interface explicit and gives each operation a stable name and description. Sorbet signatures keep the schema next to the Ruby implementation, while application-owned policy remains separate from schema generation.
Apply Toolsets in an Agent
Apply the operational recipe in Custom Toolsets, then pass only the minimum reviewed proxies to a module or bounded ReAct agent.