Spaces:
Sleeping
Sleeping
File size: 8,200 Bytes
5194558 | 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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | """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
@dataclass
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,
)
|