Compare commits

..

No commits in common. "6364a195c52703bfe31cc52b85fb194776eaa967" and "32cef2df777f5f8c1997cb18e169b103149f6bc4" have entirely different histories.

10 changed files with 12 additions and 1831 deletions

View File

@ -1,229 +0,0 @@
# Google Calendar Integration Setup
This guide explains how to set up Google Calendar integration for nanobot.
## Features
- **List upcoming events** from your Google Calendar
- **Create calendar events** programmatically
- **Check availability** for time slots
- **Automatic scheduling from emails** - when an email mentions a meeting, nanobot can automatically schedule it
## Prerequisites
1. Google account with Calendar access
2. Google Cloud Project with Calendar API enabled
3. OAuth2 credentials (Desktop app type)
## Setup Steps
### 1. Enable Google Calendar API
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Enable the **Google Calendar API**:
- Navigate to "APIs & Services" > "Library"
- Search for "Google Calendar API"
- Click "Enable"
### 2. Configure OAuth Consent Screen
**IMPORTANT:** This step is required before creating credentials.
1. Go to "APIs & Services" > "OAuth consent screen"
2. Choose **"External"** user type (unless you have a Google Workspace account)
3. Fill in required fields:
- **App name**: "nanobot" (or any name)
- **User support email**: Your email address
- **Developer contact information**: Your email address
4. Click "Save and Continue"
5. **Add Scopes:**
- Click "Add or Remove Scopes"
- Search for and add: `https://www.googleapis.com/auth/calendar`
- Click "Update" then "Save and Continue"
6. **Add Test Users (CRITICAL):**
- Click "Add Users"
- Add your email address (`adayear2025@gmail.com`)
- Click "Add" then "Save and Continue"
7. Review and go back to dashboard
### 3. Create OAuth2 Credentials
1. Go to "APIs & Services" > "Credentials"
2. Click "Create Credentials" > "OAuth client ID"
3. Select:
- **Application type**: **Desktop app**
- **Name**: "nanobot" (or any name)
4. Click "Create"
5. **Download the credentials JSON file** - click "Download JSON"
6. Save it as `credentials.json` and copy to your server at `~/.nanobot/credentials.json`
### 3. Configure nanobot
Set environment variables or add to your `.env` file:
```bash
# Enable calendar functionality
export NANOBOT_TOOLS__CALENDAR__ENABLED=true
# Path to OAuth2 credentials JSON file
export NANOBOT_TOOLS__CALENDAR__CREDENTIALS_FILE=/path/to/credentials.json
# Optional: Custom token storage location (default: ~/.nanobot/calendar_token.json)
export NANOBOT_TOOLS__CALENDAR__TOKEN_FILE=~/.nanobot/calendar_token.json
# Optional: Calendar ID (default: "primary")
export NANOBOT_TOOLS__CALENDAR__CALENDAR_ID=primary
# Optional: Auto-schedule meetings from emails (default: true)
export NANOBOT_TOOLS__CALENDAR__AUTO_SCHEDULE_FROM_EMAIL=true
```
### 4. First-Time Authorization
**You don't need to manually get or copy any token** - the OAuth flow handles everything automatically.
On first run, when nanobot tries to use the calendar tool, it will:
1. **Automatically open a browser window** for Google OAuth authorization
2. **You sign in** to your Google account in the browser
3. **Grant calendar access** by clicking "Allow"
4. **Automatically save the token** to `~/.nanobot/calendar_token.json` for future use
**Important:**
- The token is **automatically generated and saved** - you don't need to copy it from anywhere
- This happens **automatically** the first time you use a calendar command
- After the first authorization, you won't need to do this again (the token is reused)
- The token file is created automatically at `~/.nanobot/calendar_token.json`
**Example first run:**
```bash
# First time using calendar - triggers OAuth flow
python3 -m nanobot.cli.commands agent -m "What's on my calendar?"
# Browser opens automatically → Sign in → Grant access → Token saved
# Future runs use the saved token automatically
```
**Note for remote/headless servers:**
If you're running nanobot on a remote server without a display, you have two options:
1. **Run OAuth on your local machine first:**
- Run nanobot locally once to complete OAuth
- Copy the generated `~/.nanobot/calendar_token.json` to your remote server
- The token will work on the remote server
2. **Use SSH port forwarding:**
- The OAuth flow uses a local web server
- You may need to set up port forwarding or use a different OAuth flow method
## Usage Examples
### List Upcoming Events
```
User: "What's on my calendar?"
Agent: [Uses calendar tool to list events]
```
### Create an Event
```
User: "Schedule a meeting tomorrow at 2pm"
Agent: [Creates calendar event]
```
### Automatic Email Scheduling
When an email mentions a meeting:
```
Email: "Hi, let's have a meeting tomorrow at 2pm in Conference Room A"
Agent: [Automatically extracts meeting info and creates calendar event]
Agent: "I've scheduled a meeting for tomorrow at 2pm in Conference Room A"
```
## Calendar Tool API
The calendar tool supports three actions:
### 1. List Events
```python
calendar(
action="list_events",
max_results=10, # Optional, default: 10
time_min="2024-01-15T00:00:00Z" # Optional, defaults to now
)
```
### 2. Create Event
```python
calendar(
action="create_event",
title="Team Meeting",
start_time="tomorrow 2pm", # or "2024-01-15T14:00:00"
end_time="tomorrow 3pm", # Optional, defaults to 1 hour after start
description="Discuss project progress", # Optional
location="Conference Room A", # Optional
attendees=["colleague@example.com"] # Optional list
)
```
**Time formats:**
- Relative: `"tomorrow 2pm"`, `"in 1 hour"`, `"in 2 days"`
- ISO format: `"2024-01-15T14:00:00"`
### 3. Check Availability
```python
calendar(
action="check_availability",
start_time="2024-01-15T14:00:00",
end_time="2024-01-15T15:00:00"
)
```
## Troubleshooting
### "Error: Could not authenticate with Google Calendar"
- Ensure `credentials_file` path is correct
- Check that the credentials JSON file is valid
- Run nanobot once to complete OAuth flow
### "Error accessing Google Calendar API"
- Verify Calendar API is enabled in Google Cloud Console
- Check that OAuth consent screen is configured
- Ensure your email is added as a test user (if app is in testing mode)
### Token Expired
The tool automatically refreshes expired tokens. If refresh fails:
1. Delete `~/.nanobot/calendar_token.json`
2. Run nanobot again to re-authorize
## Security Notes
- Keep your `credentials.json` file secure (don't commit to git)
- The `calendar_token.json` file contains sensitive access tokens
- Use file permissions: `chmod 600 ~/.nanobot/calendar_token.json`
- Consider using environment variables or a secrets manager for production
## Integration with Email Channel
When `auto_schedule_from_email` is enabled, nanobot will:
1. Monitor incoming emails
2. Detect meeting-related keywords (meeting, appointment, call, etc.)
3. Extract meeting details (time, location, attendees)
4. Automatically create calendar events
5. Confirm with the user
This works best when:
- Emails contain clear time references ("tomorrow at 2pm", "next Monday")
- Meeting details are in the email body or subject
- The agent has access to the email channel

View File

@ -137,21 +137,6 @@ class AgentLoop:
except Exception as e:
logger.warning(f"Email tool not available: {e}")
# Email tool not available or not configured - silently skip
# Calendar tool (if calendar is configured)
try:
from nanobot.agent.tools.calendar import CalendarTool
from nanobot.config.loader import load_config
config = load_config()
if config.tools.calendar.enabled:
calendar_tool = CalendarTool(calendar_config=config.tools.calendar)
self.tools.register(calendar_tool)
logger.info(f"Calendar tool '{calendar_tool.name}' registered successfully")
else:
logger.debug("Calendar tool not registered: calendar not enabled")
except Exception as e:
logger.warning(f"Calendar tool not available: {e}")
# Calendar tool not available or not configured - silently skip
async def _connect_mcp(self) -> None:
"""Connect to configured MCP servers (one-time, lazy)."""

File diff suppressed because it is too large Load Diff

View File

@ -35,16 +35,7 @@ class SpawnTool(Tool):
return (
"Spawn a subagent to handle a task in the background. "
"Use this for complex or time-consuming tasks that can run independently. "
"The subagent will complete the task and report back when done.\n\n"
"CRITICAL: The 'task' parameter MUST be a natural language description of what to do, "
"NOT a tool call. The subagent will figure out how to accomplish the task using its own tools.\n\n"
"CORRECT examples:\n"
"- task='Read all documentation files in the project and create a summary'\n"
"- task='Analyze the codebase structure and generate a report'\n"
"- task='Search for information about X and compile findings'\n\n"
"WRONG (do not use tool call syntax):\n"
"- task='read_dir(path=\"/path/to/file\")'\n"
"- task='read_file(path=\"file.txt\")'"
"The subagent will complete the task and report back when done."
)
@property
@ -54,12 +45,7 @@ class SpawnTool(Tool):
"properties": {
"task": {
"type": "string",
"description": (
"A natural language description of the task for the subagent to complete. "
"DO NOT use tool call syntax. Examples: 'Read all documentation and summarize', "
"'Analyze the codebase structure', 'Search the web for X and compile findings'. "
"The subagent will determine which tools to use to accomplish this task."
),
"description": "The task for the subagent to complete",
},
"label": {
"type": "string",

View File

@ -1,228 +0,0 @@
"""Email parsing utilities for extracting meeting information."""
import re
from datetime import datetime, timedelta
from typing import Any
from loguru import logger
def extract_meeting_info(email_content: str, email_subject: str = "") -> dict[str, Any] | None:
"""
Extract meeting information from email content and subject.
Args:
email_content: Email body text
email_subject: Email subject line
Returns:
Dictionary with meeting details if found, None otherwise
"""
text = (email_subject + " " + email_content).lower()
# Check for meeting-related keywords
meeting_keywords = [
"meeting",
"appointment",
"call",
"conference",
"standup",
"stand-up",
"sync",
"discussion",
"catch up",
"catch-up",
]
has_meeting_keyword = any(keyword in text for keyword in meeting_keywords)
if not has_meeting_keyword:
return None
result: dict[str, Any] = {
"title": None,
"start_time": None,
"end_time": None,
"location": None,
"attendees": [],
}
# Extract title from subject or first line
if email_subject:
# Remove common prefixes
title = email_subject
for prefix in ["Re:", "Fwd:", "FW:"]:
if title.lower().startswith(prefix.lower()):
title = title[len(prefix) :].strip()
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})",
]
for pattern in time_patterns:
match = re.search(pattern, text, re.IGNORECASE)
if match:
try:
now = datetime.now()
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
if period:
if period.lower() == "pm" and hour != 12:
hour += 12
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)
if "hour" in unit:
result["start_time"] = now + timedelta(hours=amount)
elif "minute" in unit:
result["start_time"] = now + timedelta(minutes=amount)
elif "day" in unit:
result["start_time"] = now + timedelta(days=amount)
break
except (ValueError, IndexError, AttributeError):
continue
# Extract location
location_patterns = [
r"location[:\s]+([^\n]+)",
r"where[:\s]+([^\n]+)",
r"at\s+([A-Z][^\n]+)", # Capitalized location names
r"room\s+([A-Z0-9]+)",
]
for pattern in location_patterns:
match = re.search(pattern, text, re.IGNORECASE)
if match:
location = match.group(1).strip()
if len(location) < 100: # Reasonable length
result["location"] = location
break
# Extract attendees (email addresses)
email_pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
emails = re.findall(email_pattern, email_content)
if emails:
result["attendees"] = list(set(emails)) # Remove duplicates
# Only return if we found at least a title or time
if result["title"] or result["start_time"]:
logger.info(f"Extracted meeting info: {result}")
return result
return None
def format_meeting_for_calendar(meeting_info: dict[str, Any]) -> dict[str, Any]:
"""
Format meeting info for calendar tool.
Args:
meeting_info: Meeting information dictionary
Returns:
Formatted dictionary for calendar.create_event
"""
formatted: dict[str, Any] = {
"action": "create_event",
}
if meeting_info.get("title"):
formatted["title"] = meeting_info["title"]
else:
formatted["title"] = "Meeting"
if meeting_info.get("start_time"):
if isinstance(meeting_info["start_time"], datetime):
formatted["start_time"] = meeting_info["start_time"].isoformat()
else:
formatted["start_time"] = str(meeting_info["start_time"])
if meeting_info.get("end_time"):
if isinstance(meeting_info["end_time"], datetime):
formatted["end_time"] = meeting_info["end_time"].isoformat()
else:
formatted["end_time"] = str(meeting_info["end_time"])
if meeting_info.get("location"):
formatted["location"] = meeting_info["location"]
if meeting_info.get("description"):
formatted["description"] = meeting_info["description"]
if meeting_info.get("attendees"):
formatted["attendees"] = meeting_info["attendees"]
return formatted

