Add DM fixes, digest stats, tasks/reminders, calendar, and CI/CD pipeline #15

Merged
amoussa1229 merged 6 commits from feature/issues-1-4 into main 2026-05-05 14:45:15 +00:00
Showing only changes of commit 15ed335e10 - Show all commits

View file

@ -7,6 +7,7 @@ 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
@ -183,6 +184,185 @@ TOOLS = [
},
}
},
{
"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"],
}
},
}
},
]
@ -222,6 +402,14 @@ def dispatch_tool(tool_name, tool_input):
"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:
@ -328,11 +516,19 @@ def _handle_thread_detail(inputs):
def _handle_trigger_digest(inputs):
_get_lambda_client().invoke(
resp = _get_lambda_client().invoke(
FunctionName="exec-aide-daily-digest",
InvocationType="Event",
InvocationType="RequestResponse",
)
return {"status": "triggered", "message": "Daily digest has been triggered and will arrive shortly."}
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):
@ -386,3 +582,129 @@ def _handle_coordinate(inputs):
"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,
}