connectonion 0.5.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.
Files changed (113) hide show
  1. connectonion/__init__.py +78 -0
  2. connectonion/address.py +320 -0
  3. connectonion/agent.py +450 -0
  4. connectonion/announce.py +84 -0
  5. connectonion/asgi.py +287 -0
  6. connectonion/auto_debug_exception.py +181 -0
  7. connectonion/cli/__init__.py +3 -0
  8. connectonion/cli/browser_agent/__init__.py +5 -0
  9. connectonion/cli/browser_agent/browser.py +243 -0
  10. connectonion/cli/browser_agent/prompt.md +107 -0
  11. connectonion/cli/commands/__init__.py +1 -0
  12. connectonion/cli/commands/auth_commands.py +527 -0
  13. connectonion/cli/commands/browser_commands.py +27 -0
  14. connectonion/cli/commands/create.py +511 -0
  15. connectonion/cli/commands/deploy_commands.py +220 -0
  16. connectonion/cli/commands/doctor_commands.py +173 -0
  17. connectonion/cli/commands/init.py +469 -0
  18. connectonion/cli/commands/project_cmd_lib.py +828 -0
  19. connectonion/cli/commands/reset_commands.py +149 -0
  20. connectonion/cli/commands/status_commands.py +168 -0
  21. connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
  22. connectonion/cli/docs/connectonion.md +1256 -0
  23. connectonion/cli/docs.md +123 -0
  24. connectonion/cli/main.py +148 -0
  25. connectonion/cli/templates/meta-agent/README.md +287 -0
  26. connectonion/cli/templates/meta-agent/agent.py +196 -0
  27. connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
  28. connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
  29. connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
  30. connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
  31. connectonion/cli/templates/minimal/README.md +56 -0
  32. connectonion/cli/templates/minimal/agent.py +40 -0
  33. connectonion/cli/templates/playwright/README.md +118 -0
  34. connectonion/cli/templates/playwright/agent.py +336 -0
  35. connectonion/cli/templates/playwright/prompt.md +102 -0
  36. connectonion/cli/templates/playwright/requirements.txt +3 -0
  37. connectonion/cli/templates/web-research/agent.py +122 -0
  38. connectonion/connect.py +128 -0
  39. connectonion/console.py +539 -0
  40. connectonion/debug_agent/__init__.py +13 -0
  41. connectonion/debug_agent/agent.py +45 -0
  42. connectonion/debug_agent/prompts/debug_assistant.md +72 -0
  43. connectonion/debug_agent/runtime_inspector.py +406 -0
  44. connectonion/debug_explainer/__init__.py +10 -0
  45. connectonion/debug_explainer/explain_agent.py +114 -0
  46. connectonion/debug_explainer/explain_context.py +263 -0
  47. connectonion/debug_explainer/explainer_prompt.md +29 -0
  48. connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
  49. connectonion/debugger_ui.py +1039 -0
  50. connectonion/decorators.py +208 -0
  51. connectonion/events.py +248 -0
  52. connectonion/execution_analyzer/__init__.py +9 -0
  53. connectonion/execution_analyzer/execution_analysis.py +93 -0
  54. connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
  55. connectonion/host.py +579 -0
  56. connectonion/interactive_debugger.py +342 -0
  57. connectonion/llm.py +801 -0
  58. connectonion/llm_do.py +307 -0
  59. connectonion/logger.py +300 -0
  60. connectonion/prompt_files/__init__.py +1 -0
  61. connectonion/prompt_files/analyze_contact.md +62 -0
  62. connectonion/prompt_files/eval_expected.md +12 -0
  63. connectonion/prompt_files/react_evaluate.md +11 -0
  64. connectonion/prompt_files/react_plan.md +16 -0
  65. connectonion/prompt_files/reflect.md +22 -0
  66. connectonion/prompts.py +144 -0
  67. connectonion/relay.py +200 -0
  68. connectonion/static/docs.html +688 -0
  69. connectonion/tool_executor.py +279 -0
  70. connectonion/tool_factory.py +186 -0
  71. connectonion/tool_registry.py +105 -0
  72. connectonion/trust.py +166 -0
  73. connectonion/trust_agents.py +71 -0
  74. connectonion/trust_functions.py +88 -0
  75. connectonion/tui/__init__.py +57 -0
  76. connectonion/tui/divider.py +39 -0
  77. connectonion/tui/dropdown.py +251 -0
  78. connectonion/tui/footer.py +31 -0
  79. connectonion/tui/fuzzy.py +56 -0
  80. connectonion/tui/input.py +278 -0
  81. connectonion/tui/keys.py +35 -0
  82. connectonion/tui/pick.py +130 -0
  83. connectonion/tui/providers.py +155 -0
  84. connectonion/tui/status_bar.py +163 -0
  85. connectonion/usage.py +161 -0
  86. connectonion/useful_events_handlers/__init__.py +16 -0
  87. connectonion/useful_events_handlers/reflect.py +116 -0
  88. connectonion/useful_plugins/__init__.py +20 -0
  89. connectonion/useful_plugins/calendar_plugin.py +163 -0
  90. connectonion/useful_plugins/eval.py +139 -0
  91. connectonion/useful_plugins/gmail_plugin.py +162 -0
  92. connectonion/useful_plugins/image_result_formatter.py +127 -0
  93. connectonion/useful_plugins/re_act.py +78 -0
  94. connectonion/useful_plugins/shell_approval.py +159 -0
  95. connectonion/useful_tools/__init__.py +44 -0
  96. connectonion/useful_tools/diff_writer.py +192 -0
  97. connectonion/useful_tools/get_emails.py +183 -0
  98. connectonion/useful_tools/gmail.py +1596 -0
  99. connectonion/useful_tools/google_calendar.py +613 -0
  100. connectonion/useful_tools/memory.py +380 -0
  101. connectonion/useful_tools/microsoft_calendar.py +604 -0
  102. connectonion/useful_tools/outlook.py +488 -0
  103. connectonion/useful_tools/send_email.py +205 -0
  104. connectonion/useful_tools/shell.py +97 -0
  105. connectonion/useful_tools/slash_command.py +201 -0
  106. connectonion/useful_tools/terminal.py +285 -0
  107. connectonion/useful_tools/todo_list.py +241 -0
  108. connectonion/useful_tools/web_fetch.py +216 -0
  109. connectonion/xray.py +467 -0
  110. connectonion-0.5.8.dist-info/METADATA +741 -0
  111. connectonion-0.5.8.dist-info/RECORD +113 -0
  112. connectonion-0.5.8.dist-info/WHEEL +4 -0
  113. connectonion-0.5.8.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,613 @@
