tools-rs

Crates.iotools-rs
lib.rstools-rs
version0.1.2
created_at2025-02-02 11:13:41.202737+00
updated_at2025-09-24 12:14:27.898623+00
descriptionCore functionality for the tools-rs tool collection system
homepage
repository
max_upload_size
id1539493
size64,177
Arman Maurya (armanmaurya)

documentation

README

Tools-rs - Tool Collection and Execution Framework

It's pronounced tools-r-us!!

Crates.io Documentation License: MIT

Tools-rs is a framework for building, registering, and executing tools with automatic JSON schema generation for Large Language Model (LLM) integration.

Features

  • Automatic Registration - Use #[tool] to automatically register functions with compile-time discovery
  • JSON Schema Generation - Automatic schema generation for LLM integration with full type information
  • Type Safety - Full type safety with JSON serialization at boundaries, compile-time parameter validation
  • Async Support - Built for async/await from the ground up with tokio integration
  • Error Handling - Comprehensive error types with context and proper error chaining
  • LLM Integration - Export function declarations for LLM function calling APIs (OpenAI, Anthropic, etc.)
  • Manual Registration - Programmatic tool registration for dynamic scenarios
  • Inventory System - Link-time tool collection using the inventory crate for zero-runtime-cost discovery

Quick Start

use 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(())
}

Installation

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"

Crate Structure

The tools-rs system is organized as a Rust workspace with three main crates:

  • tools-rs: Main entry point, re-exports the most commonly used items
  • tools_core: Core runtime implementation including:
    • Tool collection and execution (ToolCollection)
    • JSON schema generation (ToolSchema trait)
    • Error handling (ToolError, DeserializationError)
    • Core data structures (FunctionCall, ToolRegistration, etc.)
  • tools_macros: Procedural macros for tool registration:
    • #[tool] attribute macro for automatic registration
    • #[derive(ToolSchema)] for automatic schema generation
  • examples: Comprehensive examples demonstrating different use cases

For more details about the codebase organization, see CODE_ORGANIZATION.md.

Compatibility

Rust Version Support

Tools-rs requires Rust 1.85 or later and supports:

  • Automatically generate JSON schemas for LLM consumption
  • Execute tools safely with full type checking
  • Handle errors gracefully with detailed context

Function Declarations for LLMs

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"
    }
  }
]

Manual Registration

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(())
}

Advanced Manual Registration

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(())
}

Examples

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:

  • basic: Simple tool registration with #[tool] and basic function calls
  • function_declarations: Complete LLM integration workflow with JSON schema generation
  • schema: Advanced schema generation for complex nested types and collections
  • newtype_demo: Working with custom wrapper types and serialization patterns

API Reference

Core Functions

  • collect_tools() - Discover all tools registered via #[tool] macro
  • function_declarations() - Generate JSON schema declarations for LLMs
  • call_tool(name, args) - Execute a tool by name with JSON arguments
  • call_tool_with(name, typed_args) - Execute a tool with typed arguments
  • call_tool_by_name(collection, name, args) - Execute tool on specific collection
  • list_tool_names(collection) - List all available tool names

Core Types

  • ToolCollection - Container for registered tools with execution capabilities
  • FunctionCall - Represents a tool invocation with id, name, and arguments
  • FunctionResponse - Represents the response of a tool invocation with matching id to call, name, and result
  • ToolError - Comprehensive error type for tool operations
  • ToolSchema - Trait for automatic JSON schema generation
  • ToolRegistration - Internal representation of registered tools
  • FunctionDecl - LLM-compatible function declaration structure

Macros

  • #[tool] - Attribute macro for automatic tool registration
  • #[derive(ToolSchema)] - Derive macro for automatic schema generation

Error Handling

Tools-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),
    }
}

Performance Considerations

Schema Caching

  • JSON schemas are generated once and cached.
  • Schema generation has minimal runtime overhead after first access
  • Primitive types use pre-computed static schemas for optimal performance

Tool Discovery

  • Tool registration happens at compile-time via the inventory crate
  • Runtime tool collection (collect_tools()) is a zero-cost operation
  • Tools are stored in efficient hash maps for O(1) lookup by name

Execution Performance

  • Tool calls have minimal overhead beyond JSON serialization/deserialization
  • Async execution allows for concurrent tool invocation
  • Error handling uses Result types to avoid exceptions and maintain performance

Memory Usage

  • Tool metadata is stored statically with minimal heap allocation
  • JSON schemas are shared across all instances of the same type
  • Function declarations are generated on-demand and can be cached by the application

Optimization Tips

// 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;

Troubleshooting

Common Issues

Tool not found at runtime

  • Ensure the #[tool] macro is applied to your function
  • Verify the function is in a module that gets compiled (not behind unused feature flags)
  • Check that inventory is properly collecting tools with collect_tools()

Schema generation errors

  • Ensure all parameter and return types implement ToolSchema
  • For custom types, add #[derive(ToolSchema)] to struct definitions
  • Complex generic types may need manual ToolSchema implementations

Deserialization failures

  • Verify JSON arguments match the expected parameter structure
  • Check that argument names match function parameter names exactly
  • Use serde attributes like #[serde(rename = "...")] for custom field names

Async execution issues

  • All tool functions must be async fn when using #[tool]
  • Ensure you're using tokio runtime for async execution
  • Tool execution is inherently async - use .await when calling tools

Debugging Tips

// 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)?);

Contributing

We welcome contributions!

Development Setup

# Clone the repository
git clone https://github.com/EggerMarc/tools-rs
cd tools-rs 

# Run tests
cargo test

# Run examples
cargo run --example basic

License

This project is licensed under the MIT License - see the LICENSE file for details.

Commit count: 0

cargo fmt