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