1
+ """
2
+ Purpose: Google Calendar integration tool for managing events and meetings via Google API
3
+ LLM-Note:
4
+ Dependencies: imports from [os, datetime, google.oauth2.credentials, googleapiclient.discovery] | imported by [useful_tools/__init__.py] | requires OAuth tokens from 'co auth google' | tested by [tests/unit/test_google_calendar.py]
5
+ Data flow: Agent calls GoogleCalendar methods → _get_credentials() loads tokens from env → builds Calendar API service → API calls to Calendar REST endpoints → returns formatted results (event lists, confirmations, free slots)
6
+ State/Effects: reads GOOGLE_* env vars for OAuth tokens | makes HTTP calls to Google Calendar API | can create/update/delete events | no local file persistence
7
+ Integration: exposes GoogleCalendar class with list_events(), get_today_events(), get_event(), create_event(), update_event(), delete_event(), create_meet(), get_upcoming_meetings(), find_free_slots() | used as agent tool via Agent(tools=[GoogleCalendar()])
8
+ Performance: network I/O per API call | batch fetching for list operations | date parsing for queries
9
+ Errors: raises ValueError if OAuth not configured | Google API errors propagate | returns error strings for display
10
+
11
+ Google Calendar tool for managing calendar events and meetings.
12
+
13
+ Usage:
14
+ from connectonion import Agent, GoogleCalendar
15
+
16
+ calendar = GoogleCalendar()
17
+ agent = Agent("assistant", tools=[calendar])
18
+
19
+ # Agent can now use:
20
+ # - list_events(days_ahead, max_results)
21
+ # - get_today_events()
22
+ # - get_event(event_id)
23
+ # - create_event(title, start_time, end_time, description, attendees, location)
24
+ # - update_event(event_id, title, start_time, end_time, description, attendees, location)
25
+ # - delete_event(event_id)
26
+ # - create_meet(title, start_time, end_time, attendees, description)
27
+ # - get_upcoming_meetings(days_ahead)
28
+ # - find_free_slots(date, duration_minutes)
29
+
30
+ Example:
31
+ from connectonion import Agent, GoogleCalendar
32
+
33
+ calendar = GoogleCalendar()
34
+ agent = Agent(
35
+ name="calendar-assistant",
36
+ system_prompt="You are a calendar assistant.",
37
+ tools=[calendar]
38
+ )
39
+
40
+ agent.input("What meetings do I have today?")
41
+ agent.input("Schedule a meeting with aaron@openonion.ai tomorrow at 2pm")
42
+ """
43
+
44
+ import os
45
+ from datetime import datetime, timedelta
46
+ from google.oauth2.credentials import Credentials
47
+ from googleapiclient.discovery import build
48
+
49
+
50
+ class GoogleCalendar:
51
+ """Google Calendar tool for managing events and meetings."""
52
+
53
+ def __init__(self):
54
+ """Initialize Google Calendar tool.
55
+
56
+ Validates that calendar scope is authorized.
57
+ Raises ValueError if scope is missing.
58
+ """
59
+ scopes = os.getenv("GOOGLE_SCOPES", "")
60
+ if "calendar" not in scopes:
61
+ raise ValueError(
62
+ "Missing 'calendar' scope.\n"
63
+ f"Current scopes: {scopes}\n"
64
+ "Please authorize Google Calendar access:\n"
65
+ " co auth google"
66
+ )
67
+
68
+ self._service = None
69
+
70
+ def _get_service(self):
71
+ """Get Google Calendar API service (lazy load with auto-refresh)."""
72
+ access_token = os.getenv("GOOGLE_ACCESS_TOKEN")
73
+ refresh_token = os.getenv("GOOGLE_REFRESH_TOKEN")
74
+ expires_at_str = os.getenv("GOOGLE_TOKEN_EXPIRES_AT")
75
+
76
+ if not access_token or not refresh_token:
77
+ raise ValueError(
78
+ "Google OAuth credentials not found.\n"
79
+ "Run: co auth google"
80
+ )
81
+
82
+ # Check if token is expired or about to expire (within 5 minutes)
83
+ # Always check before returning cached service
84
+ if expires_at_str:
85
+ from datetime import datetime, timedelta
86
+ expires_at = datetime.fromisoformat(expires_at_str.replace('Z', '+00:00'))
87
+ now = datetime.utcnow().replace(tzinfo=expires_at.tzinfo) if expires_at.tzinfo else datetime.utcnow()
88
+
89
+ if now >= expires_at - timedelta(minutes=5):
90
+ # Token expired or about to expire, refresh via backend
91
+ access_token = self._refresh_via_backend(refresh_token)
92
+ # Clear cached service to use new token
93
+ self._service = None
94
+
95
+ # Return cached service if available
96
+ if self._service:
97
+ return self._service
98
+
99
+ # Create credentials
100
+ creds = Credentials(
101
+ token=access_token,
102
+ refresh_token=refresh_token,
103
+ token_uri="https://oauth2.googleapis.com/token",
104
+ client_id=None,
105
+ client_secret=None,
106
+ scopes=["https://www.googleapis.com/auth/calendar"]
107
+ )
108
+
109
+ self._service = build('calendar', 'v3', credentials=creds)
110
+ return self._service
111
+
112
+ def _refresh_via_backend(self, refresh_token: str) -> str:
113
+ """Refresh access token via backend API.
114
+
115
+ Args:
116
+ refresh_token: The refresh token
117
+
118
+ Returns:
119
+ New access token
120
+ """
121
+ import httpx
122
+
123
+ # Get backend URL and auth
124
+ backend_url = os.getenv("OPENONION_API_URL", "https://oo.openonion.ai")
125
+ api_key = os.getenv("OPENONION_API_KEY")
126
+
127
+ if not api_key:
128
+ raise ValueError(
129
+ "OPENONION_API_KEY not found.\n"
130
+ "This is needed to refresh tokens via backend."
131
+ )
132
+
133
+ # Call backend refresh endpoint
134
+ response = httpx.post(
135
+ f"{backend_url}/api/v1/oauth/google/refresh",
136
+ headers={"Authorization": f"Bearer {api_key}"},
137
+ json={"refresh_token": refresh_token}
138
+ )
139
+
140
+ if response.status_code != 200:
141
+ raise ValueError(
142
+ f"Failed to refresh token via backend: {response.text}"
143
+ )
144
+
145
+ data = response.json()
146
+ new_access_token = data["access_token"]
147
+ expires_at = data["expires_at"]
148
+
149
+ # Update environment variables for this session
150
+ os.environ["GOOGLE_ACCESS_TOKEN"] = new_access_token
151
+ os.environ["GOOGLE_TOKEN_EXPIRES_AT"] = expires_at
152
+
153
+ # Update .env file if it exists
154
+ env_file = os.path.join(os.getenv("AGENT_CONFIG_PATH", os.path.expanduser("~/.co")), "keys.env")
155
+ if os.path.exists(env_file):
156
+ with open(env_file, 'r') as f:
157
+ lines = f.readlines()
158
+
159
+ with open(env_file, 'w') as f:
160
+ for line in lines:
161
+ if line.startswith("GOOGLE_ACCESS_TOKEN="):
162
+ f.write(f"GOOGLE_ACCESS_TOKEN={new_access_token}\n")
163
+ elif line.startswith("GOOGLE_TOKEN_EXPIRES_AT="):
164
+ f.write(f"GOOGLE_TOKEN_EXPIRES_AT={expires_at}\n")
165
+ else:
166
+ f.write(line)
167
+
168
+ return new_access_token
169
+
170
+ def _format_datetime(self, dt_str: str) -> str:
171
+ """Format datetime string to readable format."""
172
+ dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00'))
173
+ return dt.strftime('%Y-%m-%d %I:%M %p')
174
+
175
+ # === Reading Events ===
176
+
177
+ def list_events(self, days_ahead: int = 7, max_results: int = 20) -> str:
178
+ """List upcoming calendar events.
179
+
180
+ Args:
181
+ days_ahead: Number of days to look ahead (default: 7)
182
+ max_results: Maximum number of events to return (default: 20)
183
+
184
+ Returns:
185
+ Formatted string with event list
186
+ """
187
+ service = self._get_service()
188
+
189
+ now = datetime.utcnow().isoformat() + 'Z'
190
+ end = (datetime.utcnow() + timedelta(days=days_ahead)).isoformat() + 'Z'
191
+
192
+ events_result = service.events().list(
193
+ calendarId='primary',
194
+ timeMin=now,
195
+ timeMax=end,
196
+ maxResults=max_results,
197
+ singleEvents=True,
198
+ orderBy='startTime'
199
+ ).execute()
200
+
201
+ events = events_result.get('items', [])
202
+
203
+ if not events:
204
+ return f"No upcoming events in the next {days_ahead} days."
205
+
206
+ output = [f"Upcoming events (next {days_ahead} days):\n"]
207
+ for event in events:
208
+ start = event['start'].get('dateTime', event['start'].get('date'))
209
+ summary = event.get('summary', 'No title')
210
+ event_id = event['id']
211
+
212
+ # Get attendees if any
213
+ attendees = event.get('attendees', [])
214
+ attendee_str = ""
215
+ if attendees:
216
+ attendee_emails = [a.get('email', '') for a in attendees if a.get('email')]
217
+ if attendee_emails:
218
+ attendee_str = f"\n Attendees: {', '.join(attendee_emails)}"
219
+
220
+ # Get meet link if any
221
+ meet_link = event.get('hangoutLink', '')
222
+ meet_str = f"\n Meet: {meet_link}" if meet_link else ""
223
+
224
+ output.append(f"- {self._format_datetime(start)}: {summary}")
225
+ output.append(f" ID: {event_id}{attendee_str}{meet_str}\n")
226
+
227
+ return "\n".join(output)
228
+
229
+ def get_today_events(self) -> str:
230
+ """Get today's calendar events.
231
+
232
+ Returns:
233
+ Formatted string with today's events
234
+ """
235
+ service = self._get_service()
236
+
237
+ # Get start and end of today
238
+ now = datetime.utcnow()
239
+ start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0).isoformat() + 'Z'
240
+ end_of_day = now.replace(hour=23, minute=59, second=59, microsecond=999999).isoformat() + 'Z'
241
+
242
+ events_result = service.events().list(
243
+ calendarId='primary',
244
+ timeMin=start_of_day,
245
+ timeMax=end_of_day,
246
+ singleEvents=True,
247
+ orderBy='startTime'
248
+ ).execute()
249
+
250
+ events = events_result.get('items', [])
251
+
252
+ if not events:
253
+ return "No events scheduled for today."
254
+
255
+ output = ["Today's events:\n"]
256
+ for event in events:
257
+ start = event['start'].get('dateTime', event['start'].get('date'))
258
+ summary = event.get('summary', 'No title')
259
+
260
+ # Get meet link if any
261
+ meet_link = event.get('hangoutLink', '')
262
+ meet_str = f" [Meet: {meet_link}]" if meet_link else ""
263
+
264
+ output.append(f"- {self._format_datetime(start)}: {summary}{meet_str}")
265
+
266
+ return "\n".join(output)
267
+
268
+ def get_event(self, event_id: str) -> str:
269
+ """Get detailed information about a specific event.
270
+
271
+ Args:
272
+ event_id: Calendar event ID
273
+
274
+ Returns:
275
+ Formatted event details
276
+ """
277
+ service = self._get_service()
278
+
279
+ event = service.events().get(
280
+ calendarId='primary',
281
+ eventId=event_id
282
+ ).execute()
283
+
284
+ summary = event.get('summary', 'No title')
285
+ start = event['start'].get('dateTime', event['start'].get('date'))
286
+ end = event['end'].get('dateTime', event['end'].get('date'))
287
+ description = event.get('description', 'No description')
288
+ location = event.get('location', 'No location')
289
+
290
+ attendees = event.get('attendees', [])
291
+ attendee_list = []
292
+ for a in attendees:
293
+ email = a.get('email', '')
294
+ status = a.get('responseStatus', 'needsAction')
295
+ attendee_list.append(f"{email} ({status})")
296
+
297
+ meet_link = event.get('hangoutLink', 'No Meet link')
298
+
299
+ output = [
300
+ f"Event: {summary}",
301
+ f"Start: {self._format_datetime(start)}",
302
+ f"End: {self._format_datetime(end)}",
303
+ f"Description: {description}",
304
+ f"Location: {location}",
305
+ f"Meet: {meet_link}",
306
+ ]
307
+
308
+ if attendee_list:
309
+ output.append(f"Attendees:\n " + "\n ".join(attendee_list))
310
+
311
+ return "\n".join(output)
312
+
313
+ # === Creating Events ===
314
+
315
+ def create_event(self, title: str, start_time: str, end_time: str,
316
+ description: str = None, attendees: str = None,
317
+ location: str = None) -> str:
318
+ """Create a new calendar event.
319
+
320
+ Args:
321
+ title: Event title
322
+ start_time: Start time (ISO format or natural like "2024-01-15 14:00")
323
+ end_time: End time (ISO format or natural like "2024-01-15 15:00")
324
+ description: Optional event description
325
+ attendees: Optional comma-separated email addresses
326
+ location: Optional location
327
+
328
+ Returns:
329
+ Confirmation with event ID and details
330
+ """
331
+ service = self._get_service()
332
+
333
+ # Parse times
334
+ start_dt = self._parse_time(start_time)
335
+ end_dt = self._parse_time(end_time)
336
+
337
+ event = {
338
+ 'summary': title,
339
+ 'start': {
340
+ 'dateTime': start_dt.isoformat(),
341
+ 'timeZone': 'UTC',
342
+ },
343
+ 'end': {
344
+ 'dateTime': end_dt.isoformat(),
345
+ 'timeZone': 'UTC',
346
+ },
347
+ }
348
+
349
+ if description:
350
+ event['description'] = description
351
+
352
+ if location:
353
+ event['location'] = location
354
+
355
+ if attendees:
356
+ attendee_list = [{'email': email.strip()} for email in attendees.split(',')]
357
+ event['attendees'] = attendee_list
358
+
359
+ created_event = service.events().insert(
360
+ calendarId='primary',
361
+ body=event
362
+ ).execute()
363
+
364
+ return f"Event created: {title}\nStart: {self._format_datetime(start_dt.isoformat())}\nEvent ID: {created_event['id']}\nLink: {created_event.get('htmlLink', '')}"
365
+
366
+ def create_meet(self, title: str, start_time: str, end_time: str,
367
+ attendees: str, description: str = None) -> str:
368
+ """Create a Google Meet meeting.
369
+
370
+ Args:
371
+ title: Meeting title
372
+ start_time: Start time (ISO format or natural)
373
+ end_time: End time (ISO format or natural)
374
+ attendees: Comma-separated email addresses
375
+ description: Optional meeting description
376
+
377
+ Returns:
378
+ Confirmation with Meet link
379
+ """
380
+ service = self._get_service()
381
+
382
+ # Parse times
383
+ start_dt = self._parse_time(start_time)
384
+ end_dt = self._parse_time(end_time)
385
+
386
+ attendee_list = [{'email': email.strip()} for email in attendees.split(',')]
387
+
388
+ event = {
389
+ 'summary': title,
390
+ 'start': {
391
+ 'dateTime': start_dt.isoformat(),
392
+ 'timeZone': 'UTC',
393
+ },
394
+ 'end': {
395
+ 'dateTime': end_dt.isoformat(),
396
+ 'timeZone': 'UTC',
397
+ },
398
+ 'attendees': attendee_list,
399
+ 'conferenceData': {
400
+ 'createRequest': {
401
+ 'requestId': f"meet-{datetime.utcnow().timestamp()}",
402
+ 'conferenceSolutionKey': {'type': 'hangoutsMeet'}
403
+ }
404
+ }
405
+ }
406
+
407
+ if description:
408
+ event['description'] = description
409
+
410
+ created_event = service.events().insert(
411
+ calendarId='primary',
412
+ body=event,
413
+ conferenceDataVersion=1
414
+ ).execute()
415
+
416
+ meet_link = created_event.get('hangoutLink', 'No Meet link generated')
417
+
418
+ return f"Meeting created: {title}\nStart: {self._format_datetime(start_dt.isoformat())}\nMeet link: {meet_link}\nEvent ID: {created_event['id']}"
419
+
420
+ def update_event(self, event_id: str, title: str = None, start_time: str = None,
421
+ end_time: str = None, description: str = None,
422
+ attendees: str = None, location: str = None) -> str:
423
+ """Update an existing calendar event.
424
+
425
+ Args:
426
+ event_id: Calendar event ID
427
+ title: Optional new title
428
+ start_time: Optional new start time
429
+ end_time: Optional new end time
430
+ description: Optional new description
431
+ attendees: Optional new comma-separated attendees
432
+ location: Optional new location
433
+
434
+ Returns:
435
+ Confirmation message
436
+ """
437
+ service = self._get_service()
438
+
439
+ # Get existing event
440
+ event = service.events().get(
441
+ calendarId='primary',
442
+ eventId=event_id
443
+ ).execute()
444
+
445
+ # Update fields
446
+ if title:
447
+ event['summary'] = title
448
+ if description:
449
+ event['description'] = description
450
+ if location:
451
+ event['location'] = location
452
+ if start_time:
453
+ start_dt = self._parse_time(start_time)
454
+ event['start'] = {
455
+ 'dateTime': start_dt.isoformat(),
456
+ 'timeZone': 'UTC',
457
+ }
458
+ if end_time:
459
+ end_dt = self._parse_time(end_time)
460
+ event['end'] = {
461
+ 'dateTime': end_dt.isoformat(),
462
+ 'timeZone': 'UTC',
463
+ }
464
+ if attendees:
465
+ attendee_list = [{'email': email.strip()} for email in attendees.split(',')]
466
+ event['attendees'] = attendee_list
467
+
468
+ updated_event = service.events().update(
469
+ calendarId='primary',
470
+ eventId=event_id,
471
+ body=event
472
+ ).execute()
473
+
474
+ return f"Event updated: {updated_event['summary']}\nEvent ID: {event_id}"
475
+
476
+ def delete_event(self, event_id: str) -> str:
477
+ """Delete a calendar event.
478
+
479
+ Args:
480
+ event_id: Calendar event ID
481
+
482
+ Returns:
483
+ Confirmation message
484
+ """
485
+ service = self._get_service()
486
+
487
+ service.events().delete(
488
+ calendarId='primary',
489
+ eventId=event_id
490
+ ).execute()
491
+
492
+ return f"Event deleted: {event_id}"
493
+
494
+ # === Meeting Management ===
495
+
496
+ def get_upcoming_meetings(self, days_ahead: int = 7) -> str:
497
+ """Get upcoming meetings (events with attendees).
498
+
499
+ Args:
500
+ days_ahead: Number of days to look ahead (default: 7)
501
+
502
+ Returns:
503
+ Formatted list of upcoming meetings
504
+ """
505
+ service = self._get_service()
506
+
507
+ now = datetime.utcnow().isoformat() + 'Z'
508
+ end = (datetime.utcnow() + timedelta(days=days_ahead)).isoformat() + 'Z'
509
+
510
+ events_result = service.events().list(
511
+ calendarId='primary',
512
+ timeMin=now,
513
+ timeMax=end,
514
+ singleEvents=True,
515
+ orderBy='startTime'
516
+ ).execute()
517
+
518
+ events = events_result.get('items', [])
519
+
520
+ # Filter only events with attendees (meetings)
521
+ meetings = [e for e in events if e.get('attendees')]
522
+
523
+ if not meetings:
524
+ return f"No upcoming meetings in the next {days_ahead} days."
525
+
526
+ output = [f"Upcoming meetings (next {days_ahead} days):\n"]
527
+ for meeting in meetings:
528
+ start = meeting['start'].get('dateTime', meeting['start'].get('date'))
529
+ summary = meeting.get('summary', 'No title')
530
+ attendees = meeting.get('attendees', [])
531
+ attendee_emails = [a.get('email', '') for a in attendees if a.get('email')]
532
+ meet_link = meeting.get('hangoutLink', '')
533
+
534
+ output.append(f"- {self._format_datetime(start)}: {summary}")
535
+ output.append(f" Attendees: {', '.join(attendee_emails)}")
536
+ if meet_link:
537
+ output.append(f" Meet: {meet_link}")
538
+ output.append("")
539
+
540
+ return "\n".join(output)
541
+
542
+ def find_free_slots(self, date: str, duration_minutes: int = 60) -> str:
543
+ """Find free time slots on a specific date.
544
+
545
+ Args:
546
+ date: Date to check (YYYY-MM-DD format)
547
+ duration_minutes: Desired meeting duration (default: 60)
548
+
549
+ Returns:
550
+ List of available time slots
551
+ """
552
+ service = self._get_service()
553
+
554
+ # Parse date
555
+ target_date = datetime.strptime(date, '%Y-%m-%d')
556
+ start_of_day = target_date.replace(hour=9, minute=0, second=0).isoformat() + 'Z'
557
+ end_of_day = target_date.replace(hour=17, minute=0, second=0).isoformat() + 'Z'
558
+
559
+ # Get events for the day
560
+ events_result = service.events().list(
561
+ calendarId='primary',
562
+ timeMin=start_of_day,
563
+ timeMax=end_of_day,
564
+ singleEvents=True,
565
+ orderBy='startTime'
566
+ ).execute()
567
+
568
+ events = events_result.get('items', [])
569
+
570
+ # Find gaps
571
+ free_slots = []
572
+ current_time = target_date.replace(hour=9, minute=0)
573
+ end_time = target_date.replace(hour=17, minute=0)
574
+
575
+ for event in events:
576
+ event_start = datetime.fromisoformat(event['start'].get('dateTime', '').replace('Z', '+00:00'))
577
+ event_end = datetime.fromisoformat(event['end'].get('dateTime', '').replace('Z', '+00:00'))
578
+
579
+ # Check if there's a gap before this event
580
+ if (event_start - current_time).total_seconds() >= duration_minutes * 60:
581
+ free_slots.append(f"{current_time.strftime('%I:%M %p')} - {event_start.strftime('%I:%M %p')}")
582
+
583
+ current_time = max(current_time, event_end)
584
+
585
+ # Check gap at end of day
586
+ if (end_time - current_time).total_seconds() >= duration_minutes * 60:
587
+ free_slots.append(f"{current_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}")
588
+
589
+ if not free_slots:
590
+ return f"No free slots available on {date} for {duration_minutes} minute meetings."
591
+
592
+ return f"Free slots on {date} ({duration_minutes}+ minutes):\n" + "\n".join(f" - {slot}" for slot in free_slots)
593
+
594
+ def _parse_time(self, time_str: str) -> datetime:
595
+ """Parse time string to datetime object.
596
+
597
+ Supports formats:
598
+ - ISO: 2024-01-15T14:00:00Z, 2024-01-15T14:00:00
599
+ - Simple: 2024-01-15 14:00
600
+
601
+ Args:
602
+ time_str: Time string
603
+
604
+ Returns:
605
+ datetime object
606
+ """
607
+ for fmt in ['%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M']:
608
+ try:
609
+ return datetime.strptime(time_str, fmt)
610
+ except ValueError:
611
+ continue
612
+
613
+ raise ValueError(f"Cannot parse time: {time_str}. Use format: YYYY-MM-DD HH:MM or ISO format")