Tools
Tools extend agent capabilities by providing access to external functions, APIs, and services, enabling agents to interact with the real world.
See Also
For tool schema structure and API details, see Tools API Reference. For built-in tools list, see Built-in Tools Guide.
Overview
Tools in MARSYS are Python functions that agents can invoke to:
- Access External Services: Web search, APIs, databases
- Perform Calculations: Mathematical operations, data analysis
- Control Systems: Browser automation, file operations
- Process Data: Text extraction, format conversion
- Interact with Environment: System commands, hardware control
The framework uses OpenAI-compatible function calling for seamless tool integration.
Creating Tools
Basic Tool Definition
Every tool needs:
- Type hints on all parameters
- Comprehensive docstring with Args and Returns
- Return type annotation
- Descriptive parameter names
def search_database(query: str,database: str = "products",limit: int = 10,filters: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:"""Search database for matching records.Args:query: Search query stringdatabase: Database to search (products, users, orders)limit: Maximum number of results to returnfilters: Additional filters as key-value pairsReturns:List of matching records with all fieldsRaises:ConnectionError: If database is unavailableValueError: If query is invalid"""# Implementationresults = perform_search(query, database, limit, filters)return results
Automatic Schema Generation
The framework automatically generates OpenAI-compatible schemas:
from marsys.environment.utils import generate_openai_tool_schema# Automatic schema generation from functionschema = generate_openai_tool_schema(search_database)# Generated schema:{"type": "function","function": {"name": "search_database","description": "Search database for matching records.","parameters": {"type": "object","properties": {"query": {"type": "string","description": "Search query string"},"database": {"type": "string","description": "Database to search (products, users, orders)","default": "products"},"limit": {"type": "integer","description": "Maximum number of results to return","default": 10},"filters": {"type": "object","description": "Additional filters as key-value pairs"}},"required": ["query"]}}}
Tool Registration
Agent-Specific Tools
from marsys.agents import Agentfrom marsys.models import ModelConfigagent = Agent(model_config=ModelConfig(type="api",name="anthropic/claude-haiku-4.5",provider="openrouter",max_tokens=12000),name="DataAnalyst",goal="Expert data analyst with database access",instruction="You are a data analyst. Use your tools to search databases, analyze data, and export results.",tools={ # Dict mapping names to functions"db_search": search_database,"analyze": analyze_data,"export": export_results})
Global Tool Registry
from marsys.environment.tools import AVAILABLE_TOOLS# Register globallyAVAILABLE_TOOLS["search_database"] = search_databaseAVAILABLE_TOOLS["analyze_data"] = analyze_data# Discover tools by categoryWEB_TOOLS = {"search_web": search_web_func,"fetch_url": fetch_url_func,"scrape_page": scrape_page_func}DATA_TOOLS = {"analyze_csv": analyze_csv_func,"process_json": process_json_func,"transform_data": transform_data_func}# Combine categoriesAVAILABLE_TOOLS.update(WEB_TOOLS)AVAILABLE_TOOLS.update(DATA_TOOLS)
Tool Patterns
Async Tools
For non-blocking operations:
import aiohttpimport asyncioasync def fetch_api_data(endpoint: str,params: Optional[Dict[str, str]] = None,timeout: int = 30) -> Dict[str, Any]:"""Fetch data from API endpoint asynchronously.Args:endpoint: API endpoint URLparams: Query parameterstimeout: Request timeout in secondsReturns:API response data"""try:async with aiohttp.ClientSession() as session:async with session.get(endpoint,params=params,timeout=aiohttp.ClientTimeout(total=timeout)) as response:data = await response.json()return {"status": response.status,"data": data,"headers": dict(response.headers)}except asyncio.TimeoutError:return {"error": "Request timed out", "timeout": timeout}except Exception as e:return {"error": str(e)}
Stateful Tools
Tools that maintain state across calls:
class DatabaseConnection:def __init__(self, connection_string: str):self.connection = self._connect(connection_string)self.query_cache = {}async def query(self,sql: str,params: Optional[tuple] = None,use_cache: bool = False) -> List[Dict]:"""Execute SQL query with optional caching.Args:sql: SQL query stringparams: Query parameters for prepared statementuse_cache: Whether to use cached resultsReturns:Query results as list of dictionaries"""cache_key = f"{sql}:{params}"if use_cache and cache_key in self.query_cache:return self.query_cache[cache_key]try:cursor = await self.connection.execute(sql, params or ())results = await cursor.fetchall()if use_cache:self.query_cache[cache_key] = resultsreturn resultsexcept Exception as e:return [{"error": str(e)}]# Create instance and extract tooldb = DatabaseConnection("postgresql://...")query_tool = db.query # This method becomes the tool
Composite Tools
Tools that combine multiple operations:
async def research_topic(topic: str,depth: Literal["shallow", "medium", "deep"] = "medium",include_sources: bool = True) -> Dict[str, Any]:"""Comprehensive research on a topic from multiple sources.Args:topic: Topic to researchdepth: Research depth levelinclude_sources: Whether to include source URLsReturns:Research summary with sources and key points"""# Determine number of sources based on depthsource_count = {"shallow": 3, "medium": 5, "deep": 10}[depth]# Phase 1: Web searchsearch_results = await search_web(topic, max_results=source_count)# Phase 2: Fetch and analyze contentcontents = []for result in search_results:content = await fetch_url_content(result["url"])contents.append({"url": result["url"],"title": result["title"],"content": content})# Phase 3: Synthesize informationkey_points = extract_key_points(contents)summary = create_summary(key_points)response = {"topic": topic,"summary": summary,"key_points": key_points,"source_count": len(contents)}if include_sources:response["sources"] = [{"title": c["title"], "url": c["url"]}for c in contents]return response
Error-Handling Tools
Robust tools with proper error handling:
from functools import wrapsfrom typing import Callabledef safe_tool(func: Callable) -> Callable:"""Decorator for safe tool execution."""@wraps(func)async def async_wrapper(*args, **kwargs):try:result = await func(*args, **kwargs)return {"success": True, "result": result}except ValueError as e:return {"success": False, "error": f"Invalid input: {e}"}except TimeoutError as e:return {"success": False, "error": f"Operation timed out: {e}"}except Exception as e:return {"success": False, "error": f"Unexpected error: {e}"}@wraps(func)def sync_wrapper(*args, **kwargs):try:result = func(*args, **kwargs)return {"success": True, "result": result}except Exception as e:return {"success": False, "error": str(e)}if asyncio.iscoroutinefunction(func):return async_wrapperelse:return sync_wrapper# Usage@safe_toolasync def risky_operation(param: str) -> str:"""Operation that might fail."""if not param:raise ValueError("Parameter cannot be empty")# Risky operation herereturn f"Processed: {param}"
Built-in Tools
MARSYS includes a comprehensive tool library:
Web Tools
async def search_web(query: str,max_results: int = 5,search_engine: Literal["google", "bing", "duckduckgo"] = "google") -> List[Dict[str, str]]:"""Search the web for information."""# Returns: [{"title": "...", "url": "...", "snippet": "..."}]async def fetch_url_content(url: str,format: Literal["text", "markdown", "html"] = "text") -> str:"""Fetch and extract content from URL."""# Returns formatted contentasync def check_website_status(url: str) -> Dict[str, Any]:"""Check if website is accessible."""# Returns: {"online": bool, "status_code": int, "response_time": float}
Data Processing Tools
def analyze_data(data: List[float],operations: List[Literal["mean", "median", "std", "min", "max"]]) -> Dict[str, float]:"""Perform statistical analysis on data."""# Returns: {"mean": 5.2, "std": 1.3, ...}def transform_json(json_data: Dict,jq_filter: str) -> Any:"""Transform JSON using JQ-like syntax."""# Returns transformed datadef parse_csv(csv_text: str,delimiter: str = ",",has_headers: bool = True) -> List[Dict[str, str]]:"""Parse CSV text into records."""# Returns list of dictionaries
File System Tools
async def read_file(path: str,encoding: str = "utf-8",lines: Optional[Tuple[int, int]] = None) -> str:"""Read file contents."""# Returns file contentasync def write_file(path: str,content: str,mode: Literal["write", "append"] = "write") -> Dict[str, Any]:"""Write content to file."""# Returns: {"success": bool, "bytes_written": int}async def list_directory(path: str = ".",pattern: Optional[str] = None) -> List[Dict[str, Any]]:"""List directory contents."""# Returns: [{"name": "...", "type": "file|dir", "size": int}]
PDF Tools with Image Extraction
MARSYS supports extracting embedded images from PDFs using PyMuPDF, returning content as ordered chunks that preserve reading order.
Basic PDF Reading
# Text-only extraction (default)async def read_pdf_text(path: str) -> ToolResponse:from marsys.environment.file_operations import read_file_wrapperresult = await read_file_wrapper(path="document.pdf",start_page=1,end_page=5)return result# Returns: ToolResponse with text content only
PDF with Image Extraction
# Extract text + images in reading orderasync def read_pdf_with_images(path: str) -> ToolResponse:from marsys.environment.file_operations import read_file_wrapperresult = await read_file_wrapper(path="paper.pdf",start_page=5,end_page=10,extract_images=True, # Enable image extractionmax_images_per_page=10,max_pixels=None # Optional: downsample large images)return result# Returns: ToolResponse with ordered text/image content blocks
What Gets Extracted
When extract_images=True:
- Text blocks: Paragraphs, headings, captions
- Embedded images: Figures, photos, diagrams, charts
- Reading order: Content sorted by Y-position (top-to-bottom) then X-position
- Metadata: Page numbers, bounding boxes, token estimates
What does NOT get extracted:
- Background images or watermarks (usually)
- Text rendered as images (use OCR separately)
- Vector graphics (converted to raster if possible)
Token Cost Comparison
| Extraction Method | Text Tokens | Vision Tokens | Cost (GPT-4o) |
|---|---|---|---|
| Text-only | ~5,000 | 0 | $0.05 |
| Images as base64 strings | ~255,000 | 0 | $2.55 |
| Images with ToolResponse | ~5,000 | ~425 | $0.055 |
Savings: ~46x cheaper when using proper image extraction vs treating images as text!
Requirements
Dependencies:
# Install with file operations supportpip install marsys[file-ops]# Or manuallypip install PyMuPDF>=1.23.0
Graceful Degradation:
- If PyMuPDF not installed: PDF support will be disabled
- Warning logged: "PyMuPDF not installed. PDF support will be disabled."
Best Practices
1. Clear Documentation
# GOOD - Comprehensive documentationdef process_invoice(invoice_data: Dict[str, Any],validation_level: Literal["basic", "strict"] = "basic",currency: str = "USD") -> Dict[str, Any]:"""Process and validate invoice data.Args:invoice_data: Invoice information containing:- invoice_number (str): Unique invoice identifier- amount (float): Total amount- items (list): Line itemsvalidation_level: How strictly to validatecurrency: Currency code for amount conversionReturns:Processed invoice with:- status: "valid" or "invalid"- processed_amount: Converted amount- warnings: List of validation warningsRaises:ValueError: If invoice_data is malformedKeyError: If required fields are missing"""# Implementationpass# BAD - Poor documentationdef process(data, level="basic"):"""Process data."""pass
2. Input Validation
# GOOD - Validates inputsdef send_email(to: str,subject: str,body: str,cc: Optional[List[str]] = None) -> Dict[str, Any]:"""Send email with validation."""# Validate email formatemail_pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'if not re.match(email_pattern, to):raise ValueError(f"Invalid email address: {to}")# Validate CC addressesif cc:for addr in cc:if not re.match(email_pattern, addr):raise ValueError(f"Invalid CC address: {addr}")# Validate contentif not subject.strip():raise ValueError("Subject cannot be empty")if len(body) > 100000:raise ValueError("Body too large (max 100KB)")# Send emailreturn {"sent": True, "message_id": "..."}
3. Consistent Returns
# GOOD - Always returns same structuredef database_query(sql: str) -> Dict[str, Any]:"""Query database with consistent response."""try:results = execute_query(sql)return {"success": True,"data": results,"row_count": len(results),"error": None}except Exception as e:return {"success": False,"data": [],"row_count": 0,"error": str(e)}# BAD - Inconsistent returnsdef bad_query(sql: str):try:return execute_query(sql) # Returns listexcept Exception as e:return str(e) # Returns string
4. Resource Management
# GOOD - Proper resource cleanupasync def process_large_file(file_path: str,chunk_size: int = 8192) -> Dict[str, Any]:"""Process large file with proper resource management."""file_handle = Nonetry:file_handle = await aiofiles.open(file_path, 'r')total_lines = 0async for chunk in file_handle:# Process chunktotal_lines += chunk.count('\n')return {"lines": total_lines, "status": "completed"}except Exception as e:return {"error": str(e), "status": "failed"}finally:if file_handle:await file_handle.close()
Advanced Patterns
Complex Tool Returns with ToolResponse
Important: Understanding Tool Return Types
What happens when you DON'T use ToolResponse:
- Dictionaries - Converted to JSON strings. The LLM sees:
'{"key": "value"}'instead of structured data - Images (paths/URLs) - Converted to strings. The LLM sees:
'data:image/png;base64,iVBOR...'as TEXT (not as an image!). This wastes thousands of tokens treating image data as text - Lists - Converted to string representations. The LLM sees:
'[1, 2, 3]'instead of structured data
Solution: Use ToolResponse for complex returns!
When to Use ToolResponse
Use ToolResponse when your tool needs to return:
- Structured data (dicts, lists) that shouldn't be stringified
- Images that the LLM should actually SEE (not read as text)
- Multimodal content (text + images together)
- Metadata separate from main content
Basic Usage
from marsys.environment import ToolResponsedef analyze_document(file_path: str) -> ToolResponse:"""Analyze a document and return structured results with images.Returns:ToolResponse with analysis data and document images"""# Process the documentanalysis = {"word_count": 1523,"topics": ["AI", "Machine Learning", "Neural Networks"],"sentiment": "positive","key_findings": [...]}# Extract images (as base64 data URLs)images = extract_images_as_base64(file_path)return ToolResponse(content=analysis, # Dict stays as dict (NOT stringified!)metadata="Successfully analyzed 5-page technical document",images=images # LLM will SEE these images)
Token Efficiency
| Content Type | Without ToolResponseContent | With ToolResponseContent |
|---|---|---|
| Small dict | ~25 tokens (stringified) | ~25 tokens (structured) |
| Image (50KB) | ~75,000 text tokens | ~85 vision tokens |
| PDF 5 pages | ~250,000 text tokens | ~425 vision tokens |
Savings: Up to 750x cheaper for images!
Critical: Image Token Cost
Returning base64 images WITHOUT ToolResponse can cost hundreds of times more in API usage. Always use ToolResponse for images!
Rate-Limited Tools
from asyncio import Semaphorefrom collections import defaultdictimport timeclass RateLimiter:def __init__(self, calls: int, period: float):self.calls = callsself.period = periodself.semaphore = Semaphore(calls)self.call_times = []async def acquire(self):async with self.semaphore:now = time.time()# Remove old calls outside the periodself.call_times = [t for t in self.call_times if now - t < self.period]if len(self.call_times) >= self.calls:sleep_time = self.period - (now - self.call_times[0])await asyncio.sleep(sleep_time)self.call_times.append(time.time())# Rate-limited API toolrate_limiter = RateLimiter(calls=10, period=60) # 10 calls per minuteasync def api_call(endpoint: str, params: Dict) -> Dict:"""Rate-limited API call."""await rate_limiter.acquire()# Make actual API callresponse = await make_request(endpoint, params)return response
Cached Tools
from functools import lru_cacheimport hashlibimport jsonclass CachedTool:def __init__(self, ttl: int = 300):self.cache = {}self.ttl = ttldef _cache_key(self, *args, **kwargs):"""Generate cache key from arguments."""key_data = json.dumps({"args": args, "kwargs": kwargs}, sort_keys=True)return hashlib.md5(key_data.encode()).hexdigest()async def expensive_operation(self,param1: str,param2: int,use_cache: bool = True) -> Dict:"""Expensive operation with caching."""cache_key = self._cache_key(param1, param2)# Check cacheif use_cache and cache_key in self.cache:cached_time, cached_result = self.cache[cache_key]if time.time() - cached_time < self.ttl:return {"result": cached_result, "cached": True}# Perform expensive operationresult = await self._do_expensive_work(param1, param2)# Cache resultself.cache[cache_key] = (time.time(), result)return {"result": result, "cached": False}
Next Steps
Agents
How agents use tools
Models
Model configurations for tool calling
Browser Automation
Web interaction tools
Tool API Reference
Complete tool API documentation
Tool System Ready!
You now understand how to create and use tools in MARSYS. Tools are the bridge between AI intelligence and real-world actions.