import json import logging import os from datetime import datetime, timezone import boto3 from shared.dynamo import get_todays_messages, get_unanswered_threads from shared.gmail import get_authenticated_service, fetch_thread from shared.secrets import get_config logger = logging.getLogger(__name__) _lambda_client = None TOOLS = [ { "toolSpec": { "name": "get_inbox_summary", "description": "Get a summary of today's inbox: total emails processed, counts by classification (HIGH, NORMAL, LOW), and a list of the most recent high-priority items.", "inputSchema": { "json": { "type": "object", "properties": { "date": { "type": "string", "description": "Date in YYYY-MM-DD format. Defaults to today.", } }, } }, } }, { "toolSpec": { "name": "get_high_priority_emails", "description": "Get all HIGH-priority emails for a given date.", "inputSchema": { "json": { "type": "object", "properties": { "date": { "type": "string", "description": "Date in YYYY-MM-DD format. Defaults to today.", } }, } }, } }, { "toolSpec": { "name": "get_unanswered_threads", "description": "Get email threads where Adam hasn't replied in 24+ hours.", "inputSchema": { "json": { "type": "object", "properties": {}, } }, } }, { "toolSpec": { "name": "get_bypassed_work_orders", "description": "Get emails sent directly to Adam without CC'ing the work-orders address. These should have gone through the work order system.", "inputSchema": { "json": { "type": "object", "properties": { "date": { "type": "string", "description": "Date in YYYY-MM-DD format. Defaults to today.", } }, } }, } }, { "toolSpec": { "name": "search_emails_by_sender", "description": "Search for emails from a specific sender by name or email address. Searches across all classified emails in the database.", "inputSchema": { "json": { "type": "object", "properties": { "sender": { "type": "string", "description": "Sender name or email address to search for (case-insensitive partial match).", }, "limit": { "type": "integer", "description": "Maximum number of results to return. Defaults to 10.", }, }, "required": ["sender"], } }, } }, { "toolSpec": { "name": "get_email_thread_detail", "description": "Get the full message history of a specific email thread from Gmail, including all participants and message snippets.", "inputSchema": { "json": { "type": "object", "properties": { "thread_id": { "type": "string", "description": "Gmail thread ID.", } }, "required": ["thread_id"], } }, } }, { "toolSpec": { "name": "trigger_daily_digest", "description": "Send Adam his daily inbox digest right now, regardless of the scheduled time.", "inputSchema": { "json": { "type": "object", "properties": {}, } }, } }, ] def _get_lambda_client(): global _lambda_client if _lambda_client is None: _lambda_client = boto3.client("lambda") return _lambda_client def _today(): return datetime.now(timezone.utc).strftime("%Y-%m-%d") def _format_email(msg): return { "message_id": msg.get("message_id") or msg.get("pk", "").replace("MSG#", ""), "thread_id": msg.get("thread_id", ""), "from": msg.get("from_name") or msg.get("from_email", ""), "from_email": msg.get("from_email", ""), "subject": msg.get("subject", ""), "snippet": msg.get("snippet", ""), "classification": msg.get("classification", ""), "classification_reason": msg.get("classification_reason", ""), "internal_date": msg.get("internal_date", 0), } def dispatch_tool(tool_name, tool_input): handlers = { "get_inbox_summary": _handle_inbox_summary, "get_high_priority_emails": _handle_high_priority, "get_unanswered_threads": _handle_unanswered, "get_bypassed_work_orders": _handle_bypassed, "search_emails_by_sender": _handle_search_sender, "get_email_thread_detail": _handle_thread_detail, "trigger_daily_digest": _handle_trigger_digest, } handler = handlers.get(tool_name) if not handler: return {"error": f"Unknown tool: {tool_name}"} try: return handler(tool_input) except Exception: logger.exception("Tool %s failed", tool_name) return {"error": f"Tool {tool_name} failed"} def _handle_inbox_summary(inputs): date = inputs.get("date") or _today() messages = get_todays_messages(date) by_class = {} for msg in messages: c = msg.get("classification", "UNKNOWN") by_class[c] = by_class.get(c, 0) + 1 high = [_format_email(m) for m in messages if m.get("classification") == "HIGH"] return { "date": date, "total": len(messages), "by_classification": by_class, "high_priority_emails": high[:10], } def _handle_high_priority(inputs): date = inputs.get("date") or _today() messages = get_todays_messages(date) high = [_format_email(m) for m in messages if m.get("classification") == "HIGH"] return {"date": date, "count": len(high), "emails": high} def _handle_unanswered(inputs): config = get_config() threads = get_unanswered_threads(config["unanswered_threshold_hours"]) return { "count": len(threads), "threads": [ { "thread_id": t.get("pk", "").replace("THD#", ""), "subject": t.get("subject", ""), "last_message_from": t.get("last_message_from", ""), "unanswered_since": t.get("unanswered_since", ""), } for t in threads ], } def _handle_bypassed(inputs): date = inputs.get("date") or _today() messages = get_todays_messages(date) bypassed = [_format_email(m) for m in messages if m.get("bypassed_wo")] return {"date": date, "count": len(bypassed), "emails": bypassed} def _handle_search_sender(inputs): from shared.dynamo import _get_table sender = inputs["sender"].lower() limit = inputs.get("limit", 10) results = [] scan_kwargs = { "FilterExpression": "sk = :sk AND (contains(from_email, :sender) OR contains(from_name, :sender))", "ExpressionAttributeValues": {":sk": "MSG", ":sender": sender}, "Limit": 500, } resp = _get_table().scan(**scan_kwargs) results.extend(resp.get("Items", [])) while "LastEvaluatedKey" in resp and len(results) < limit: scan_kwargs["ExclusiveStartKey"] = resp["LastEvaluatedKey"] resp = _get_table().scan(**scan_kwargs) results.extend(resp.get("Items", [])) results.sort(key=lambda m: int(m.get("internal_date", 0)), reverse=True) return { "sender_query": inputs["sender"], "count": len(results[:limit]), "emails": [_format_email(m) for m in results[:limit]], } def _handle_thread_detail(inputs): service = get_authenticated_service() messages = fetch_thread(service, inputs["thread_id"]) return { "thread_id": inputs["thread_id"], "message_count": len(messages), "messages": [ { "from": m.get("from_name") or m.get("from_email", ""), "from_email": m.get("from_email", ""), "subject": m.get("subject", ""), "snippet": m.get("snippet", ""), "internal_date": m.get("internal_date", 0), } for m in messages ], } def _handle_trigger_digest(inputs): _get_lambda_client().invoke( FunctionName="exec-aide-daily-digest", InvocationType="Event", ) return {"status": "triggered", "message": "Daily digest has been triggered and will arrive shortly."}