feature/web-search-and-cron-improvements #2

Merged
tanyar09 merged 12 commits from feature/web-search-and-cron-improvements into feature/cleanup-providers-llama-only 2026-03-06 13:20:19 -05:00
4 changed files with 187 additions and 16 deletions
Showing only changes of commit bc5f169bc8 - Show all commits

View File

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

View File

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

View File

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

View File

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