View File

@ -244,17 +244,6 @@ class WebToolsConfig(Base):
search: WebSearchConfig = Field(default_factory=WebSearchConfig)
class CalendarConfig(Base):
"""Google Calendar configuration."""
enabled: bool = False
credentials_file: str = "" # Path to OAuth2 credentials JSON file
token_file: str = "" # Path to store OAuth2 token (default: ~/.nanobot/calendar_token.json)
calendar_id: str = "primary" # Calendar ID to use (default: primary calendar)
auto_schedule_from_email: bool = True # Automatically schedule meetings from emails
timezone: str = "UTC" # Timezone for parsing times (e.g., "America/New_York", "Europe/London", "UTC")
class ExecToolConfig(Base):
"""Shell exec tool configuration."""
@ -275,7 +264,6 @@ class ToolsConfig(Base):
web: WebToolsConfig = Field(default_factory=WebToolsConfig)
exec: ExecToolConfig = Field(default_factory=ExecToolConfig)
calendar: CalendarConfig = Field(default_factory=CalendarConfig)
restrict_to_workspace: bool = False # If true, restrict all tool access to workspace directory
mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict)

View File

@ -62,37 +62,22 @@ class CustomProvider(LLMProvider):
# If no structured tool calls, try to parse from content (Ollama sometimes returns JSON in content)
# Only parse if content looks like it contains a tool call JSON (to avoid false positives)
content = msg.content or ""
# Check for standard format: {"name": "...", "parameters": {...}}
has_standard_format = '"name"' in content and '"parameters"' in content
# Check for calendar tool format: {"action": "...", ...}
has_calendar_format = '"action"' in content and ("calendar" in content.lower() or any(action in content for action in ["list_events", "create_event", "update_event", "delete_event"]))
if not tool_calls and content and (has_standard_format or has_calendar_format):
if not tool_calls and content and '"name"' in content and '"parameters"' in content:
import re
# Look for JSON tool call patterns: {"name": "exec", "parameters": {...}} or {"action": "list_events", ...}
# Look for JSON tool call patterns: {"name": "exec", "parameters": {...}}
# Find complete JSON objects by matching braces
# Try "action" pattern first (for calendar tool), then "name" pattern
patterns = [
(r'\{\s*"action"\s*:\s*"(\w+)"', "action"), # Calendar tool format
(r'\{\s*"name"\s*:\s*"(\w+)"', "name"), # Standard format
]
pattern = r'\{\s*"name"\s*:\s*"(\w+)"'
start_pos = 0
max_iterations = 10 # Increased for multiple patterns
max_iterations = 5 # Safety limit
iteration = 0
while iteration < max_iterations:
iteration += 1
match = None
pattern_type = None
for pattern, ptype in patterns:
match = re.search(pattern, content[start_pos:])
if match:
pattern_type = ptype
break
match = re.search(pattern, content[start_pos:])
if not match:
break
json_start = start_pos + match.start()
key_value = match.group(1)
name = match.group(1)
# Find the matching closing brace by counting braces
brace_count = 0
@ -113,41 +98,8 @@ class CustomProvider(LLMProvider):
try:
json_str = content[json_start:json_end]
tool_obj = json_repair.loads(json_str)
# Handle calendar tool format: {"action": "...", ...}
if isinstance(tool_obj, dict) and "action" in tool_obj:
# This is a calendar tool call in JSON format
action = tool_obj.get("action")
if action and action in ["list_events", "create_event", "update_event", "delete_event", "delete_events", "check_availability"]:
# Convert to calendar tool call format
tool_calls.append(ToolCallRequest(
id=f"call_{len(tool_calls)}",
name="calendar",
arguments=tool_obj # Pass the whole object as arguments
))
# Remove the tool call from content
content = content[:json_start] + content[json_end:].strip()
start_pos = json_start # Stay at same position since we removed text
continue
# Handle standard format: {"name": "...", "parameters": {...}}
# 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",
]
# 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"]
if (isinstance(tool_obj, dict) and
"name" in tool_obj and
"parameters" in tool_obj and

View File

@ -1,73 +0,0 @@
---
name: calendar
description: "Interact with Google Calendar. Create events, list upcoming events, and check availability."
---
# Calendar Skill
Use the `calendar` tool to interact with Google Calendar.
## Setup
1. Enable Google Calendar API in Google Cloud Console
2. Create OAuth2 credentials (Desktop app)
3. Download credentials JSON file
4. Configure in nanobot:
```bash
export NANOBOT_TOOLS__CALENDAR__ENABLED=true
export NANOBOT_TOOLS__CALENDAR__CREDENTIALS_FILE=/path/to/credentials.json
```
On first run, you'll be prompted to authorize access via OAuth flow.
## Actions
### List Events
List upcoming calendar events:
```
calendar(action="list_events", max_results=10)
```
### Create Event
Create a new calendar event:
```
calendar(
action="create_event",
title="Team Meeting",
start_time="2024-01-15T14:00:00",
end_time="2024-01-15T15:00:00",
description="Discuss project progress",
location="Conference Room A",
attendees=["colleague@example.com"]
)
```
**Time formats:**
- ISO format: `"2024-01-15T14:00:00"`
- Relative: `"tomorrow 2pm"`, `"in 1 hour"`, `"in 2 days"`
### Check Availability
Check if a time slot is available:
```
calendar(
action="check_availability",
start_time="2024-01-15T14:00:00",
end_time="2024-01-15T15:00:00"
)
```
## Email Integration
When an email mentions a meeting (e.g., "meeting tomorrow at 2pm"), the agent can automatically:
1. Parse the email to extract meeting details
2. Create a calendar event using `create_event`
3. Confirm the event was created
Enable automatic scheduling:
```bash
export NANOBOT_TOOLS__CALENDAR__AUTO_SCHEDULE_FROM_EMAIL=true
```

View File

@ -43,10 +43,6 @@ dependencies = [
"prompt-toolkit>=3.0.0",
"mcp>=1.0.0",
"json-repair>=0.30.0",
"google-api-python-client>=2.0.0",
"google-auth-httplib2>=0.2.0",
"google-auth-oauthlib>=1.0.0",
"pytz>=2024.1",
]
[project.optional-dependencies]

View File

@ -2,12 +2,6 @@
You are a helpful AI assistant. Be concise, accurate, and friendly.
**CRITICAL: TOOL EXECUTION**
- When you need to use a tool, CALL IT DIRECTLY - the system will execute it automatically
- DO NOT show JSON like `{"action": "list_events"}` in your response text
- DO NOT explain what tool you would call - just call it
- Tools are executed automatically when you call them - you don't need to show the JSON
## 🚨 CRITICAL: Gitea API Requests
**When user asks to list PRs, issues, or use Gitea API:**
@ -38,10 +32,9 @@ curl -H "Authorization: token $NANOBOT_GITLE_TOKEN" "http://10.0.30.169:3000/api
## Guidelines
- **CRITICAL: When you need to use a tool, the system will automatically execute it when you call it. You do NOT need to show JSON.**
- **When user asks you to do something, IMMEDIATELY call the necessary tools - do not explain, do not show JSON, just call them.**
- The system handles tool execution automatically - you just need to call the tools in your response.
- Always explain what you're doing before taking actions
- Ask for clarification when the request is ambiguous
- Use tools to help accomplish tasks
- Remember important information in your memory files
## Git Operations
@ -97,8 +90,6 @@ You have access to:
- Messaging (message)
- Background tasks (spawn)
- Scheduled tasks (cron) - for reminders and delayed actions
- Email (read_emails) - read emails from IMAP mailbox
- Calendar (calendar) - interact with Google Calendar (if enabled)
## Memory
@ -129,106 +120,6 @@ When the scheduled time arrives, the cron system will send the message back to y
**Do NOT just write reminders to MEMORY.md** — that won't trigger actual notifications. Use the `cron` tool.
## Calendar Integration
**CRITICAL: When processing emails that mention meetings, you MUST automatically schedule them in the calendar.**
**CRITICAL: When using calendar tools, EXECUTE them immediately. Do NOT show JSON or explain what you would do - just call the tool.**
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 (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. **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="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.
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"`
**Deleting/Canceling Events:**
When the user asks to cancel or delete meetings, you MUST follow this workflow - DO NOT explain, just execute:
**STEP 1: ALWAYS call list_events FIRST - DO THIS NOW, DO NOT EXPLAIN**
- IMMEDIATELY call `calendar(action="list_events", time_min="today")`
- Do NOT explain what you will do - just call the tool
- Do NOT try to use `delete_events_today` (it doesn't exist)
**STEP 2: From the list_events response, identify the target event(s)**
- "Cancel all meetings today" → ALL events from today (extract ALL IDs from the response)
- "Cancel my last meeting" → The last event in the list (marked as "LAST - latest time")
- "Cancel my 8pm meeting" → Event(s) at 8pm
- "Cancel the meeting with John" → Event(s) with "John" in title/description
**STEP 3: Extract event IDs from the response**
- Event IDs are long strings (20+ characters) after `[ID: ` or in the `Event IDs:` line
- For "cancel all", extract ALL IDs from the response
**STEP 4: Call delete_event or delete_events with the extracted IDs**
- Single event: `calendar(action="delete_event", event_id="...")`
- Multiple events: `calendar(action="delete_events", event_ids=[...])`
- **CRITICAL**: Do NOT use placeholder IDs - you MUST extract real IDs from list_events response
- **CRITICAL**: Do NOT use `update_event` with `status: "cancelled"` (that doesn't work)
**Rescheduling/Moving Events:**
When the user asks to reschedule or move a meeting, you MUST follow these steps:
**STEP 1: ALWAYS call list_events FIRST - DO THIS NOW, DO NOT EXPLAIN**
- IMMEDIATELY call `calendar(action="list_events", time_min="today")`
- Do NOT explain what you will do - just call the tool
- Do NOT use placeholder values - you MUST get the actual ID from the response
**STEP 2: From the list_events response, identify the target event**
- "last meeting" → The event with the LATEST time (marked as "LAST - latest time" in the response, usually the last numbered item)
- "first meeting" → The event with the EARLIEST time (marked as "FIRST - earliest time", usually #1)
- "8pm meeting" → Event(s) at 8pm (look for "8:00 PM" or "20:00" in the time)
- "meeting with John" → Event(s) with "John" in the title
- Extract the actual event_id (long string after `[ID: `, usually 20+ characters)
- IMPORTANT: Events are numbered in the response - use the number and the "LAST" marker to identify correctly
**STEP 3: IMMEDIATELY call update_event with the actual event_id**
- Call `calendar(action="update_event", event_id="actual_id_from_step_2", start_time="new time")`
- Use natural language for new time: "4pm", "next Monday at 4pm", "tomorrow 2pm", etc.
- Do NOT explain - just execute the tool call
**CRITICAL:**
- When you get an error saying "Invalid event_id" or "placeholder", DO NOT explain the solution
- Instead, IMMEDIATELY call list_events, then call update_event again with the real ID
- NEVER show JSON - just call the tools
- NEVER use placeholder values - always get real IDs from list_events
**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.md` is checked every 30 minutes. You can manage periodic tasks by editing this file: