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): # 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): # (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)}"}