"""Blueprint that surfaces upcoming CS events, optionally filtered.""" from __future__ import annotations from datetime import datetime, timedelta from typing import Any, Dict, List from .base import AnalysisContext, Blueprint, BlueprintResult, Fact, normalize_datetime class UpcomingEventsBlueprint(Blueprint): """Return upcoming department events.""" name = "upcoming_events" def run(self, context: AnalysisContext, **kwargs: Any) -> BlueprintResult: keyword = (kwargs.get("keyword") or "").strip().lower() horizon_days = kwargs.get("horizon_days", 14) max_results = kwargs.get("max_results", 5) now = datetime.now() horizon = now + timedelta(days=horizon_days) filtered: List[Dict[str, Any]] = [] for event in context.events: start = event.get("start") if start and isinstance(start, str): start = normalize_datetime(start) if start and isinstance(start, datetime): if start < now or (horizon_days is not None and start > horizon): continue if keyword: haystack = " ".join( str(bit or "").lower() for bit in (event.get("title"), event.get("description"), event.get("tags")) ) if keyword not in haystack: continue filtered.append(event) filtered.sort(key=lambda evt: evt.get("start") or datetime.max) if max_results: filtered = filtered[:max_results] facts: List[Fact] = [] for event in filtered: start_value = event.get("start") if isinstance(start_value, datetime): start_serialized = start_value.isoformat() else: start_serialized = start_value facts.append( Fact( subject=event.get("title", "Untitled Event"), predicate="upcoming_event", value={ "title": event.get("title"), "start": start_serialized, "location": event.get("location"), "description": event.get("description"), "url": event.get("url"), }, source=event.get("source"), confidence=0.8, ) ) notes: List[str] = [] if not facts: if keyword: notes.append(f"No upcoming events matched keyword '{keyword}'.") else: notes.append("No upcoming events found in cache.") return BlueprintResult(self.name, kwargs, facts=facts, notes=notes)