File size: 4,295 Bytes
3aaa9f7
 
 
cbfc437
3aaa9f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cbfc437
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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}"