Add Google Contacts integration and fix critical Windows issues

- Add People API integration with contact management tools (create, list, get)
- Fix OAuth flow: replace deprecated OOB with localhost callback
- Fix Ctrl+C handling with proper signal handlers for graceful shutdown
- Fix UTF-8 encoding in scheduled_tasks.py for Windows compatibility
- Add users/ directory to gitignore to protect personal data

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 15:12:01 -07:00
parent 19af20e700
commit 0eb5d2cab4
7 changed files with 557 additions and 52 deletions

193
tools.py
View File

@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional
# Google tools (lazy loaded when needed)
_gmail_client: Optional[Any] = None
_calendar_client: Optional[Any] = None
_people_client: Optional[Any] = None
# Tool definitions in Anthropic's tool use format
@@ -255,6 +256,71 @@ TOOL_DEFINITIONS = [
"required": ["query"],
},
},
# Contacts tools
{
"name": "create_contact",
"description": "Create a new Google contact. Requires prior OAuth setup (--setup-google).",
"input_schema": {
"type": "object",
"properties": {
"given_name": {
"type": "string",
"description": "Contact's first name",
},
"family_name": {
"type": "string",
"description": "Contact's last name",
"default": "",
},
"email": {
"type": "string",
"description": "Contact's email address",
"default": "",
},
"phone": {
"type": "string",
"description": "Contact's phone number",
},
"notes": {
"type": "string",
"description": "Optional notes about the contact",
},
},
"required": ["given_name"],
},
},
{
"name": "list_contacts",
"description": "List or search Google contacts. Without a query, lists all contacts sorted by last name.",
"input_schema": {
"type": "object",
"properties": {
"max_results": {
"type": "integer",
"description": "Maximum number of contacts to return (default: 100, max: 1000)",
"default": 100,
},
"query": {
"type": "string",
"description": "Optional search query to filter contacts by name, email, etc.",
},
},
},
},
{
"name": "get_contact",
"description": "Get full details of a specific Google contact by resource name.",
"input_schema": {
"type": "object",
"properties": {
"resource_name": {
"type": "string",
"description": "Contact resource name (e.g., 'people/c1234567890')",
},
},
"required": ["resource_name"],
},
},
]
@@ -320,6 +386,24 @@ def execute_tool(tool_name: str, tool_input: Dict[str, Any]) -> str:
query=tool_input["query"],
calendar_id=tool_input.get("calendar_id", "primary"),
)
# Contacts tools
elif tool_name == "create_contact":
return _create_contact(
given_name=tool_input["given_name"],
family_name=tool_input.get("family_name", ""),
email=tool_input.get("email", ""),
phone=tool_input.get("phone"),
notes=tool_input.get("notes"),
)
elif tool_name == "list_contacts":
return _list_contacts(
max_results=tool_input.get("max_results", 100),
query=tool_input.get("query"),
)
elif tool_name == "get_contact":
return _get_contact(
resource_name=tool_input["resource_name"],
)
else:
return f"Error: Unknown tool '{tool_name}'"
except Exception as e:
@@ -442,36 +526,38 @@ def _run_command(command: str, working_dir: str) -> str:
# Google Tools Handlers
def _initialize_google_clients() -> tuple[Optional[Any], Optional[Any]]:
def _initialize_google_clients() -> tuple[Optional[Any], Optional[Any], Optional[Any]]:
"""Lazy initialization of Google clients.
Returns:
Tuple of (gmail_client, calendar_client) or (None, None) if not authorized
Tuple of (gmail_client, calendar_client, people_client) or (None, None, None) if not authorized
"""
global _gmail_client, _calendar_client
global _gmail_client, _calendar_client, _people_client
if _gmail_client is not None and _calendar_client is not None:
return _gmail_client, _calendar_client
if _gmail_client is not None and _calendar_client is not None and _people_client is not None:
return _gmail_client, _calendar_client, _people_client
try:
from google_tools.oauth_manager import GoogleOAuthManager
from google_tools.gmail_client import GmailClient
from google_tools.calendar_client import CalendarClient
from google_tools.people_client import PeopleClient
oauth_manager = GoogleOAuthManager()
credentials = oauth_manager.get_credentials()
if not credentials:
return None, None
return None, None, None
_gmail_client = GmailClient(oauth_manager)
_calendar_client = CalendarClient(oauth_manager)
_people_client = PeopleClient(oauth_manager)
return _gmail_client, _calendar_client
return _gmail_client, _calendar_client, _people_client
except Exception as e:
print(f"[Google Tools] Failed to initialize: {e}")
return None, None
return None, None, None
def _send_email(
@@ -482,7 +568,7 @@ def _send_email(
reply_to_message_id: Optional[str] = None,
) -> str:
"""Send an email via Gmail API."""
gmail_client, _ = _initialize_google_clients()
gmail_client, _, _ = _initialize_google_clients()
if not gmail_client:
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
@@ -508,7 +594,7 @@ def _read_emails(
include_body: bool = False,
) -> str:
"""Search and read emails from Gmail."""
gmail_client, _ = _initialize_google_clients()
gmail_client, _, _ = _initialize_google_clients()
if not gmail_client:
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
@@ -530,7 +616,7 @@ def _read_emails(
def _get_email(message_id: str, format_type: str = "text") -> str:
"""Get full content of a specific email."""
gmail_client, _ = _initialize_google_clients()
gmail_client, _, _ = _initialize_google_clients()
if not gmail_client:
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
@@ -566,7 +652,7 @@ def _read_calendar(
max_results: int = 20,
) -> str:
"""Read upcoming calendar events."""
_, calendar_client = _initialize_google_clients()
_, calendar_client, _ = _initialize_google_clients()
if not calendar_client:
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
@@ -595,7 +681,7 @@ def _create_calendar_event(
calendar_id: str = "primary",
) -> str:
"""Create a new calendar event."""
_, calendar_client = _initialize_google_clients()
_, calendar_client, _ = _initialize_google_clients()
if not calendar_client:
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
@@ -634,7 +720,7 @@ def _create_calendar_event(
def _search_calendar(query: str, calendar_id: str = "primary") -> str:
"""Search calendar events by text query."""
_, calendar_client = _initialize_google_clients()
_, calendar_client, _ = _initialize_google_clients()
if not calendar_client:
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
@@ -648,3 +734,82 @@ def _search_calendar(query: str, calendar_id: str = "primary") -> str:
return f'Search results for "{query}":\n\n{summary}'
else:
return f"Error searching calendar: {result.get('error', 'Unknown error')}"
# Contacts Tools Handlers
def _create_contact(
given_name: str,
family_name: str = "",
email: str = "",
phone: Optional[str] = None,
notes: Optional[str] = None,
) -> str:
"""Create a new Google contact."""
_, _, people_client = _initialize_google_clients()
if not people_client:
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
result = people_client.create_contact(
given_name=given_name,
family_name=family_name,
email=email,
phone=phone,
notes=notes,
)
if result["success"]:
name = result.get("name", given_name)
resource = result.get("resource_name", "")
return f"Contact created: {name}\nResource: {resource}"
else:
return f"Error creating contact: {result.get('error', 'Unknown error')}"
def _list_contacts(
max_results: int = 100,
query: Optional[str] = None,
) -> str:
"""List or search Google contacts."""
_, _, people_client = _initialize_google_clients()
if not people_client:
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
result = people_client.list_contacts(max_results=max_results, query=query)
if result["success"]:
summary = result.get("summary", "No contacts found.")
if len(summary) > _MAX_TOOL_OUTPUT:
summary = summary[:_MAX_TOOL_OUTPUT] + "\n... (results truncated)"
return f"Contacts ({result.get('count', 0)} found):\n\n{summary}"
else:
return f"Error listing contacts: {result.get('error', 'Unknown error')}"
def _get_contact(resource_name: str) -> str:
"""Get full details of a specific contact."""
_, _, people_client = _initialize_google_clients()
if not people_client:
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
result = people_client.get_contact(resource_name=resource_name)
if result["success"]:
c = result.get("contact", {})
output = []
name = c.get("display_name") or f"{c.get('given_name', '')} {c.get('family_name', '')}".strip()
output.append(f"Name: {name or '(no name)'}")
if c.get("email"):
output.append(f"Email: {c['email']}")
if c.get("phone"):
output.append(f"Phone: {c['phone']}")
if c.get("notes"):
output.append(f"Notes: {c['notes']}")
output.append(f"Resource: {c.get('resource_name', resource_name)}")
return "\n".join(output)
else:
return f"Error getting contact: {result.get('error', 'Unknown error')}"