agentr 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,74 +1,548 @@
1
- from agentr.application import APIApplication
2
- from agentr.integration import Integration
3
- from loguru import logger
4
- from datetime import datetime, timedelta
5
-
6
- class GoogleCalendarApp(APIApplication):
7
- def __init__(self, integration: Integration) -> None:
8
- super().__init__(name="google-calendar", integration=integration)
9
-
10
- def _get_headers(self):
11
- credentials = self.integration.get_credentials()
12
- if "headers" in credentials:
13
- return credentials["headers"]
14
- return {
15
- "Authorization": f"Bearer {credentials['access_token']}",
16
- "Accept": "application/json"
17
- }
18
-
19
-
20
- def get_today_events(self) -> str:
21
- """Get events from your Google Calendar for today
22
-
23
- Returns:
24
- A formatted list of today's events or an error message
25
- """
26
- if not self.validate():
27
- logger.warning("Connection not configured correctly")
28
- return self.authorize()
29
-
30
- try:
31
- # Get today's date in ISO format
32
- today = datetime.now().date()
33
- tomorrow = today + timedelta(days=1)
34
-
35
- # Format dates for API
36
- time_min = f"{today.isoformat()}T00:00:00Z"
37
- time_max = f"{tomorrow.isoformat()}T00:00:00Z"
38
-
39
- url = "https://www.googleapis.com/calendar/v3/calendars/primary/events"
40
- params = {
41
- "timeMin": time_min,
42
- "timeMax": time_max,
43
- "singleEvents": "true",
44
- "orderBy": "startTime"
45
- }
46
-
47
- response = self._get(url, params=params)
48
-
49
- if response.status_code == 200:
50
- events = response.json().get("items", [])
51
- if not events:
52
- return "No events scheduled for today."
53
-
54
- result = "Today's events:\n\n"
55
- for event in events:
56
- start = event.get("start", {})
57
- start_time = start.get("dateTime", start.get("date", "All day"))
58
- if "T" in start_time: # Format datetime
59
- start_dt = datetime.fromisoformat(start_time.replace("Z", "+00:00"))
60
- start_time = start_dt.strftime("%I:%M %p")
61
-
62
- summary = event.get("summary", "Untitled event")
63
- result += f"- {start_time}: {summary}\n"
64
-
65
- return result
66
- else:
67
- logger.error(response.text)
68
- return f"Error retrieving calendar events: {response.text}"
69
- except Exception as e:
70
- logger.error(e)
71
- return f"Error retrieving calendar events: {e}"
72
-
73
- def list_tools(self):
74
- return [self.get_today_events]
1
+ from agentr.application import APIApplication
2
+ from agentr.integration import Integration
3
+ from agentr.exceptions import NotAuthorizedError
4
+ from loguru import logger
5
+ from datetime import datetime, timedelta
6
+ from urllib.parse import urlencode
7
+
8
+ class GoogleCalendarApp(APIApplication):
9
+ def __init__(self, integration: Integration) -> None:
10
+ super().__init__(name="google-calendar", integration=integration)
11
+ self.base_api_url = "https://www.googleapis.com/calendar/v3/calendars/primary"
12
+
13
+ def _get_headers(self):
14
+ if not self.integration:
15
+ raise ValueError("Integration not configured for GoogleCalendarApp")
16
+ credentials = self.integration.get_credentials()
17
+ if not credentials:
18
+ logger.warning("No Google Calendar credentials found via integration.")
19
+ action = self.integration.authorize()
20
+ raise NotAuthorizedError(action)
21
+
22
+ if "headers" in credentials:
23
+ return credentials["headers"]
24
+ return {
25
+ "Authorization": f"Bearer {credentials['access_token']}",
26
+ "Accept": "application/json"
27
+ }
28
+
29
+ def _format_datetime(self, dt_string: str) -> str:
30
+ """Format a datetime string from ISO format to a human-readable format.
31
+
32
+ Args:
33
+ dt_string: A datetime string in ISO format (e.g., "2023-06-01T10:00:00Z")
34
+
35
+ Returns:
36
+ A formatted datetime string (e.g., "2023-06-01 10:00 AM") or the original string with
37
+ "(All day)" appended if it's just a date
38
+ """
39
+ if not dt_string or dt_string == "Unknown":
40
+ return "Unknown"
41
+
42
+ # Check if it's just a date (all-day event) or a datetime
43
+ if "T" in dt_string:
44
+ # It's a datetime - parse and format it
45
+ try:
46
+ # Handle Z (UTC) suffix by replacing with +00:00 timezone
47
+ if dt_string.endswith("Z"):
48
+ dt_string = dt_string.replace("Z", "+00:00")
49
+
50
+ # Parse the ISO datetime string
51
+ dt = datetime.fromisoformat(dt_string)
52
+
53
+ # Format to a more readable form
54
+ return dt.strftime("%Y-%m-%d %I:%M %p")
55
+ except ValueError:
56
+ # In case of parsing error, return the original
57
+ logger.warning(f"Could not parse datetime string: {dt_string}")
58
+ return dt_string
59
+ else:
60
+ # It's just a date (all-day event)
61
+ return f"{dt_string} (All day)"
62
+
63
+ def get_today_events(self, days: int = 1, max_results: int = None, time_zone: str = None) -> str:
64
+ """Get events from your Google Calendar for today or a specified number of days
65
+
66
+ Args:
67
+ days: Number of days to retrieve events for (default: 1, which is just today)
68
+ max_results: Maximum number of events to return (optional)
69
+ time_zone: Time zone used in the response (optional, default is calendar's time zone)
70
+
71
+ Returns:
72
+ A formatted list of events or an error message
73
+ """
74
+ try:
75
+ # Get today's date in ISO format
76
+ today = datetime.utcnow().date()
77
+ end_date = today + timedelta(days=days)
78
+
79
+ # Format dates for API
80
+ time_min = f"{today.isoformat()}T00:00:00Z"
81
+ time_max = f"{end_date.isoformat()}T00:00:00Z"
82
+
83
+ url = f"{self.base_api_url}/events"
84
+
85
+ # Build query parameters
86
+ params = {
87
+ "timeMin": time_min,
88
+ "timeMax": time_max,
89
+ "singleEvents": "true",
90
+ "orderBy": "startTime"
91
+ }
92
+
93
+ if max_results is not None:
94
+ params["maxResults"] = max_results
95
+
96
+ if time_zone:
97
+ params["timeZone"] = time_zone
98
+
99
+ date_range = "today" if days == 1 else f"the next {days} days"
100
+ logger.info(f"Retrieving calendar events for {date_range}")
101
+
102
+ response = self._get(url, params=params)
103
+
104
+ if response.status_code == 200:
105
+ events = response.json().get("items", [])
106
+ if not events:
107
+ return f"No events scheduled for {date_range}."
108
+
109
+ result = f"Events for {date_range}:\n\n"
110
+ for event in events:
111
+ # Extract event date and time
112
+ start = event.get("start", {})
113
+ event_date = start.get("date", start.get("dateTime", "")).split("T")[0] if "T" in start.get("dateTime", "") else start.get("date", "")
114
+
115
+ # Extract and format time
116
+ start_time = start.get("dateTime", start.get("date", "All day"))
117
+
118
+ # Format the time display
119
+ if "T" in start_time: # It's a datetime
120
+ formatted_time = self._format_datetime(start_time)
121
+ # For multi-day view, keep the date; for single day, just show time
122
+ if days > 1:
123
+ time_display = formatted_time
124
+ else:
125
+ # Extract just the time part
126
+ time_display = formatted_time.split(" ")[1] + " " + formatted_time.split(" ")[2]
127
+ else: # It's an all-day event
128
+ if days > 1:
129
+ time_display = f"{event_date} (All day)"
130
+ else:
131
+ time_display = "All day"
132
+
133
+ # Get event details
134
+ summary = event.get("summary", "Untitled event")
135
+ event_id = event.get("id", "No ID")
136
+
137
+ result += f"- {time_display}: {summary} (ID: {event_id})\n"
138
+
139
+ return result
140
+ else:
141
+ logger.error(f"Google Calendar API Error: {response.status_code} - {response.text}")
142
+ return f"Error retrieving calendar events: {response.status_code} - {response.text}"
143
+ except NotAuthorizedError as e:
144
+ logger.warning(f"Google Calendar authorization required: {e.message}")
145
+ return e.message
146
+ except Exception as e:
147
+ logger.exception(f"Error retrieving calendar events: {type(e).__name__} - {str(e)}")
148
+ return f"Error retrieving calendar events: {type(e).__name__} - {str(e)}"
149
+
150
+ def get_event(self, event_id: str, max_attendees: int = None, time_zone: str = None) -> str:
151
+ """Get a specific event from your Google Calendar by ID
152
+
153
+ Args:
154
+ event_id: The ID of the event to retrieve
155
+ max_attendees: Optional. The maximum number of attendees to include in the response
156
+ time_zone: Optional. Time zone used in the response (default is calendar's time zone)
157
+
158
+ Returns:
159
+ A formatted event details or an error message
160
+ """
161
+ try:
162
+ url = f"{self.base_api_url}/events/{event_id}"
163
+
164
+ # Build query parameters
165
+ params = {}
166
+ if max_attendees is not None:
167
+ params["maxAttendees"] = max_attendees
168
+ if time_zone:
169
+ params["timeZone"] = time_zone
170
+
171
+ logger.info(f"Retrieving calendar event with ID: {event_id}")
172
+
173
+ response = self._get(url, params=params)
174
+
175
+ if response.status_code == 200:
176
+ event = response.json()
177
+
178
+ # Extract event details
179
+ summary = event.get("summary", "Untitled event")
180
+ description = event.get("description", "No description")
181
+ location = event.get("location", "No location specified")
182
+
183
+ # Format dates
184
+ start = event.get("start", {})
185
+ end = event.get("end", {})
186
+
187
+ start_time = start.get("dateTime", start.get("date", "Unknown"))
188
+ end_time = end.get("dateTime", end.get("date", "Unknown"))
189
+
190
+ # Format datetimes using the helper function
191
+ start_formatted = self._format_datetime(start_time)
192
+ end_formatted = self._format_datetime(end_time)
193
+
194
+ # Get creator and organizer
195
+ creator = event.get("creator", {}).get("email", "Unknown")
196
+ organizer = event.get("organizer", {}).get("email", "Unknown")
197
+
198
+ # Check if it's a recurring event
199
+ recurrence = "Yes" if "recurrence" in event else "No"
200
+
201
+ # Get attendees if any
202
+ attendees = event.get("attendees", [])
203
+ attendee_info = ""
204
+ if attendees:
205
+ attendee_info = "\nAttendees:\n"
206
+ for i, attendee in enumerate(attendees, 1):
207
+ email = attendee.get("email", "No email")
208
+ name = attendee.get("displayName", email)
209
+ response_status = attendee.get("responseStatus", "Unknown")
210
+
211
+ status_mapping = {
212
+ "accepted": "Accepted",
213
+ "declined": "Declined",
214
+ "tentative": "Maybe",
215
+ "needsAction": "Not responded"
216
+ }
217
+
218
+ formatted_status = status_mapping.get(response_status, response_status)
219
+ attendee_info += f" {i}. {name} ({email}) - {formatted_status}\n"
220
+
221
+ # Format the response
222
+ result = f"Event: {summary}\n"
223
+ result += f"ID: {event_id}\n"
224
+ result += f"When: {start_formatted} to {end_formatted}\n"
225
+ result += f"Where: {location}\n"
226
+ result += f"Description: {description}\n"
227
+ result += f"Creator: {creator}\n"
228
+ result += f"Organizer: {organizer}\n"
229
+ result += f"Recurring: {recurrence}\n"
230
+ result += attendee_info
231
+
232
+ return result
233
+ elif response.status_code == 404:
234
+ return f"Event not found with ID: {event_id}"
235
+ else:
236
+ logger.error(f"Google Calendar API Error: {response.status_code} - {response.text}")
237
+ return f"Error retrieving event: {response.status_code} - {response.text}"
238
+ except NotAuthorizedError as e:
239
+ logger.warning(f"Google Calendar authorization required: {e.message}")
240
+ return e.message
241
+ except Exception as e:
242
+ logger.exception(f"Error retrieving event: {type(e).__name__} - {str(e)}")
243
+ return f"Error retrieving event: {type(e).__name__} - {str(e)}"
244
+
245
+ def list_events(self, max_results: int = 10, time_min: str = None, time_max: str = None,
246
+ q: str = None, order_by: str = "startTime", single_events: bool = True,
247
+ time_zone: str = None, page_token: str = None) -> str:
248
+ """List events from your Google Calendar with various filtering options
249
+
250
+ Args:
251
+ max_results: Maximum number of events to return (default: 10, max: 2500)
252
+ time_min: Start time (ISO format, e.g. '2023-12-01T00:00:00Z') - defaults to now if not specified
253
+ time_max: End time (ISO format, e.g. '2023-12-31T23:59:59Z')
254
+ q: Free text search terms (searches summary, description, location, attendees, etc.)
255
+ order_by: How to order results - 'startTime' (default) or 'updated'
256
+ single_events: Whether to expand recurring events (default: True)
257
+ time_zone: Time zone used in the response (default is calendar's time zone)
258
+ page_token: Token for retrieving a specific page of results
259
+
260
+ Returns:
261
+ A formatted list of events or an error message
262
+ """
263
+ try:
264
+ url = f"{self.base_api_url}/events"
265
+
266
+ # Build query parameters
267
+ params = {
268
+ "maxResults": max_results,
269
+ "singleEvents": str(single_events).lower(),
270
+ "orderBy": order_by
271
+ }
272
+
273
+ # Set time boundaries if provided, otherwise default to now for time_min
274
+ if time_min:
275
+ params["timeMin"] = time_min
276
+ else:
277
+ # Default to current time if not specified
278
+ now = datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time
279
+ params["timeMin"] = now
280
+
281
+ if time_max:
282
+ params["timeMax"] = time_max
283
+
284
+ # Add optional filters if provided
285
+ if q:
286
+ params["q"] = q
287
+
288
+ if time_zone:
289
+ params["timeZone"] = time_zone
290
+
291
+ if page_token:
292
+ params["pageToken"] = page_token
293
+
294
+ logger.info(f"Retrieving calendar events with params: {params}")
295
+
296
+ response = self._get(url, params=params)
297
+
298
+ if response.status_code == 200:
299
+ data = response.json()
300
+ events = data.get("items", [])
301
+
302
+ if not events:
303
+ return "No events found matching your criteria."
304
+
305
+ # Extract calendar information
306
+ calendar_summary = data.get("summary", "Your Calendar")
307
+ time_zone_info = data.get("timeZone", "Unknown")
308
+
309
+ result = f"Events from {calendar_summary} (Time Zone: {time_zone_info}):\n\n"
310
+
311
+ # Process and format each event
312
+ for i, event in enumerate(events, 1):
313
+ # Get basic event details
314
+ event_id = event.get("id", "No ID")
315
+ summary = event.get("summary", "Untitled event")
316
+
317
+ # Get event times and format them
318
+ start = event.get("start", {})
319
+ start_time = start.get("dateTime", start.get("date", "Unknown"))
320
+
321
+ # Format the start time using the helper function
322
+ start_formatted = self._format_datetime(start_time)
323
+
324
+ # Get location if available
325
+ location = event.get("location", "No location specified")
326
+
327
+ # Check if it's a recurring event
328
+ is_recurring = "recurrence" in event
329
+ recurring_info = " (Recurring)" if is_recurring else ""
330
+
331
+ # Format the event information
332
+ result += f"{i}. {summary}{recurring_info}\n"
333
+ result += f" ID: {event_id}\n"
334
+ result += f" When: {start_formatted}\n"
335
+ result += f" Where: {location}\n"
336
+
337
+ # Add a separator between events
338
+ if i < len(events):
339
+ result += "\n"
340
+
341
+ # Add pagination info if available
342
+ if "nextPageToken" in data:
343
+ next_token = data.get("nextPageToken")
344
+ result += f"\nMore events available. Use page_token='{next_token}' to see more."
345
+
346
+ return result
347
+ else:
348
+ logger.error(f"Google Calendar API Error: {response.status_code} - {response.text}")
349
+ return f"Error retrieving events: {response.status_code} - {response.text}"
350
+ except NotAuthorizedError as e:
351
+ logger.warning(f"Google Calendar authorization required: {e.message}")
352
+ return e.message
353
+ except Exception as e:
354
+ logger.exception(f"Error retrieving events: {type(e).__name__} - {str(e)}")
355
+ return f"Error retrieving events: {type(e).__name__} - {str(e)}"
356
+
357
+ def quick_add_event(self, text: str, send_updates: str = "none") -> str:
358
+ """Create a calendar event using natural language description
359
+
360
+ This method allows you to quickly create an event using a simple text string,
361
+ similar to how you would add events in the Google Calendar UI.
362
+
363
+ Args:
364
+ text: Text describing the event (e.g., "Meeting with John at Coffee Shop tomorrow 3pm-4pm")
365
+ send_updates: Who should receive notifications - "all", "externalOnly", or "none" (default)
366
+
367
+ Returns:
368
+ A confirmation message with the created event details or an error message
369
+ """
370
+ try:
371
+ url = f"{self.base_api_url}/events/quickAdd"
372
+
373
+ # Use params argument instead of manually constructing URL
374
+ params = {
375
+ "text": text,
376
+ "sendUpdates": send_updates
377
+ }
378
+
379
+ logger.info(f"Creating event via quickAdd: '{text}'")
380
+
381
+ # Pass params to _post method
382
+ response = self._post(url, data=None, params=params)
383
+
384
+ if response.status_code in [200, 201]:
385
+ event = response.json()
386
+
387
+ # Extract event details
388
+ event_id = event.get("id", "Unknown")
389
+ summary = event.get("summary", "Untitled event")
390
+
391
+ # Format dates
392
+ start = event.get("start", {})
393
+ end = event.get("end", {})
394
+
395
+ start_time = start.get("dateTime", start.get("date", "Unknown"))
396
+ end_time = end.get("dateTime", end.get("date", "Unknown"))
397
+
398
+ # Format datetimes using the helper function
399
+ start_formatted = self._format_datetime(start_time)
400
+ end_formatted = self._format_datetime(end_time)
401
+
402
+ # Get location if available
403
+ location = event.get("location", "No location specified")
404
+
405
+ # Format the confirmation message
406
+ result = f"Successfully created event!\n\n"
407
+ result += f"Summary: {summary}\n"
408
+ result += f"When: {start_formatted}"
409
+
410
+ # Only add end time if it's different from start (for all-day events they might be the same)
411
+ if start_formatted != end_formatted:
412
+ result += f" to {end_formatted}"
413
+
414
+ result += f"\nWhere: {location}\n"
415
+ result += f"Event ID: {event_id}\n"
416
+
417
+ # Add a note about viewing the event
418
+ result += f"\nUse get_event('{event_id}') to see full details."
419
+
420
+ return result
421
+ else:
422
+ logger.error(f"Google Calendar API Error: {response.status_code} - {response.text}")
423
+ return f"Error creating event: {response.status_code} - {response.text}"
424
+ except NotAuthorizedError as e:
425
+ logger.warning(f"Google Calendar authorization required: {e.message}")
426
+ return e.message
427
+ except Exception as e:
428
+ logger.exception(f"Error creating event: {type(e).__name__} - {str(e)}")
429
+ return f"Error creating event: {type(e).__name__} - {str(e)}"
430
+
431
+ def get_event_instances(self, event_id: str, max_results: int = 25, time_min: str = None,
432
+ time_max: str = None, time_zone: str = None, show_deleted: bool = False,
433
+ page_token: str = None) -> str:
434
+ """Get all instances of a recurring event
435
+
436
+ This method retrieves all occurrences of a recurring event within a specified time range.
437
+
438
+ Args:
439
+ event_id: ID of the recurring event
440
+ max_results: Maximum number of event instances to return (default: 25, max: 2500)
441
+ time_min: Lower bound (inclusive) for event's end time (ISO format)
442
+ time_max: Upper bound (exclusive) for event's start time (ISO format)
443
+ time_zone: Time zone used in the response (default is calendar's time zone)
444
+ show_deleted: Whether to include deleted instances (default: False)
445
+ page_token: Token for retrieving a specific page of results
446
+
447
+ Returns:
448
+ A formatted list of event instances or an error message
449
+ """
450
+ try:
451
+ url = f"{self.base_api_url}/events/{event_id}/instances"
452
+
453
+ # Build query parameters
454
+ params = {
455
+ "maxResults": max_results,
456
+ "showDeleted": str(show_deleted).lower()
457
+ }
458
+
459
+ # Add optional parameters if provided
460
+ if time_min:
461
+ params["timeMin"] = time_min
462
+
463
+ if time_max:
464
+ params["timeMax"] = time_max
465
+
466
+ if time_zone:
467
+ params["timeZone"] = time_zone
468
+
469
+ if page_token:
470
+ params["pageToken"] = page_token
471
+
472
+ logger.info(f"Retrieving instances of recurring event with ID: {event_id}")
473
+
474
+ response = self._get(url, params=params)
475
+
476
+ if response.status_code == 200:
477
+ data = response.json()
478
+ instances = data.get("items", [])
479
+
480
+ if not instances:
481
+ return f"No instances found for recurring event with ID: {event_id}"
482
+
483
+ # Extract event summary from the first instance
484
+ parent_summary = instances[0].get("summary", "Untitled recurring event")
485
+
486
+ result = f"Instances of recurring event: {parent_summary}\n\n"
487
+
488
+ # Process and format each instance
489
+ for i, instance in enumerate(instances, 1):
490
+ # Get instance ID and status
491
+ instance_id = instance.get("id", "No ID")
492
+ status = instance.get("status", "confirmed")
493
+
494
+ # Format status for display
495
+ status_display = ""
496
+ if status == "cancelled":
497
+ status_display = " [CANCELLED]"
498
+ elif status == "tentative":
499
+ status_display = " [TENTATIVE]"
500
+
501
+ # Get instance time
502
+ start = instance.get("start", {})
503
+ original_start_time = instance.get("originalStartTime", {})
504
+
505
+ # Determine if this is a modified instance
506
+ is_modified = original_start_time and "dateTime" in original_start_time
507
+ modified_indicator = " [MODIFIED]" if is_modified else ""
508
+
509
+ # Get the time information
510
+ start_time = start.get("dateTime", start.get("date", "Unknown"))
511
+
512
+ # Format the time using the helper function
513
+ formatted_time = self._format_datetime(start_time)
514
+
515
+ # Format the instance information
516
+ result += f"{i}. {formatted_time}{status_display}{modified_indicator}\n"
517
+ result += f" Instance ID: {instance_id}\n"
518
+
519
+ # Show original start time if modified
520
+ if is_modified:
521
+ orig_time = original_start_time.get("dateTime", original_start_time.get("date", "Unknown"))
522
+ orig_formatted = self._format_datetime(orig_time)
523
+ result += f" Original time: {orig_formatted}\n"
524
+
525
+ # Add a separator between instances
526
+ if i < len(instances):
527
+ result += "\n"
528
+
529
+ # Add pagination info if available
530
+ if "nextPageToken" in data:
531
+ next_token = data.get("nextPageToken")
532
+ result += f"\nMore instances available. Use page_token='{next_token}' to see more."
533
+
534
+ return result
535
+ elif response.status_code == 404:
536
+ return f"Error: Event with ID {event_id} not found or is not a recurring event."
537
+ else:
538
+ logger.error(f"Google Calendar API Error: {response.status_code} - {response.text}")
539
+ return f"Error retrieving event instances: {response.status_code} - {response.text}"
540
+ except NotAuthorizedError as e:
541
+ logger.warning(f"Google Calendar authorization required: {e.message}")
542
+ return e.message
543
+ except Exception as e:
544
+ logger.exception(f"Error retrieving event instances: {type(e).__name__} - {str(e)}")
545
+ return f"Error retrieving event instances: {type(e).__name__} - {str(e)}"
546
+
547
+ def list_tools(self):
548
+ return [self.get_event, self.get_today_events, self.list_events, self.quick_add_event, self.get_event_instances]