AI-Agent / tools.py
Valtry's picture
Upload 2 files
cbfc437 verified
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}"