ToolStore Agent
feat: calc-toolkit (eval, convert, stats), publish all toolsets, 7 live on HF Space
481db54
Raw
History Blame Contribute Delete
8.37 kB
"""
calc‑toolkit β€” Evaluate math expressions, convert units, and compute statistics.
===================================================================================
Safe expression evaluation with proper operator precedence, unit conversions,
and basic statistical operations β€” all stdlib, no external deps.
"""
import math
import operator
import re
from typing import Any
try:
from toolstore.toolset import tool
except ImportError:
def tool(fn):
return fn
# ── Safe expression evaluator ─────────────────────────────────────────
_SAFE_OPS = {
"+": operator.add, "-": operator.sub, "*": operator.mul,
"/": operator.truediv, "**": operator.pow, "%": operator.mod,
"//": operator.floordiv,
}
_SAFE_FUNCTIONS = {
"abs": abs, "round": round, "min": min, "max": max, "sum": sum,
"sqrt": math.sqrt, "log": math.log, "log10": math.log10,
"log2": math.log2, "exp": math.exp,
"sin": math.sin, "cos": math.cos, "tan": math.tan,
"pi": math.pi, "e": math.e, "tau": math.tau,
"ceil": math.ceil, "floor": math.floor, "factorial": math.factorial,
}
# ── eval_expression ────────────────────────────────────────────────────
@tool
def eval_expression(*, expr: str) -> dict:
"""Safely evaluate a mathematical expression.
Supports: +, -, *, /, **, //, %, sqrt(), log(), sin(), cos(),
pi, e, tau, abs(), round(), min(), max(), factorial(), ceil(), floor().
Args:
expr: Math expression string (e.g. "sqrt(16) + 2 * pi").
Returns:
dict with "result" (number) or "error".
"""
import ast
try:
tree = ast.parse(expr.strip(), mode="eval")
except SyntaxError as exc:
return {"error": f"Syntax error: {exc}"}
def _eval(node):
if isinstance(node, ast.Expression):
return _eval(node.body)
if isinstance(node, ast.Constant):
return node.value
if isinstance(node, ast.Name):
name = node.id
if name in _SAFE_FUNCTIONS:
return _SAFE_FUNCTIONS[name]
return {"error": f"Unknown name: {name}"}
if isinstance(node, ast.BinOp):
op_name = type(node.op).__name__
python_op = {
"Add": "+", "Sub": "-", "Mult": "*", "Div": "/",
"Pow": "**", "Mod": "%", "FloorDiv": "//",
}.get(op_name)
if python_op not in _SAFE_OPS:
return {"error": f"Unsupported operator: {op_name}"}
left = _eval(node.left)
right = _eval(node.right)
if isinstance(left, dict) and "error" in left:
return left
if isinstance(right, dict) and "error" in right:
return right
try:
return _SAFE_OPS[python_op](left, right)
except Exception as exc:
return {"error": f"Compute error: {exc}"}
if isinstance(node, ast.Call):
func = _eval(node.func)
if isinstance(func, dict) and "error" in func:
return func
args = [_eval(a) for a in node.args]
for a in args:
if isinstance(a, dict) and "error" in a:
return a
try:
return func(*args)
except Exception as exc:
return {"error": f"Function error: {exc}"}
if isinstance(node, ast.UnaryOp):
op_name = type(node.op).__name__
val = _eval(node.operand)
if isinstance(val, dict) and "error" in val:
return val
if op_name == "USub":
return -val
if op_name == "UAdd":
return +val
return {"error": f"Unsupported expression: {type(node).__name__}"}
result = _eval(tree)
if isinstance(result, dict) and "error" in result:
return result
if isinstance(result, float) and result == int(result):
result = int(result)
return {"result": result, "expression": expr}
# ── convert_unit ───────────────────────────────────────────────────────
_UNIT_CONVERSIONS = {
# Length
("m", "km"): 0.001, ("km", "m"): 1000,
("m", "cm"): 100, ("cm", "m"): 0.01,
("m", "mm"): 1000, ("mm", "m"): 0.001,
("km", "mile"): 0.621371, ("mile", "km"): 1.60934,
("ft", "m"): 0.3048, ("m", "ft"): 3.28084,
("in", "cm"): 2.54, ("cm", "in"): 0.393701,
# Weight
("kg", "g"): 1000, ("g", "kg"): 0.001,
("kg", "lb"): 2.20462, ("lb", "kg"): 0.453592,
("lb", "oz"): 16, ("oz", "lb"): 0.0625,
# Temperature (special handling)
("C", "K"): "add:273.15", ("K", "C"): "sub:273.15",
("C", "F"): "c_to_f", ("F", "C"): "f_to_c",
# Time
("h", "min"): 60, ("min", "h"): 1/60,
("min", "s"): 60, ("s", "min"): 1/60,
("h", "s"): 3600, ("s", "h"): 1/3600,
("day", "h"): 24, ("h", "day"): 1/24,
# Digital
("B", "KB"): 1/1024, ("KB", "B"): 1024,
("KB", "MB"): 1/1024, ("MB", "KB"): 1024,
("MB", "GB"): 1/1024, ("GB", "MB"): 1024,
("GB", "TB"): 1/1024, ("TB", "GB"): 1024,
}
@tool
def convert_unit(*, value: float, from_unit: str, to_unit: str) -> dict:
"""Convert a value between units (length, weight, temperature, time, digital).
Supported units:
Length: m, km, cm, mm, ft, in, mile
Weight: kg, g, lb, oz
Temp: C, F, K
Time: h, min, s, day
Digital: B, KB, MB, GB, TB
Args:
value: Numeric value to convert.
from_unit: Source unit (case‑sensitive).
to_unit: Target unit (case‑sensitive).
Returns:
dict with "result", "from_unit", "to_unit", "original_value" or "error".
"""
fkey = from_unit.strip()
tkey = to_unit.strip()
if fkey == tkey:
return {"result": value, "from_unit": fkey, "to_unit": tkey,
"original_value": value}
factor = _UNIT_CONVERSIONS.get((fkey, tkey))
if factor is None:
return {"error": f"Unknown conversion: {fkey} β†’ {tkey}. "
f"Supported: {sorted(set(k[0] for k in _UNIT_CONVERSIONS))}"}
if isinstance(factor, str):
if factor == "add:273.15":
result = value + 273.15
elif factor == "sub:273.15":
result = value - 273.15
elif factor == "c_to_f":
result = value * 9/5 + 32
elif factor == "f_to_c":
result = (value - 32) * 5/9
else:
return {"error": f"Unknown conversion function: {factor}"}
else:
result = value * factor
if isinstance(result, float) and result == int(result):
result = int(result)
return {"result": result, "from_unit": fkey, "to_unit": tkey,
"original_value": value, "formula": f"{value} {fkey} β†’ {result} {tkey}"}
# ── basic_stats ────────────────────────────────────────────────────────
@tool
def basic_stats(*, numbers: list) -> dict:
"""Compute basic statistics for a list of numbers.
Args:
numbers: List of numeric values.
Returns:
dict with: count, sum, min, max, mean, median, stdev, variance.
"""
if not numbers:
return {"error": "Empty list"}
try:
nums = [float(n) for n in numbers]
except (ValueError, TypeError) as exc:
return {"error": f"Invalid number in list: {exc}"}
n = len(nums)
total = sum(nums)
mean_val = total / n
sorted_nums = sorted(nums)
# Median
if n % 2 == 0:
median_val = (sorted_nums[n // 2 - 1] + sorted_nums[n // 2]) / 2
else:
median_val = sorted_nums[n // 2]
# Standard deviation (population)
variance = sum((x - mean_val) ** 2 for x in nums) / n
stdev = math.sqrt(variance)
return {
"count": n, "sum": total, "min": sorted_nums[0],
"max": sorted_nums[-1], "mean": round(mean_val, 4),
"median": round(median_val, 4),
"stdev": round(stdev, 4), "variance": round(variance, 4),
}