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
This commit is contained in:
parent
36bd41e238
commit
15ed335e10
1 changed files with 325 additions and 3 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue