adding ai summary - default is disabled

This commit is contained in:
2026-01-26 17:13:11 -06:00
parent 2820944ec6
commit 921b6a81a4
13 changed files with 1357 additions and 43 deletions

74
app/models/ai_summary.py Normal file
View File

@@ -0,0 +1,74 @@
"""Data models for AI summarization feature."""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
@dataclass
class ForecastContext:
"""Forecast data to include in AI summary prompt."""
start_time: str
end_time: str
min_temp: float
max_temp: float
max_wind_speed: float
max_wind_gust: float
max_precip_prob: float
conditions: list[str] = field(default_factory=list)
def to_prompt_text(self) -> str:
"""Format forecast context for LLM prompt."""
unique_conditions = list(dict.fromkeys(self.conditions))
conditions_str = ", ".join(unique_conditions[:5]) if unique_conditions else "N/A"
return (
f"Period: {self.start_time} to {self.end_time}\n"
f"Temperature: {self.min_temp:.0f}F - {self.max_temp:.0f}F\n"
f"Wind: up to {self.max_wind_speed:.0f} mph, gusts to {self.max_wind_gust:.0f} mph\n"
f"Precipitation probability: up to {self.max_precip_prob:.0f}%\n"
f"Conditions: {conditions_str}"
)
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for serialization."""
return {
"start_time": self.start_time,
"end_time": self.end_time,
"min_temp": self.min_temp,
"max_temp": self.max_temp,
"max_wind_speed": self.max_wind_speed,
"max_wind_gust": self.max_wind_gust,
"max_precip_prob": self.max_precip_prob,
"conditions": self.conditions,
}
@dataclass
class SummaryNotification:
"""AI-generated weather alert summary notification."""
title: str
message: str
location: str
alert_count: int
has_changes: bool
created_at: datetime = field(default_factory=datetime.now)
@property
def tags(self) -> list[str]:
"""Get notification tags for AI summary."""
return ["robot", "weather", "summary"]
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for serialization."""
return {
"title": self.title,
"message": self.message,
"location": self.location,
"alert_count": self.alert_count,
"has_changes": self.has_changes,
"created_at": self.created_at.isoformat(),
"tags": self.tags,
}

View File

@@ -1,8 +1,8 @@
"""State management models for alert deduplication."""
"""State management models for alert deduplication and change detection."""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
from typing import Any, Optional
@dataclass
@@ -41,12 +41,59 @@ class SentAlertRecord:
)
@dataclass
class AlertSnapshot:
"""Snapshot of an alert for change detection between runs."""
alert_type: str
extreme_value: float
threshold: float
start_time: str
end_time: str
hour_count: int
captured_at: datetime = field(default_factory=datetime.now)
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
"alert_type": self.alert_type,
"extreme_value": self.extreme_value,
"threshold": self.threshold,
"start_time": self.start_time,
"end_time": self.end_time,
"hour_count": self.hour_count,
"captured_at": self.captured_at.isoformat(),
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "AlertSnapshot":
"""Create from dictionary.
Args:
data: The serialized snapshot dict.
Returns:
An AlertSnapshot instance.
"""
return cls(
alert_type=data["alert_type"],
extreme_value=data["extreme_value"],
threshold=data["threshold"],
start_time=data["start_time"],
end_time=data["end_time"],
hour_count=data["hour_count"],
captured_at=datetime.fromisoformat(data["captured_at"]),
)
@dataclass
class AlertState:
"""State container for tracking sent alerts."""
"""State container for tracking sent alerts and change detection."""
sent_alerts: dict[str, SentAlertRecord] = field(default_factory=dict)
last_updated: datetime = field(default_factory=datetime.now)
previous_alert_snapshots: dict[str, AlertSnapshot] = field(default_factory=dict)
last_ai_summary_sent: Optional[datetime] = None
def is_duplicate(self, dedup_key: str) -> bool:
"""Check if an alert with this dedup key has already been sent.
@@ -106,6 +153,15 @@ class AlertState:
key: record.to_dict() for key, record in self.sent_alerts.items()
},
"last_updated": self.last_updated.isoformat(),
"previous_alert_snapshots": {
key: snapshot.to_dict()
for key, snapshot in self.previous_alert_snapshots.items()
},
"last_ai_summary_sent": (
self.last_ai_summary_sent.isoformat()
if self.last_ai_summary_sent
else None
),
}
@classmethod
@@ -130,4 +186,21 @@ class AlertState:
else datetime.now()
)
return cls(sent_alerts=sent_alerts, last_updated=last_updated)
previous_alert_snapshots = {
key: AlertSnapshot.from_dict(snapshot_data)
for key, snapshot_data in data.get("previous_alert_snapshots", {}).items()
}
last_ai_summary_str = data.get("last_ai_summary_sent")
last_ai_summary_sent = (
datetime.fromisoformat(last_ai_summary_str)
if last_ai_summary_str
else None
)
return cls(
sent_alerts=sent_alerts,
last_updated=last_updated,
previous_alert_snapshots=previous_alert_snapshots,
last_ai_summary_sent=last_ai_summary_sent,
)