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.
- agentr/__init__.py +1 -1
- agentr/application.py +93 -56
- agentr/applications/__init__.py +27 -0
- agentr/applications/github/app.py +319 -56
- agentr/applications/google_calendar/app.py +489 -74
- agentr/applications/google_mail/app.py +565 -68
- agentr/applications/reddit/app.py +309 -29
- agentr/applications/resend/app.py +43 -43
- agentr/applications/tavily/app.py +57 -57
- agentr/applications/zenquotes/app.py +20 -20
- agentr/cli.py +76 -75
- agentr/config.py +15 -0
- agentr/exceptions.py +6 -5
- agentr/integration.py +161 -98
- agentr/integrations/README.md +25 -0
- agentr/integrations/__init__.py +5 -0
- agentr/integrations/agentr.py +87 -0
- agentr/integrations/api_key.py +16 -0
- agentr/integrations/base.py +60 -0
- agentr/server.py +128 -105
- agentr/store.py +70 -70
- agentr/test.py +14 -37
- agentr/utils/openapi.py +273 -184
- {agentr-0.1.6.dist-info → agentr-0.1.8.dist-info}/METADATA +4 -1
- agentr-0.1.8.dist-info/RECORD +30 -0
- {agentr-0.1.6.dist-info → agentr-0.1.8.dist-info}/licenses/LICENSE +21 -21
- agentr-0.1.6.dist-info/RECORD +0 -23
- {agentr-0.1.6.dist-info → agentr-0.1.8.dist-info}/WHEEL +0 -0
- {agentr-0.1.6.dist-info → agentr-0.1.8.dist-info}/entry_points.txt +0 -0
@@ -1,74 +1,489 @@
|
|
1
|
-
from agentr.application import APIApplication
|
2
|
-
from agentr.integration import Integration
|
3
|
-
from
|
4
|
-
from
|
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
|
-
|
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]
|