2026-05-01 11:31:29 -04:00
import json
import logging
import os
from datetime import datetime , timezone
import boto3
2026-05-01 13:32:06 -04:00
from shared . dynamo import (
get_todays_messages , get_unanswered_threads , save_coordination ,
Add DM fixes, digest stats, tasks/reminders, calendar, and CI/CD pipeline (#15)
* 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
2026-05-05 10:45:14 -04:00
create_task , get_tasks , update_task_status , delete_task ,
2026-05-01 13:32:06 -04:00
)
2026-05-01 11:31:29 -04:00
from shared . gmail import get_authenticated_service , fetch_thread
2026-05-01 13:32:06 -04:00
from shared . secrets import get_config , get_slack_token
from shared . slack import post_message
2026-05-01 11:31:29 -04:00
logger = logging . getLogger ( __name__ )
_lambda_client = None
2026-05-01 13:32:06 -04:00
_tool_context = { }
def set_tool_context ( channel , thread_ts ) :
global _tool_context
_tool_context = { " channel " : channel , " thread_ts " : thread_ts }
2026-05-01 11:31:29 -04:00
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 " : { } ,
}
} ,
}
} ,
2026-05-01 13:32:06 -04:00
{
" 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 " ] ,
}
} ,
}
} ,
Add DM fixes, digest stats, tasks/reminders, calendar, and CI/CD pipeline (#15)
* 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
2026-05-05 10:45:14 -04:00
{
" 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 " ] ,
}
} ,
}
} ,
2026-05-01 11:31:29 -04:00
]
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 " , " " ) ,
2026-05-01 13:32:06 -04:00
" internal_date " : int ( msg . get ( " internal_date " , 0 ) ) ,
2026-05-01 11:31:29 -04:00
}
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 ,
2026-05-01 13:32:06 -04:00
" search_inbox " : _handle_search_inbox ,
" coordinate_with_user " : _handle_coordinate ,
Add DM fixes, digest stats, tasks/reminders, calendar, and CI/CD pipeline (#15)
* 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
2026-05-05 10:45:14 -04:00
" 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 ,
2026-05-01 11:31:29 -04:00
}
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 ) :
Add DM fixes, digest stats, tasks/reminders, calendar, and CI/CD pipeline (#15)
* 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
2026-05-05 10:45:14 -04:00
resp = _get_lambda_client ( ) . invoke (
2026-05-01 11:31:29 -04:00
FunctionName = " exec-aide-daily-digest " ,
Add DM fixes, digest stats, tasks/reminders, calendar, and CI/CD pipeline (#15)
* 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
2026-05-05 10:45:14 -04:00
InvocationType = " RequestResponse " ,
2026-05-01 11:31:29 -04:00
)
Add DM fixes, digest stats, tasks/reminders, calendar, and CI/CD pipeline (#15)
* 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
2026-05-05 10:45:14 -04:00
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 ) ,
}
2026-05-01 13:32:06 -04:00
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. " ,
}
Add DM fixes, digest stats, tasks/reminders, calendar, and CI/CD pipeline (#15)
* 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
2026-05-05 10:45:14 -04:00
# ── 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 ,
}