| import ast
|
| import datetime as dt
|
| import math
|
| import re
|
| from functools import lru_cache
|
| from typing import Dict, List
|
|
|
| from duckduckgo_search import DDGS
|
|
|
|
|
| _ALLOWED_MATH_FUNCS = {
|
| "sqrt": math.sqrt,
|
| "sin": math.sin,
|
| "cos": math.cos,
|
| "tan": math.tan,
|
| "log": math.log,
|
| "log10": math.log10,
|
| "exp": math.exp,
|
| "fabs": math.fabs,
|
| "ceil": math.ceil,
|
| "floor": math.floor,
|
| "pow": math.pow,
|
| }
|
|
|
| _ALLOWED_CONSTANTS = {
|
| "pi": math.pi,
|
| "e": math.e,
|
| }
|
|
|
|
|
| class UnsafeExpressionError(ValueError):
|
| pass
|
|
|
|
|
| def _safe_eval(node: ast.AST) -> float:
|
| if isinstance(node, ast.Expression):
|
| return _safe_eval(node.body)
|
|
|
| if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
| return float(node.value)
|
|
|
| if isinstance(node, ast.BinOp):
|
| left = _safe_eval(node.left)
|
| right = _safe_eval(node.right)
|
|
|
| if isinstance(node.op, ast.Add):
|
| return left + right
|
| if isinstance(node.op, ast.Sub):
|
| return left - right
|
| if isinstance(node.op, ast.Mult):
|
| return left * right
|
| if isinstance(node.op, ast.Div):
|
| return left / right
|
| if isinstance(node.op, ast.FloorDiv):
|
| return left // right
|
| if isinstance(node.op, ast.Mod):
|
| return left % right
|
| if isinstance(node.op, ast.Pow):
|
| return left ** right
|
| raise UnsafeExpressionError("Unsupported binary operator.")
|
|
|
| if isinstance(node, ast.UnaryOp):
|
| value = _safe_eval(node.operand)
|
| if isinstance(node.op, ast.UAdd):
|
| return +value
|
| if isinstance(node.op, ast.USub):
|
| return -value
|
| raise UnsafeExpressionError("Unsupported unary operator.")
|
|
|
| if isinstance(node, ast.Name):
|
| if node.id in _ALLOWED_CONSTANTS:
|
| return _ALLOWED_CONSTANTS[node.id]
|
| raise UnsafeExpressionError(f"Unknown identifier: {node.id}")
|
|
|
| if isinstance(node, ast.Call):
|
| if not isinstance(node.func, ast.Name):
|
| raise UnsafeExpressionError("Only direct function calls are allowed.")
|
|
|
| func_name = node.func.id
|
| func = _ALLOWED_MATH_FUNCS.get(func_name)
|
| if func is None:
|
| raise UnsafeExpressionError(f"Unsupported function: {func_name}")
|
|
|
| args = [_safe_eval(arg) for arg in node.args]
|
| return float(func(*args))
|
|
|
| raise UnsafeExpressionError("Unsupported expression.")
|
|
|
|
|
| def calculator_tool(expression: str) -> str:
|
| try:
|
| parsed = ast.parse(expression, mode="eval")
|
| value = _safe_eval(parsed)
|
| if value.is_integer():
|
| return str(int(value))
|
| return str(round(value, 10))
|
| except Exception as exc:
|
| return f"Calculation error: {exc}"
|
|
|
|
|
| @lru_cache(maxsize=128)
|
| def web_search_tool(query: str, max_results: int = 5) -> str:
|
| try:
|
| rows: List[Dict] = []
|
| with DDGS() as ddgs:
|
| for item in ddgs.text(query, max_results=max_results):
|
| rows.append(item)
|
|
|
| if not rows:
|
| return "No web results found."
|
|
|
| summary_lines = []
|
| for idx, row in enumerate(rows, start=1):
|
| title = row.get("title", "Untitled")
|
| snippet = row.get("body", "").strip()
|
| href = row.get("href", "")
|
| snippet = snippet[:220] + ("..." if len(snippet) > 220 else "")
|
| summary_lines.append(f"{idx}. {title} | {snippet} | Source: {href}")
|
|
|
| return "\n".join(summary_lines)
|
| except Exception as exc:
|
| return f"Web search unavailable: {exc}"
|
|
|
|
|
| def datetime_tool() -> str:
|
| now = dt.datetime.now().astimezone()
|
| return (
|
| f"Current date: {now.strftime('%Y-%m-%d')}\n"
|
| f"Current time: {now.strftime('%H:%M:%S %Z')}"
|
| )
|
|
|
|
|
| def text_stats_tool(text: str) -> str:
|
| cleaned = text.strip()
|
| if not cleaned:
|
| return "Words: 0\nCharacters: 0\nSentences: 0"
|
|
|
| words = len(re.findall(r"\b\w+\b", cleaned))
|
| chars = len(cleaned)
|
| sentences = len([s for s in re.split(r"[.!?]+", cleaned) if s.strip()])
|
| return f"Words: {words}\nCharacters: {chars}\nSentences: {sentences}"
|
|
|