feature/web-search-and-cron-improvements #2
@ -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(
|
||||
|
||||
@ -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)
|
||||
@ -91,7 +151,7 @@ def extract_meeting_info(email_content: str, email_subject: str = "") -> dict[st
|
||||
elif "day" in unit:
|
||||
result["start_time"] = now + timedelta(days=amount)
|
||||
break
|
||||
except (ValueError, IndexError):
|
||||
except (ValueError, IndexError, AttributeError):
|
||||
continue
|
||||
|
||||
# Extract location
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
```
|
||||
|
||||
3. **Confirm to the user** that the meeting was scheduled.
|
||||
**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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user