File size: 12,284 Bytes
74f6db0
4d20cf7
 
 
74f6db0
 
 
 
4d20cf7
74f6db0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d20cf7
74f6db0
 
 
 
 
4d20cf7
 
 
 
74f6db0
 
 
 
 
 
 
 
 
4d20cf7
74f6db0
 
 
4d20cf7
 
 
 
74f6db0
 
 
 
4d20cf7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74f6db0
 
 
 
 
4d20cf7
74f6db0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d20cf7
74f6db0
 
 
 
 
4d20cf7
74f6db0
 
 
4d20cf7
74f6db0
 
 
 
 
 
 
4d20cf7
74f6db0
 
 
 
 
 
 
 
 
 
 
 
 
4d20cf7
 
74f6db0
 
 
 
 
 
 
 
 
 
 
 
 
4d20cf7
 
74f6db0
 
4d20cf7
74f6db0
 
4d20cf7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74f6db0
 
4d20cf7
74f6db0
4d20cf7
 
74f6db0
 
 
 
 
 
 
 
 
 
 
 
 
4d20cf7
 
74f6db0
 
 
 
 
 
 
4d20cf7
 
74f6db0
 
 
 
4d20cf7
 
74f6db0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d20cf7
 
74f6db0
 
4d20cf7
74f6db0
 
 
 
 
 
 
 
4d20cf7
 
 
 
 
 
 
74f6db0
 
4d20cf7
74f6db0
 
4d20cf7
74f6db0
 
 
4d20cf7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74f6db0
 
4d20cf7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
"""
Enhanced Timer Extension
Set and track timers/countdowns with proactive notifications
Now with better state management and orchestrator integration
"""

from base_extension import BaseExtension
from google.genai import types
from typing import Dict, Any, List, Optional
import datetime
import uuid


