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}"