Local Tools Tutorial¶
Learn how to create and use local Python tools with ToolUniverse. Local tools run within the ToolUniverse process and provide the most efficient way to add custom functionality.
Note
💡 Self-Use: This tutorial covers using tools in your own projects.
🚀 Contributing: If you want to contribute tools to the ToolUniverse repository, see Contributing Local Tools to ToolUniverse for additional steps.
What are Local Tools?¶
Local tools are Python classes that extend ToolUniverse with custom functionality. They run in the same process as ToolUniverse and provide:
High Performance: No network overhead
Easy Development: Simple Python classes
Automatic Discovery: Tools auto-register with decorators
Full Integration: Access to all ToolUniverse features
Best for: - Data processing and analysis - File manipulation utilities - Simple API wrappers - Computational tools
Complete Working Example¶
Here’s a complete protein molecular weight calculator you can copy and run:
# protein_calculator.py - Save this file anywhere
from tooluniverse.tool_registry import register_tool
from tooluniverse.base_tool import BaseTool
from tooluniverse.exceptions import ToolValidationError
from typing import Dict, Any
@register_tool('ProteinCalculator', config={
"name": "protein_calculator",
"type": "ProteinCalculator",
"description": "Calculate molecular weight of protein sequences",
"parameter": {
"type": "object",
"properties": {
"sequence": {"type": "string", "description": "Protein sequence (single letter amino acid codes)"}
},
"required": ["sequence"]
}
})
class ProteinCalculator(BaseTool):
"""Calculate molecular weight of protein sequences."""
def __init__(self, tool_config: Dict[str, Any] = None):
super().__init__(tool_config)
# Amino acid molecular weights (in Daltons)
self.aa_weights = {
'A': 89.09, 'R': 174.20, 'N': 132.12, 'D': 133.10,
'C': 121.16, 'Q': 146.15, 'E': 147.13, 'G': 75.07,
'H': 155.16, 'I': 131.17, 'L': 131.17, 'K': 146.19,
'M': 149.21, 'F': 165.19, 'P': 115.13, 'S': 105.09,
'T': 119.12, 'W': 204.23, 'Y': 181.19, 'V': 117.15
}
def run(self, arguments=None, **kwargs) -> Dict[str, Any]:
"""Calculate molecular weight of a protein sequence."""
# Handle both direct calls and ToolUniverse calls
if arguments is None:
arguments = kwargs
# Extract sequence from arguments
sequence = arguments.get('sequence') if isinstance(arguments, dict) else arguments
# Validate inputs
self.validate_input(sequence=sequence)
# Clean sequence (remove whitespace, convert to uppercase)
clean_sequence = sequence.strip().upper()
# Calculate molecular weight
total_weight = sum(self.aa_weights.get(aa, 0) for aa in clean_sequence)
# Subtract water molecules for peptide bonds
water_weight = (len(clean_sequence) - 1) * 18.015
molecular_weight = total_weight - water_weight
return {
"molecular_weight": round(molecular_weight, 2),
"sequence_length": len(clean_sequence),
"sequence": clean_sequence,
"success": True
}
def validate_input(self, **kwargs) -> None:
"""Validate input parameters."""
sequence = kwargs.get('sequence')
if not sequence:
raise ValueError("Sequence is required")
if not isinstance(sequence, str):
raise ValueError("Sequence must be a string")
if len(sequence.strip()) == 0:
raise ValueError("Sequence cannot be empty")
# Check for valid amino acid codes
valid_aa = set(self.aa_weights.keys())
invalid_chars = set(sequence.upper()) - valid_aa
if invalid_chars:
raise ValueError(f"Invalid amino acid codes: {', '.join(invalid_chars)}")
# Usage
from tooluniverse import ToolUniverse
# Import your tool (this registers it)
from protein_calculator import ProteinCalculator
tu = ToolUniverse()
tu.load_tools() # Load built-in tools
result = tu.run_one_function({
"name": "protein_calculator",
"arguments": {"sequence": "GIVEQCCTSICSLYQLENYCN"}
})
print(result) # {"molecular_weight": 2401.45, "sequence_length": 20, "success": True}
How to use: Save as protein_calculator.py and import it - the tool registers automatically.
Adapt to Your Own Tool¶
You only need to modify 3 places:
- 1. Tool Name and Description (lines 8-9)
name
:"protein_calculator"
→ change to your tool namedescription
:"Calculate molecular weight..."
→ change to your description
- 2. Input Parameters (lines 10-15)
Define the parameters you need:
Your Need
Parameter Type
Example
Text input
"type": "string"
username, query, file_path
Number input
"type": "number"
age, amount, limit
Dropdown options
"type": "string", "enum": [...]
status, category, format
Optional param
Don’t put in
"required"
optional filters, defaults
- 3. Core Logic (run method at line 30)
Implement your business logic and return results:
def run(self, arguments=None, **kwargs) -> Dict[str, Any]: """Your tool description.""" # Handle both direct calls and ToolUniverse calls if arguments is None: arguments = kwargs # Extract your parameter from arguments your_param = arguments.get('your_param') if isinstance(arguments, dict) else arguments # Validate inputs self.validate_input(your_param=your_param) # Your logic here result = do_something(your_param) return {"result": result, "success": True}
Common Scenarios¶
I want to call an external API¶
import requests
def run(self, arguments=None, **kwargs) -> Dict[str, Any]:
"""Make API call to specified URL."""
# Handle both direct calls and ToolUniverse calls
if arguments is None:
arguments = kwargs
url = arguments.get('url') if isinstance(arguments, dict) else arguments
method = arguments.get('method', 'GET') if isinstance(arguments, dict) else 'GET'
self.validate_input(url=url, method=method)
try:
if method == "GET":
response = requests.get(url)
else:
response = requests.post(url)
response.raise_for_status()
return {"data": response.json(), "success": True}
except Exception as e:
return {"error": str(e), "success": False}
I want to process files¶
def run(self, arguments=None, **kwargs) -> Dict[str, Any]:
"""Process file based on specified operation."""
# Handle both direct calls and ToolUniverse calls
if arguments is None:
arguments = kwargs
file_path = arguments.get('file_path') if isinstance(arguments, dict) else arguments
operation = arguments.get('operation', 'read') if isinstance(arguments, dict) else 'read'
self.validate_input(file_path=file_path, operation=operation)
try:
with open(file_path, 'r') as f:
content = f.read()
if operation == "analyze":
result = {"lines": len(content.split('\n')), "chars": len(content)}
else:
result = {"content": content}
return {"result": result, "success": True}
except Exception as e:
return {"error": str(e), "success": False}
I want to use API keys (environment variables)¶
Add to your config:
@register_tool('MyAPITool', config={
"name": "my_api_tool",
"description": "Tool that uses API keys",
"parameter": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"}
},
"required": ["query"]
},
"settings": { # ← Add this section
"api_key": "env:MY_API_KEY", # ← Reference environment variable
"base_url": "https://api.example.com"
}
})
Then in your run method:
def __init__(self, tool_config: Dict[str, Any] = None):
super().__init__(tool_config)
self.api_key = self.config.get("settings", {}).get("api_key")
self.base_url = self.config.get("settings", {}).get("base_url")
I want custom error handling¶
def run(self, arguments=None, **kwargs) -> Dict[str, Any]:
"""Execute with proper error handling."""
# Handle both direct calls and ToolUniverse calls
if arguments is None:
arguments = kwargs
param = arguments.get('param') if isinstance(arguments, dict) else arguments
try:
# Validate inputs first
self.validate_input(param=param)
# Your tool logic here
result = self.process_data(param)
return {"result": result, "success": True}
except ValueError as e:
# Input validation errors
return {"error": f"Invalid input: {str(e)}", "success": False}
except requests.RequestException as e:
# Network errors
return {"error": f"Network error: {str(e)}", "success": False}
except Exception as e:
# Unexpected errors
return {"error": f"Unexpected error: {str(e)}", "success": False}
Troubleshooting¶
Tool not found¶
Is the tool file imported? (need to
import
or run directly)Is the
@register_tool
decorator used correctly?Is ToolUniverse instantiated after tool import?
Parameter errors¶
Do
"parameter"
definitions in config matchrun()
method parameters?Are required parameters listed in
"required"
array?Are parameter types (
string
/number
/object
) correct?
Execution failures¶
Does the class inherit from
BaseTool
?Does
__init__
callsuper().__init__(tool_config)
?Does
run()
return a dict with"success"
field?Is
validate_input()
implemented for parameter validation?
Next Steps¶
Now that you can create local tools:
🔗 Remote Tools: Remote Tools Tutorial - Learn about remote tool integration
🚀 Contributing: Contributing Local Tools to ToolUniverse - Submit your tools to ToolUniverse
🤖 AI Integration: ../guide/building_ai_scientists/mcp_integration - Connect your tools with AI assistants
🔬 Scientific Workflows: ../guide/scientific_workflows - Build research pipelines
Tip
Development tip: Start simple, test thoroughly, and gradually add complexity. The ToolUniverse community is here to help if you get stuck!