File size: 6,593 Bytes
8421ec4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168

import math
import requests
import ast
import operator
from typing import Union, Dict, Any

# ==============================
# MATH TOOL
# ==============================

def calculate_expression(expression: str) -> Union[float, str]:
    """
    Safely evaluate a mathematical expression.
    Supported operators: +, -, *, /, **, %, ^ (as power), sqrt, abs, round, sin, cos, tan, log, pi, e
    """
    # Safe operators map
    operators = {
        ast.Add: operator.add,
        ast.Sub: operator.sub,
        ast.Mult: operator.mul,
        ast.Div: operator.truediv,
        ast.Pow: operator.pow,
        ast.Mod: operator.mod,
        ast.USub: operator.neg,
        ast.UAdd: operator.pos,
    }

    # Safe functions map
    functions = {
        "sqrt": math.sqrt,
        "abs": abs,
        "round": round,
        "sin": math.sin,
        "cos": math.cos,
        "tan": math.tan,
        "log": math.log,
        "max": max,
        "min": min,
        "ceil": math.ceil,
        "floor": math.floor,
        "degrees": math.degrees,
        "radians": math.radians,
    }

    # Safe constants
    constants = {
        "pi": math.pi,
        "e": math.e,
        "tau": math.tau,
    }

    def eval_node(node):
        if isinstance(node, ast.Num):  # < 3.8
            return node.n
        elif isinstance(node, ast.Constant):  # >= 3.8
            if isinstance(node.value, (int, float)):
                return node.value
            raise ValueError(f"Unsupported constant type: {type(node.value)}")
        elif isinstance(node, ast.BinOp): # <left> <operator> <right>
            op = type(node.op)
            if op not in operators:
                raise ValueError(f"Unsupported operator: {op}")
            return operators[op](eval_node(node.left), eval_node(node.right))
        elif isinstance(node, ast.UnaryOp): # <operator> <operand> (e.g., -1)
            op = type(node.op)
            if op not in operators:
                raise ValueError(f"Unsupported unary operator: {op}")
            return operators[op](eval_node(node.operand))
        elif isinstance(node, ast.Call): # Function calls like sqrt(4)
            if not isinstance(node.func, ast.Name):
                raise ValueError("Only named functions are supported")
            if node.func.id not in functions:
                raise ValueError(f"Unsupported function: {node.func.id}")
            args = [eval_node(arg) for arg in node.args]
            return functions[node.func.id](*args)
        elif isinstance(node, ast.Name): # Variables/Constants
            if node.id in constants:
                return constants[node.id]
            raise ValueError(f"Unsupported name: {node.id}")
        else:
            raise TypeError(f"Unsupported expression node: {type(node)}")

    try:
        # Pre-process: replace ^ with ** for power
        expression = expression.replace("^", "**")
        node = ast.parse(expression, mode='eval')
        result = eval_node(node.body)
        return float(result)
    except Exception as e:
        return f"Error calculating '{expression}': {str(e)}"

# ==============================
# WEATHER TOOL
# ==============================

def get_current_weather(location: str) -> Dict[str, Any]:
    """
    Get current weather for a specific city using Open-Meteo API.
    Returns temperature (C), humidity, wind speed, etc.
    """
    try:
        # 1. Geocoding
        geo_url = "https://geocoding-api.open-meteo.com/v1/search"
        geo_params = {"name": location, "count": 1, "language": "en", "format": "json"}
        
        geo_res = requests.get(geo_url, params=geo_params, timeout=5)
        geo_data = geo_res.json()

        if not geo_data.get("results"):
            return {"error": f"City '{location}' not found."}

        location = geo_data["results"][0]
        lat = location["latitude"]
        lon = location["longitude"]
        city_name = location["name"]
        country = location.get("country", "")

        # 2. Weather Data
        weather_url = "https://api.open-meteo.com/v1/forecast"
        weather_params = {
            "latitude": lat,
            "longitude": lon,
            "current": "temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,rain,showers,snowfall,weather_code,cloud_cover,wind_speed_10m",
            "timezone": "auto"
        }

        w_res = requests.get(weather_url, params=weather_params, timeout=5)
        w_data = w_res.json()

        if "current" not in w_data:
            return {"error": "{location} Weather data not available."}

        current = w_data["current"]
        current_units = w_data["current_units"]

        # Decode WMO Weather Code
        # source: https://open-meteo.com/en/docs
        wmo_code = current["weather_code"]
        condition = "Unknown"
        if wmo_code == 0: condition = "Clear sky"
        elif 1 <= wmo_code <= 3: condition = "Mainly clear, partly cloudy, and overcast"
        elif 45 <= wmo_code <= 48: condition = "Fog and depositing rime fog"
        elif 51 <= wmo_code <= 55: condition = "Drizzle: Light, moderate, and dense intensity"
        elif 56 <= wmo_code <= 57: condition = "Freezing Drizzle: Light and dense intensity"
        elif 61 <= wmo_code <= 65: condition = "Rain: Slight, moderate and heavy intensity"
        elif 66 <= wmo_code <= 67: condition = "Freezing Rain: Light and heavy intensity"
        elif 71 <= wmo_code <= 75: condition = "Snow fall: Slight, moderate, and heavy intensity"
        elif 77: condition = "Snow grains"
        elif 80 <= wmo_code <= 82: condition = "Rain showers: Slight, moderate, and violent"
        elif 85 <= wmo_code <= 86: condition = "Snow showers slight and heavy"
        elif 95: condition = "Thunderstorm: Slight or moderate"
        elif 96 <= wmo_code <= 99: condition = "Thunderstorm with slight and heavy hail"

        return {
            "location": f"{city_name}, {country}",
            "temperature": f"{current['temperature_2m']} {current_units['temperature_2m']}",
            "feels_like": f"{current['apparent_temperature']} {current_units['apparent_temperature']}",
            "humidity": f"{current['relative_humidity_2m']} {current_units['relative_humidity_2m']}",
            "wind_speed": f"{current['wind_speed_10m']} {current_units['wind_speed_10m']}",
            "condition": condition,
            "cloud_cover": f"{current['cloud_cover']} {current_units['cloud_cover']}",
            "timestamp": current["time"]
        }

    except Exception as e:
        return {"error": f"Failed to fetch weather: {str(e)}"}