Add AI-driven marketing filter, activate Fargate listener, update docs
Bedrock Haiku now classifies marketing/promotional emails as MARKETING and they are filtered out before saving or alerting. This replaces Gmail label-based filtering for more accurate classification. Also: enable Fargate listener (desiredCount=1), update README for CDK, update Notion AWS Architecture Map and Slack Apps Inventory.
This commit is contained in:
parent
460cf200c7
commit
df56bd6e9a
5 changed files with 57 additions and 43 deletions
76
README.md
76
README.md
|
|
@ -1,17 +1,28 @@
|
|||
# Exec Aide
|
||||
|
||||
Personal AI agent that monitors Adam Moussa's Gmail inbox, classifies emails by urgency using Claude Haiku (Bedrock), and delivers Slack alerts and daily digests.
|
||||
Personal AI agent that monitors Adam Moussa's Gmail inbox, classifies emails by urgency using Claude Haiku (Bedrock), and delivers Slack alerts and daily digests via a dedicated Slack app.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
EventBridge (15 min) → fetch-classify Lambda → Gmail API → Bedrock Haiku → DynamoDB + Slack DM
|
||||
EventBridge Scheduler (5 PM ET M-F) → daily-digest Lambda → DynamoDB + Gmail API → Slack DM
|
||||
Fargate (Socket Mode) → Slack listener → two-way conversation with Adam
|
||||
```
|
||||
|
||||
- **exec-aide-fetch-classify**: Polls Gmail via History API, classifies each new message, sends immediate Slack DM for HIGH priority items, tracks thread state for unanswered detection.
|
||||
- **exec-aide-daily-digest**: Summarizes the day's high-priority items, bypassed work orders (emails not CC'ing work-orders@), and unanswered threads older than 24h.
|
||||
- **DynamoDB table `exec-aide`**: Single-table design with message records, thread tracking, and sync metadata. 90-day TTL.
|
||||
**Stack:** CDK (TypeScript), stack name `exec-aide`, region us-east-1
|
||||
|
||||
### Services
|
||||
|
||||
- **exec-aide-fetch-classify** (Lambda): Polls Gmail via History API every 15 minutes, classifies each new message via Bedrock Haiku, filters out marketing, sends immediate Slack DM for HIGH priority, tracks thread state for unanswered detection.
|
||||
- **exec-aide-daily-digest** (Lambda): Runs weekdays at 5 PM ET (DST-aware). Summarizes high-priority items, bypassed work orders, and unanswered threads older than 24h.
|
||||
- **exec-aide-listener** (Fargate): Socket Mode Slack bot for two-way conversation. Runs in a dedicated VPC (10.30.0.0/16) on ARM64.
|
||||
- **DynamoDB `exec-aide`**: Single-table design (MSG#, THD#, META# prefixes). GSI `by-date` for daily digest queries. 90-day TTL.
|
||||
|
||||
### CDK Constructs
|
||||
|
||||
- `lib/constructs/email-pipeline.ts` — DynamoDB table, both Lambda functions, EventBridge schedules
|
||||
- `lib/constructs/socket-mode.ts` — VPC, ECS cluster, Fargate service, ECR repo (image built automatically via `ContainerImage.fromAsset()`)
|
||||
|
||||
## Classification Rules
|
||||
|
||||
|
|
@ -19,73 +30,64 @@ EventBridge Scheduler (5 PM ET M-F) → daily-digest Lambda → DynamoDB + Gmail
|
|||
|---|---|
|
||||
| **HIGH** | VIP sender, urgency keywords, escalation language, financial/legal matters |
|
||||
| **NORMAL** | Standard business correspondence |
|
||||
| **LOW** | Newsletters, automated notifications, marketing |
|
||||
| **LOW** | FYI-only, calendar confirmations, informational notifications |
|
||||
| **MARKETING** | Newsletters, promotions, bulk mailings, cold outreach (filtered out) |
|
||||
| **Bypassed WO** | Email to Adam without work-orders@ in TO/CC |
|
||||
| **Unanswered** | Thread where Adam hasn't replied in 24h+ |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- AWS CLI + SAM CLI
|
||||
- Python 3.12
|
||||
- Gmail API enabled in Google Cloud Console
|
||||
- Slack app with `chat:write` + `im:write` scopes
|
||||
- AWS CDK CLI (`npm install -g aws-cdk`)
|
||||
- Node.js 18+, Python 3.12
|
||||
- Docker (for Lambda bundling and Fargate image builds)
|
||||
- Gmail API enabled in Google Cloud Console (project: `exec-aide`)
|
||||
- Slack app "Exec Aide" with Socket Mode enabled
|
||||
|
||||
## One-Time Setup
|
||||
|
||||
### 1. Gmail OAuth
|
||||
|
||||
```bash
|
||||
# Install the setup dependency (not needed in Lambda)
|
||||
pip install google-auth-oauthlib
|
||||
|
||||
# Download OAuth client JSON from Google Cloud Console, then:
|
||||
python scripts/get_gmail_token.py --client-secrets-file path/to/client_secret.json
|
||||
# Authorize as adam@seahavenind.com in the browser
|
||||
# Copy the output into Secrets Manager
|
||||
# Authorize as adam@seahavenind.com, copy output to Secrets Manager
|
||||
```
|
||||
|
||||
### 2. Secrets Manager
|
||||
|
||||
```bash
|
||||
# Gmail OAuth (output from step 1)
|
||||
aws secretsmanager create-secret --name exec-aide/gmail-oauth --secret-string '...'
|
||||
|
||||
# Slack bot token
|
||||
aws secretsmanager create-secret --name exec-aide/slack-bot-token --secret-string 'xoxb-...'
|
||||
```
|
||||
| Secret | Contents |
|
||||
|---|---|
|
||||
| `exec-aide/gmail-oauth` | `{ client_id, client_secret, refresh_token, access_token, token_expiry }` |
|
||||
| `exec-aide/slack-credentials` | `{ botToken, signingSecret, appToken }` |
|
||||
|
||||
### 3. SSM Parameters
|
||||
|
||||
```bash
|
||||
aws ssm put-parameter --name /exec-aide/adam-email --value adam@seahavenind.com --type String
|
||||
aws ssm put-parameter --name /exec-aide/adam-slack-user-id --value U01XXXXXXXX --type String
|
||||
aws ssm put-parameter --name /exec-aide/vip-senders --value '["ceo@example.com"]' --type String
|
||||
aws ssm put-parameter --name /exec-aide/vip-domains --value '["example.com"]' --type String
|
||||
aws ssm put-parameter --name /exec-aide/work-order-addresses --value '["work-orders@seahavenind.com","work-orders@seahaven.com"]' --type String
|
||||
aws ssm put-parameter --name /exec-aide/unanswered-threshold-hours --value 24 --type String
|
||||
```
|
||||
| Parameter | Value |
|
||||
|---|---|
|
||||
| `/exec-aide/adam-email` | `adam@seahavenind.com` |
|
||||
| `/exec-aide/adam-slack-user-id` | Slack user ID |
|
||||
| `/exec-aide/vip-senders` | JSON array of email addresses |
|
||||
| `/exec-aide/vip-domains` | JSON array of domains |
|
||||
| `/exec-aide/work-order-addresses` | `["work-orders@seahavenind.com","work-orders@seahaven.com"]` |
|
||||
| `/exec-aide/unanswered-threshold-hours` | `24` |
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
cp samconfig.toml.example samconfig.toml # edit if needed
|
||||
sam build
|
||||
sam deploy
|
||||
npm install
|
||||
cdk deploy
|
||||
```
|
||||
|
||||
## Manual Testing
|
||||
|
||||
```bash
|
||||
# Trigger a poll
|
||||
aws lambda invoke --function-name exec-aide-fetch-classify /dev/stdout
|
||||
|
||||
# Trigger the daily digest
|
||||
aws lambda invoke --function-name exec-aide-daily-digest /dev/stdout
|
||||
```
|
||||
|
||||
## Updating VIP List or Keywords
|
||||
## Updating VIP List
|
||||
|
||||
Edit the SSM parameters directly — changes take effect on the next Lambda cold start (or within ~15 minutes):
|
||||
Edit the SSM parameters — changes take effect on the next Lambda cold start:
|
||||
|
||||
```bash
|
||||
aws ssm put-parameter --name /exec-aide/vip-senders --value '["new@example.com"]' --type String --overwrite
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export class SocketModeConstruct extends Construct {
|
|||
serviceName: 'exec-aide-listener',
|
||||
cluster,
|
||||
taskDefinition: taskDef,
|
||||
desiredCount: 0,
|
||||
desiredCount: 1,
|
||||
assignPublicIp: true,
|
||||
securityGroups: [sg],
|
||||
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ from shared.slack import send_dm, build_high_priority_alert
|
|||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def lambda_handler(event, context):
|
||||
config = get_config()
|
||||
service = get_authenticated_service()
|
||||
|
|
@ -27,7 +26,7 @@ def lambda_handler(event, context):
|
|||
msg_ids, history_id = initial_sync(service)
|
||||
|
||||
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
results = {"processed": 0, "high": 0, "skipped": 0, "errors": 0}
|
||||
results = {"processed": 0, "high": 0, "skipped": 0, "filtered": 0, "errors": 0}
|
||||
|
||||
for msg_id in msg_ids:
|
||||
try:
|
||||
|
|
@ -39,6 +38,11 @@ def lambda_handler(event, context):
|
|||
classification = classify_email(
|
||||
msg, config["vip_senders"], config["vip_domains"]
|
||||
)
|
||||
|
||||
if classification["classification"] == "MARKETING":
|
||||
results["filtered"] += 1
|
||||
continue
|
||||
|
||||
bypassed = check_bypassed_workorder(msg, config["work_order_addresses"])
|
||||
|
||||
slack_ts = ""
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ You are an email triage assistant for Adam Moussa, President of Sea Haven Indust
|
|||
|
||||
Classify this email's urgency. Return ONLY valid JSON — no markdown, no explanation:
|
||||
|
||||
{"classification": "HIGH" or "NORMAL" or "LOW", "reason": "one sentence"}
|
||||
{"classification": "HIGH" or "NORMAL" or "LOW" or "MARKETING", "reason": "one sentence"}
|
||||
|
||||
HIGH:
|
||||
- Sender is on the VIP list
|
||||
|
|
@ -30,8 +30,15 @@ NORMAL:
|
|||
- Meeting invites, project updates, routine vendor communications
|
||||
|
||||
LOW:
|
||||
- Newsletters, marketing, automated notifications, bulk mailings
|
||||
- FYI-only messages, calendar confirmations, read receipts\
|
||||
- FYI-only messages, calendar confirmations, read receipts
|
||||
- Internal automated notifications that are informational only
|
||||
|
||||
MARKETING:
|
||||
- Newsletters, promotional emails, product announcements, sales pitches
|
||||
- Bulk mailings, mailing list digests, webinar invitations
|
||||
- Social media notifications, app notifications
|
||||
- "Unsubscribe" link present with no actionable content for Adam
|
||||
- Automated marketing sequences, drip campaigns, cold outreach\
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ def _parse_message(msg):
|
|||
return {
|
||||
"message_id": msg["id"],
|
||||
"thread_id": msg["threadId"],
|
||||
"label_ids": msg.get("labelIds", []),
|
||||
"from_email": from_email,
|
||||
"from_name": from_name,
|
||||
"to_emails": _parse_address_list(headers.get("To", "")),
|
||||
|
|
|
|||
Reference in a new issue