fix: Calendar adapter uses calendarView with date range, agent shows event summaries inline
All checks were successful
Deploy Plattform-Core / test (push) Successful in 48s
Deploy Plattform-Core (Int) / test (push) Successful in 1m1s
Deploy Plattform-Core / deploy (push) Successful in 11s
Deploy Plattform-Core (Int) / deploy (push) Successful in 9s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-06-02 09:29:28 +02:00
parent 33dd694ba1
commit a6c89c5159
2 changed files with 86 additions and 9 deletions

View file

@ -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", [])

View file

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