Spaces:
Sleeping
Sleeping
| """LLM-backed agent and deterministic weather briefing helpers.""" | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import re | |
| from dataclasses import dataclass | |
| from typing import Any | |
| import pandas as pd | |
| from weatherpred.prompts import AGENT_RESPONSE_PROMPT, AGENT_SYSTEM_PROMPT | |
| from weatherpred.tools import clamp_horizon, normalize_variable, run_tool | |
| class AgentRun: | |
| response: str | |
| action: dict[str, Any] | |
| tool_result: dict[str, Any] | |
| comparison: pd.DataFrame | None = None | |
| def has_gemini_key() -> bool: | |
| return bool(os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")) | |
| def make_rule_based_brief(comparison: pd.DataFrame, summary: dict[str, Any]) -> str: | |
| warmest = comparison.sort_values("model_high_c", ascending=False).iloc[0] | |
| rainiest = comparison.sort_values("api_rain_mm", ascending=False).iloc[0] | |
| confidence = round(summary["avg_model_confidence"] * 100) | |
| return ( | |
| f"The ONNX model expects the warmest day around {warmest['date']} " | |
| f"at {warmest['model_high_c']:.1f}C, while Open-Meteo peaks at " | |
| f"{summary['max_api_high_c']:.1f}C. API rainfall is highest on " | |
| f"{rainiest['date']} at {rainiest['api_rain_mm']:.1f} mm. " | |
| f"The model confidence proxy averages {confidence}%, based on historical " | |
| "seasonal variability rather than a calibrated meteorological probability." | |
| ) | |
| def make_gemini_brief(comparison: pd.DataFrame, summary: dict[str, Any]) -> str: | |
| if not has_gemini_key(): | |
| return make_rule_based_brief(comparison, summary) | |
| try: | |
| from google import genai | |
| except ImportError: | |
| return make_rule_based_brief(comparison, summary) | |
| prompt = ( | |
| "Write a concise weather analyst briefing for Tokyo. Compare an ONNX " | |
| "model forecast against Open-Meteo API forecast. Mention uncertainty and " | |
| "avoid claiming this model is production-grade.\n\n" | |
| f"Summary: {json.dumps(summary)}\n" | |
| f"Forecast rows: {comparison.to_json(orient='records')}" | |
| ) | |
| try: | |
| client = genai.Client() | |
| response = client.models.generate_content( | |
| model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"), | |
| contents=prompt, | |
| ) | |
| return response.text or make_rule_based_brief(comparison, summary) | |
| except Exception: | |
| return make_rule_based_brief(comparison, summary) | |
| def extract_json_object(text: str) -> dict[str, Any] | None: | |
| try: | |
| return json.loads(text) | |
| except json.JSONDecodeError: | |
| pass | |
| match = re.search(r"\{.*\}", text, flags=re.DOTALL) | |
| if not match: | |
| return None | |
| try: | |
| return json.loads(match.group(0)) | |
| except json.JSONDecodeError: | |
| return None | |
| def fallback_action(user_message: str) -> dict[str, Any]: | |
| text = user_message.lower() | |
| horizon_match = re.search(r"(?:next|for)\s+(\d+)\s*(?:day|days)?", text) | |
| horizon = clamp_horizon(horizon_match.group(1) if horizon_match else None) | |
| if any(word in text for word in ["rain", "precip", "precipitation", "shower"]): | |
| variable = "rain" | |
| elif "wind" in text: | |
| variable = "wind" | |
| elif any(word in text for word in ["temperature", "temp", "hot", "cold", "high", "low"]): | |
| variable = "temperature" | |
| else: | |
| variable = "all" | |
| if any(word in text for word in ["season", "seasonal", "cycle", "cyclic", "monthly", "summer", "winter"]): | |
| tool = "analyze_seasonality" | |
| elif any(word in text for word in ["trend", "past", "historical", "history", "30 years", "long-term"]): | |
| tool = "analyze_historical_trend" | |
| elif any(word in text for word in ["model", "onnx", "confidence", "method", "how", "source", "data"]): | |
| tool = "explain_model" | |
| elif any(word in text for word in ["compare", "difference", "versus", "vs"]): | |
| tool = "compare_model_vs_open_meteo" | |
| elif horizon_match or any(word in text for word in ["predict", "forecast", "next"]): | |
| tool = "forecast_next_days" | |
| else: | |
| tool = "show_dashboard_view" | |
| return {"tool": tool, "variable": variable, "horizon_days": horizon, "reason": "fallback parser"} | |
| def infer_action(user_message: str) -> dict[str, Any]: | |
| if not has_gemini_key(): | |
| return fallback_action(user_message) | |
| try: | |
| from google import genai | |
| client = genai.Client() | |
| prompt = f"{AGENT_SYSTEM_PROMPT}\n\nUser request: {user_message}" | |
| response = client.models.generate_content( | |
| model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"), | |
| contents=prompt, | |
| ) | |
| action = extract_json_object(response.text or "") | |
| if action and "tool" in action: | |
| action["variable"] = normalize_variable(action.get("variable")) | |
| action["horizon_days"] = clamp_horizon(action.get("horizon_days")) | |
| return action | |
| except Exception: | |
| pass | |
| return fallback_action(user_message) | |
| def deterministic_agent_response(action: dict[str, Any], tool_result: dict[str, Any]) -> str: | |
| tool = tool_result.get("tool", action.get("tool")) | |
| summary = tool_result.get("summary", {}) | |
| variable = tool_result.get("variable", action.get("variable", "all")) | |
| if tool == "forecast_next_days": | |
| return ( | |
| f"Showing the next {tool_result['horizon_days']} days for {variable}. " | |
| "Open-Meteo is shown beside the local model. Note: ONNX directly predicts five-day max temperature; " | |
| "longer horizons, rain, wind, and low temperature use historical seasonal statistics." | |
| ) | |
| if tool == "compare_model_vs_open_meteo": | |
| return f"Model vs Open-Meteo comparison for {variable}: {json.dumps(summary.get('average_absolute_differences', {}))}." | |
| if tool == "analyze_historical_trend": | |
| return ( | |
| f"Historical {variable} trend from {summary['history_start']} to {summary['history_end']}: " | |
| f"the recent five-year mean is {summary['last_5_year_mean']} {summary['unit']}, " | |
| f"versus {summary['first_5_year_mean']} {summary['unit']} in the first five years. " | |
| f"Change: {summary['change_last_vs_first']} {summary['unit']}." | |
| ) | |
| if tool == "analyze_seasonality": | |
| return ( | |
| f"Seasonality for {variable}: peak month is {summary['peak_month']} " | |
| f"at {summary['peak_value']} {summary['unit']}; lowest month is {summary['low_month']} " | |
| f"at {summary['low_value']} {summary['unit']}. Seasonal amplitude is " | |
| f"{summary['seasonal_amplitude']} {summary['unit']}." | |
| ) | |
| if tool == "explain_model": | |
| return " ".join(str(value) for value in summary.values()) | |
| if tool == "show_dashboard_view": | |
| return f"{variable.title()} is available in {summary['view']}." | |
| return "I handled the request with the weather dashboard tools." | |
| def summarize_agent_response(user_message: str, action: dict[str, Any], tool_result: dict[str, Any]) -> str: | |
| if not has_gemini_key(): | |
| return deterministic_agent_response(action, tool_result) | |
| try: | |
| from google import genai | |
| safe_result = {key: value for key, value in tool_result.items() if key != "comparison"} | |
| prompt = AGENT_RESPONSE_PROMPT.format( | |
| user_message=user_message, | |
| tool_result=json.dumps(safe_result, default=str), | |
| ) | |
| client = genai.Client() | |
| response = client.models.generate_content( | |
| model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"), | |
| contents=prompt, | |
| ) | |
| return response.text or deterministic_agent_response(action, tool_result) | |
| except Exception: | |
| return deterministic_agent_response(action, tool_result) | |
| def run_weather_agent(user_message: str) -> AgentRun: | |
| action = infer_action(user_message) | |
| tool_result = run_tool(action) | |
| response = summarize_agent_response(user_message, action, tool_result) | |
| comparison = tool_result.get("comparison") | |
| if not isinstance(comparison, pd.DataFrame): | |
| comparison = None | |
| return AgentRun( | |
| response=response, | |
| action=action, | |
| tool_result=tool_result, | |
| comparison=comparison, | |
| ) | |