feature/web-search-and-cron-improvements #2
@ -26,9 +26,10 @@ class CalendarTool(Tool):
|
|||||||
"Interact with Google Calendar. REQUIRED: Always include 'action' parameter. "
|
"Interact with Google Calendar. REQUIRED: Always include 'action' parameter. "
|
||||||
"Actions: 'list_events' (list upcoming events), 'create_event' (create new event), "
|
"Actions: 'list_events' (list upcoming events), 'create_event' (create new event), "
|
||||||
"'check_availability' (check if time slot is available). "
|
"'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 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):
|
def __init__(self, calendar_config: Any = None):
|
||||||
@ -148,7 +149,13 @@ class CalendarTool(Tool):
|
|||||||
},
|
},
|
||||||
"start_time": {
|
"start_time": {
|
||||||
"type": "string",
|
"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": {
|
"end_time": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -191,16 +198,77 @@ class CalendarTool(Tool):
|
|||||||
original_str = time_str
|
original_str = time_str
|
||||||
time_str = time_str.strip().lower()
|
time_str = time_str.strip().lower()
|
||||||
|
|
||||||
# Try ISO format first
|
# Try ISO format first, but validate year is reasonable
|
||||||
try:
|
try:
|
||||||
dt = datetime.fromisoformat(time_str.replace("Z", "+00:00"))
|
dt = datetime.fromisoformat(time_str.replace("Z", "+00:00"))
|
||||||
# Ensure timezone-aware
|
# Ensure timezone-aware
|
||||||
if dt.tzinfo is None:
|
if dt.tzinfo is None:
|
||||||
dt = dt.replace(tzinfo=timezone.utc)
|
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
|
return dt
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
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
|
# Parse relative times
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
@ -540,6 +608,16 @@ class CalendarTool(Tool):
|
|||||||
else:
|
else:
|
||||||
end_dt = start_dt + timedelta(hours=1) # Default 1 hour
|
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
|
# Validate time range
|
||||||
if end_dt <= start_dt:
|
if end_dt <= start_dt:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|||||||
@ -56,12 +56,35 @@ def extract_meeting_info(email_content: str, email_subject: str = "") -> dict[st
|
|||||||
result["title"] = title[:100] # Limit length
|
result["title"] = title[:100] # Limit length
|
||||||
|
|
||||||
# Extract time information
|
# 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 = [
|
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+at\s+(\d{1,2})\s*(am|pm)?",
|
||||||
r"tomorrow\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})\s*(am|pm)\s+tomorrow",
|
||||||
r"(\d{1,2}):(\d{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)",
|
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{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})",
|
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:
|
if match:
|
||||||
try:
|
try:
|
||||||
now = datetime.now()
|
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)
|
base_date = now + timedelta(days=1)
|
||||||
hour = int(match.group(1))
|
hour = int(match.group(1))
|
||||||
period = match.group(2) if len(match.groups()) > 1 else None
|
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:
|
elif period.lower() == "am" and hour == 12:
|
||||||
hour = 0
|
hour = 0
|
||||||
result["start_time"] = base_date.replace(hour=hour, minute=0, second=0, microsecond=0)
|
result["start_time"] = base_date.replace(hour=hour, minute=0, second=0, microsecond=0)
|
||||||
|
break
|
||||||
elif "in" in pattern:
|
elif "in" in pattern:
|
||||||
amount = int(match.group(1))
|
amount = int(match.group(1))
|
||||||
unit = match.group(2)
|
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)
|
result["start_time"] = now + timedelta(minutes=amount)
|
||||||
elif "day" in unit:
|
elif "day" in unit:
|
||||||
result["start_time"] = now + timedelta(days=amount)
|
result["start_time"] = now + timedelta(days=amount)
|
||||||
break
|
break
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError, AttributeError):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract location
|
# Extract location
|
||||||
|
|||||||
@ -99,7 +99,23 @@ class CustomProvider(LLMProvider):
|
|||||||
json_str = content[json_start:json_end]
|
json_str = content[json_start:json_end]
|
||||||
tool_obj = json_repair.loads(json_str)
|
tool_obj = json_repair.loads(json_str)
|
||||||
# Only accept if it has both name and parameters, and name is a valid tool name
|
# 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
|
if (isinstance(tool_obj, dict) and
|
||||||
"name" in tool_obj and
|
"name" in tool_obj and
|
||||||
"parameters" 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
|
## 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:
|
1. **Extract meeting details** from the email:
|
||||||
- Title/subject
|
- Title/subject (use email subject if no explicit title)
|
||||||
- Date and time
|
- Date and time (parse formats like "March 7 at 15:00", "tomorrow 2pm", etc.)
|
||||||
- Location (if mentioned)
|
- Location (if mentioned)
|
||||||
- Attendees (email addresses)
|
- 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(
|
calendar(
|
||||||
action="create_event",
|
action="create_event",
|
||||||
title="Meeting Title",
|
title="Meeting Title",
|
||||||
start_time="tomorrow 2pm", # or ISO format
|
start_time="March 7 15:00", # Use natural language format, NOT ISO format
|
||||||
end_time="tomorrow 3pm", # optional, defaults to 1 hour after start
|
end_time="March 7 16:00", # optional, defaults to 1 hour after start
|
||||||
location="Conference Room A", # optional
|
location="Conference Room A", # optional
|
||||||
attendees=["colleague@example.com"] # 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:**
|
**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"`
|
- Relative: `"tomorrow 2pm"`, `"in 1 hour"`, `"in 2 days"`
|
||||||
- ISO format: `"2024-01-15T14:00:00"`
|
- 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
|
## Heartbeat Tasks
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user