class TimerExtension(BaseExtension):
    
    @property
    def name(self) -> str:
        return "timer"
    
    @property
    def display_name(self) -> str:
        return "Timer & Reminders"
    
    @property
    def description(self) -> str:
        return "Set timers and reminders for time-sensitive tasks with automatic notifications"
    
    @property
    def icon(self) -> str:
        return "⏰"
    
    @property
    def version(self) -> str:
        return "2.0.0"
    
    def get_system_context(self) -> str:
        return """
You have access to a Timer system for tracking time-sensitive activities.

You can:
- Set timers with a name and duration (in minutes, hours, or days)
- List active timers and check elapsed time
- Cancel timers
- Calculate remaining time
- Get automatic notifications when timers complete

When users mention time-sensitive activities (fermentation stages, cooking, steeping,
reminders, etc.), offer to set timers. Be proactive about time management.

IMPORTANT: Timers persist across conversations! When users ask about "the timer" or 
"my timers", they're referring to timers created earlier. Always use list_timers to 
check existing timers before assuming none exist.
"""
    
    def _get_default_state(self) -> Dict[str, Any]:
        return {
            "timers": [],
            "total_created": 0,
            "total_completed": 0,
            "created_at": datetime.datetime.now().isoformat(),
            "last_updated": datetime.datetime.now().isoformat()
        }
    
    def get_state_summary(self, user_id: str) -> Optional[str]:
        """Provide state summary for system prompt"""
        state = self.get_state(user_id)
        active_timers = [t for t in state.get("timers", []) if t.get("active", False)]
        if active_timers:
            return f"{len(active_timers)} active timer(s)"
        return None
    
    def get_metrics(self, user_id: str) -> Dict[str, Any]:
        """Provide usage metrics"""
        state = self.get_state(user_id)
        return {
            "total_created": state.get("total_created", 0),
            "total_completed": state.get("total_completed", 0),
            "currently_active": len([t for t in state.get("timers", []) if t.get("active", False)])
        }
    
    def get_tools(self) -> List[types.Tool]:
        set_timer = types.FunctionDeclaration(
            name="set_timer",
            description="Set a new timer with a name and duration",
            parameters={
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "Timer name/description"},
                    "duration_minutes": {
                        "type": "number",
                        "description": "Duration in minutes (can be fractional, e.g., 1440 for 1 day)"
                    }
                },
                "required": ["name", "duration_minutes"]
            }
        )
        
        list_timers = types.FunctionDeclaration(
            name="list_timers",
            description="List all active timers with elapsed and remaining time. ALWAYS call this when user asks about 'the timer' or 'my timers'.",
            parameters={"type": "object", "properties": {}}
        )
        
        check_timer = types.FunctionDeclaration(
            name="check_timer",
            description="Check status of a specific timer by ID",
            parameters={
                "type": "object",
                "properties": {
                    "timer_id": {"type": "string", "description": "Timer ID from list_timers"}
                },
                "required": ["timer_id"]
            }
        )
        
        cancel_timer = types.FunctionDeclaration(
            name="cancel_timer",
            description="Cancel/delete a timer by ID",
            parameters={
                "type": "object",
                "properties": {
                    "timer_id": {"type": "string", "description": "Timer ID to cancel"}
                },
                "required": ["timer_id"]
            }
        )
        
        return [types.Tool(function_declarations=[
            set_timer, list_timers, check_timer, cancel_timer
        ])]
    
    def _execute_tool(self, user_id: str, tool_name: str, args: Dict[str, Any]) -> Any:
        """Execute tool logic"""
        state = self.get_state(user_id)
        
        if tool_name == "set_timer":
            start_time = datetime.datetime.now()
            duration_mins = args["duration_minutes"]
            end_time = start_time + datetime.timedelta(minutes=duration_mins)
            
            timer = {
                "id": str(uuid.uuid4())[:8],
                "name": args["name"],
                "start_time": start_time.isoformat(),
                "end_time": end_time.isoformat(),
                "duration_minutes": duration_mins,
                "active": True,
                "notified": False
            }
            state["timers"].append(timer)
            state["total_created"] = state.get("total_created", 0) + 1
            self.update_state(user_id, state)
            
            # Log activity
            self.log_activity(user_id, "timer_created", {
                "timer_id": timer["id"],
                "name": timer["name"],
                "duration": duration_mins
            })
            
            # Format duration for display
            if duration_mins < 60:
                duration_str = f"{duration_mins} minutes"
            elif duration_mins < 1440:
                hours = duration_mins / 60
                duration_str = f"{hours:.1f} hours"
            else:
                days = duration_mins / 1440
                duration_str = f"{days:.1f} days"
            
            return {
                "success": True,
                "message": f"⏰ Timer set: '{timer['name']}' for {duration_str}",
                "timer_id": timer["id"],
                "end_time": end_time.strftime("%Y-%m-%d %H:%M:%S"),
                "duration_display": duration_str
            }
        
        elif tool_name == "list_timers":
            now = datetime.datetime.now()
            active_timers = []
            
            for timer in state["timers"]:
                if not timer["active"]:
                    continue
                
                start = datetime.datetime.fromisoformat(timer["start_time"])
                end = datetime.datetime.fromisoformat(timer["end_time"])
                
                elapsed = (now - start).total_seconds() / 60
                remaining = (end - now).total_seconds() / 60
                
                active_timers.append({
                    "id": timer["id"],
                    "name": timer["name"],
                    "elapsed_minutes": round(elapsed, 1),
                    "remaining_minutes": round(remaining, 1),
                    "is_complete": remaining <= 0,
                    "end_time": timer["end_time"],
                    "percentage_complete": min(100, (elapsed / timer["duration_minutes"]) * 100)
                })
            
            return {
                "active_timers": active_timers,
                "count": len(active_timers),
                "message": f"Found {len(active_timers)} active timer(s)" if active_timers else "No active timers"
            }
        
        elif tool_name == "check_timer":
            timer_id = args["timer_id"]
            now = datetime.datetime.now()
            
            for timer in state["timers"]:
                if timer["id"] == timer_id and timer["active"]:
                    start = datetime.datetime.fromisoformat(timer["start_time"])
                    end = datetime.datetime.fromisoformat(timer["end_time"])
                    
                    elapsed = (now - start).total_seconds() / 60
                    remaining = (end - now).total_seconds() / 60
                    
                    return {
                        "id": timer["id"],
                        "name": timer["name"],
                        "elapsed_minutes": round(elapsed, 1),
                        "remaining_minutes": round(remaining, 1),
                        "is_complete": remaining <= 0,
                        "percentage_complete": min(100, (elapsed / timer["duration_minutes"]) * 100),
                        "end_time": timer["end_time"]
                    }
            
            return {"success": False, "error": f"Timer {timer_id} not found"}
        
        elif tool_name == "cancel_timer":
            timer_id = args["timer_id"]
            
            for timer in state["timers"]:
                if timer["id"] == timer_id:
                    timer["active"] = False
                    self.update_state(user_id, state)
                    
                    # Log activity
                    self.log_activity(user_id, "timer_cancelled", {
                        "timer_id": timer_id,
                        "name": timer["name"]
                    })
                    
                    return {
                        "success": True,
                        "message": f"✅ Cancelled timer: {timer['name']}"
                    }
            
            return {"success": False, "error": f"Timer {timer_id} not found"}
        
        return {"error": f"Unknown tool: {tool_name}"}
    
    def get_proactive_message(self, user_id: str) -> Optional[str]:
        """Check for completed timers and send notifications"""
        state = self.get_state(user_id)
        now = datetime.datetime.now()
        
        newly_completed = []
        
        for timer in state.get("timers", []):
            if timer.get("active") and not timer.get("notified", False):
                end_time = datetime.datetime.fromisoformat(timer["end_time"])
                if now >= end_time:
                    newly_completed.append(timer)
                    timer["notified"] = True
        
        if newly_completed:
            self.update_state(user_id, state)
            state["total_completed"] = state.get("total_completed", 0) + len(newly_completed)
            
            # Create notification message
            if len(newly_completed) == 1:
                timer = newly_completed[0]
                return f"⏰ **Timer Complete!** Your timer '{timer['name']}' has finished!"
            else:
                timer_names = ", ".join([f"'{t['name']}'" for t in newly_completed])
                return f"⏰ **{len(newly_completed)} Timers Complete!** {timer_names}"
        
        return None
    
    def on_enable(self, user_id: str) -> str:
        self.initialize_state(user_id)
        return "⏰ Timer & Reminders enabled! I can now help you track time-sensitive tasks with automatic notifications."
    
    def on_disable(self, user_id: str) -> str:
        state = self.get_state(user_id)
        active = len([t for t in state.get("timers", []) if t.get("active", False)])
        return f"⏰ Timer & Reminders disabled. {active} active timer(s) will be cleared."
    
    def health_check(self, user_id: str) -> Dict[str, Any]:
        """Check extension health"""
        state = self.get_state(user_id)
        issues = []
        
        # Check for stale timers (older than 30 days)
        now = datetime.datetime.now()
        stale_count = 0
        for timer in state.get("timers", []):
            if timer.get("active"):
                end_time = datetime.datetime.fromisoformat(timer["end_time"])
                age_days = (now - end_time).days
                if age_days > 30:
                    stale_count += 1
        
        if stale_count > 0:
            issues.append(f"{stale_count} stale timer(s) older than 30 days")
        
        return {
            "healthy": len(issues) == 0,
            "extension": self.name,
            "version": self.version,
            "issues": issues if issues else None
        }