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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -50,6 +50,9 @@ memory_workspace/memory_index.db
|
|||||||
memory_workspace/users/*.md # User profiles (jordan.md, etc.)
|
memory_workspace/users/*.md # User profiles (jordan.md, etc.)
|
||||||
memory_workspace/vectors.usearch
|
memory_workspace/vectors.usearch
|
||||||
|
|
||||||
|
# User profiles (personal info)
|
||||||
|
users/
|
||||||
|
|
||||||
# Usage tracking
|
# Usage tracking
|
||||||
usage_data.json
|
usage_data.json
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Environment variables:
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
@@ -45,6 +47,7 @@ class BotRunner:
|
|||||||
self.runtime: AdapterRuntime = None
|
self.runtime: AdapterRuntime = None
|
||||||
self.agent: Agent = None
|
self.agent: Agent = None
|
||||||
self.scheduler: TaskScheduler = None
|
self.scheduler: TaskScheduler = None
|
||||||
|
self.shutdown_event = asyncio.Event()
|
||||||
|
|
||||||
def _load_adapter(self, platform: str) -> bool:
|
def _load_adapter(self, platform: str) -> bool:
|
||||||
"""Load and register a single adapter. Returns True if loaded."""
|
"""Load and register a single adapter. Returns True if loaded."""
|
||||||
@@ -127,6 +130,17 @@ class BotRunner:
|
|||||||
if not self.setup():
|
if not self.setup():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Set up signal handlers
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
print(f"\n\n[Shutdown] Received signal {signum}...")
|
||||||
|
loop.call_soon_threadsafe(self.shutdown_event.set)
|
||||||
|
|
||||||
|
# Register signal handlers (works on both Windows and Unix)
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("Starting bot...")
|
print("Starting bot...")
|
||||||
print("=" * 60 + "\n")
|
print("=" * 60 + "\n")
|
||||||
@@ -143,11 +157,9 @@ class BotRunner:
|
|||||||
print("Bot is running! Press Ctrl+C to stop.")
|
print("Bot is running! Press Ctrl+C to stop.")
|
||||||
print("=" * 60 + "\n")
|
print("=" * 60 + "\n")
|
||||||
|
|
||||||
while True:
|
# Wait for shutdown signal
|
||||||
await asyncio.sleep(1)
|
await self.shutdown_event.wait()
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\n\n[Shutdown] Received interrupt signal...")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\n[Error] {e}")
|
print(f"\n[Error] {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""Google Tools - Gmail and Calendar integration for ajarbot."""
|
"""Google Tools - Gmail, Calendar, and Contacts integration for ajarbot."""
|
||||||
|
|
||||||
from .calendar_client import CalendarClient
|
from .calendar_client import CalendarClient
|
||||||
from .gmail_client import GmailClient
|
from .gmail_client import GmailClient
|
||||||
from .oauth_manager import GoogleOAuthManager
|
from .oauth_manager import GoogleOAuthManager
|
||||||
|
from .people_client import PeopleClient
|
||||||
|
|
||||||
__all__ = ["GoogleOAuthManager", "GmailClient", "CalendarClient"]
|
__all__ = ["GoogleOAuthManager", "GmailClient", "CalendarClient", "PeopleClient"]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""OAuth2 Manager for Google APIs (Gmail, Calendar)."""
|
"""OAuth2 Manager for Google APIs (Gmail, Calendar, Contacts)."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -18,6 +18,7 @@ from google_auth_oauthlib.flow import InstalledAppFlow
|
|||||||
SCOPES = [
|
SCOPES = [
|
||||||
"https://www.googleapis.com/auth/gmail.modify",
|
"https://www.googleapis.com/auth/gmail.modify",
|
||||||
"https://www.googleapis.com/auth/calendar",
|
"https://www.googleapis.com/auth/calendar",
|
||||||
|
"https://www.googleapis.com/auth/contacts",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Token file location
|
# Token file location
|
||||||
@@ -155,47 +156,43 @@ class GoogleOAuthManager:
|
|||||||
"client_secret": creds_config["client_secret"],
|
"client_secret": creds_config["client_secret"],
|
||||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
"token_uri": "https://oauth2.googleapis.com/token",
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
"redirect_uris": ["http://localhost:8080"],
|
"redirect_uris": ["http://localhost:8081"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if manual:
|
# Both manual and automatic modes use local server
|
||||||
# Manual flow - user pastes code
|
# (Google deprecated OOB flow in 2022)
|
||||||
flow = InstalledAppFlow.from_client_config(
|
flow = InstalledAppFlow.from_client_config(client_config, SCOPES)
|
||||||
client_config, SCOPES
|
flow.redirect_uri = "http://localhost:8081"
|
||||||
)
|
|
||||||
self.credentials = flow.run_console()
|
|
||||||
else:
|
|
||||||
# Automated flow with local server
|
|
||||||
flow = InstalledAppFlow.from_client_config(
|
|
||||||
client_config, SCOPES
|
|
||||||
)
|
|
||||||
flow.redirect_uri = "http://localhost:8080"
|
|
||||||
|
|
||||||
auth_url, _ = flow.authorization_url(prompt="consent")
|
auth_url, _ = flow.authorization_url(prompt="consent")
|
||||||
|
|
||||||
print("\nPlease visit this URL to authorize:")
|
print("\nPlease visit this URL to authorize:")
|
||||||
print(f"\n{auth_url}\n")
|
print(f"\n{auth_url}\n")
|
||||||
|
|
||||||
# Try to open browser automatically
|
# Auto-open browser unless manual mode
|
||||||
|
if not manual:
|
||||||
try:
|
try:
|
||||||
webbrowser.open(auth_url)
|
webbrowser.open(auth_url)
|
||||||
|
print("Opening browser automatically...\n")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
print("Could not open browser automatically. Please visit the URL above.\n")
|
||||||
|
else:
|
||||||
|
print("Please open the URL above in your browser manually.\n")
|
||||||
|
|
||||||
# Start local server to receive callback
|
# Start local server to receive callback
|
||||||
server = HTTPServer(("localhost", 8080), OAuthCallbackHandler)
|
server = HTTPServer(("localhost", 8081), OAuthCallbackHandler)
|
||||||
print("Waiting for authorization... (press Ctrl+C to cancel)\n")
|
print("Waiting for authorization... (press Ctrl+C to cancel)\n")
|
||||||
|
|
||||||
# Wait for callback
|
# Wait for callback
|
||||||
server.handle_request()
|
server.handle_request()
|
||||||
|
|
||||||
if OAuthCallbackHandler.authorization_code:
|
if OAuthCallbackHandler.authorization_code:
|
||||||
flow.fetch_token(code=OAuthCallbackHandler.authorization_code)
|
flow.fetch_token(code=OAuthCallbackHandler.authorization_code)
|
||||||
self.credentials = flow.credentials
|
self.credentials = flow.credentials
|
||||||
else:
|
else:
|
||||||
print("[OAuth] Authorization failed - no code received")
|
print("[OAuth] Authorization failed - no code received")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Save tokens
|
# Save tokens
|
||||||
self._save_credentials()
|
self._save_credentials()
|
||||||
|
|||||||
327
google_tools/people_client.py
Normal file
327
google_tools/people_client.py
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
"""Google People API client for managing contacts."""
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
from googleapiclient.errors import HttpError
|
||||||
|
|
||||||
|
from .oauth_manager import GoogleOAuthManager
|
||||||
|
|
||||||
|
PERSON_FIELDS = "names,emailAddresses,phoneNumbers,biographies"
|
||||||
|
|
||||||
|
|
||||||
|
class PeopleClient:
|
||||||
|
"""Client for Google People API operations."""
|
||||||
|
|
||||||
|
def __init__(self, oauth_manager: GoogleOAuthManager):
|
||||||
|
"""Initialize People client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
oauth_manager: Initialized OAuth manager with valid credentials
|
||||||
|
"""
|
||||||
|
self.oauth_manager = oauth_manager
|
||||||
|
self.service = None
|
||||||
|
self._initialize_service()
|
||||||
|
|
||||||
|
def _initialize_service(self) -> bool:
|
||||||
|
"""Initialize People API service.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if initialized successfully, False otherwise
|
||||||
|
"""
|
||||||
|
credentials = self.oauth_manager.get_credentials()
|
||||||
|
if not credentials:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.service = build("people", "v1", credentials=credentials)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[People] Failed to initialize service: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _ensure_service(self) -> bool:
|
||||||
|
"""Ensure the service is initialized."""
|
||||||
|
if not self.service:
|
||||||
|
return self._initialize_service()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_contact(
|
||||||
|
self,
|
||||||
|
given_name: str,
|
||||||
|
family_name: str = "",
|
||||||
|
email: str = "",
|
||||||
|
phone: Optional[str] = None,
|
||||||
|
notes: Optional[str] = None,
|
||||||
|
) -> Dict:
|
||||||
|
"""Create a new contact.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
given_name: First name
|
||||||
|
family_name: Last name
|
||||||
|
email: Email address
|
||||||
|
phone: Optional phone number
|
||||||
|
notes: Optional notes/biography
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with success status and contact details or error
|
||||||
|
"""
|
||||||
|
if not self._ensure_service():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
body: Dict[str, Any] = {
|
||||||
|
"names": [{"givenName": given_name, "familyName": family_name}],
|
||||||
|
}
|
||||||
|
|
||||||
|
if email:
|
||||||
|
body["emailAddresses"] = [{"value": email}]
|
||||||
|
|
||||||
|
if phone:
|
||||||
|
body["phoneNumbers"] = [{"value": phone}]
|
||||||
|
|
||||||
|
if notes:
|
||||||
|
body["biographies"] = [{"value": notes, "contentType": "TEXT_PLAIN"}]
|
||||||
|
|
||||||
|
result = (
|
||||||
|
self.service.people()
|
||||||
|
.createContact(body=body)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"resource_name": result.get("resourceName", ""),
|
||||||
|
"name": f"{given_name} {family_name}".strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
except HttpError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def list_contacts(
|
||||||
|
self,
|
||||||
|
max_results: int = 100,
|
||||||
|
query: Optional[str] = None,
|
||||||
|
) -> Dict:
|
||||||
|
"""List or search contacts.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_results: Maximum number of contacts to return (max: 1000)
|
||||||
|
query: Optional search query string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with success status and contacts or error
|
||||||
|
"""
|
||||||
|
if not self._ensure_service():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
max_results = min(max_results, 1000)
|
||||||
|
|
||||||
|
if query:
|
||||||
|
# Use searchContacts for query-based search
|
||||||
|
result = (
|
||||||
|
self.service.people()
|
||||||
|
.searchContacts(
|
||||||
|
query=query,
|
||||||
|
readMask="names,emailAddresses,phoneNumbers,biographies",
|
||||||
|
pageSize=min(max_results, 30), # searchContacts max is 30
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
people = [r["person"] for r in result.get("results", [])]
|
||||||
|
else:
|
||||||
|
# Use connections.list for full listing
|
||||||
|
result = (
|
||||||
|
self.service.people()
|
||||||
|
.connections()
|
||||||
|
.list(
|
||||||
|
resourceName="people/me",
|
||||||
|
pageSize=max_results,
|
||||||
|
personFields=PERSON_FIELDS,
|
||||||
|
sortOrder="LAST_NAME_ASCENDING",
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
people = result.get("connections", [])
|
||||||
|
|
||||||
|
contacts = [self._format_contact(p) for p in people]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"contacts": contacts,
|
||||||
|
"count": len(contacts),
|
||||||
|
"summary": self._format_contacts_summary(contacts),
|
||||||
|
}
|
||||||
|
|
||||||
|
except HttpError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def get_contact(self, resource_name: str) -> Dict:
|
||||||
|
"""Get a single contact by resource name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource_name: Contact resource name (e.g., "people/c1234567890")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with success status and contact details or error
|
||||||
|
"""
|
||||||
|
if not self._ensure_service():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = (
|
||||||
|
self.service.people()
|
||||||
|
.get(resourceName=resource_name, personFields=PERSON_FIELDS)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"contact": self._format_contact(result),
|
||||||
|
}
|
||||||
|
|
||||||
|
except HttpError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def update_contact(self, resource_name: str, updates: Dict) -> Dict:
|
||||||
|
"""Update an existing contact.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource_name: Contact resource name (e.g., "people/c1234567890")
|
||||||
|
updates: Dict with fields to update (given_name, family_name, email, phone, notes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with success status or error
|
||||||
|
"""
|
||||||
|
if not self._ensure_service():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get current contact to obtain etag
|
||||||
|
current = (
|
||||||
|
self.service.people()
|
||||||
|
.get(resourceName=resource_name, personFields=PERSON_FIELDS)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
body: Dict[str, Any] = {"etag": current["etag"]}
|
||||||
|
update_fields = []
|
||||||
|
|
||||||
|
if "given_name" in updates or "family_name" in updates:
|
||||||
|
names = current.get("names", [{}])
|
||||||
|
name = names[0] if names else {}
|
||||||
|
body["names"] = [{
|
||||||
|
"givenName": updates.get("given_name", name.get("givenName", "")),
|
||||||
|
"familyName": updates.get("family_name", name.get("familyName", "")),
|
||||||
|
}]
|
||||||
|
update_fields.append("names")
|
||||||
|
|
||||||
|
if "email" in updates:
|
||||||
|
body["emailAddresses"] = [{"value": updates["email"]}]
|
||||||
|
update_fields.append("emailAddresses")
|
||||||
|
|
||||||
|
if "phone" in updates:
|
||||||
|
body["phoneNumbers"] = [{"value": updates["phone"]}]
|
||||||
|
update_fields.append("phoneNumbers")
|
||||||
|
|
||||||
|
if "notes" in updates:
|
||||||
|
body["biographies"] = [{"value": updates["notes"], "contentType": "TEXT_PLAIN"}]
|
||||||
|
update_fields.append("biographies")
|
||||||
|
|
||||||
|
if not update_fields:
|
||||||
|
return {"success": False, "error": "No valid fields to update"}
|
||||||
|
|
||||||
|
result = (
|
||||||
|
self.service.people()
|
||||||
|
.updateContact(
|
||||||
|
resourceName=resource_name,
|
||||||
|
body=body,
|
||||||
|
updatePersonFields=",".join(update_fields),
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"resource_name": result.get("resourceName", resource_name),
|
||||||
|
"updated_fields": update_fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
except HttpError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def delete_contact(self, resource_name: str) -> Dict:
|
||||||
|
"""Delete a contact.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource_name: Contact resource name (e.g., "people/c1234567890")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with success status or error
|
||||||
|
"""
|
||||||
|
if not self._ensure_service():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.service.people().deleteContact(
|
||||||
|
resourceName=resource_name,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
return {"success": True, "deleted": resource_name}
|
||||||
|
|
||||||
|
except HttpError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def _format_contact(self, person: Dict) -> Dict:
|
||||||
|
"""Format a person resource into a simple contact dict."""
|
||||||
|
names = person.get("names", [])
|
||||||
|
emails = person.get("emailAddresses", [])
|
||||||
|
phones = person.get("phoneNumbers", [])
|
||||||
|
bios = person.get("biographies", [])
|
||||||
|
|
||||||
|
name = names[0] if names else {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"resource_name": person.get("resourceName", ""),
|
||||||
|
"given_name": name.get("givenName", ""),
|
||||||
|
"family_name": name.get("familyName", ""),
|
||||||
|
"display_name": name.get("displayName", ""),
|
||||||
|
"email": emails[0].get("value", "") if emails else "",
|
||||||
|
"phone": phones[0].get("value", "") if phones else "",
|
||||||
|
"notes": bios[0].get("value", "") if bios else "",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _format_contacts_summary(self, contacts: List[Dict]) -> str:
|
||||||
|
"""Format contacts into a readable summary."""
|
||||||
|
if not contacts:
|
||||||
|
return "No contacts found."
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for i, c in enumerate(contacts, 1):
|
||||||
|
name = c.get("display_name") or f"{c.get('given_name', '')} {c.get('family_name', '')}".strip()
|
||||||
|
parts = [f"{i}. {name or '(no name)'}"]
|
||||||
|
|
||||||
|
if c.get("email"):
|
||||||
|
parts.append(f" Email: {c['email']}")
|
||||||
|
if c.get("phone"):
|
||||||
|
parts.append(f" Phone: {c['phone']}")
|
||||||
|
|
||||||
|
lines.append("\n".join(parts))
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -93,7 +93,7 @@ class TaskScheduler:
|
|||||||
# Track file modification time
|
# Track file modification time
|
||||||
self._last_mtime = self.config_file.stat().st_mtime
|
self._last_mtime = self.config_file.stat().st_mtime
|
||||||
|
|
||||||
with open(self.config_file) as f:
|
with open(self.config_file, encoding="utf-8") as f:
|
||||||
config = yaml.safe_load(f) or {}
|
config = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
self.tasks.clear() # Clear existing tasks before reload
|
self.tasks.clear() # Clear existing tasks before reload
|
||||||
@@ -155,7 +155,7 @@ class TaskScheduler:
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(self.config_file, "w") as f:
|
with open(self.config_file, "w", encoding="utf-8") as f:
|
||||||
yaml.dump(
|
yaml.dump(
|
||||||
default_config, f,
|
default_config, f,
|
||||||
default_flow_style=False,
|
default_flow_style=False,
|
||||||
|
|||||||
193
tools.py
193
tools.py
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
# Google tools (lazy loaded when needed)
|
# Google tools (lazy loaded when needed)
|
||||||
_gmail_client: Optional[Any] = None
|
_gmail_client: Optional[Any] = None
|
||||||
_calendar_client: Optional[Any] = None
|
_calendar_client: Optional[Any] = None
|
||||||
|
_people_client: Optional[Any] = None
|
||||||
|
|
||||||
|
|
||||||
# Tool definitions in Anthropic's tool use format
|
# Tool definitions in Anthropic's tool use format
|
||||||
@@ -255,6 +256,71 @@ TOOL_DEFINITIONS = [
|
|||||||
"required": ["query"],
|
"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"],
|
query=tool_input["query"],
|
||||||
calendar_id=tool_input.get("calendar_id", "primary"),
|
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:
|
else:
|
||||||
return f"Error: Unknown tool '{tool_name}'"
|
return f"Error: Unknown tool '{tool_name}'"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -442,36 +526,38 @@ def _run_command(command: str, working_dir: str) -> str:
|
|||||||
# Google Tools Handlers
|
# 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.
|
"""Lazy initialization of Google clients.
|
||||||
|
|
||||||
Returns:
|
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:
|
if _gmail_client is not None and _calendar_client is not None and _people_client is not None:
|
||||||
return _gmail_client, _calendar_client
|
return _gmail_client, _calendar_client, _people_client
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from google_tools.oauth_manager import GoogleOAuthManager
|
from google_tools.oauth_manager import GoogleOAuthManager
|
||||||
from google_tools.gmail_client import GmailClient
|
from google_tools.gmail_client import GmailClient
|
||||||
from google_tools.calendar_client import CalendarClient
|
from google_tools.calendar_client import CalendarClient
|
||||||
|
from google_tools.people_client import PeopleClient
|
||||||
|
|
||||||
oauth_manager = GoogleOAuthManager()
|
oauth_manager = GoogleOAuthManager()
|
||||||
credentials = oauth_manager.get_credentials()
|
credentials = oauth_manager.get_credentials()
|
||||||
|
|
||||||
if not credentials:
|
if not credentials:
|
||||||
return None, None
|
return None, None, None
|
||||||
|
|
||||||
_gmail_client = GmailClient(oauth_manager)
|
_gmail_client = GmailClient(oauth_manager)
|
||||||
_calendar_client = CalendarClient(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:
|
except Exception as e:
|
||||||
print(f"[Google Tools] Failed to initialize: {e}")
|
print(f"[Google Tools] Failed to initialize: {e}")
|
||||||
return None, None
|
return None, None, None
|
||||||
|
|
||||||
|
|
||||||
def _send_email(
|
def _send_email(
|
||||||
@@ -482,7 +568,7 @@ def _send_email(
|
|||||||
reply_to_message_id: Optional[str] = None,
|
reply_to_message_id: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Send an email via Gmail API."""
|
"""Send an email via Gmail API."""
|
||||||
gmail_client, _ = _initialize_google_clients()
|
gmail_client, _, _ = _initialize_google_clients()
|
||||||
|
|
||||||
if not gmail_client:
|
if not gmail_client:
|
||||||
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
|
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
|
||||||
@@ -508,7 +594,7 @@ def _read_emails(
|
|||||||
include_body: bool = False,
|
include_body: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Search and read emails from Gmail."""
|
"""Search and read emails from Gmail."""
|
||||||
gmail_client, _ = _initialize_google_clients()
|
gmail_client, _, _ = _initialize_google_clients()
|
||||||
|
|
||||||
if not gmail_client:
|
if not gmail_client:
|
||||||
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
|
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:
|
def _get_email(message_id: str, format_type: str = "text") -> str:
|
||||||
"""Get full content of a specific email."""
|
"""Get full content of a specific email."""
|
||||||
gmail_client, _ = _initialize_google_clients()
|
gmail_client, _, _ = _initialize_google_clients()
|
||||||
|
|
||||||
if not gmail_client:
|
if not gmail_client:
|
||||||
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
|
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
|
||||||
@@ -566,7 +652,7 @@ def _read_calendar(
|
|||||||
max_results: int = 20,
|
max_results: int = 20,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Read upcoming calendar events."""
|
"""Read upcoming calendar events."""
|
||||||
_, calendar_client = _initialize_google_clients()
|
_, calendar_client, _ = _initialize_google_clients()
|
||||||
|
|
||||||
if not calendar_client:
|
if not calendar_client:
|
||||||
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
|
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
|
||||||
@@ -595,7 +681,7 @@ def _create_calendar_event(
|
|||||||
calendar_id: str = "primary",
|
calendar_id: str = "primary",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a new calendar event."""
|
"""Create a new calendar event."""
|
||||||
_, calendar_client = _initialize_google_clients()
|
_, calendar_client, _ = _initialize_google_clients()
|
||||||
|
|
||||||
if not calendar_client:
|
if not calendar_client:
|
||||||
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
|
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:
|
def _search_calendar(query: str, calendar_id: str = "primary") -> str:
|
||||||
"""Search calendar events by text query."""
|
"""Search calendar events by text query."""
|
||||||
_, calendar_client = _initialize_google_clients()
|
_, calendar_client, _ = _initialize_google_clients()
|
||||||
|
|
||||||
if not calendar_client:
|
if not calendar_client:
|
||||||
return "Error: Google not authorized. Run: python bot_runner.py --setup-google"
|
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}'
|
return f'Search results for "{query}":\n\n{summary}'
|
||||||
else:
|
else:
|
||||||
return f"Error searching calendar: {result.get('error', 'Unknown error')}"
|
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