""" Timers and Reminders Tool - Create and manage timers and reminders. """ import sqlite3 import threading import time from datetime import datetime, timedelta from pathlib import Path from typing import Any, Dict, List, Optional from tools.base import BaseTool # Database file location DB_PATH = Path(__file__).parent.parent.parent / "data" / "timers.db" class TimerService: """Service for managing timers and reminders.""" def __init__(self, db_path: Path = DB_PATH): """Initialize timer service with database.""" self.db_path = db_path self.db_path.parent.mkdir(parents=True, exist_ok=True) self._init_db() self._running = False self._thread: Optional[threading.Thread] = None self._lock = threading.Lock() def _init_db(self): """Initialize database schema.""" conn = sqlite3.connect(str(self.db_path)) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS timers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL, -- 'timer' or 'reminder' duration_seconds INTEGER, -- For timers target_time TEXT, -- ISO format for reminders message TEXT, status TEXT DEFAULT 'active', -- 'active', 'completed', 'cancelled' created_at TEXT NOT NULL, completed_at TEXT ) """) conn.commit() conn.close() def create_timer(self, name: str, duration_seconds: int, message: str = "") -> int: """Create a new timer.""" target_time = datetime.now() + timedelta(seconds=duration_seconds) conn = sqlite3.connect(str(self.db_path)) cursor = conn.cursor() cursor.execute(""" INSERT INTO timers (name, type, duration_seconds, target_time, message, created_at) VALUES (?, ?, ?, ?, ?, ?) """, (name, "timer", duration_seconds, target_time.isoformat(), message, datetime.now().isoformat())) timer_id = cursor.lastrowid conn.commit() conn.close() self._start_if_needed() return timer_id def create_reminder(self, name: str, target_time: datetime, message: str = "") -> int: """Create a new reminder.""" conn = sqlite3.connect(str(self.db_path)) cursor = conn.cursor() cursor.execute(""" INSERT INTO timers (name, type, target_time, message, created_at) VALUES (?, ?, ?, ?, ?) """, (name, "reminder", target_time.isoformat(), message, datetime.now().isoformat())) reminder_id = cursor.lastrowid conn.commit() conn.close() self._start_if_needed() return reminder_id def list_timers(self, status: Optional[str] = None, timer_type: Optional[str] = None) -> List[Dict[str, Any]]: """List timers/reminders, optionally filtered by status or type.""" conn = sqlite3.connect(str(self.db_path)) conn.row_factory = sqlite3.Row cursor = conn.cursor() query = "SELECT * FROM timers WHERE 1=1" params = [] if status: query += " AND status = ?" params.append(status) if timer_type: query += " AND type = ?" params.append(timer_type) query += " ORDER BY created_at DESC" cursor.execute(query, params) rows = cursor.fetchall() conn.close() return [dict(row) for row in rows] def cancel_timer(self, timer_id: int) -> bool: """Cancel a timer or reminder.""" conn = sqlite3.connect(str(self.db_path)) cursor = conn.cursor() cursor.execute(""" UPDATE timers SET status = 'cancelled' WHERE id = ? AND status = 'active' """, (timer_id,)) updated = cursor.rowcount > 0 conn.commit() conn.close() return updated def _start_if_needed(self): """Start background thread if not already running.""" with self._lock: if not self._running: self._running = True self._thread = threading.Thread(target=self._check_timers, daemon=True) self._thread.start() def _check_timers(self): """Background thread to check and trigger timers.""" while self._running: try: now = datetime.now() conn = sqlite3.connect(str(self.db_path)) cursor = conn.cursor() # Find active timers/reminders that should trigger cursor.execute(""" SELECT id, name, type, message, target_time FROM timers WHERE status = 'active' AND target_time <= ? """, (now.isoformat(),)) timers_to_trigger = cursor.fetchall() for timer_id, name, timer_type, message, target_time_str in timers_to_trigger: # Mark as completed cursor.execute(""" UPDATE timers SET status = 'completed', completed_at = ? WHERE id = ? """, (now.isoformat(), timer_id)) # Log the trigger (in production, this would send notification) print(f"[TIMER TRIGGERED] {timer_type}: {name}") if message: print(f" Message: {message}") conn.commit() conn.close() except Exception as e: print(f"Error checking timers: {e}") # Check every 5 seconds time.sleep(5) # Global timer service instance _timer_service = TimerService() class TimersTool(BaseTool): """Tool for managing timers and reminders.""" @property def name(self) -> str: return "create_timer" @property def description(self) -> str: return "Create a timer that will trigger after a specified duration. Returns timer ID." def get_schema(self) -> Dict[str, Any]: """Get tool schema.""" return { "name": self.name, "description": self.description, "inputSchema": { "type": "object", "properties": { "name": { "type": "string", "description": "Name or description of the timer" }, "duration_seconds": { "type": "integer", "description": "Duration in seconds (e.g., 300 for 5 minutes)" }, "message": { "type": "string", "description": "Optional message to display when timer triggers" } }, "required": ["name", "duration_seconds"] } } def execute(self, arguments: Dict[str, Any]) -> str: """Execute create_timer tool.""" name = arguments.get("name", "").strip() duration_seconds = arguments.get("duration_seconds") message = arguments.get("message", "").strip() if not name: raise ValueError("Missing required argument: name") if duration_seconds is None: raise ValueError("Missing required argument: duration_seconds") if duration_seconds <= 0: raise ValueError("duration_seconds must be positive") timer_id = _timer_service.create_timer(name, duration_seconds, message) # Format duration nicely if duration_seconds < 60: duration_str = f"{duration_seconds} seconds" elif duration_seconds < 3600: minutes = duration_seconds // 60 seconds = duration_seconds % 60 duration_str = f"{minutes} minute{'s' if minutes != 1 else ''}" if seconds > 0: duration_str += f" {seconds} second{'s' if seconds != 1 else ''}" else: hours = duration_seconds // 3600 minutes = (duration_seconds % 3600) // 60 duration_str = f"{hours} hour{'s' if hours != 1 else ''}" if minutes > 0: duration_str += f" {minutes} minute{'s' if minutes != 1 else ''}" return f"Timer '{name}' created (ID: {timer_id}). Will trigger in {duration_str}." class RemindersTool(BaseTool): """Tool for creating reminders at specific times.""" @property def name(self) -> str: return "create_reminder" @property def description(self) -> str: return "Create a reminder that will trigger at a specific date and time. Returns reminder ID." def get_schema(self) -> Dict[str, Any]: """Get tool schema.""" return { "name": self.name, "description": self.description, "inputSchema": { "type": "object", "properties": { "name": { "type": "string", "description": "Name or description of the reminder" }, "target_time": { "type": "string", "description": "Target date and time in ISO format (YYYY-MM-DDTHH:MM:SS) or relative (e.g., 'in 2 hours', 'tomorrow at 3pm')" }, "message": { "type": "string", "description": "Optional message to display when reminder triggers" } }, "required": ["name", "target_time"] } } def _parse_time(self, time_str: str) -> datetime: """Parse time string (ISO format or relative).""" time_str = time_str.strip().lower() # Try ISO format first try: return datetime.fromisoformat(time_str.replace('Z', '+00:00')) except ValueError: pass # Simple relative parsing (can be enhanced) now = datetime.now() if time_str.startswith("in "): # Parse "in X hours/minutes" parts = time_str[3:].split() if len(parts) >= 2: try: amount = int(parts[0]) unit = parts[1] if "hour" in unit: return now + timedelta(hours=amount) elif "minute" in unit: return now + timedelta(minutes=amount) elif "day" in unit: return now + timedelta(days=amount) except ValueError: pass raise ValueError(f"Could not parse time: {time_str}. Use ISO format (YYYY-MM-DDTHH:MM:SS) or relative (e.g., 'in 2 hours')") def execute(self, arguments: Dict[str, Any]) -> str: """Execute create_reminder tool.""" name = arguments.get("name", "").strip() target_time_str = arguments.get("target_time", "").strip() message = arguments.get("message", "").strip() if not name: raise ValueError("Missing required argument: name") if not target_time_str: raise ValueError("Missing required argument: target_time") try: target_time = self._parse_time(target_time_str) except ValueError as e: raise ValueError(f"Invalid time format: {e}") if target_time <= datetime.now(): raise ValueError("Target time must be in the future") reminder_id = _timer_service.create_reminder(name, target_time, message) return f"Reminder '{name}' created (ID: {reminder_id}). Will trigger at {target_time.strftime('%Y-%m-%d %H:%M:%S')}." class ListTimersTool(BaseTool): """Tool for listing timers and reminders.""" @property def name(self) -> str: return "list_timers" @property def description(self) -> str: return "List all timers and reminders, optionally filtered by status (active, completed, cancelled) or type (timer, reminder)." def get_schema(self) -> Dict[str, Any]: """Get tool schema.""" return { "name": self.name, "description": self.description, "inputSchema": { "type": "object", "properties": { "status": { "type": "string", "description": "Filter by status: 'active', 'completed', 'cancelled'", "enum": ["active", "completed", "cancelled"] }, "type": { "type": "string", "description": "Filter by type: 'timer' or 'reminder'", "enum": ["timer", "reminder"] } } } } def execute(self, arguments: Dict[str, Any]) -> str: """Execute list_timers tool.""" status = arguments.get("status") timer_type = arguments.get("type") timers = _timer_service.list_timers(status=status, timer_type=timer_type) if not timers: filter_str = "" if status: filter_str += f" with status '{status}'" if timer_type: filter_str += f" of type '{timer_type}'" return f"No timers or reminders found{filter_str}." result = f"Found {len(timers)} timer(s)/reminder(s):\n\n" for timer in timers: timer_id = timer['id'] name = timer['name'] timer_type_str = timer['type'] status_str = timer['status'] target_time = datetime.fromisoformat(timer['target_time']) message = timer.get('message', '') result += f"ID {timer_id}: {timer_type_str.title()} '{name}'\n" result += f" Status: {status_str}\n" result += f" Target: {target_time.strftime('%Y-%m-%d %H:%M:%S')}\n" if message: result += f" Message: {message}\n" result += "\n" return result.strip() class CancelTimerTool(BaseTool): """Tool for cancelling timers and reminders.""" @property def name(self) -> str: return "cancel_timer" @property def description(self) -> str: return "Cancel an active timer or reminder by ID." def get_schema(self) -> Dict[str, Any]: """Get tool schema.""" return { "name": self.name, "description": self.description, "inputSchema": { "type": "object", "properties": { "timer_id": { "type": "integer", "description": "ID of the timer or reminder to cancel" } }, "required": ["timer_id"] } } def execute(self, arguments: Dict[str, Any]) -> str: """Execute cancel_timer tool.""" timer_id = arguments.get("timer_id") if timer_id is None: raise ValueError("Missing required argument: timer_id") if _timer_service.cancel_timer(timer_id): return f"Timer/reminder {timer_id} cancelled successfully." else: return f"Timer/reminder {timer_id} not found or already completed/cancelled."