fix: Calendar adapter uses calendarView with date range, agent shows event summaries inline
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
33dd694ba1
commit
a6c89c5159
2 changed files with 86 additions and 9 deletions
|
|
@ -932,10 +932,22 @@ class CalendarAdapter(_GraphApiMixin, ServiceAdapter):
|
||||||
calendarId = cleanPath.split("/", 1)[0]
|
calendarId = cleanPath.split("/", 1)[0]
|
||||||
effectiveLimit = self._DEFAULT_EVENT_LIMIT if limit is None else max(1, min(int(limit), self._MAX_EVENT_LIMIT))
|
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)
|
pageSize = min(self._PAGE_SIZE, effectiveLimit)
|
||||||
endpoint: Optional[str] = (
|
|
||||||
f"me/calendars/{calendarId}/events"
|
startDateTime, endDateTime = self._parseDateRange(filter)
|
||||||
f"?$top={pageSize}&$orderby=start/dateTime desc"
|
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]] = []
|
events: List[Dict[str, Any]] = []
|
||||||
while endpoint and len(events) < effectiveLimit:
|
while endpoint and len(events) < effectiveLimit:
|
||||||
result = await self._graphGet(endpoint)
|
result = await self._graphGet(endpoint)
|
||||||
|
|
@ -968,6 +980,33 @@ class CalendarAdapter(_GraphApiMixin, ServiceAdapter):
|
||||||
for ev in events
|
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:
|
async def download(self, path: str) -> DownloadResult:
|
||||||
cleanPath = (path or "").strip("/")
|
cleanPath = (path or "").strip("/")
|
||||||
if "/" not in cleanPath:
|
if "/" not in cleanPath:
|
||||||
|
|
@ -995,22 +1034,37 @@ class CalendarAdapter(_GraphApiMixin, ServiceAdapter):
|
||||||
path: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
limit: Optional[int] = None,
|
limit: Optional[int] = None,
|
||||||
) -> List[ExternalEntry]:
|
) -> List[ExternalEntry]:
|
||||||
safeQuery = query.replace("'", "''")
|
|
||||||
effectiveLimit = self._DEFAULT_EVENT_LIMIT if limit is None else max(1, min(int(limit), self._MAX_EVENT_LIMIT))
|
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)
|
result = await self._graphGet(endpoint)
|
||||||
if "error" in result:
|
if "error" in result:
|
||||||
return []
|
return []
|
||||||
|
calendarId = (path or "").strip("/").split("/")[0] if path else "search"
|
||||||
return [
|
return [
|
||||||
ExternalEntry(
|
ExternalEntry(
|
||||||
name=ev.get("subject", "(no subject)"),
|
name=ev.get("subject", "(no subject)"),
|
||||||
path=f"/search/{ev.get('id', '')}",
|
path=f"/{calendarId}/{ev.get('id', '')}",
|
||||||
isFolder=False,
|
isFolder=False,
|
||||||
mimeType="text/calendar",
|
mimeType="text/calendar",
|
||||||
metadata={
|
metadata={
|
||||||
"id": ev.get("id"),
|
"id": ev.get("id"),
|
||||||
"start": (ev.get("start") or {}).get("dateTime"),
|
"start": (ev.get("start") or {}).get("dateTime"),
|
||||||
"end": (ev.get("end") 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", [])
|
for ev in result.get("value", [])
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ def _registerDataSourceTools(registry: ToolRegistry, services):
|
||||||
return connectionId, service, path, neutralize
|
return connectionId, service, path, neutralize
|
||||||
|
|
||||||
_MAIL_SERVICES = {"outlook", "gmail"}
|
_MAIL_SERVICES = {"outlook", "gmail"}
|
||||||
|
_CALENDAR_SERVICES = {"calendar", "calendarFolder"}
|
||||||
|
|
||||||
async def _browseDataSource(args: Dict[str, Any], context: Dict[str, Any]):
|
async def _browseDataSource(args: Dict[str, Any], context: Dict[str, Any]):
|
||||||
dsId = args.get("dataSourceId", "")
|
dsId = args.get("dataSourceId", "")
|
||||||
|
|
@ -116,10 +117,18 @@ def _registerDataSourceTools(registry: ToolRegistry, services):
|
||||||
if not entries:
|
if not entries:
|
||||||
return ToolResult(toolCallId="", toolName="browseDataSource", success=True, data="Empty directory.")
|
return ToolResult(toolCallId="", toolName="browseDataSource", success=True, data="Empty directory.")
|
||||||
lines = []
|
lines = []
|
||||||
|
isCalendar = service in _CALENDAR_SERVICES
|
||||||
for e in entries:
|
for e in entries:
|
||||||
prefix = "[DIR]" if e.isFolder else "[FILE]"
|
prefix = "[DIR]" if e.isFolder else "[FILE]"
|
||||||
sizeInfo = f" ({e.size} bytes)" if e.size else ""
|
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)
|
result = "\n".join(lines)
|
||||||
countLine = f"\n\n({len(entries)} entries returned"
|
countLine = f"\n\n({len(entries)} entries returned"
|
||||||
if limit is not None:
|
if limit is not None:
|
||||||
|
|
@ -128,6 +137,8 @@ def _registerDataSourceTools(registry: ToolRegistry, services):
|
||||||
result += countLine
|
result += countLine
|
||||||
if service in _MAIL_SERVICES:
|
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."
|
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)
|
return ToolResult(toolCallId="", toolName="browseDataSource", success=True, data=result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return ToolResult(toolCallId="", toolName="browseDataSource", success=False, error=str(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)
|
entries = await adapter.search(query, path=basePath, limit=limit)
|
||||||
if not entries:
|
if not entries:
|
||||||
return ToolResult(toolCallId="", toolName="searchDataSource", success=True, data="No results found.")
|
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)
|
result = "\n".join(lines)
|
||||||
countLine = f"\n\n({len(entries)} entries returned"
|
countLine = f"\n\n({len(entries)} entries returned"
|
||||||
if limit is not None:
|
if limit is not None:
|
||||||
|
|
@ -170,6 +191,8 @@ def _registerDataSourceTools(registry: ToolRegistry, services):
|
||||||
result += countLine
|
result += countLine
|
||||||
if service in _MAIL_SERVICES:
|
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."
|
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)
|
return ToolResult(toolCallId="", toolName="searchDataSource", success=True, data=result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return ToolResult(toolCallId="", toolName="searchDataSource", success=False, error=str(e))
|
return ToolResult(toolCallId="", toolName="searchDataSource", success=False, error=str(e))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue