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,604 @@
1
+ """
2
+ Purpose: Microsoft Calendar integration tool for managing events via Microsoft Graph API
3
+ LLM-Note:
4
+ Dependencies: imports from [os, datetime, httpx] | imported by [useful_tools/__init__.py] | requires OAuth tokens from 'co auth microsoft' | tested by [tests/unit/test_microsoft_calendar.py]
5
+ Data flow: Agent calls MicrosoftCalendar methods → _get_headers() loads MICROSOFT_ACCESS_TOKEN from env → HTTP calls to Graph API (https://graph.microsoft.com/v1.0) → returns formatted results (event lists, confirmations, free slots)
6
+ State/Effects: reads MICROSOFT_* env vars for OAuth tokens | makes HTTP calls to Microsoft Graph API | can create/update/delete events, create Teams meetings | no local file persistence
7
+ Integration: exposes MicrosoftCalendar class with list_events(), get_today_events(), get_event(), create_event(), update_event(), delete_event(), create_teams_meeting(), get_upcoming_meetings(), find_free_slots(), check_availability() | used as agent tool via Agent(tools=[MicrosoftCalendar()])
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 | HTTP errors from Graph API propagate | returns error strings for display
10
+
11
+ Microsoft Calendar tool for managing calendar events via Microsoft Graph API.
12
+
13
+ Usage:
14
+ from connectonion import Agent, MicrosoftCalendar
15
+
16
+ calendar = MicrosoftCalendar()
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_teams_meeting(title, start_time, end_time, attendees, description)
27
+ # - get_upcoming_meetings(days_ahead)
28
+ # - find_free_slots(date, duration_minutes)
29
+ # - check_availability(datetime_str)
30
+
31
+ Example:
32
+ from connectonion import Agent, MicrosoftCalendar
33
+
34
+ calendar = MicrosoftCalendar()
35
+ agent = Agent(
36
+ name="calendar-assistant",
37
+ system_prompt="You are a calendar assistant.",
38
+ tools=[calendar]
39
+ )
40
+
41
+ agent.input("What meetings do I have today?")
42
+ agent.input("Schedule a meeting with alice@example.com tomorrow at 2pm")
43
+ """
44
+
45
+ import os
46
+ from datetime import datetime, timedelta
47
+ import httpx
48
+
49
+
50
+ class MicrosoftCalendar:
51
+ """Microsoft Calendar tool for managing events via Microsoft Graph API."""
52
+
53
+ GRAPH_API_URL = "https://graph.microsoft.com/v1.0"
54
+
55
+ def __init__(self):
56
+ """Initialize Microsoft Calendar tool.
57
+
58
+ Validates that Microsoft OAuth is configured with Calendar scopes.
59
+ Raises ValueError if credentials are missing.
60
+ """
61
+ scopes = os.getenv("MICROSOFT_SCOPES", "")
62
+ if not scopes or "Calendars" not in scopes:
63
+ raise ValueError(
64
+ "Missing Microsoft Calendar scopes.\n"
65
+ f"Current scopes: {scopes}\n"
66
+ "Please authorize Microsoft Calendar access:\n"
67
+ " co auth microsoft"
68
+ )
69
+
70
+ self._access_token = None
71
+
72
+ def _get_access_token(self) -> str:
73
+ """Get Microsoft access token (with auto-refresh)."""
74
+ access_token = os.getenv("MICROSOFT_ACCESS_TOKEN")
75
+ refresh_token = os.getenv("MICROSOFT_REFRESH_TOKEN")
76
+ expires_at_str = os.getenv("MICROSOFT_TOKEN_EXPIRES_AT")
77
+
78
+ if not access_token or not refresh_token:
79
+ raise ValueError(
80
+ "Microsoft OAuth credentials not found.\n"
81
+ "Run: co auth microsoft"
82
+ )
83
+
84
+ # Check if token is expired or about to expire (within 5 minutes)
85
+ if expires_at_str:
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
+ access_token = self._refresh_via_backend(refresh_token)
91
+ self._access_token = None
92
+
93
+ if self._access_token:
94
+ return self._access_token
95
+
96
+ self._access_token = access_token
97
+ return self._access_token
98
+
99
+ def _refresh_via_backend(self, refresh_token: str) -> str:
100
+ """Refresh access token via backend API."""
101
+ backend_url = os.getenv("OPENONION_API_URL", "https://oo.openonion.ai")
102
+ api_key = os.getenv("OPENONION_API_KEY")
103
+
104
+ if not api_key:
105
+ raise ValueError(
106
+ "OPENONION_API_KEY not found.\n"
107
+ "This is needed to refresh tokens via backend."
108
+ )
109
+
110
+ response = httpx.post(
111
+ f"{backend_url}/api/v1/oauth/microsoft/refresh",
112
+ headers={"Authorization": f"Bearer {api_key}"},
113
+ json={"refresh_token": refresh_token}
114
+ )
115
+
116
+ if response.status_code != 200:
117
+ raise ValueError(
118
+ f"Failed to refresh Microsoft token via backend: {response.text}"
119
+ )
120
+
121
+ data = response.json()
122
+ new_access_token = data["access_token"]
123
+ expires_at = data["expires_at"]
124
+
125
+ os.environ["MICROSOFT_ACCESS_TOKEN"] = new_access_token
126
+ os.environ["MICROSOFT_TOKEN_EXPIRES_AT"] = expires_at
127
+
128
+ env_file = os.path.join(os.getenv("AGENT_CONFIG_PATH", os.path.expanduser("~/.co")), "keys.env")
129
+ if os.path.exists(env_file):
130
+ with open(env_file, 'r') as f:
131
+ lines = f.readlines()
132
+
133
+ with open(env_file, 'w') as f:
134
+ for line in lines:
135
+ if line.startswith("MICROSOFT_ACCESS_TOKEN="):
136
+ f.write(f"MICROSOFT_ACCESS_TOKEN={new_access_token}\n")
137
+ elif line.startswith("MICROSOFT_TOKEN_EXPIRES_AT="):
138
+ f.write(f"MICROSOFT_TOKEN_EXPIRES_AT={expires_at}\n")
139
+ else:
140
+ f.write(line)
141
+
142
+ return new_access_token
143
+
144
+ def _request(self, method: str, endpoint: str, **kwargs) -> dict:
145
+ """Make authenticated request to Microsoft Graph API."""
146
+ token = self._get_access_token()
147
+ headers = {
148
+ "Authorization": f"Bearer {token}",
149
+ "Content-Type": "application/json"
150
+ }
151
+
152
+ url = f"{self.GRAPH_API_URL}{endpoint}"
153
+ response = httpx.request(method, url, headers=headers, **kwargs)
154
+
155
+ if response.status_code == 401:
156
+ refresh_token = os.getenv("MICROSOFT_REFRESH_TOKEN")
157
+ if refresh_token:
158
+ self._access_token = None
159
+ token = self._refresh_via_backend(refresh_token)
160
+ headers["Authorization"] = f"Bearer {token}"
161
+ response = httpx.request(method, url, headers=headers, **kwargs)
162
+
163
+ if response.status_code not in [200, 201, 202, 204]:
164
+ raise ValueError(f"Microsoft Graph API error: {response.status_code} - {response.text}")
165
+
166
+ if response.status_code == 204:
167
+ return {}
168
+ return response.json()
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
+ def _parse_time(self, time_str: str) -> datetime:
176
+ """Parse time string to datetime object."""
177
+ for fmt in ['%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M']:
178
+ try:
179
+ return datetime.strptime(time_str, fmt)
180
+ except ValueError:
181
+ continue
182
+
183
+ raise ValueError(f"Cannot parse time: {time_str}. Use format: YYYY-MM-DD HH:MM or ISO format")
184
+
185
+ # === Reading Events ===
186
+
187
+ def list_events(self, days_ahead: int = 7, max_results: int = 20) -> str:
188
+ """List upcoming calendar events.
189
+
190
+ Args:
191
+ days_ahead: Number of days to look ahead (default: 7)
192
+ max_results: Maximum number of events to return (default: 20)
193
+
194
+ Returns:
195
+ Formatted string with event list
196
+ """
197
+ now = datetime.utcnow()
198
+ end = now + timedelta(days=days_ahead)
199
+
200
+ endpoint = "/me/calendar/calendarView"
201
+ params = {
202
+ "startDateTime": now.isoformat() + 'Z',
203
+ "endDateTime": end.isoformat() + 'Z',
204
+ "$top": max_results,
205
+ "$orderby": "start/dateTime",
206
+ "$select": "id,subject,start,end,attendees,onlineMeeting,onlineMeetingUrl"
207
+ }
208
+
209
+ result = self._request("GET", endpoint, params=params)
210
+ events = result.get('value', [])
211
+
212
+ if not events:
213
+ return f"No upcoming events in the next {days_ahead} days."
214
+
215
+ output = [f"Upcoming events (next {days_ahead} days):\n"]
216
+ for event in events:
217
+ start = event.get('start', {}).get('dateTime', '')
218
+ subject = event.get('subject', 'No title')
219
+ event_id = event['id']
220
+
221
+ attendees = event.get('attendees', [])
222
+ attendee_str = ""
223
+ if attendees:
224
+ attendee_emails = [a.get('emailAddress', {}).get('address', '') for a in attendees]
225
+ if attendee_emails:
226
+ attendee_str = f"\n Attendees: {', '.join(attendee_emails)}"
227
+
228
+ meeting_url = event.get('onlineMeetingUrl', '')
229
+ meeting_str = f"\n Meeting: {meeting_url}" if meeting_url else ""
230
+
231
+ output.append(f"- {self._format_datetime(start)}: {subject}")
232
+ output.append(f" ID: {event_id}{attendee_str}{meeting_str}\n")
233
+
234
+ return "\n".join(output)
235
+
236
+ def get_today_events(self) -> str:
237
+ """Get today's calendar events.
238
+
239
+ Returns:
240
+ Formatted string with today's events
241
+ """
242
+ now = datetime.utcnow()
243
+ start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
244
+ end_of_day = now.replace(hour=23, minute=59, second=59, microsecond=999999)
245
+
246
+ endpoint = "/me/calendar/calendarView"
247
+ params = {
248
+ "startDateTime": start_of_day.isoformat() + 'Z',
249
+ "endDateTime": end_of_day.isoformat() + 'Z',
250
+ "$orderby": "start/dateTime",
251
+ "$select": "id,subject,start,end,onlineMeetingUrl"
252
+ }
253
+
254
+ result = self._request("GET", endpoint, params=params)
255
+ events = result.get('value', [])
256
+
257
+ if not events:
258
+ return "No events scheduled for today."
259
+
260
+ output = ["Today's events:\n"]
261
+ for event in events:
262
+ start = event.get('start', {}).get('dateTime', '')
263
+ subject = event.get('subject', 'No title')
264
+ meeting_url = event.get('onlineMeetingUrl', '')
265
+ meeting_str = f" [Meeting: {meeting_url}]" if meeting_url else ""
266
+
267
+ output.append(f"- {self._format_datetime(start)}: {subject}{meeting_str}")
268
+
269
+ return "\n".join(output)
270
+
271
+ def get_event(self, event_id: str) -> str:
272
+ """Get detailed information about a specific event.
273
+
274
+ Args:
275
+ event_id: Calendar event ID
276
+
277
+ Returns:
278
+ Formatted event details
279
+ """
280
+ endpoint = f"/me/calendar/events/{event_id}"
281
+ params = {
282
+ "$select": "subject,start,end,body,location,attendees,onlineMeetingUrl"
283
+ }
284
+
285
+ event = self._request("GET", endpoint, params=params)
286
+
287
+ subject = event.get('subject', 'No title')
288
+ start = event.get('start', {}).get('dateTime', '')
289
+ end = event.get('end', {}).get('dateTime', '')
290
+ body = event.get('body', {}).get('content', 'No description')
291
+ location = event.get('location', {}).get('displayName', 'No location')
292
+
293
+ attendees = event.get('attendees', [])
294
+ attendee_list = []
295
+ for a in attendees:
296
+ email = a.get('emailAddress', {}).get('address', '')
297
+ status = a.get('status', {}).get('response', 'none')
298
+ attendee_list.append(f"{email} ({status})")
299
+
300
+ meeting_url = event.get('onlineMeetingUrl', 'No meeting link')
301
+
302
+ output = [
303
+ f"Event: {subject}",
304
+ f"Start: {self._format_datetime(start)}",
305
+ f"End: {self._format_datetime(end)}",
306
+ f"Location: {location}",
307
+ f"Meeting: {meeting_url}",
308
+ ]
309
+
310
+ if attendee_list:
311
+ output.append(f"Attendees:\n " + "\n ".join(attendee_list))
312
+
313
+ return "\n".join(output)
314
+
315
+ # === Creating Events ===
316
+
317
+ def create_event(self, title: str, start_time: str, end_time: str,
318
+ description: str = None, attendees: str = None,
319
+ location: str = None) -> str:
320
+ """Create a new calendar event.
321
+
322
+ Args:
323
+ title: Event title
324
+ start_time: Start time (ISO format or "YYYY-MM-DD HH:MM")
325
+ end_time: End time (ISO format or "YYYY-MM-DD HH:MM")
326
+ description: Optional event description
327
+ attendees: Optional comma-separated email addresses
328
+ location: Optional location
329
+
330
+ Returns:
331
+ Confirmation with event ID and details
332
+ """
333
+ start_dt = self._parse_time(start_time)
334
+ end_dt = self._parse_time(end_time)
335
+
336
+ event = {
337
+ "subject": title,
338
+ "start": {
339
+ "dateTime": start_dt.isoformat(),
340
+ "timeZone": "UTC"
341
+ },
342
+ "end": {
343
+ "dateTime": end_dt.isoformat(),
344
+ "timeZone": "UTC"
345
+ }
346
+ }
347
+
348
+ if description:
349
+ event["body"] = {
350
+ "contentType": "Text",
351
+ "content": description
352
+ }
353
+
354
+ if location:
355
+ event["location"] = {"displayName": location}
356
+
357
+ if attendees:
358
+ event["attendees"] = [
359
+ {
360
+ "emailAddress": {"address": email.strip()},
361
+ "type": "required"
362
+ }
363
+ for email in attendees.split(',')
364
+ ]
365
+
366
+ created_event = self._request("POST", "/me/calendar/events", json=event)
367
+
368
+ return f"Event created: {title}\nStart: {self._format_datetime(start_dt.isoformat())}\nEvent ID: {created_event['id']}\nLink: {created_event.get('webLink', '')}"
369
+
370
+ def create_teams_meeting(self, title: str, start_time: str, end_time: str,
371
+ attendees: str, description: str = None) -> str:
372
+ """Create a Microsoft Teams meeting.
373
+
374
+ Args:
375
+ title: Meeting title
376
+ start_time: Start time (ISO format or "YYYY-MM-DD HH:MM")
377
+ end_time: End time (ISO format or "YYYY-MM-DD HH:MM")
378
+ attendees: Comma-separated email addresses
379
+ description: Optional meeting description
380
+
381
+ Returns:
382
+ Confirmation with Teams meeting link
383
+ """
384
+ start_dt = self._parse_time(start_time)
385
+ end_dt = self._parse_time(end_time)
386
+
387
+ event = {
388
+ "subject": title,
389
+ "start": {
390
+ "dateTime": start_dt.isoformat(),
391
+ "timeZone": "UTC"
392
+ },
393
+ "end": {
394
+ "dateTime": end_dt.isoformat(),
395
+ "timeZone": "UTC"
396
+ },
397
+ "attendees": [
398
+ {
399
+ "emailAddress": {"address": email.strip()},
400
+ "type": "required"
401
+ }
402
+ for email in attendees.split(',')
403
+ ],
404
+ "isOnlineMeeting": True,
405
+ "onlineMeetingProvider": "teamsForBusiness"
406
+ }
407
+
408
+ if description:
409
+ event["body"] = {
410
+ "contentType": "Text",
411
+ "content": description
412
+ }
413
+
414
+ created_event = self._request("POST", "/me/calendar/events", json=event)
415
+
416
+ meeting_url = created_event.get('onlineMeeting', {}).get('joinUrl', '') or created_event.get('onlineMeetingUrl', 'No meeting link')
417
+
418
+ return f"Teams meeting created: {title}\nStart: {self._format_datetime(start_dt.isoformat())}\nTeams link: {meeting_url}\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
+ updates = {}
438
+
439
+ if title:
440
+ updates["subject"] = title
441
+ if description:
442
+ updates["body"] = {"contentType": "Text", "content": description}
443
+ if location:
444
+ updates["location"] = {"displayName": location}
445
+ if start_time:
446
+ start_dt = self._parse_time(start_time)
447
+ updates["start"] = {"dateTime": start_dt.isoformat(), "timeZone": "UTC"}
448
+ if end_time:
449
+ end_dt = self._parse_time(end_time)
450
+ updates["end"] = {"dateTime": end_dt.isoformat(), "timeZone": "UTC"}
451
+ if attendees:
452
+ updates["attendees"] = [
453
+ {"emailAddress": {"address": email.strip()}, "type": "required"}
454
+ for email in attendees.split(',')
455
+ ]
456
+
457
+ endpoint = f"/me/calendar/events/{event_id}"
458
+ updated_event = self._request("PATCH", endpoint, json=updates)
459
+
460
+ return f"Event updated: {updated_event.get('subject', 'Unknown')}\nEvent ID: {event_id}"
461
+
462
+ def delete_event(self, event_id: str) -> str:
463
+ """Delete a calendar event.
464
+
465
+ Args:
466
+ event_id: Calendar event ID
467
+
468
+ Returns:
469
+ Confirmation message
470
+ """
471
+ endpoint = f"/me/calendar/events/{event_id}"
472
+ self._request("DELETE", endpoint)
473
+
474
+ return f"Event deleted: {event_id}"
475
+
476
+ # === Meeting Management ===
477
+
478
+ def get_upcoming_meetings(self, days_ahead: int = 7) -> str:
479
+ """Get upcoming meetings (events with attendees).
480
+
481
+ Args:
482
+ days_ahead: Number of days to look ahead (default: 7)
483
+
484
+ Returns:
485
+ Formatted list of upcoming meetings
486
+ """
487
+ now = datetime.utcnow()
488
+ end = now + timedelta(days=days_ahead)
489
+
490
+ endpoint = "/me/calendar/calendarView"
491
+ params = {
492
+ "startDateTime": now.isoformat() + 'Z',
493
+ "endDateTime": end.isoformat() + 'Z',
494
+ "$orderby": "start/dateTime",
495
+ "$select": "id,subject,start,attendees,onlineMeetingUrl"
496
+ }
497
+
498
+ result = self._request("GET", endpoint, params=params)
499
+ events = result.get('value', [])
500
+
501
+ meetings = [e for e in events if e.get('attendees')]
502
+
503
+ if not meetings:
504
+ return f"No upcoming meetings in the next {days_ahead} days."
505
+
506
+ output = [f"Upcoming meetings (next {days_ahead} days):\n"]
507
+ for meeting in meetings:
508
+ start = meeting.get('start', {}).get('dateTime', '')
509
+ subject = meeting.get('subject', 'No title')
510
+ attendees = meeting.get('attendees', [])
511
+ attendee_emails = [a.get('emailAddress', {}).get('address', '') for a in attendees]
512
+ meeting_url = meeting.get('onlineMeetingUrl', '')
513
+
514
+ output.append(f"- {self._format_datetime(start)}: {subject}")
515
+ output.append(f" Attendees: {', '.join(attendee_emails)}")
516
+ if meeting_url:
517
+ output.append(f" Meeting: {meeting_url}")
518
+ output.append("")
519
+
520
+ return "\n".join(output)
521
+
522
+ def find_free_slots(self, date: str, duration_minutes: int = 60) -> str:
523
+ """Find free time slots on a specific date.
524
+
525
+ Args:
526
+ date: Date to check (YYYY-MM-DD format)
527
+ duration_minutes: Desired meeting duration (default: 60)
528
+
529
+ Returns:
530
+ List of available time slots
531
+ """
532
+ target_date = datetime.strptime(date, '%Y-%m-%d')
533
+ start_of_day = target_date.replace(hour=9, minute=0, second=0)
534
+ end_of_day = target_date.replace(hour=17, minute=0, second=0)
535
+
536
+ endpoint = "/me/calendar/calendarView"
537
+ params = {
538
+ "startDateTime": start_of_day.isoformat() + 'Z',
539
+ "endDateTime": end_of_day.isoformat() + 'Z',
540
+ "$orderby": "start/dateTime",
541
+ "$select": "start,end"
542
+ }
543
+
544
+ result = self._request("GET", endpoint, params=params)
545
+ events = result.get('value', [])
546
+
547
+ free_slots = []
548
+ current_time = start_of_day
549
+
550
+ for event in events:
551
+ event_start_str = event.get('start', {}).get('dateTime', '')
552
+ event_end_str = event.get('end', {}).get('dateTime', '')
553
+
554
+ if not event_start_str or not event_end_str:
555
+ continue
556
+
557
+ event_start = datetime.fromisoformat(event_start_str.replace('Z', '+00:00')).replace(tzinfo=None)
558
+ event_end = datetime.fromisoformat(event_end_str.replace('Z', '+00:00')).replace(tzinfo=None)
559
+
560
+ if (event_start - current_time).total_seconds() >= duration_minutes * 60:
561
+ free_slots.append(f"{current_time.strftime('%I:%M %p')} - {event_start.strftime('%I:%M %p')}")
562
+
563
+ current_time = max(current_time, event_end)
564
+
565
+ if (end_of_day - current_time).total_seconds() >= duration_minutes * 60:
566
+ free_slots.append(f"{current_time.strftime('%I:%M %p')} - {end_of_day.strftime('%I:%M %p')}")
567
+
568
+ if not free_slots:
569
+ return f"No free slots available on {date} for {duration_minutes} minute meetings."
570
+
571
+ return f"Free slots on {date} ({duration_minutes}+ minutes):\n" + "\n".join(f" - {slot}" for slot in free_slots)
572
+
573
+ def check_availability(self, datetime_str: str) -> str:
574
+ """Check if a specific time is free.
575
+
576
+ Args:
577
+ datetime_str: DateTime to check (ISO format or "YYYY-MM-DD HH:MM")
578
+
579
+ Returns:
580
+ Whether the time slot is available
581
+ """
582
+ target_time = self._parse_time(datetime_str)
583
+ check_end = target_time + timedelta(hours=1)
584
+
585
+ endpoint = "/me/calendar/calendarView"
586
+ params = {
587
+ "startDateTime": target_time.isoformat() + 'Z',
588
+ "endDateTime": check_end.isoformat() + 'Z',
589
+ "$select": "subject,start,end"
590
+ }
591
+
592
+ result = self._request("GET", endpoint, params=params)
593
+ events = result.get('value', [])
594
+
595
+ if not events:
596
+ return f"Time slot {self._format_datetime(target_time.isoformat())} is FREE"
597
+
598
+ conflicts = []
599
+ for event in events:
600
+ subject = event.get('subject', 'Untitled')
601
+ start = event.get('start', {}).get('dateTime', '')
602
+ conflicts.append(f"- {subject} at {self._format_datetime(start)}")
603
+
604
+ return f"Time slot {self._format_datetime(target_time.isoformat())} is BUSY:\n" + "\n".join(conflicts)