Crates.io | tools-rs |
lib.rs | tools-rs |
version | 0.1.2 |
created_at | 2025-02-02 11:13:41.202737+00 |
updated_at | 2025-09-24 12:14:27.898623+00 |
description | Core functionality for the tools-rs tool collection system |
homepage | |
repository | |
max_upload_size | |
id | 1539493 |
size | 64,177 |
It's pronounced tools-r-us!!
Tools-rs is a framework for building, registering, and executing tools with automatic JSON schema generation for Large Language Model (LLM) integration.
#[tool]
to automatically register functions with compile-time discoverytokio
integrationinventory
crate for zero-runtime-cost discoveryuse serde_json::json;
use tools_rs::{collect_tools, FunctionCall, tool};
#[tool]
/// Adds two numbers.
async fn add(pair: (i32, i32)) -> i32 {
pair.0 + pair.1
}
#[tool]
/// Greets a person.
async fn greet(name: String) -> String {
format!("Hello, {name}!")
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let tools = collect_tools();
let sum = tools
.call(FunctionCall::new(
"add".into(),
json!({ "pair": [3, 4] }),
))
.await?.result;
println!("add → {sum}"); // Outputs: "add → 7"
let hi = tools
.call(FunctionCall::new(
"greet".into(),
json!({ "name": "Alice" }),
))
.await?.result;
println!("greet → {hi}"); // Outputs: "greet → \"Hello, Alice!\""
// Export function declarations for LLM APIs
let declarations = tools.json()?;
println!("Function declarations: {}", serde_json::to_string_pretty(&declarations)?);
Ok(())
}
Add the following to your Cargo.toml
:
[dependencies]
tools-rs = "0.1.1"
tokio = { version = "1.45", features = ["macros", "rt-multi-thread"] }
serde_json = "1.0"
The tools-rs system is organized as a Rust workspace with three main crates:
ToolCollection
)ToolSchema
trait)ToolError
, DeserializationError
)FunctionCall
, ToolRegistration
, etc.)#[tool]
attribute macro for automatic registration#[derive(ToolSchema)]
for automatic schema generationFor more details about the codebase organization, see CODE_ORGANIZATION.md.
Tools-rs requires Rust 1.85 or later and supports:
Tools-rs can automatically generate function declarations suitable for LLM APIs:
use tools_rs::{function_declarations, tool};
#[tool]
/// Return the current date in ISO-8601 format.
async fn today() -> String {
chrono::Utc::now().date_naive().to_string()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate function declarations for an LLM
let declarations = function_declarations()?;
// Use in API request
let llm_request = serde_json::json!({
"model": "gpt-4o",
"messages": [/* ... */],
"tools": declarations
});
Ok(())
}
The generated declarations follow proper JSON Schema format:
[
{
"description": "Return the current date in ISO-8601 format.",
"name": "today",
"parameters": {
"properties": {},
"required": [],
"type": "object"
}
}
]
While the #[tool]
macro provides the most convenient way to register tools, you can also register tools manually for more dynamic scenarios:
use tools_rs::ToolCollection;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut tools = ToolCollection::new();
// Register a simple tool manually
tools.register(
"multiply",
"Multiplies two numbers",
|args: serde_json::Value| async move {
let a = args["a"].as_i64().unwrap_or(0);
let b = args["b"].as_i64().unwrap_or(0);
Ok(json!(a * b))
}
)?;
// Call the manually registered tool
let result = tools.call(tools_rs::FunctionCall {
id: None, // Refers to the call ID
name: "multiply".to_string(),
arguments: json!({"a": 6, "b": 7}),
}).await?;
println!("6 * 7 = {}", result);
Ok(())
}
For complex scenarios with custom types:
use tools_rs::{ToolCollection, ToolSchema};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, ToolSchema)]
struct Calculator {
operation: String,
operands: Vec<f64>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut tools = ToolCollection::new();
tools.register(
"calculate",
"Performs arithmetic operations on a list of numbers",
|input: Calculator| async move {
match input.operation.as_str() {
"sum" => Ok(input.operands.iter().sum::<f64>()),
"product" => Ok(input.operands.iter().product::<f64>()),
"mean" => Ok(input.operands.iter().sum::<f64>() / input.operands.len() as f64),
_ => Err(format!("Unknown operation: {}", input.operation)),
}
}
)?;
Ok(())
}
Check out the examples directory for comprehensive sample code:
# Run the basic example - simple tool registration and calling
cargo run --example basic
# Run the function declarations example - LLM integration demo
cargo run --example function_declarations
# Run the schema example - complex type schemas and validation
cargo run --example schema
# Run the newtype demo - custom type wrapping examples
cargo run --example newtype_demo
Each example demonstrates different aspects of the framework:
#[tool]
and basic function callscollect_tools()
- Discover all tools registered via #[tool]
macrofunction_declarations()
- Generate JSON schema declarations for LLMscall_tool(name, args)
- Execute a tool by name with JSON argumentscall_tool_with(name, typed_args)
- Execute a tool with typed argumentscall_tool_by_name(collection, name, args)
- Execute tool on specific collectionlist_tool_names(collection)
- List all available tool namesToolCollection
- Container for registered tools with execution capabilitiesFunctionCall
- Represents a tool invocation with id, name, and argumentsFunctionResponse
- Represents the response of a tool invocation with matching id to call, name, and resultToolError
- Comprehensive error type for tool operationsToolSchema
- Trait for automatic JSON schema generationToolRegistration
- Internal representation of registered toolsFunctionDecl
- LLM-compatible function declaration structure#[tool]
- Attribute macro for automatic tool registration#[derive(ToolSchema)]
- Derive macro for automatic schema generationTools-rs provides comprehensive error handling with detailed context:
use tools_rs::{ToolError, collect_tools, FunctionCall};
use serde_json::json;
#[tokio::main]
async fn main() {
let tools = collect_tools();
match tools.call(FunctionCall::new(
"nonexistent".to_string(),
json!({}),
)).await {
Ok(response) => println!("Result: {}", response.result),
Err(ToolError::FunctionNotFound { name }) => {
println!("Tool '{}' not found", name);
},
Err(ToolError::Deserialize(err)) => {
println!("Deserialization error: {}", err.source);
},
Err(e) => println!("Other error: {}", e),
}
}
inventory
cratecollect_tools()
) is a zero-cost operationResult
types to avoid exceptions and maintain performance// Reuse ToolCollection instances to avoid repeated discovery
let tools = collect_tools(); // Call once, reuse multiple times
// Generate function declarations once for LLM integration
let declarations = function_declarations()?;
// Cache and reuse declarations across multiple LLM requests
// Use typed parameters to avoid repeated JSON parsing
use tools_rs::call_tool_with;
let result = call_tool_with("my_tool", &my_typed_args).await?.result;
Tool not found at runtime
#[tool]
macro is applied to your functioninventory
is properly collecting tools with collect_tools()
Schema generation errors
ToolSchema
#[derive(ToolSchema)]
to struct definitionsToolSchema
implementationsDeserialization failures
serde
attributes like #[serde(rename = "...")]
for custom field namesAsync execution issues
async fn
when using #[tool]
tokio
runtime for async execution.await
when calling tools// Enable debug logging to see tool registration and execution
use tools_rs::{collect_tools, list_tool_names};
let tools = collect_tools();
println!("Registered tools: {:?}", list_tool_names(&tools));
// Inspect generated schemas
let declarations = tools.json()?;
println!("Function declarations: {}", serde_json::to_string_pretty(&declarations)?);
We welcome contributions!
# Clone the repository
git clone https://github.com/EggerMarc/tools-rs
cd tools-rs
# Run tests
cargo test
# Run examples
cargo run --example basic
This project is licensed under the MIT License - see the LICENSE file for details.