* Fix flat replies in DMs DM replies were threaded under Adam's message instead of appearing flat. Root cause: listener fell back to event["ts"] as thread_ts for new DMs, causing say() to post as a threaded reply. Removed the fallback so DMs always use flat messages with placeholder update. Closes #4 * Add synchronous digest trigger for inline stats Changed trigger_daily_digest from async (fire-and-forget) to synchronous invocation so Lauren can report summary stats inline while the full Block Kit digest arrives as a separate DM. Closes #1 * Add tasks and reminders - DynamoDB task CRUD with TASK#{ulid} prefix - 5 new tools: create_task, list_tasks, complete_task, delete_task, create_reminder - New exec-aide-reminder Lambda triggered by EventBridge Scheduler one-shots - CDK: reminder Lambda, scheduler IAM role, conversation Lambda permissions - Updated system prompt with task/reminder capabilities Closes #3 * Add Google Calendar integration - New src/shared/calendar.py: OAuth service builder, list_events, create_event, check_availability (reuses gmail-oauth secret) - 3 new tools: get_calendar_events, create_calendar_event, check_availability - Extended OAuth scopes to include calendar in gmail.py and token script - Updated system prompt with calendar capabilities Requires re-running scripts/get_gmail_token.py to grant the calendar scope and updating the exec-aide/gmail-oauth secret with the new refresh token. Closes #2 * Add CodeBuild CI/CD pipeline CodePipeline triggers on pushes to main, CodeBuild runs cdk deploy via buildspec. * Update README for tasks, reminders, calendar, and CI/CD pipeline
710 lines
24 KiB
Python
710 lines
24 KiB
Python
import json
|
|
import logging
|
|
import os
|
|
from datetime import datetime, timezone
|
|
|
|
import boto3
|
|
|
|
from shared.dynamo import (
|
|
get_todays_messages, get_unanswered_threads, save_coordination,
|
|
create_task, get_tasks, update_task_status, delete_task,
|
|
)
|
|
from shared.gmail import get_authenticated_service, fetch_thread
|
|
from shared.secrets import get_config, get_slack_token
|
|
from shared.slack import post_message
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_lambda_client = None
|
|
_tool_context = {}
|
|
|
|
|
|
def set_tool_context(channel, thread_ts):
|
|
global _tool_context
|
|
_tool_context = {"channel": channel, "thread_ts": thread_ts}
|
|
|
|
|
|
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": {},
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"toolSpec": {
|
|
"name": "search_inbox",
|
|
"description": "Search Adam's Gmail inbox with a query. Supports Gmail search syntax: from:, to:, subject:, after:, before:, is:unread, label:, has:attachment, etc.",
|
|
"inputSchema": {
|
|
"json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "Gmail search query (e.g., 'from:jane after:2024/01/01 subject:invoice').",
|
|
},
|
|
"max_results": {
|
|
"type": "integer",
|
|
"description": "Maximum results to return. Defaults to 10.",
|
|
},
|
|
},
|
|
"required": ["query"],
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"toolSpec": {
|
|
"name": "coordinate_with_user",
|
|
"description": "Start a coordination thread to interact with someone in the channel. Posts a message in the channel and starts a threaded conversation with the mentioned person. Use when Adam asks you to coordinate, schedule, or communicate with someone.",
|
|
"inputSchema": {
|
|
"json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"flat_message": {
|
|
"type": "string",
|
|
"description": "Brief message to post flat in the channel (e.g., 'Coordinating a meeting with Jane').",
|
|
},
|
|
"thread_message": {
|
|
"type": "string",
|
|
"description": "The message to post in the thread, @mentioning the person (use Slack format <@USER_ID>).",
|
|
},
|
|
},
|
|
"required": ["flat_message", "thread_message"],
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"toolSpec": {
|
|
"name": "create_task",
|
|
"description": "Create a new task/to-do item for Adam.",
|
|
"inputSchema": {
|
|
"json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Task description.",
|
|
},
|
|
"due_date": {
|
|
"type": "string",
|
|
"description": "Optional due date in YYYY-MM-DD format.",
|
|
},
|
|
},
|
|
"required": ["title"],
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"toolSpec": {
|
|
"name": "list_tasks",
|
|
"description": "List Adam's tasks. Can filter by status (open or completed).",
|
|
"inputSchema": {
|
|
"json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"status": {
|
|
"type": "string",
|
|
"description": "Filter by status: 'open' or 'completed'. Omit for all tasks.",
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"toolSpec": {
|
|
"name": "complete_task",
|
|
"description": "Mark a task as completed.",
|
|
"inputSchema": {
|
|
"json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"task_id": {
|
|
"type": "string",
|
|
"description": "The task ID (ULID) to mark as completed.",
|
|
},
|
|
},
|
|
"required": ["task_id"],
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"toolSpec": {
|
|
"name": "delete_task",
|
|
"description": "Delete a task permanently.",
|
|
"inputSchema": {
|
|
"json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"task_id": {
|
|
"type": "string",
|
|
"description": "The task ID (ULID) to delete.",
|
|
},
|
|
},
|
|
"required": ["task_id"],
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"toolSpec": {
|
|
"name": "create_reminder",
|
|
"description": "Set a reminder that will send Adam a Slack DM at a specific time.",
|
|
"inputSchema": {
|
|
"json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"text": {
|
|
"type": "string",
|
|
"description": "The reminder message text.",
|
|
},
|
|
"remind_at": {
|
|
"type": "string",
|
|
"description": "When to send the reminder in ISO 8601 format (e.g., 2026-05-05T09:00:00). Timezone is America/New_York.",
|
|
},
|
|
},
|
|
"required": ["text", "remind_at"],
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"toolSpec": {
|
|
"name": "get_calendar_events",
|
|
"description": "Get Adam's upcoming calendar events for a date range.",
|
|
"inputSchema": {
|
|
"json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"start_date": {
|
|
"type": "string",
|
|
"description": "Start date in YYYY-MM-DD format. Defaults to today.",
|
|
},
|
|
"end_date": {
|
|
"type": "string",
|
|
"description": "End date in YYYY-MM-DD format. Defaults to start_date + 1 day.",
|
|
},
|
|
"max_results": {
|
|
"type": "integer",
|
|
"description": "Maximum events to return. Defaults to 10.",
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"toolSpec": {
|
|
"name": "create_calendar_event",
|
|
"description": "Create a new event on Adam's Google Calendar. Always confirm details with Adam before calling this.",
|
|
"inputSchema": {
|
|
"json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"summary": {
|
|
"type": "string",
|
|
"description": "Event title.",
|
|
},
|
|
"start_time": {
|
|
"type": "string",
|
|
"description": "Start time in ISO 8601 (e.g., 2026-05-05T10:00:00).",
|
|
},
|
|
"end_time": {
|
|
"type": "string",
|
|
"description": "End time in ISO 8601 (e.g., 2026-05-05T11:00:00).",
|
|
},
|
|
"attendees": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Email addresses of attendees.",
|
|
},
|
|
"description": {
|
|
"type": "string",
|
|
"description": "Event description or notes.",
|
|
},
|
|
},
|
|
"required": ["summary", "start_time", "end_time"],
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"toolSpec": {
|
|
"name": "check_availability",
|
|
"description": "Check Adam's calendar availability (free/busy) for a time range.",
|
|
"inputSchema": {
|
|
"json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"start_time": {
|
|
"type": "string",
|
|
"description": "Range start in ISO 8601 format.",
|
|
},
|
|
"end_time": {
|
|
"type": "string",
|
|
"description": "Range end in ISO 8601 format.",
|
|
},
|
|
},
|
|
"required": ["start_time", "end_time"],
|
|
}
|
|
},
|
|
}
|
|
},
|
|
]
|
|
|
|
|
|
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": int(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,
|
|
"search_inbox": _handle_search_inbox,
|
|
"coordinate_with_user": _handle_coordinate,
|
|
"create_task": _handle_create_task,
|
|
"list_tasks": _handle_list_tasks,
|
|
"complete_task": _handle_complete_task,
|
|
"delete_task": _handle_delete_task,
|
|
"create_reminder": _handle_create_reminder,
|
|
"get_calendar_events": _handle_get_calendar_events,
|
|
"create_calendar_event": _handle_create_calendar_event,
|
|
"check_availability": _handle_check_availability,
|
|
}
|
|
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):
|
|
resp = _get_lambda_client().invoke(
|
|
FunctionName="exec-aide-daily-digest",
|
|
InvocationType="RequestResponse",
|
|
)
|
|
payload = json.loads(resp["Payload"].read())
|
|
return {
|
|
"status": "sent",
|
|
"high_priority": payload.get("high", 0),
|
|
"bypassed_work_orders": payload.get("bypassed", 0),
|
|
"unanswered_threads": payload.get("unanswered", 0),
|
|
"normal": payload.get("normal", 0),
|
|
"low": payload.get("low", 0),
|
|
}
|
|
|
|
|
|
def _handle_search_inbox(inputs):
|
|
service = get_authenticated_service()
|
|
query = inputs["query"]
|
|
max_results = inputs.get("max_results", 10)
|
|
|
|
resp = service.users().messages().list(
|
|
userId="me", q=query, maxResults=max_results
|
|
).execute()
|
|
|
|
messages = []
|
|
for msg_meta in resp.get("messages", []):
|
|
msg = service.users().messages().get(
|
|
userId="me", id=msg_meta["id"], format="metadata",
|
|
metadataHeaders=["From", "To", "Subject", "Date"],
|
|
).execute()
|
|
headers = {h["name"]: h["value"] for h in msg["payload"]["headers"]}
|
|
messages.append({
|
|
"id": msg["id"],
|
|
"thread_id": msg["threadId"],
|
|
"from": headers.get("From", ""),
|
|
"to": headers.get("To", ""),
|
|
"subject": headers.get("Subject", ""),
|
|
"date": headers.get("Date", ""),
|
|
"snippet": msg.get("snippet", ""),
|
|
})
|
|
|
|
return {"query": query, "count": len(messages), "messages": messages}
|
|
|
|
|
|
def _handle_coordinate(inputs):
|
|
ctx = _tool_context
|
|
if not ctx.get("channel"):
|
|
return {"error": "Coordination is only available in channels, not DMs."}
|
|
|
|
token = get_slack_token()
|
|
channel = ctx["channel"]
|
|
|
|
coord_ts = post_message(channel, inputs["flat_message"], token)
|
|
post_message(channel, inputs["thread_message"], token, thread_ts=coord_ts)
|
|
|
|
save_coordination(
|
|
channel=channel,
|
|
coord_thread_ts=coord_ts,
|
|
conversation_thread_ts=ctx.get("thread_ts", ""),
|
|
started_by="lauren",
|
|
)
|
|
|
|
return {
|
|
"status": "coordinating",
|
|
"message": "Thread started. I'll follow up when they respond.",
|
|
}
|
|
|
|
|
|
# ── Task & Reminder Handlers ──────────────────────────────────
|
|
|
|
|
|
def _handle_create_task(inputs):
|
|
task_id = create_task(inputs["title"], inputs.get("due_date"))
|
|
return {"task_id": task_id, "status": "created", "title": inputs["title"]}
|
|
|
|
|
|
def _handle_list_tasks(inputs):
|
|
status_filter = inputs.get("status")
|
|
tasks = get_tasks(status_filter)
|
|
return {
|
|
"count": len(tasks),
|
|
"tasks": [
|
|
{
|
|
"task_id": t["pk"].replace("TASK#", ""),
|
|
"title": t["title"],
|
|
"due_date": t.get("due_date", ""),
|
|
"status": t["status"],
|
|
"created_at": t.get("created_at", ""),
|
|
}
|
|
for t in tasks
|
|
],
|
|
}
|
|
|
|
|
|
def _handle_complete_task(inputs):
|
|
update_task_status(inputs["task_id"], "completed")
|
|
return {"task_id": inputs["task_id"], "status": "completed"}
|
|
|
|
|
|
def _handle_delete_task(inputs):
|
|
delete_task(inputs["task_id"])
|
|
return {"task_id": inputs["task_id"], "status": "deleted"}
|
|
|
|
|
|
def _handle_create_reminder(inputs):
|
|
import ulid as _ulid
|
|
|
|
scheduler = boto3.client("scheduler")
|
|
schedule_name = f"exec-aide-reminder-{_ulid.new()}"
|
|
remind_at = inputs["remind_at"]
|
|
|
|
scheduler.create_schedule(
|
|
Name=schedule_name,
|
|
ScheduleExpression=f"at({remind_at})",
|
|
ScheduleExpressionTimezone="America/New_York",
|
|
FlexibleTimeWindow={"Mode": "OFF"},
|
|
Target={
|
|
"Arn": os.environ["REMINDER_FN_ARN"],
|
|
"RoleArn": os.environ["REMINDER_SCHEDULER_ROLE_ARN"],
|
|
"Input": json.dumps({"text": inputs["text"]}),
|
|
},
|
|
ActionAfterCompletion="DELETE",
|
|
)
|
|
return {"status": "scheduled", "remind_at": remind_at, "text": inputs["text"]}
|
|
|
|
|
|
# ── Calendar Handlers ──────────────────────────────────────────
|
|
|
|
|
|
def _handle_get_calendar_events(inputs):
|
|
from datetime import timedelta
|
|
from shared.calendar import get_calendar_service, list_events
|
|
|
|
start_date = inputs.get("start_date") or _today()
|
|
end_date = inputs.get("end_date")
|
|
if not end_date:
|
|
d = datetime.strptime(start_date, "%Y-%m-%d")
|
|
end_date = (d + timedelta(days=1)).strftime("%Y-%m-%d")
|
|
|
|
time_min = f"{start_date}T00:00:00-04:00"
|
|
time_max = f"{end_date}T23:59:59-04:00"
|
|
max_results = inputs.get("max_results", 10)
|
|
|
|
service = get_calendar_service()
|
|
events = list_events(service, time_min, time_max, max_results)
|
|
|
|
return {
|
|
"count": len(events),
|
|
"events": [
|
|
{
|
|
"id": e.get("id", ""),
|
|
"summary": e.get("summary", "(no title)"),
|
|
"start": e.get("start", {}).get("dateTime") or e.get("start", {}).get("date", ""),
|
|
"end": e.get("end", {}).get("dateTime") or e.get("end", {}).get("date", ""),
|
|
"attendees": [a.get("email", "") for a in e.get("attendees", [])],
|
|
"location": e.get("location", ""),
|
|
"status": e.get("status", ""),
|
|
}
|
|
for e in events
|
|
],
|
|
}
|
|
|
|
|
|
def _handle_create_calendar_event(inputs):
|
|
from shared.calendar import get_calendar_service, create_event
|
|
|
|
service = get_calendar_service()
|
|
event = create_event(
|
|
service,
|
|
summary=inputs["summary"],
|
|
start={"dateTime": inputs["start_time"], "timeZone": "America/New_York"},
|
|
end={"dateTime": inputs["end_time"], "timeZone": "America/New_York"},
|
|
attendees=inputs.get("attendees"),
|
|
description=inputs.get("description"),
|
|
)
|
|
return {
|
|
"status": "created",
|
|
"event_id": event["id"],
|
|
"summary": event.get("summary", ""),
|
|
"html_link": event.get("htmlLink", ""),
|
|
}
|
|
|
|
|
|
def _handle_check_availability(inputs):
|
|
from shared.calendar import get_calendar_service, check_availability
|
|
|
|
service = get_calendar_service()
|
|
busy = check_availability(service, inputs["start_time"], inputs["end_time"])
|
|
return {
|
|
"busy_slots": busy,
|
|
"is_free": len(busy) == 0,
|
|
}
|