From a6c89c51591e61cc95bfbaf0a30496d6fcb77be9 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Tue, 2 Jun 2026 09:29:28 +0200 Subject: [PATCH] fix: Calendar adapter uses calendarView with date range, agent shows event summaries inline Co-authored-by: Cursor --- .../connectors/providerMsft/connectorMsft.py | 68 +++++++++++++++++-- .../coreTools/_dataSourceTools.py | 27 +++++++- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/modules/connectors/providerMsft/connectorMsft.py b/modules/connectors/providerMsft/connectorMsft.py index 52e50af2..4b60db55 100644 --- a/modules/connectors/providerMsft/connectorMsft.py +++ b/modules/connectors/providerMsft/connectorMsft.py @@ -932,10 +932,22 @@ class CalendarAdapter(_GraphApiMixin, ServiceAdapter): calendarId = cleanPath.split("/", 1)[0] effectiveLimit = self._DEFAULT_EVENT_LIMIT if limit is None else max(1, min(int(limit), self._MAX_EVENT_LIMIT)) pageSize = min(self._PAGE_SIZE, effectiveLimit) - endpoint: Optional[str] = ( - f"me/calendars/{calendarId}/events" - f"?$top={pageSize}&$orderby=start/dateTime desc" - ) + + startDateTime, endDateTime = self._parseDateRange(filter) + if startDateTime and endDateTime: + endpoint: Optional[str] = ( + f"me/calendars/{calendarId}/calendarView" + f"?startDateTime={startDateTime}&endDateTime={endDateTime}" + f"&$top={pageSize}&$orderby=start/dateTime" + f"&$select=id,subject,start,end,location,organizer,isAllDay,webLink" + ) + else: + endpoint = ( + f"me/calendars/{calendarId}/events" + f"?$top={pageSize}&$orderby=start/dateTime desc" + f"&$select=id,subject,start,end,location,organizer,isAllDay,webLink" + ) + events: List[Dict[str, Any]] = [] while endpoint and len(events) < effectiveLimit: result = await self._graphGet(endpoint) @@ -968,6 +980,33 @@ class CalendarAdapter(_GraphApiMixin, ServiceAdapter): for ev in events ] + @staticmethod + def _parseDateRange(filterStr: Optional[str]) -> tuple: + """Parse date range from filter string. Supports ISO dates or YYYY-MM patterns.""" + import re + from datetime import datetime, timedelta + if not filterStr: + return (None, None) + isoMatch = re.findall(r'\d{4}-\d{2}-\d{2}(?:T[\d:]+)?', filterStr) + if len(isoMatch) >= 2: + return (isoMatch[0], isoMatch[1]) + if len(isoMatch) == 1: + try: + dt = datetime.fromisoformat(isoMatch[0]) + return (isoMatch[0], (dt + timedelta(days=31)).strftime('%Y-%m-%dT00:00:00')) + except ValueError: + pass + monthMatch = re.match(r'^(\d{4})-(\d{2})$', filterStr.strip()) + if monthMatch: + year, month = int(monthMatch.group(1)), int(monthMatch.group(2)) + start = f"{year}-{month:02d}-01T00:00:00" + if month == 12: + end = f"{year + 1}-01-01T00:00:00" + else: + end = f"{year}-{month + 1:02d}-01T00:00:00" + return (start, end) + return (None, None) + async def download(self, path: str) -> DownloadResult: cleanPath = (path or "").strip("/") if "/" not in cleanPath: @@ -995,22 +1034,37 @@ class CalendarAdapter(_GraphApiMixin, ServiceAdapter): path: Optional[str] = None, limit: Optional[int] = None, ) -> List[ExternalEntry]: - safeQuery = query.replace("'", "''") effectiveLimit = self._DEFAULT_EVENT_LIMIT if limit is None else max(1, min(int(limit), self._MAX_EVENT_LIMIT)) - endpoint = f"me/events?$search=\"{safeQuery}\"&$top={effectiveLimit}" + + startDateTime, endDateTime = self._parseDateRange(query) + if startDateTime and endDateTime: + endpoint = ( + f"me/calendarView" + f"?startDateTime={startDateTime}&endDateTime={endDateTime}" + f"&$top={effectiveLimit}&$orderby=start/dateTime" + f"&$select=id,subject,start,end,location,organizer,isAllDay" + ) + else: + safeQuery = query.replace("'", "''").replace('"', '\\"') + endpoint = f'me/events?$search="{safeQuery}"&$top={effectiveLimit}&$select=id,subject,start,end,location,organizer,isAllDay' + result = await self._graphGet(endpoint) if "error" in result: return [] + calendarId = (path or "").strip("/").split("/")[0] if path else "search" return [ ExternalEntry( name=ev.get("subject", "(no subject)"), - path=f"/search/{ev.get('id', '')}", + path=f"/{calendarId}/{ev.get('id', '')}", isFolder=False, mimeType="text/calendar", metadata={ "id": ev.get("id"), "start": (ev.get("start") or {}).get("dateTime"), "end": (ev.get("end") or {}).get("dateTime"), + "location": (ev.get("location") or {}).get("displayName"), + "organizer": (ev.get("organizer") or {}).get("emailAddress", {}).get("address"), + "isAllDay": ev.get("isAllDay", False), }, ) for ev in result.get("value", []) diff --git a/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py b/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py index dbd28dd4..5ad4310d 100644 --- a/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py +++ b/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py @@ -80,6 +80,7 @@ def _registerDataSourceTools(registry: ToolRegistry, services): return connectionId, service, path, neutralize _MAIL_SERVICES = {"outlook", "gmail"} + _CALENDAR_SERVICES = {"calendar", "calendarFolder"} async def _browseDataSource(args: Dict[str, Any], context: Dict[str, Any]): dsId = args.get("dataSourceId", "") @@ -116,10 +117,18 @@ def _registerDataSourceTools(registry: ToolRegistry, services): if not entries: return ToolResult(toolCallId="", toolName="browseDataSource", success=True, data="Empty directory.") lines = [] + isCalendar = service in _CALENDAR_SERVICES for e in entries: prefix = "[DIR]" if e.isFolder else "[FILE]" sizeInfo = f" ({e.size} bytes)" if e.size else "" - lines.append(f"- {prefix} {e.name}{sizeInfo} path: {e.path}") + if isCalendar and not e.isFolder and e.metadata: + start = (e.metadata.get("start") or "")[:16] + end = (e.metadata.get("end") or "")[:16] + loc = e.metadata.get("location") or "" + locStr = f" 📍 {loc}" if loc else "" + lines.append(f"- 📅 {start} – {end} {e.name}{locStr}") + else: + lines.append(f"- {prefix} {e.name}{sizeInfo} path: {e.path}") result = "\n".join(lines) countLine = f"\n\n({len(entries)} entries returned" if limit is not None: @@ -128,6 +137,8 @@ def _registerDataSourceTools(registry: ToolRegistry, services): result += countLine if service in _MAIL_SERVICES: result += "\n\nIMPORTANT: These are email subjects only. To read the full email content, use downloadFromDataSource with the path, then readFile on the returned file ID." + if isCalendar and not any(e.isFolder for e in entries): + result += "\n\nThese are calendar event summaries with date/time. You do NOT need to download individual events — this listing already contains subject, start, end, and location. Use the filter parameter with a date range (e.g. '2026-06') for specific periods." return ToolResult(toolCallId="", toolName="browseDataSource", success=True, data=result) except Exception as e: return ToolResult(toolCallId="", toolName="browseDataSource", success=False, error=str(e)) @@ -161,7 +172,17 @@ def _registerDataSourceTools(registry: ToolRegistry, services): entries = await adapter.search(query, path=basePath, limit=limit) if not entries: return ToolResult(toolCallId="", toolName="searchDataSource", success=True, data="No results found.") - lines = [f"- {e.name} (path: {e.path})" for e in entries] + isCalendar = service in _CALENDAR_SERVICES + lines = [] + for e in entries: + if isCalendar and e.metadata: + start = (e.metadata.get("start") or "")[:16] + end = (e.metadata.get("end") or "")[:16] + loc = e.metadata.get("location") or "" + locStr = f" 📍 {loc}" if loc else "" + lines.append(f"- 📅 {start} – {end} {e.name}{locStr}") + else: + lines.append(f"- {e.name} (path: {e.path})") result = "\n".join(lines) countLine = f"\n\n({len(entries)} entries returned" if limit is not None: @@ -170,6 +191,8 @@ def _registerDataSourceTools(registry: ToolRegistry, services): result += countLine if service in _MAIL_SERVICES: result += "\n\nIMPORTANT: These are email subjects only. To read the full email content, use downloadFromDataSource with the path, then readFile on the returned file ID." + if isCalendar: + result += "\n\nThese are calendar event summaries. You do NOT need to download individual events — subject, start, end, and location are shown above. For date-specific queries, use a date range as query (e.g. '2026-06')." return ToolResult(toolCallId="", toolName="searchDataSource", success=True, data=result) except Exception as e: return ToolResult(toolCallId="", toolName="searchDataSource", success=False, error=str(e))