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))