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:
193
tools.py
193
tools.py
@@ -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')}"
|
||||
|
||||
Reference in New Issue
Block a user