Compare commits
5 Commits
32cef2df77
...
6364a195c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 6364a195c5 | |||
| bc53dc6535 | |||
| bc5f169bc8 | |||
| 760a7d776e | |||
| f39325c846 |
229
CALENDAR_SETUP.md
Normal file
229
CALENDAR_SETUP.md
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
@ -138,6 +138,21 @@ class AgentLoop:
|
|||||||
logger.warning(f"Email tool not available: {e}")
|
logger.warning(f"Email tool not available: {e}")
|
||||||
# Email tool not available or not configured - silently skip
|
# 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:
|
async def _connect_mcp(self) -> None:
|
||||||
"""Connect to configured MCP servers (one-time, lazy)."""
|
"""Connect to configured MCP servers (one-time, lazy)."""
|
||||||
if self._mcp_connected or not self._mcp_servers:
|
if self._mcp_connected or not self._mcp_servers:
|
||||||
|
|||||||
1087
nanobot/agent/tools/calendar.py
Normal file
1087
nanobot/agent/tools/calendar.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -35,7 +35,16 @@ class SpawnTool(Tool):
|
|||||||
return (
|
return (
|
||||||
"Spawn a subagent to handle a task in the background. "
|
"Spawn a subagent to handle a task in the background. "
|
||||||
"Use this for complex or time-consuming tasks that can run independently. "
|
"Use this for complex or time-consuming tasks that can run independently. "
|
||||||
"The subagent will complete the task and report back when done."
|
"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\")' ❌"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -45,7 +54,12 @@ class SpawnTool(Tool):
|
|||||||
"properties": {
|
"properties": {
|
||||||
"task": {
|
"task": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The task for the subagent to complete",
|
"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."
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
228
nanobot/agent/utils/email_parser.py
Normal file
228
nanobot/agent/utils/email_parser.py
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
"""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
|
||||||
|
|
||||||
@ -244,6 +244,17 @@ class WebToolsConfig(Base):
|
|||||||
search: WebSearchConfig = Field(default_factory=WebSearchConfig)
|
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):
|
class ExecToolConfig(Base):
|
||||||
"""Shell exec tool configuration."""
|
"""Shell exec tool configuration."""
|
||||||
|
|
||||||
@ -264,6 +275,7 @@ class ToolsConfig(Base):
|
|||||||
|
|
||||||
web: WebToolsConfig = Field(default_factory=WebToolsConfig)
|
web: WebToolsConfig = Field(default_factory=WebToolsConfig)
|
||||||
exec: ExecToolConfig = Field(default_factory=ExecToolConfig)
|
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
|
restrict_to_workspace: bool = False # If true, restrict all tool access to workspace directory
|
||||||
mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict)
|
mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|||||||
@ -62,22 +62,37 @@ class CustomProvider(LLMProvider):
|
|||||||
# If no structured tool calls, try to parse from content (Ollama sometimes returns JSON in content)
|
# 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)
|
# Only parse if content looks like it contains a tool call JSON (to avoid false positives)
|
||||||
content = msg.content or ""
|
content = msg.content or ""
|
||||||
if not tool_calls and content and '"name"' in content and '"parameters"' in content:
|
# 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):
|
||||||
import re
|
import re
|
||||||
# Look for JSON tool call patterns: {"name": "exec", "parameters": {...}}
|
# Look for JSON tool call patterns: {"name": "exec", "parameters": {...}} or {"action": "list_events", ...}
|
||||||
# Find complete JSON objects by matching braces
|
# Find complete JSON objects by matching braces
|
||||||
pattern = r'\{\s*"name"\s*:\s*"(\w+)"'
|
# 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
|
||||||
|
]
|
||||||
start_pos = 0
|
start_pos = 0
|
||||||
max_iterations = 5 # Safety limit
|
max_iterations = 10 # Increased for multiple patterns
|
||||||
iteration = 0
|
iteration = 0
|
||||||
while iteration < max_iterations:
|
while iteration < max_iterations:
|
||||||
iteration += 1
|
iteration += 1
|
||||||
match = re.search(pattern, content[start_pos:])
|
match = None
|
||||||
|
pattern_type = None
|
||||||
|
for pattern, ptype in patterns:
|
||||||
|
match = re.search(pattern, content[start_pos:])
|
||||||
|
if match:
|
||||||
|
pattern_type = ptype
|
||||||
|
break
|
||||||
if not match:
|
if not match:
|
||||||
break
|
break
|
||||||
|
|
||||||
json_start = start_pos + match.start()
|
json_start = start_pos + match.start()
|
||||||
name = match.group(1)
|
key_value = match.group(1)
|
||||||
|
|
||||||
# Find the matching closing brace by counting braces
|
# Find the matching closing brace by counting braces
|
||||||
brace_count = 0
|
brace_count = 0
|
||||||
@ -98,8 +113,41 @@ class CustomProvider(LLMProvider):
|
|||||||
try:
|
try:
|
||||||
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
|
|
||||||
valid_tools = ["exec", "read_file", "write_file", "list_dir", "web_search"]
|
# 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",
|
||||||
|
]
|
||||||
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
|
||||||
|
|||||||
73
nanobot/skills/calendar/SKILL.md
Normal file
73
nanobot/skills/calendar/SKILL.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
@ -43,6 +43,10 @@ dependencies = [
|
|||||||
"prompt-toolkit>=3.0.0",
|
"prompt-toolkit>=3.0.0",
|
||||||
"mcp>=1.0.0",
|
"mcp>=1.0.0",
|
||||||
"json-repair>=0.30.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]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
You are a helpful AI assistant. Be concise, accurate, and friendly.
|
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
|
## 🚨 CRITICAL: Gitea API Requests
|
||||||
|
|
||||||
**When user asks to list PRs, issues, or use Gitea API:**
|
**When user asks to list PRs, issues, or use Gitea API:**
|
||||||
@ -32,9 +38,10 @@ curl -H "Authorization: token $NANOBOT_GITLE_TOKEN" "http://10.0.30.169:3000/api
|
|||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
|
|
||||||
- Always explain what you're doing before taking actions
|
- **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.
|
||||||
- Ask for clarification when the request is ambiguous
|
- Ask for clarification when the request is ambiguous
|
||||||
- Use tools to help accomplish tasks
|
|
||||||
- Remember important information in your memory files
|
- Remember important information in your memory files
|
||||||
|
|
||||||
## Git Operations
|
## Git Operations
|
||||||
@ -90,6 +97,8 @@ You have access to:
|
|||||||
- Messaging (message)
|
- Messaging (message)
|
||||||
- Background tasks (spawn)
|
- Background tasks (spawn)
|
||||||
- Scheduled tasks (cron) - for reminders and delayed actions
|
- 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
|
## Memory
|
||||||
|
|
||||||
@ -120,6 +129,106 @@ 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.
|
**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 Tasks
|
||||||
|
|
||||||
`HEARTBEAT.md` is checked every 30 minutes. You can manage periodic tasks by editing this file:
|
`HEARTBEAT.md` is checked every 30 minutes. You can manage periodic tasks by editing this file:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user