diff --git a/agentclaw/app/agent_claw_main/main.py b/agentclaw/app/agent_claw_main/main.py index ce75b99..bd31163 100644 --- a/agentclaw/app/agent_claw_main/main.py +++ b/agentclaw/app/agent_claw_main/main.py @@ -16,15 +16,13 @@ from tools import messaging from tools.scheduler import schedule_reminder, list_reminders, cancel_reminder import tools.scheduler as _scheduler_module from tools.home_assistant import home_assistant, set_ha_config -from mcp.client.streamable_http import streamablehttp_client -from strands.tools.mcp.mcp_client import MCPClient +from tools.google_workspace import list_calendars, get_calendar_events, list_gmail_messages, get_gmail_message import httpx import botocore.auth import botocore.awsrequest import boto3 from urllib.parse import urlparse as _urlparse -WORKSPACE_MCP_URL = 'https://sptejrymri.execute-api.us-east-1.amazonaws.com/workspace/mcp' OAUTH_START_URL = ( os.environ.get('OAUTH_START_URL') or 'https://sptejrymri.execute-api.us-east-1.amazonaws.com/oauth/start' @@ -283,28 +281,14 @@ async def main(payload: dict, context): base_tools = [web_search, web_fetch, read_workspace_file, write_workspace_file, _code_interpreter.code_interpreter, home_assistant, connect_google_account, - manage_service, schedule_reminder, list_reminders, cancel_reminder] - - workspace_mcp_client = MCPClient( - lambda: streamablehttp_client(WORKSPACE_MCP_URL, timeout=20) - ) - workspace_tools = [] - google_email = user_profile.get('google_email', '') - print(f'[main] actor={actor_id} google_email={google_email!r} workspace_mcp_url={WORKSPACE_MCP_URL!r}') - if google_email: - try: - with workspace_mcp_client: - workspace_tools = workspace_mcp_client.list_tools_sync() - except Exception as e: - print(f'[main] workspace_mcp unavailable ({type(e).__name__}) — continuing without it') - else: - print(f'[main] actor={actor_id} has no google_email — skipping workspace_mcp') + manage_service, schedule_reminder, list_reminders, cancel_reminder, + list_calendars, get_calendar_events, list_gmail_messages, get_gmail_message] agent = Agent( model=model, system_prompt=system_prompt, session_manager=session_manager, - tools=base_tools + list(workspace_tools), + tools=base_tools, ) final_message = None diff --git a/agentclaw/app/agent_claw_main/pyproject.toml b/agentclaw/app/agent_claw_main/pyproject.toml index d9e6d09..c8313f2 100644 --- a/agentclaw/app/agent_claw_main/pyproject.toml +++ b/agentclaw/app/agent_claw_main/pyproject.toml @@ -14,7 +14,9 @@ dependencies = [ "botocore[crt] >= 1.35.0", "strands-agents-tools >= 0.5.0", "strands-agents >= 1.13.0", - + "google-api-python-client >= 2.0.0", + "google-auth >= 2.0.0", + "google-auth-httplib2 >= 0.2.0", ] [tool.hatch.build.targets.wheel] diff --git a/agentclaw/app/agent_claw_main/tools/google_workspace.py b/agentclaw/app/agent_claw_main/tools/google_workspace.py new file mode 100644 index 0000000..50548c2 --- /dev/null +++ b/agentclaw/app/agent_claw_main/tools/google_workspace.py @@ -0,0 +1,169 @@ +"""Direct Google Calendar and Gmail tools using google-api-python-client.""" +import json +import boto3 +from strands import tool +from google.oauth2.credentials import Credentials +from google.auth.transport.requests import Request +from googleapiclient.discovery import build +from datetime import datetime, timezone, timedelta + +_sm = None + + +def _secrets(): + global _sm + if _sm is None: + _sm = boto3.client('secretsmanager', region_name='us-east-1') + return _sm + + +def _get_creds(actor_id: str) -> Credentials: + secret_name = 'agent-claw/google-credentials/' + actor_id.replace(':', '-') + resp = _secrets().get_secret_value(SecretId=secret_name) + data = json.loads(resp['SecretString']) + + # Load OAuth client info + client_resp = _secrets().get_secret_value(SecretId='agent-claw/google-oauth-client') + client = json.loads(client_resp['SecretString']) + + creds = Credentials( + token=data.get('token'), + refresh_token=data.get('refresh_token'), + token_uri=data.get('token_uri', 'https://oauth2.googleapis.com/token'), + client_id=client.get('client_id'), + client_secret=client.get('client_secret'), + scopes=data.get('scopes'), + ) + + if creds.expired and creds.refresh_token: + creds.refresh(Request()) + # Persist refreshed token + data['token'] = creds.token + _secrets().put_secret_value(SecretId=secret_name, SecretString=json.dumps(data)) + + return creds + + +def _actor_id(): + # Import here to avoid circular import; set per-invocation in main.py + import main as _main + return _main._current_actor_id + + +@tool +def list_calendars() -> str: + """List all Google Calendars for the current user.""" + try: + creds = _get_creds(_actor_id()) + svc = build('calendar', 'v3', credentials=creds) + result = svc.calendarList().list().execute() + items = result.get('items', []) + if not items: + return 'No calendars found.' + return '\n'.join(f"{c['id']}: {c['summary']}" for c in items) + except Exception as e: + return f'Error listing calendars: {e}' + + +@tool +def get_calendar_events(calendar_id: str = 'primary', days_ahead: int = 7) -> str: + """Get upcoming Google Calendar events. + + Args: + calendar_id: Calendar ID (default: 'primary') + days_ahead: Number of days ahead to fetch (default: 7) + """ + try: + creds = _get_creds(_actor_id()) + svc = build('calendar', 'v3', credentials=creds) + now = datetime.now(timezone.utc) + time_max = now + timedelta(days=days_ahead) + result = svc.events().list( + calendarId=calendar_id, + timeMin=now.isoformat(), + timeMax=time_max.isoformat(), + singleEvents=True, + orderBy='startTime', + maxResults=50, + ).execute() + events = result.get('items', []) + if not events: + return 'No upcoming events.' + lines = [] + for e in events: + start = e['start'].get('dateTime', e['start'].get('date', '')) + lines.append(f"{start} — {e.get('summary', '(no title)')}") + return '\n'.join(lines) + except Exception as e: + return f'Error fetching calendar events: {e}' + + +@tool +def list_gmail_messages(max_results: int = 10, query: str = 'in:inbox') -> str: + """List Gmail messages. + + Args: + max_results: Maximum number of messages to return (default: 10) + query: Gmail search query (default: 'in:inbox') + """ + try: + creds = _get_creds(_actor_id()) + svc = build('gmail', 'v1', credentials=creds) + result = svc.users().messages().list( + userId='me', q=query, maxResults=max_results + ).execute() + messages = result.get('messages', []) + if not messages: + return 'No messages found.' + lines = [] + for m in messages: + msg = svc.users().messages().get( + userId='me', id=m['id'], format='metadata', + metadataHeaders=['Subject', 'From', 'Date'] + ).execute() + headers = {h['name']: h['value'] for h in msg.get('payload', {}).get('headers', [])} + lines.append( + f"id={m['id']} | {headers.get('Date', '')} | From: {headers.get('From', '')} | {headers.get('Subject', '(no subject)')}" + ) + return '\n'.join(lines) + except Exception as e: + return f'Error listing Gmail messages: {e}' + + +@tool +def get_gmail_message(message_id: str) -> str: + """Get the full content of a Gmail message by ID. + + Args: + message_id: The Gmail message ID + """ + try: + creds = _get_creds(_actor_id()) + svc = build('gmail', 'v1', credentials=creds) + msg = svc.users().messages().get( + userId='me', id=message_id, format='full' + ).execute() + headers = {h['name']: h['value'] for h in msg.get('payload', {}).get('headers', [])} + body = _extract_body(msg.get('payload', {})) + return ( + f"From: {headers.get('From', '')}\n" + f"To: {headers.get('To', '')}\n" + f"Date: {headers.get('Date', '')}\n" + f"Subject: {headers.get('Subject', '')}\n\n" + f"{body}" + ) + except Exception as e: + return f'Error fetching Gmail message: {e}' + + +def _extract_body(payload: dict) -> str: + import base64 + mime = payload.get('mimeType', '') + if mime == 'text/plain': + data = payload.get('body', {}).get('data', '') + return base64.urlsafe_b64decode(data + '==').decode('utf-8', errors='replace') if data else '' + for part in payload.get('parts', []): + text = _extract_body(part) + if text: + return text + return ''