📄 appointment.py 3,963 bytes Apr 23, 2026 📋 Raw

"""Appointment handler — Extract event → Calendar + Telegram."""

import logging
from typing import Dict, Any
from shared.llm import LLMClient
from shared.notify import TelegramNotifier
from family.calendar import CalendarClient
from family.email import EmailProcessor

logger = logging.getLogger(name)

class AppointmentHandler:
"""Handle appointment emails: extract event, create calendar, notify."""

def __init__(
    self,
    llm_client: LLMClient,
    calendar_client: CalendarClient,
    telegram: TelegramNotifier
):
    self.llm = llm_client
    self.calendar = calendar_client
    self.telegram = telegram
    self.email_processor = EmailProcessor(llm_client)

async def process(
    self,
    subject: str,
    body: str,
    sender: str,
    received_at: str
) -> Dict[str, Any]:
    """Process appointment email.

    Returns:
        Dict with event_created, notification_sent, parsed_event
    """
    result = {
        "type": "appointment",
        "event_created": False,
        "notification_sent": False,
        "parsed": {},
        "errors": []
    }

    # Extract event details
    parsed = await self.email_processor.extract_event(subject, body)
    if not parsed:
        result["errors"].append("LLM extraction failed")
        # Still notify about the email
        await self._notify_fallback(subject, body, sender)
        return result

    result["parsed"] = parsed

    # Skip low confidence
    if parsed.get("confidence", 0) < 0.5:
        logger.warning(f"Low confidence extraction: {parsed.get('confidence')}")
        result["errors"].append(f"Low confidence: {parsed.get('confidence')}")
        await self._notify_fallback(subject, body, sender, parsed)
        return result

    # Create calendar event
    calendar_result = await self.calendar.create_event(
        summary=parsed["summary"],
        start_datetime=parsed["start_datetime"],
        end_datetime=parsed["end_datetime"],
        description=parsed.get("description", f"From: {sender}\n\n{subject}"),
        location=parsed.get("location", "")
    )

    result["event_created"] = calendar_result.get("created", False)

    if not result["event_created"]:
        result["errors"].append(f"Calendar failed: {calendar_result.get('error', 'unknown')}")

    # Notify
    await self._notify(subject, parsed, result["event_created"])
    result["notification_sent"] = True

    return result

async def _notify(self, subject: str, parsed: Dict[str, Any], created: bool) -> None:
    """Send Telegram notification."""
    event_time = parsed.get("start_datetime", "unknown")
    msg = f"📅 <b>New Event</b>\n\n"
    msg += f"<b>{parsed['summary']}</b>\n"
    msg += f"🕐 {event_time}\n"
    if parsed.get("location"):
        msg += f"📍 {parsed['location']}\n"
    msg += f"\n{'✅ Added to family calendar' if created else '⚠️ Calendar failed — check manually'}"

    await self.telegram.to_family(msg)

async def _notify_fallback(
    self,
    subject: str,
    body: str,
    sender: str,
    parsed: Dict[str, Any] = None
) -> None:
    """Notify when extraction fails or confidence is low."""
    msg = f"📧 <b>Appointment Email</b>\n\n"
    msg += f"<b>{subject}</b>\n"
    msg += f"From: {sender}\n\n"
    if parsed:
        msg += f"⚠️ Low confidence extraction: {parsed.get('confidence', 'unknown')}\n"
    else:
        msg += f"⚠️ Could not extract event details\n"
    msg += f"Please check email and add manually if needed."

    await self.telegram.to_family(msg)