08. Building a Calculator Tool
Chapter 8 of 16 · 15 min
The calculator tool is the hello-world of agent tools. It lets the model perform precise arithmetic that is error-prone when done in floating point by hand.
Safe evaluation
Never pass user input directly to eval() in a production system. Use a restricted executor that blocks dangerous builtins:
import ast
import operator
import sys
class CalculatorTool(Tool):
ALLOWED_MODULES = {"math": ["sqrt", "pow", "log10", "sin", "cos", "tan", "pi", "e"],
"statistics": ["mean", "median", "stdev"]}
def __init__(self):
super().__init__(
name="calculator",
description=(
"Evaluate a mathematical expression. Supports basic arithmetic (+, -, *, /, **), "
"parentheses, and functions from the math module (e.g., sqrt, pow, sin, cos). "
"Input must be a valid Python expression as a string."
),
input_schema={
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A valid Python mathematical expression. Example: '(15 + 7) ** 2 / 3'"
}
},
"required": ["expression"]
}
)
def invoke(self, expression: str) -> str:
# Disallow imports, attribute access, and function calls outside whitelist
allowed_names = {
"abs": abs, "min": min, "max": max, "round": round,
"sqrt": __import__("math").sqrt,
"pow": pow, "sin": __import__("math").sin,
"cos": __import__("math").cos, "tan": __import__("math").tan,
"log10": __import__("math").log10, "pi": __import__("math").pi,
"e": __import__("math").e,
**({k: v for k, v in vars(operator).items() if not k.startswith("_")})
}
try:
result = eval(expression, {"__builtins__": {}}, allowed_names)
# Convert numpy types to native Python types for JSON serialization
if hasattr(result, 'item'):
result = result.item()
return str(result)
except SyntaxError:
return f"Error: Invalid syntax in expression '{expression}'"
except NameError as e:
return f"Error: Unknown function or constant in expression - {e}"
except ZeroDivisionError:
return "Error: Division by zero"
except Exception as e:
return f"Error: {e}"
Handling edge cases
The calculator must handle:
- Division by zero → return clear error message
- Non-numeric input → return syntax error with the problematic string
- Overflow → return inf instead of crashing
Testing the tool
calc = CalculatorTool()
assert calc.invoke("2**10") == "1024"
assert calc.invoke("(14 + 8) / 2") == "11.0"
assert "division by zero" in calc.invoke("10 / 0").lower()
assert "invalid syntax" in calc.invoke("10 ++ 3").lower()
print("Calculator test suite passed")
EXERCISE
Add support for the statistics.mean function to the calculator. Write tests verifying that valid inputs produce correct results and that invalid inputs (like non-numeric data) fail gracefully.