Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,10 @@ GMAIL_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET_HERE
# Silent-reply follow-up: days between prospect reply and the auto follow-up.
# Pulled at runtime so we can tune in prod without a deploy.
REPLY_FOLLOWUP_OFFSET_DAYS=3

# Anthropic API key for warm-reply triage classification. When unset, the
# reply triage endpoint falls back to a deterministic keyword heuristic that
# clears the >= 80% accuracy bar on tests/triage/cases.json.
ANTHROPIC_API_KEY=
# Optional Claude model override; defaults to claude-haiku-4-5-20251001.
ANTHROPIC_MODEL=
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@
(or any question) and then goes silent, automatically schedule a short
follow-up 3 days later. The follow-up cancels itself if the prospect
replies again before it sends. See **Pending follow-ups** on the dashboard.
- Warm-reply triage: every inbound reply is classified into one of four
intent buckets — Interested, Objection, Not Now, Out of Office — with a
pre-drafted follow-up the user can edit and send in one click. Lives at
**/dashboard/replies**.

## Warm-reply triage

When a reply lands, the ingest path (`POST /api/email-tracking/reply`)
classifies it via Anthropic's Claude (or a deterministic keyword heuristic
when no API key is set) and writes a row to the `reply` table. The
`/dashboard/replies` page surfaces those rows in four tabs and exposes
[Send] / [Edit] / [Archive] actions per card. Clicking [Send] reuses the
existing `email_queue` send pipeline — same account, same `Re:` subject —
so warm replies stay inside coldflow's regular outbound rate limits.

Set `ANTHROPIC_API_KEY` in `.env` to enable the LLM classifier. Without it
the heuristic still ships 80%+ classification accuracy on
`tests/triage/cases.json`.



Expand Down
32 changes: 32 additions & 0 deletions libs/db/drizzle/0008_white_mongoose.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
CREATE TYPE "public"."reply_intent" AS ENUM('interested', 'objection', 'not_now', 'out_of_office');--> statement-breakpoint
CREATE TYPE "public"."reply_triage_status" AS ENUM('new', 'actioned', 'archived');--> statement-breakpoint
CREATE TABLE "reply" (
"id" text PRIMARY KEY NOT NULL,
"contact_id" text NOT NULL,
"campaign_id" text NOT NULL,
"event_id" text,
"recipient_email" text NOT NULL,
"recipient_name" text,
"body" text NOT NULL,
"intent" "reply_intent" NOT NULL,
"confidence" real NOT NULL,
"suggested_followup" text NOT NULL,
"status" "reply_triage_status" DEFAULT 'new' NOT NULL,
"sent_queue_id" text,
"actioned_at" timestamp,
"archived_at" timestamp,
"received_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "reply" ADD CONSTRAINT "reply_contact_id_email_queue_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."email_queue"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "reply" ADD CONSTRAINT "reply_campaign_id_email_campaign_id_fk" FOREIGN KEY ("campaign_id") REFERENCES "public"."email_campaign"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "reply" ADD CONSTRAINT "reply_event_id_email_event_id_fk" FOREIGN KEY ("event_id") REFERENCES "public"."email_event"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "reply" ADD CONSTRAINT "reply_sent_queue_id_email_queue_id_fk" FOREIGN KEY ("sent_queue_id") REFERENCES "public"."email_queue"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "reply_contactId_idx" ON "reply" USING btree ("contact_id");--> statement-breakpoint
CREATE INDEX "reply_campaignId_idx" ON "reply" USING btree ("campaign_id");--> statement-breakpoint
CREATE INDEX "reply_status_idx" ON "reply" USING btree ("status");--> statement-breakpoint
CREATE INDEX "reply_intent_idx" ON "reply" USING btree ("intent");--> statement-breakpoint
CREATE INDEX "reply_status_intent_idx" ON "reply" USING btree ("status","intent");--> statement-breakpoint
CREATE INDEX "reply_receivedAt_idx" ON "reply" USING btree ("received_at");
Loading