Spaces:
Sleeping
Sleeping
| import os | |
| import requests | |
| import joblib | |
| import time | |
| import matplotlib.dates as mdates | |
| from datetime import datetime, timedelta | |
| from zoneinfo import ZoneInfo | |
| from dateutil import parser as date_parser | |
| from typing import Union | |
| from geopy.geocoders import Nominatim | |
| from timezonefinder import TimezoneFinder | |
| from tabulate import tabulate | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| from langchain.schema import AIMessage | |
| try: | |
| from langchain_openai import ChatOpenAI | |
| except ImportError: | |
| from langchain.chat_models import ChatOpenAI | |
| #os.environ["OPENAI_API_KEY"] = "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" | |
| #os.environ["WEATHER_API_KEY"] = "b9b44f4c0e8949bb95a90524250204" | |
| os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") | |
| os.environ["WEATHER_API_KEY"] = os.getenv("WEATHER_API_KEY") | |
| try: | |
| llm_gpt4 = ChatOpenAI(model="gpt-4o-mini", temperature=0) | |
| except Exception: | |
| try: | |
| llm_gpt4 = ChatOpenAI(model_name="gpt-4o-mini", temperature=0) | |
| except Exception as e: | |
| print(f"LangChain initialization failed: {e}") | |
| llm_gpt4 = None | |
| def location_to_timezone(location: str) -> str: | |
| try: | |
| geo = Nominatim(user_agent="time_agent_demo") | |
| loc = geo.geocode(location) | |
| if not loc: | |
| return "Europe/London" | |
| tf = TimezoneFinder() | |
| return tf.timezone_at(lng=loc.longitude, lat=loc.latitude) or "Europe/London" | |
| except Exception: | |
| return "Europe/London" | |
| def get_time_tool2(query: str) -> tuple[datetime, int, str]: | |
| try: | |
| location_prompt = f""" | |
| You are a location extractor. Given a user's query about time or date, return the location mentioned in it. | |
| If not found, return "London". | |
| Query: "{query}" | |
| """ | |
| location_response = llm_gpt4.invoke(location_prompt) | |
| location = location_response.content.strip() if isinstance(location_response, AIMessage) else str(location_response).strip() | |
| #print(f"[DEBUG] Extracted Location: {location}") | |
| tz_str = location_to_timezone(location) | |
| #print(f"[DEBUG] Timezone: {tz_str}") | |
| now = datetime.now(ZoneInfo(tz_str)) | |
| #print(f"[DEBUG] Local Time at {location}: {now}") | |
| examples = [ | |
| # Pure hourly relative expression | |
| ("five hours later", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 5"), | |
| ("later", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 2"), | |
| ("soon", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("shortly", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("after a while", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| # Expressions throughout the day (no specific time) | |
| ("today", f"START_TIME: {now.replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("tomorrow", f"START_TIME: {(now + timedelta(days=1)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("yesterday", f"START_TIME: {(now - timedelta(days=1)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("the day before yesterday", f"START_TIME: {(now - timedelta(days=2)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("the day after tomorrow", f"START_TIME: {(now + timedelta(days=2)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("昨天", f"START_TIME: {(now - timedelta(days=1)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("今天", f"START_TIME: {now.replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("明天", f"START_TIME: {(now + timedelta(days=1)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("前天", f"START_TIME: {(now - timedelta(days=2)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| # Specific time point (single point) | |
| ("tomorrow at 3pm", f"START_TIME: {(now + timedelta(days=1)).replace(hour=15,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("tomorrow at 10am", f"START_TIME: {(now + timedelta(days=1)).replace(hour=10,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("yesterday at 5pm", f"START_TIME: {(now - timedelta(days=1)).replace(hour=17,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("today at 6pm", f"START_TIME: {now.replace(hour=18,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("the day after tomorrow at 10am", f"START_TIME: {(now + timedelta(days=2)).replace(hour=10,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("昨天下午五點", f"START_TIME: {(now - timedelta(days=1)).replace(hour=17,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("昨天早上八點", f"START_TIME: {(now - timedelta(days=1)).replace(hour=8,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("下週一下午三點", f"START_TIME: {(now + timedelta(days=(7 - now.weekday() + 0) % 7)).replace(hour=15,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("last Monday 9am", f"START_TIME: {(now - timedelta(days=(now.weekday() + 7))).replace(hour=9,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| # Day of the week (all day) | |
| ("next Monday", f"START_TIME: {(now + timedelta(days=(7 - now.weekday()))).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("last Friday", f"START_TIME: {(now - timedelta(days=(now.weekday() - 4 + 7) % 7)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("next Friday", f"START_TIME: {(now + timedelta(days=(4 - now.weekday() + 7) % 7)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("this Monday", f"START_TIME: {(now - timedelta(days=now.weekday())).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("this Sunday", f"START_TIME: {(now + timedelta(days=(6 - now.weekday()))).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| # X days before/after (all day) | |
| ("5 days ago", f"START_TIME: {(now - timedelta(days=5)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("3 days ago", f"START_TIME: {(now - timedelta(days=3)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("in 5 days", f"START_TIME: {(now + timedelta(days=5)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| # Multi-day range | |
| ("past 3 days", f"START_TIME: {(now - timedelta(days=2)).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 72"), | |
| ("last 5 days", f"START_TIME: {(now - timedelta(days=4)).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 120"), | |
| ("next 7 days", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 168"), | |
| ("past 3 days", f"START_TIME: {(now - timedelta(days=3)).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 72"), | |
| ("last 5 days", f"START_TIME: {(now - timedelta(days=5)).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 120"), | |
| ("last 10 days", f"START_TIME: {(now - timedelta(days=10)).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 240"), | |
| # Weekly/Monthly Range | |
| ("this week", f"START_TIME: {(now - timedelta(days=now.weekday())).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 168"), | |
| ("last week", f"START_TIME: {(now - timedelta(days=now.weekday() + 7)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 168"), | |
| ("next week", f"START_TIME: {(now + timedelta(days=(7 - now.weekday()))).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 168"), | |
| ("last month", f"START_TIME: {(now - timedelta(days=30)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 720"), | |
| ("本週", f"START_TIME: {(now - timedelta(days=now.weekday())).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 168"), | |
| ("上週", f"START_TIME: {(now - timedelta(days=now.weekday() + 7)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 168"), | |
| # Past X hours | |
| ("過去 24 小時", f"START_TIME: {(now - timedelta(hours=24)).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 24"), | |
| ("past 48 hours", f"START_TIME: {(now - timedelta(hours=48)).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 48"), | |
| ("last 12 hours", f"START_TIME: {(now - timedelta(hours=12)).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 12"), | |
| # The next X hours | |
| ("in 10 hours", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 10"), | |
| ("in 2 hours", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 2"), | |
| ("in one hour", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("next 2 hours", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 2"), | |
| ("next 8 hours", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 8"), | |
| # A few minutes of expression (regarded as one hour) | |
| ("in 30 minutes", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("in a few minutes", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| # Special Segment | |
| ("later this evening", f"START_TIME: {now.replace(hour=20,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("this weekend", f"START_TIME: {(now + timedelta(days=(5 - now.weekday()) % 7)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 48"), | |
| ("next weekend", f"START_TIME: {(now + timedelta(days=((5 - now.weekday()) % 7) + 7)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 48"), | |
| ("tonight", f"START_TIME: {now.replace(hour=20,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 4"), | |
| ("this morning", f"START_TIME: {now.replace(hour=6,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 6"), | |
| ("this afternoon", f"START_TIME: {now.replace(hour=12,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 6"), | |
| # Current Time | |
| ("now", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("right now", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| ("現在", f"START_TIME: {now.strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 1"), | |
| # Scope of Expression | |
| ("from today 15:00 to 20:00", f"START_TIME: {now.replace(hour=15,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 5"), | |
| ("從今天下午 3 點到晚上 8 點", f"START_TIME: {now.replace(hour=15,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 5"), | |
| ("from tomorrow 14:00 to tomorrow 18:00", f"START_TIME: {(now + timedelta(days=1)).replace(hour=14,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 4"), | |
| ("from 3pm today to 2am tomorrow", f"START_TIME: {now.replace(hour=15,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 11"), | |
| ("from 4pm to 8pm today", f"START_TIME: {now.replace(hour=16,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 4"), | |
| ("Provide a 6-day weather summary for Tokyo ending today.", f"START_TIME: {(now - timedelta(days=5)).replace(hour=0,minute=0).strftime('%Y-%m-%d %H:%M')}\nDURATION_HOURS: 144"), | |
| ] | |
| # few‐shot prompt | |
| examples_header = f"""Assume the current local time in {location} is exactly: | |
| **{now.strftime('%Y-%m-%d %H:%M')}** (timezone: {tz_str}) | |
| Use this exact time to reason all examples below. | |
| """ | |
| examples_str = "\n".join([f'User Query: "{q}"\n→ {out}' for q, out in examples]) | |
| time_query_prompt = f""" | |
| You are a timezone-aware time reasoner. Based on the user's query, calculate: | |
| 1. START_TIME (format: YYYY-MM-DD HH:MM) in the local timezone. | |
| 2. DURATION_HOURS (integer hours) for how many hours this query spans. | |
| Examples: | |
| {examples_str} | |
| Now process: | |
| User Query: "{query}" | |
| → | |
| """ | |
| time_response = llm_gpt4.invoke(time_query_prompt) | |
| resp_lines = time_response.content.strip().splitlines() | |
| # # Parse START_TIME and DURATION_HOURS from the return. | |
| start_time = now | |
| duration_hours = 1 | |
| for line in resp_lines: | |
| if line.startswith("START_TIME:"): | |
| t_str = line.split(":", 1)[1].strip() | |
| try: | |
| start_time = datetime.strptime(t_str, "%Y-%m-%d %H:%M") | |
| start_time = start_time.replace(tzinfo=ZoneInfo(tz_str)) | |
| except: | |
| pass | |
| elif line.startswith("DURATION_HOURS:"): | |
| try: | |
| duration_hours = int(line.split(":", 1)[1].strip()) | |
| except: | |
| pass | |
| return start_time, duration_hours, location | |
| except Exception as e: | |
| # If parsing fails, fallback to "current time + 1 hour". | |
| tz_str_fallback = "Europe/London" | |
| try: | |
| tz_str_fallback = location_to_timezone(location) | |
| except: | |
| pass | |
| now = datetime.now(ZoneInfo(tz_str_fallback)) | |
| return now, 1, location | |
| def render_chart(df: pd.DataFrame, location: str, title: str = "Weather Forecast") -> str: | |
| try: | |
| chart_path = "/tmp/weather_chart.png" | |
| if os.path.exists(chart_path): | |
| os.remove(chart_path) | |
| # Converting time fields to datetime objects | |
| if "time" in df.columns: | |
| df_plot = df.copy() | |
| df_plot["datetime"] = pd.to_datetime(df_plot["time"]) | |
| # Create Submap | |
| fig, axes = plt.subplots(2, 2, figsize=(15, 10)) | |
| fig.suptitle(f"{title}", fontsize=16) | |
| # Temperature Chart | |
| if "temp_c" in df_plot.columns: | |
| axes[0, 0].plot(df_plot["datetime"], df_plot["temp_c"], | |
| marker='o', color='red', linewidth=2, markersize=4) | |
| axes[0, 0].set_title("Temperature (°C)") | |
| axes[0, 0].set_ylabel("Temperature (°C)") | |
| axes[0, 0].grid(True, alpha=0.3) | |
| axes[0, 0].tick_params(axis='x', rotation=45) | |
| # Humidity Chart | |
| if "humidity" in df_plot.columns: | |
| axes[0, 1].plot(df_plot["datetime"], df_plot["humidity"], | |
| marker='s', color='blue', linewidth=2, markersize=4) | |
| axes[0, 1].set_title("Humidity (%)") | |
| axes[0, 1].set_ylabel("Humidity (%)") | |
| axes[0, 1].grid(True, alpha=0.3) | |
| axes[0, 1].tick_params(axis='x', rotation=45) | |
| # Wind Speed Chart | |
| if "wind_kph" in df_plot.columns: | |
| axes[1, 0].plot(df_plot["datetime"], df_plot["wind_kph"], | |
| marker='^', color='green', linewidth=2, markersize=4) | |
| axes[1, 0].set_title("Wind Speed (kph)") | |
| axes[1, 0].set_ylabel("Wind Speed (kph)") | |
| axes[1, 0].grid(True, alpha=0.3) | |
| axes[1, 0].tick_params(axis='x', rotation=45) | |
| # Rainfall Probability Chart | |
| if "chance_of_rain" in df_plot.columns: | |
| axes[1, 1].plot(df_plot["datetime"], df_plot["chance_of_rain"], | |
| marker='d', color='purple', linewidth=2, markersize=4) | |
| axes[1, 1].set_title("Chance of Rain (%)") | |
| axes[1, 1].set_ylabel("Chance of Rain (%)") | |
| axes[1, 1].grid(True, alpha=0.3) | |
| axes[1, 1].tick_params(axis='x', rotation=45) | |
| for ax in axes.flat: | |
| if len(df_plot) > 1: | |
| # Adjust the timeline format according to the data range | |
| time_span = (df_plot["datetime"].iloc[-1] - df_plot["datetime"].iloc[0]).total_seconds() / 3600 | |
| if time_span <= 24: # In 24 hours, show hours | |
| ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) | |
| ax.xaxis.set_major_locator(mdates.HourLocator(interval=max(1, int(time_span/6)))) | |
| elif time_span <= 168: # Within one week, date and hour are displayed | |
| ax.xaxis.set_major_formatter(mdates.DateFormatter('%m/%d %H')) | |
| ax.xaxis.set_major_locator(mdates.HourLocator(interval=12)) | |
| else: # More than one week, only the date is displayed | |
| ax.xaxis.set_major_formatter(mdates.DateFormatter('%m/%d')) | |
| ax.xaxis.set_major_locator(mdates.DayLocator(interval=1)) | |
| timestamp = int(time.time()) | |
| plt.tight_layout() | |
| chart_path = "/tmp/weather_chart.png" | |
| plt.savefig(chart_path, dpi=300, bbox_inches='tight') | |
| print(f" Chart saved to: {chart_path}") | |
| plt.close() | |
| return chart_path | |
| else: | |
| # Returns an error if there is no time field | |
| return "Error: No time column found in DataFrame" | |
| except Exception as e: | |
| plt.close() # Ensure closure charts | |
| return f"Chart generation error: {str(e)}" | |
| def render_table(df: pd.DataFrame) -> str: | |
| try: | |
| if df.empty: | |
| return "No data available" | |
| # Create a more readable table format | |
| display_df = df.copy() | |
| # Rename fields to more friendly names | |
| column_mapping = { | |
| "time": "Time", | |
| "temp_c": "Temp (°C)", | |
| "feelslike_c": "Feels Like (°C)", | |
| "humidity": "Humidity (%)", | |
| "condition": "Condition", | |
| "chance_of_rain": "Rain (%)", | |
| "chance_of_snow": "Snow (%)", | |
| "wind_kph": "Wind (kph)", | |
| "uv": "UV Index", | |
| "cloud": "Cloud (%)", | |
| "vis_km": "Visibility (km)" | |
| } | |
| # Rename existing columns | |
| for old_name, new_name in column_mapping.items(): | |
| if old_name in display_df.columns: | |
| display_df = display_df.rename(columns={old_name: new_name}) | |
| # Retain 1 decimal place in the numeric field | |
| numeric_columns = display_df.select_dtypes(include=['float64', 'float32']).columns | |
| for col in numeric_columns: | |
| display_df[col] = display_df[col].round(1) | |
| # 使用 tabulate 創建完美對齊的表格 | |
| return tabulate( | |
| display_df, | |
| headers='keys', | |
| tablefmt='grid', | |
| showindex=False, | |
| numalign="center", | |
| stralign="center" | |
| ) | |
| except Exception as e: | |
| return f"Table generation error: {str(e)}" | |
| def render_text_summary(df: pd.DataFrame, location: str, time_type: str) -> str: | |
| try: | |
| lines = [f"Weather {time_type} summary for {location}:"] | |
| for _, row in df.iterrows(): | |
| if "time" in row and "temp_c" in row and "humidity" in row: | |
| time_str = row["time"] | |
| temp = row["temp_c"] | |
| humidity = row["humidity"] | |
| condition = row.get("condition", "N/A") | |
| lines.append(f"{time_str}: {temp}°C, {humidity}% humidity, {condition}") | |
| return "\n".join(lines) | |
| except Exception as e: | |
| return f"Text summary generation error: {str(e)}" | |
| def predict_weather_fallback(location: str, target_dt: datetime) -> dict: | |
| try: | |
| # Load previously trained model and metadata | |
| all_models = joblib.load("weather_multi_parameter_models.joblib") | |
| forecast_horizons = joblib.load("weather_forecast_horizons.joblib") | |
| # Get the actual weather of the location as input to the model | |
| weather_api_key = os.environ.get("WEATHER_API_KEY") | |
| current_url = f"http://api.weatherapi.com/v1/current.json?key={weather_api_key}&q={location}" | |
| current_data = requests.get(current_url).json() | |
| current = current_data["current"] | |
| # Prepare X_input: a DataFrame that is consistent with the "features" of the training | |
| X_input = pd.DataFrame([{ | |
| "temp": current["temp_c"], | |
| "humidity": current["humidity"], | |
| "pressure": current["pressure_mb"], | |
| "wind": current["wind_kph"] / 3.6, # km/h → m/s | |
| "hour": target_dt.hour, | |
| "day": target_dt.day, | |
| "month": target_dt.month, | |
| "day_of_week": target_dt.weekday(), | |
| "is_weekend": 1 if target_dt.weekday() >= 5 else 0 | |
| }]) | |
| # fix the offset-naive vs offset-aware bug | |
| # Use location to get the timezone string, then take the now_aware of the current "offset-aware" | |
| tz_str_model = location_to_timezone(location) | |
| now_aware = datetime.now(ZoneInfo(tz_str_model)) | |
| hours_ahead = int((target_dt - now_aware).total_seconds() / 3600) | |
| # Find the nearest trained model to hours_ahead horizon | |
| closest_horizon = min(forecast_horizons, key=lambda h: abs(h - hours_ahead)) | |
| # The corresponding sub-models are used to make their own predictions | |
| predictions = {} | |
| if closest_horizon in all_models["temperature"]: | |
| predictions["temp_c"] = float(all_models["temperature"][closest_horizon].predict(X_input)[0]) | |
| else: | |
| # If the model doesn't have the horizon you want, fallback to 0 or some other default value | |
| predictions["temp_c"] = current["temp_c"] | |
| if closest_horizon in all_models["humidity"]: | |
| predictions["humidity"] = float(all_models["humidity"][closest_horizon].predict(X_input)[0]) | |
| else: | |
| predictions["humidity"] = current["humidity"] | |
| if closest_horizon in all_models["pressure"]: | |
| predictions["pressure"] = float(all_models["pressure"][closest_horizon].predict(X_input)[0]) | |
| else: | |
| predictions["pressure"] = current["pressure_mb"] | |
| if closest_horizon in all_models["wind"]: | |
| pred_wind_ms = float(all_models["wind"][closest_horizon].predict(X_input)[0]) | |
| predictions["wind_kph"] = pred_wind_ms * 3.6 # m/s → km/h | |
| else: | |
| predictions["wind_kph"] = current["wind_kph"] | |
| # Use predicted temperature and humidity to extrapolate other parameters (rain or shine, cloud cover etc.) | |
| temp = predictions["temp_c"] | |
| humidity = predictions["humidity"] | |
| if humidity > 80: | |
| condition = "Cloudy" if temp < 25 else "Partly cloudy" | |
| chance_of_rain = 60 if humidity > 90 else 40 | |
| elif temp > 30: | |
| condition = "Sunny" | |
| chance_of_rain = 5 | |
| else: | |
| condition = "Clear" if humidity < 60 else "Partly cloudy" | |
| chance_of_rain = 20 | |
| # Assembles the complete dictionary to be returned to the Agent | |
| return { | |
| "temp_c": round(predictions["temp_c"], 1), | |
| "feelslike_c": round(predictions["temp_c"] - 2, 1), | |
| "humidity": round(predictions["humidity"]), | |
| "pressure": round(predictions["pressure"]), | |
| "wind_kph": round(predictions["wind_kph"], 1), | |
| "condition": condition, | |
| "chance_of_rain": chance_of_rain, | |
| "uv": 5, # fix | |
| "cloud": round(humidity * 0.8), | |
| "vis_km": 10 if humidity < 80 else 5 | |
| } | |
| except Exception as e: | |
| print(f"ML prediction error: {e}") | |
| # If the model or other link throws an exception, it returns a simple default prediction | |
| return { | |
| "temp_c": 25, | |
| "feelslike_c": 23, | |
| "humidity": 60, | |
| "pressure": 1013, | |
| "wind_kph": 15, | |
| "condition": "Partly cloudy", | |
| "chance_of_rain": 20, | |
| "uv": 5, | |
| "cloud": 40, | |
| "vis_km": 10 | |
| } | |
| def weather_agent_tool(query: str) -> str: | |
| try: | |
| weather_api_key = os.environ.get("WEATHER_API_KEY") | |
| if not weather_api_key: | |
| return "Weather API key not found. Please set WEATHER_API_KEY env variable." | |
| # Use get_time_tool2 to get (start_dt, duration_hours, location) | |
| time_result = get_time_tool2(query) | |
| if not isinstance(time_result, tuple) or len(time_result) != 3: | |
| return "Error in retrieving time information." | |
| start_dt, duration_hours, location = time_result | |
| tz_str = location_to_timezone(location) | |
| start_dt = start_dt.replace(tzinfo=ZoneInfo(tz_str)) | |
| now = datetime.now(ZoneInfo(tz_str)) | |
| end_dt = start_dt + timedelta(hours=duration_hours) | |
| if start_dt < now - timedelta(days=7): | |
| return "Only supports up to 7 days of historical data." | |
| if end_dt > now + timedelta(days=13): | |
| return "Only supports up to 13 days of future forecast." | |
| weather_data = [] | |
| current_time = start_dt | |
| while current_time <= end_dt: | |
| time_diff_hours = (current_time - now).total_seconds() / 3600 | |
| if time_diff_hours > 72: | |
| # Over the next 3 days, using ML modelling | |
| try: | |
| model_result = predict_weather_fallback(location, current_time) | |
| weather_point = { | |
| "time": current_time.strftime('%Y-%m-%d %H:%M'), | |
| "condition": model_result["condition"], | |
| "temp_c": model_result["temp_c"], | |
| "feelslike_c": model_result["feelslike_c"], | |
| "humidity": model_result["humidity"], | |
| "chance_of_rain": model_result.get("chance_of_rain", 0), | |
| "chance_of_snow": model_result.get("chance_of_snow", 0), | |
| "wind_kph": model_result.get("wind_kph", 0), | |
| "uv": model_result.get("uv", 0), | |
| "cloud": model_result.get("cloud", 0), | |
| "vis_km": model_result.get("vis_km", 0) | |
| } | |
| except Exception as e: | |
| # ML model failing fallback | |
| weather_point = { | |
| "time": current_time.strftime('%Y-%m-%d %H:%M'), | |
| "condition": "Partly cloudy", | |
| "temp_c": 20, | |
| "feelslike_c": 18, | |
| "humidity": 60, | |
| "chance_of_rain": 20, | |
| "chance_of_snow": 0, | |
| "wind_kph": 15, | |
| "uv": 5, | |
| "cloud": 40, | |
| "vis_km": 10 | |
| } | |
| else: | |
| # Use WeatherAPI within API support. | |
| try: | |
| if time_diff_hours < -168: | |
| current_time += timedelta(hours=1) | |
| continue | |
| elif time_diff_hours < -24: # Over 1 day ago with historical APIs | |
| url = f"http://api.weatherapi.com/v1/history.json?key={weather_api_key}&q={location}&dt={current_time.strftime('%Y-%m-%d')}" | |
| else: # Use the Predictive API for everything within 1 day (including now) | |
| url = f"http://api.weatherapi.com/v1/forecast.json?key={weather_api_key}&q={location}&days=3&aqi=no&alerts=no" | |
| data = requests.get(url).json() | |
| # print(f"[DEBUG] Processing time: {current_time}, time difference: {time_diff_hours:.1f}hour") | |
| # Collect all available hourly data | |
| forecast_hours = [] | |
| if "forecast" in data: | |
| for day in data["forecast"]["forecastday"]: | |
| for hour in day["hour"]: | |
| forecast_hours.append(hour) | |
| # Find the closest hour | |
| min_diff = float("inf") | |
| closest_hour = None | |
| for hour_data in forecast_hours: | |
| hour_dt = date_parser.parse(hour_data["time"]).replace(tzinfo=ZoneInfo(tz_str)) | |
| diff = abs((hour_dt - current_time).total_seconds()) | |
| if diff < min_diff: | |
| min_diff = diff | |
| closest_hour = hour_data | |
| if closest_hour: | |
| #print(f"[DEBUG] best match: Objectives{current_time.strftime('%H:%M')}, Selected{date_parser.parse(closest_hour['time']).strftime('%H:%M')}, Temperature{closest_hour['temp_c']}°C") | |
| weather_point = { | |
| "time": current_time.strftime('%Y-%m-%d %H:%M'), | |
| "condition": closest_hour["condition"]["text"], | |
| "temp_c": closest_hour["temp_c"], | |
| "feelslike_c": closest_hour["feelslike_c"], | |
| "humidity": closest_hour["humidity"], | |
| "chance_of_rain": closest_hour.get("chance_of_rain", 0), | |
| "chance_of_snow": closest_hour.get("chance_of_snow", 0), | |
| "wind_kph": closest_hour.get("wind_kph", 0), | |
| "uv": closest_hour.get("uv", 0), | |
| "cloud": closest_hour.get("cloud", 0), | |
| "vis_km": closest_hour.get("vis_km", 0) | |
| } | |
| else: | |
| # API fallback when no data is available | |
| weather_point = { | |
| "time": current_time.strftime('%Y-%m-%d %H:%M'), | |
| "condition": "No data", | |
| "temp_c": 20, | |
| "feelslike_c": 18, | |
| "humidity": 60, | |
| "chance_of_rain": 0, | |
| "chance_of_snow": 0, | |
| "wind_kph": 0, | |
| "uv": 0, | |
| "cloud": 0, | |
| "vis_km": 0 | |
| } | |
| except Exception as e: | |
| # fallback when the API fails | |
| weather_point = { | |
| "time": current_time.strftime('%Y-%m-%d %H:%M'), | |
| "condition": "API Error", | |
| "temp_c": 20, | |
| "feelslike_c": 18, | |
| "humidity": 60, | |
| "chance_of_rain": 0, | |
| "chance_of_snow": 0, | |
| "wind_kph": 0, | |
| "uv": 0, | |
| "cloud": 0, | |
| "vis_km": 0 | |
| } | |
| weather_data.append(weather_point) | |
| # Hourly Sampling | |
| current_time += timedelta(hours=1) | |
| # Formatted as a DataFrame | |
| df = pd.DataFrame(weather_data) | |
| # Step 5:Variables for Summary Prompt | |
| if duration_hours == 1: | |
| # Single point enquiry | |
| time_description = f"at a specific time: {start_dt.strftime('%Y-%m-%d %H:%M')} in {location}" | |
| if len(weather_data) > 0: | |
| wd = weather_data[0] | |
| weather_data_text = f"""Location: {location} | |
| Time: {start_dt.strftime('%Y-%m-%d')} at {start_dt.strftime('%H:%M')} | |
| Condition: {wd['condition']} | |
| Temperature: {wd['temp_c']}°C (Feels like {wd['feelslike_c']}°C) | |
| Humidity: {wd['humidity']}% | |
| Chance of rain: {wd['chance_of_rain']}% | |
| Chance of snow: {wd['chance_of_snow']}% | |
| Wind speed: {wd['wind_kph']} kph | |
| UV index: {wd['uv']} | |
| Cloud cover: {wd['cloud']}% | |
| Visibility: {wd['vis_km']} km""" | |
| else: | |
| weather_data_text = "No weather data available." | |
| else: | |
| # Range Enquiry - Using Tabular Format | |
| time_description = f"from {start_dt.strftime('%Y-%m-%d %H:%M')} to {end_dt.strftime('%Y-%m-%d %H:%M')} in {location}" | |
| weather_data_text = f"Location: {location}\n\nWeather Data Table:\n{df.to_string(index=False)}" | |
| summary_prompt = f""" | |
| You are a helpful weather reasoning assistant with intelligent output selection. | |
| The user wants to know about the weather conditions {time_description}. | |
| Use the data below to answer their question. This may refer to the past, present, or future — do not assume it is the current weather. | |
| Based on the following weather data and the user's question, think step-by-step to extract the most relevant information, and give a natural, friendly, and cautious answer in British English. | |
| Avoid being overly confident — never say "Yes, it will..." or "Definitely." Instead, use expressions like: | |
| - "It is very likely that..." | |
| - "There is a high chance of..." | |
| - "Based on the available data, it seems that..." | |
| - "There may be..." | |
| Also, after answering the question, include a short weather summary and a useful suggestion. | |
| **Do not use markdown formatting such as `*`, `**`, or list symbols.** | |
| --- Weather Data --- | |
| {weather_data_text} | |
| --- User Question --- | |
| {query} | |
| --- Final Answer --- | |
| First, provide your weather analysis and recommendations. | |
| Then, intelligently decide if the user would benefit from visual aids: | |
| **Add "chart: true" if:** | |
| - The query involves trends, changes over time, or comparisons | |
| - Multiple time periods are mentioned (e.g., "next 3 days", "this week") | |
| - The user asks about patterns, variations, or forecasts | |
| - Weather data spans several hours/days | |
| - Questions like "how will it change", "show me", "what's the trend" | |
| **Add "chart: true" if:** | |
| - Weather data spans MORE than 1 hour (time series visualization helpful) | |
| - ANY time range query (next 2 hours, today, tomorrow, this week, etc.) | |
| - Multiple time points are involved (even implicit ranges) | |
| - Trend analysis would be useful for the user | |
| - DEFAULT: If duration > 1 hour → ALWAYS add chart: true | |
| **Add "table: true" if:** | |
| - User wants comprehensive details, precise values, or reference data | |
| - Multiple weather parameters need exact numbers | |
| - Detailed breakdown is specifically requested | |
| **Single point queries (1 hour or specific moment):** | |
| - "Will it rain at 3pm tomorrow?" → neither (just text answer) | |
| - "Temperature right now in London" → neither (single value) | |
| **Time range queries (>1 hour):** | |
| - "Weather today" → chart: true (shows daily trend) | |
| - "Next 2 hours" → chart: true (shows progression) | |
| - "This weekend" → chart: true (trend visualization) | |
| - "Tomorrow" → chart: true (daily pattern) | |
| **Remember: Humans prefer visual information. When in doubt about time range, lean towards providing charts.** | |
| Think about what would be most helpful for the user, even if they didn't explicitly ask. | |
| """ | |
| response = llm_gpt4.invoke(summary_prompt) | |
| response_text = response.content.strip() if isinstance(response, AIMessage) else str(response) | |
| # Capture output mode (chart / table) | |
| def extract_output_mode(text: str) -> list[str]: | |
| modes = [] | |
| lower_text = text.lower() | |
| if "chart: true" in lower_text: | |
| modes.append("chart") | |
| if "table: true" in lower_text: | |
| modes.append("table") | |
| return modes | |
| output_modes = extract_output_mode(response_text) | |
| # Remove the chart/table prompt string and keep only the narrative. | |
| clean_response = response_text.split("chart:")[0].split("table:")[0].strip() | |
| final_response = clean_response | |
| # Inserting a Chart or Table (as indicated by LLM) | |
| if "chart" in output_modes: | |
| try: | |
| chart_path = render_chart(df, location, f"Weather Data for {location}") | |
| final_response += f"\n\nHere is your chart:\n{chart_path}" | |
| except Exception as e: | |
| final_response += f"\n\nChart generation failed: {e}" | |
| if "table" in output_modes: | |
| try: | |
| table_text = render_table(df) | |
| final_response += f"\n\nHere is your table:\n{table_text}" | |
| except Exception as e: | |
| final_response += f"\n\nTable generation failed: {e}" | |
| return final_response | |
| except Exception as e: | |
| return f"Weather Agent Error: {e}" | |
| if __name__ == "__main__": | |
| test_queries = [ | |
| "Will it rain in Tokyo tomorrow at 3pm?", | |
| "Show me the weather for the next 2 hours in London.", | |
| "What's the temperature in New York from 3pm today to 2am tomorrow?", | |
| "Generate a 3-day weather chart for Paris starting next Wednesday.", | |
| "Give me a table of average humidity for the next 5 days in Sydney.", | |
| "What was the average temperature in Kaohsiung 5 days ago?", | |
| "Provide a 6-day weather summary for Tokyo ending today.", | |
| "Will it rain in Taipei from 4pm to 8pm today?", | |
| "What is the average temperature in London for the next 8 days?", | |
| "How hot was it in New York yesterday at 5pm?" | |
| ] | |
| for q in test_queries: | |
| print("─" * 60) | |
| print("Query:", q) | |
| print("Response:") | |
| print(weather_agent_tool(q)) | |
| print("\n") |