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]
|
||||
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", [])
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue