From bc5f169bc8e6ce4b8e2ccc8cad0296f6a235a825 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Thu, 5 Mar 2026 16:52:34 -0500 Subject: [PATCH] Fix calendar tool execution: add calendar to CustomProvider valid_tools list - Added calendar and other missing tools to valid_tools whitelist in CustomProvider - This fixes issue where calendar tool calls were shown in response instead of being executed - Also added edit_file, cron, email to the whitelist for completeness --- nanobot/agent/tools/calendar.py | 86 ++++++++++++++++++++++++++-- nanobot/agent/utils/email_parser.py | 66 ++++++++++++++++++++- nanobot/providers/custom_provider.py | 18 +++++- workspace/AGENTS.md | 33 ++++++++--- 4 files changed, 187 insertions(+), 16 deletions(-) diff --git a/nanobot/agent/tools/calendar.py b/nanobot/agent/tools/calendar.py index ca20728..ca35439 100644 --- a/nanobot/agent/tools/calendar.py +++ b/nanobot/agent/tools/calendar.py @@ -26,9 +26,10 @@ class CalendarTool(Tool): "Interact with Google Calendar. REQUIRED: Always include 'action' parameter. " "Actions: 'list_events' (list upcoming events), 'create_event' (create new event), " "'check_availability' (check if time slot is available). " - "Examples: calendar(action='list_events'), calendar(action='create_event', title='Meeting', start_time='tomorrow 2pm'). " "When user asks 'what's on my calendar' or 'show my calendar', use action='list_events'. " - "When user mentions a meeting or asks to schedule something, use action='create_event'." + "When user mentions a meeting or asks to schedule something, use action='create_event' with title and start_time. " + "CRITICAL: When scheduling from emails, you MUST call this tool - do not just acknowledge. " + "Use natural language time formats like 'March 6 19:00' or 'tomorrow 2pm' - DO NOT use ISO format with wrong years." ) def __init__(self, calendar_config: Any = None): @@ -148,7 +149,13 @@ class CalendarTool(Tool): }, "start_time": { "type": "string", - "description": "Event start time in ISO format (YYYY-MM-DDTHH:MM:SS) or relative (e.g., 'tomorrow 2pm', 'in 1 hour'). Required for create_event.", + "description": ( + "Event start time. REQUIRED for create_event. " + "Use natural language formats like 'March 7 15:00', 'tomorrow 2pm', 'today at 5pm', " + "or relative formats like 'in 1 hour'. " + "DO NOT use ISO format - the tool will parse natural language and handle the current year automatically. " + "Examples: 'March 7 15:00', 'tomorrow at 2pm', 'today 18:00'" + ), }, "end_time": { "type": "string", @@ -191,16 +198,77 @@ class CalendarTool(Tool): original_str = time_str time_str = time_str.strip().lower() - # Try ISO format first + # Try ISO format first, but validate year is reasonable try: dt = datetime.fromisoformat(time_str.replace("Z", "+00:00")) # Ensure timezone-aware if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) + + # Validate year is current year or future (prevent scheduling in past years) + current_year = now.year + if dt.year < current_year: + # If year is in the past, assume user meant current year + dt = dt.replace(year=current_year) + # If still in the past, use next year + if dt < now: + dt = dt.replace(year=current_year + 1) + return dt except ValueError: pass + # Try parsing month names (e.g., "March 7 15:00", "March 7th at 3pm") + month_names = { + "january": 1, "jan": 1, "february": 2, "feb": 2, "march": 3, "mar": 3, + "april": 4, "apr": 4, "may": 5, "june": 6, "jun": 6, "july": 7, "jul": 7, + "august": 8, "aug": 8, "september": 9, "sep": 9, "sept": 9, + "october": 10, "oct": 10, "november": 11, "nov": 11, "december": 12, "dec": 12, + } + + # Pattern: "March 7 15:00" or "March 7th at 3pm" or "on March 7 at 15:00" + month_patterns = [ + r"(?:on\s+)?(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\s+(\d{1,2})(?:st|nd|rd|th)?\s+(?:at\s+)?(\d{1,2}):(\d{2})", # 24-hour + r"(?:on\s+)?(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\s+(\d{1,2})(?:st|nd|rd|th)?\s+(?:at\s+)?(\d{1,2})\s*(am|pm)", # 12-hour + r"(?:on\s+)?(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\s+(\d{1,2})(?:st|nd|rd|th)?", # Date only + ] + + for pattern in month_patterns: + match = re.search(pattern, time_str, re.IGNORECASE) + if match: + groups = match.groups() + month_name = groups[0].lower() + if month_name in month_names: + month = month_names[month_name] + day = int(groups[1]) + year = now.year + + # Check if date is in the past + test_date = datetime(year, month, day, tzinfo=timezone.utc) + if test_date < now.replace(hour=0, minute=0, second=0, microsecond=0): + year += 1 + + if len(groups) >= 4 and groups[2] and groups[3]: + if groups[3].lower() in ['am', 'pm']: + # 12-hour format + hour = int(groups[2]) + period = groups[3].lower() + minute = 0 + if period == "pm" and hour != 12: + hour += 12 + elif period == "am" and hour == 12: + hour = 0 + else: + # 24-hour format + hour = int(groups[2]) + minute = int(groups[3]) + result = datetime(year, month, day, hour, minute, tzinfo=timezone.utc) + return result + else: + # Date only, default to 9am + result = datetime(year, month, day, 9, 0, tzinfo=timezone.utc) + return result + # Parse relative times from datetime import timezone now = datetime.now(timezone.utc) @@ -540,6 +608,16 @@ class CalendarTool(Tool): else: end_dt = start_dt + timedelta(hours=1) # Default 1 hour + # Validate that start time is not in the past + from datetime import timezone + now = datetime.now(timezone.utc) + if start_dt < now: + raise ValueError( + f"Cannot schedule event in the past. Start time ({start_dt}) is before current time ({now}). " + f"Parsed from start_time='{start_time}'. " + f"Tip: Use natural language formats like 'March 7 15:00' or 'tomorrow 2pm' instead of ISO format." + ) + # Validate time range if end_dt <= start_dt: raise ValueError( diff --git a/nanobot/agent/utils/email_parser.py b/nanobot/agent/utils/email_parser.py index 7c7fccb..30ad671 100644 --- a/nanobot/agent/utils/email_parser.py +++ b/nanobot/agent/utils/email_parser.py @@ -56,12 +56,35 @@ def extract_meeting_info(email_content: str, email_subject: str = "") -> dict[st result["title"] = title[:100] # Limit length # Extract time information + # Month names mapping + month_names = { + "january": 1, "jan": 1, + "february": 2, "feb": 2, + "march": 3, "mar": 3, + "april": 4, "apr": 4, + "may": 5, + "june": 6, "jun": 6, + "july": 7, "jul": 7, + "august": 8, "aug": 8, + "september": 9, "sep": 9, "sept": 9, + "october": 10, "oct": 10, + "november": 11, "nov": 11, + "december": 12, "dec": 12, + } + time_patterns = [ + # "March 7 at 15:00" or "March 7th at 3pm" or "on March 7 at 15:00" + r"(?:on\s+)?(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\s+(\d{1,2})(?:st|nd|rd|th)?\s+(?:at\s+)?(\d{1,2}):(\d{2})", + r"(?:on\s+)?(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\s+(\d{1,2})(?:st|nd|rd|th)?\s+(?:at\s+)?(\d{1,2})\s*(am|pm)", + # "March 7" (date only, assume current year) + r"(?:on\s+)?(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\s+(\d{1,2})(?:st|nd|rd|th)?", + # Relative dates r"tomorrow\s+at\s+(\d{1,2})\s*(am|pm)?", r"tomorrow\s+(\d{1,2})\s*(am|pm)?", r"(\d{1,2})\s*(am|pm)\s+tomorrow", r"(\d{1,2}):(\d{2})\s*(am|pm)?\s+tomorrow", r"in\s+(\d+)\s+(hour|hours|minute|minutes|day|days)", + # Date formats r"(\d{1,2})/(\d{1,2})/(\d{4})\s+at\s+(\d{1,2}):(\d{2})\s*(am|pm)?", r"(\d{4})-(\d{2})-(\d{2})\s+(\d{1,2}):(\d{2})", ] @@ -71,7 +94,43 @@ def extract_meeting_info(email_content: str, email_subject: str = "") -> dict[st if match: try: now = datetime.now() - if "tomorrow" in pattern: + groups = match.groups() + + # Check if this is a month name pattern (first group is month name) + if groups and groups[0].lower() in month_names: + month_name = groups[0].lower() + month = month_names[month_name] + day = int(groups[1]) + year = now.year + + # Check if date is in the past (assume next year if so) + test_date = datetime(year, month, day) + if test_date < now.replace(hour=0, minute=0, second=0, microsecond=0): + year += 1 + + # Check if time is provided (pattern with 4 groups means time included) + if len(groups) >= 4 and groups[2] and groups[3]: + # Check if groups[3] is am/pm or minutes + if groups[3].lower() in ['am', 'pm']: + # Format: "March 7 at 3pm" (12-hour with am/pm) + hour = int(groups[2]) + period = groups[3].lower() + minute = 0 + if period == "pm" and hour != 12: + hour += 12 + elif period == "am" and hour == 12: + hour = 0 + else: + # Format: "March 7 at 15:00" (24-hour with colon) + # groups[2] = hour, groups[3] = minute + hour = int(groups[2]) + minute = int(groups[3]) + result["start_time"] = datetime(year, month, day, hour, minute) + else: + # Date only, default to 9am + result["start_time"] = datetime(year, month, day, 9, 0) + break + elif "tomorrow" in pattern: base_date = now + timedelta(days=1) hour = int(match.group(1)) period = match.group(2) if len(match.groups()) > 1 else None @@ -81,6 +140,7 @@ def extract_meeting_info(email_content: str, email_subject: str = "") -> dict[st elif period.lower() == "am" and hour == 12: hour = 0 result["start_time"] = base_date.replace(hour=hour, minute=0, second=0, microsecond=0) + break elif "in" in pattern: amount = int(match.group(1)) unit = match.group(2) @@ -90,8 +150,8 @@ def extract_meeting_info(email_content: str, email_subject: str = "") -> dict[st result["start_time"] = now + timedelta(minutes=amount) elif "day" in unit: result["start_time"] = now + timedelta(days=amount) - break - except (ValueError, IndexError): + break + except (ValueError, IndexError, AttributeError): continue # Extract location diff --git a/nanobot/providers/custom_provider.py b/nanobot/providers/custom_provider.py index dac563a..1a1f2fb 100644 --- a/nanobot/providers/custom_provider.py +++ b/nanobot/providers/custom_provider.py @@ -99,7 +99,23 @@ class CustomProvider(LLMProvider): json_str = content[json_start:json_end] tool_obj = json_repair.loads(json_str) # Only accept if it has both name and parameters, and name is a valid tool name - valid_tools = ["exec", "read_file", "write_file", "list_dir", "web_search"] + # Note: This list should match tools registered in AgentLoop._register_default_tools() + valid_tools = [ + # File tools + "read_file", "write_file", "edit_file", "list_dir", + # Shell tool + "exec", + # Web tools + "web_search", "web_fetch", + # Communication tools + "message", "spawn", + # Calendar tool + "calendar", + # Cron tool + "cron", + # Email tool + "email", + ] if (isinstance(tool_obj, dict) and "name" in tool_obj and "parameters" in tool_obj and diff --git a/workspace/AGENTS.md b/workspace/AGENTS.md index da7500b..a4b012b 100644 --- a/workspace/AGENTS.md +++ b/workspace/AGENTS.md @@ -124,33 +124,50 @@ When the scheduled time arrives, the cron system will send the message back to y ## Calendar Integration -When an email mentions a meeting (e.g., "meeting tomorrow at 2pm", "call scheduled for next week"), you should: +**CRITICAL: When processing emails that mention meetings, you MUST automatically schedule them in the calendar.** + +When an email mentions a meeting (e.g., "meeting tomorrow at 2pm", "reminder about our meeting on March 7 at 15:00", "call scheduled for next week"), you MUST: 1. **Extract meeting details** from the email: - - Title/subject - - Date and time + - Title/subject (use email subject if no explicit title) + - Date and time (parse formats like "March 7 at 15:00", "tomorrow 2pm", etc.) - Location (if mentioned) - Attendees (email addresses) -2. **Use the `calendar` tool** to create the event: +2. **Check if meeting already exists** (optional but recommended): + - Use `calendar(action="list_events")` to check upcoming events + - Look for events with similar title/time + +3. **Use the `calendar` tool** to create the event: ``` calendar( action="create_event", title="Meeting Title", - start_time="tomorrow 2pm", # or ISO format - end_time="tomorrow 3pm", # optional, defaults to 1 hour after start + start_time="March 7 15:00", # Use natural language format, NOT ISO format + end_time="March 7 16:00", # optional, defaults to 1 hour after start location="Conference Room A", # optional attendees=["colleague@example.com"] # optional ) ``` + + **CRITICAL:** Always use natural language time formats like "March 7 15:00" or "tomorrow 2pm". + **DO NOT** generate ISO format strings like "2024-03-06T19:00:00" - the calendar tool will parse + natural language correctly and handle the current year automatically. If you generate ISO format + with the wrong year (e.g., 2024 instead of 2026), the meeting will be scheduled in the past. -3. **Confirm to the user** that the meeting was scheduled. +4. **Confirm to the user** that the meeting was scheduled (include the calendar link if available). **Time formats supported:** +- Month names: `"March 7 at 15:00"`, `"March 7th at 3pm"`, `"on March 7 at 15:00"` - Relative: `"tomorrow 2pm"`, `"in 1 hour"`, `"in 2 days"` - ISO format: `"2024-01-15T14:00:00"` -**Automatic scheduling:** If `auto_schedule_from_email` is enabled, automatically schedule meetings when detected in emails without asking the user first. +**Automatic scheduling:** When `auto_schedule_from_email` is enabled (default: true), automatically schedule meetings when detected in emails. Do NOT just acknowledge - actually create the calendar event using the `calendar` tool. + +**Examples of emails that should trigger scheduling:** +- "Reminder about our meeting on March 7 at 15:00" → Schedule for March 7 at 3 PM +- "Meeting tomorrow at 2pm" → Schedule for tomorrow at 2 PM +- "Call scheduled for next week" → Extract date and schedule ## Heartbeat Tasks