A commercial email intake assistant for a French SME that sells and installs heat pumps, sells spare parts, and runs installer trainings. It reads incoming emails, matches requests against a product catalog, and drafts replies that a human always reviews before sending.
Commercial staff spend time reading emails, looking up the right product reference in the catalog, checking availability, and writing a reply, several times a day, across three product families, on messages that are sometimes ambiguous or mix several requests together.
- One LLM call reads raw email text. Everything after that (catalog lookup, stock check, the review gate) is plain Python, not another model call.
- Never fabricate. No price is ever calculated (the catalog has none). No reference or stock status is trusted from the model alone: every claim is cross-checked against the real catalog and a stock snapshot before anything reaches a human.
- Human in the loop, always. No email is sent automatically. Every draft is reviewed, editable, and sent only on an explicit action.
- FastAPI, async throughout, for an I/O bound service that mostly waits on the Anthropic API and Resend.
- LangGraph for Agent Intake: the pipeline has a real branch (draftable vs human review), so modeling it as a graph keeps that branch explicit and testable.
- Claude Haiku for extraction and reply drafting: both are closer to structured parsing and templated writing than open ended reasoning, so a small model is enough. Extraction uses forced tool calling instead of free text plus parsing.
- In-memory store, no database: the app is single-process and stateless between
requests. Swapping in Postgres later would only touch
store.py. - Next.js for the frontend, fully client-side once loaded, talking to the API directly.
- Azure Container Apps for the backend, built from source through ACR, minimum one replica to avoid cold starts.
- Vercel for the frontend, matching the Next.js deployment model with free preview deployments.
- Resend for both inbound and outbound email, one provider instead of two.
A three-node LangGraph pipeline. The review gate is a graph branch, not a side check: if
reconcile doesn't clear every item, the graph never reaches draft.
app/
agents/
extraction.py # the LLM call, catalog/stock reconciliation, the review gate
intake.py # the LangGraph pipeline (Agent Intake)
routes/
webhook.py # Resend inbound webhook + manual test endpoint
requests.py # review queue listing + send action
health.py
reply.py # per-item reply drafting, grounded in already-verified fields
resend_client.py # inbound signature verification, outbound send
catalog.py, stock.py, store.py, schemas.py, config.py
data/
catalog.yaml # product reference data (no pricing)
stock.yaml # simulated stock snapshot, no ERP connection
frontend/
app/
components/ # RequestList, RequestDetail, ReplyAccordion, OnboardingTour
hooks/ # useOnboarding
exampleEmails.ts # sample messages for quick testing
page.tsx # master-detail layout: list on the left, detail on the right
tests/
test_intake_live.py # integration tests against a deployed instance
pip install -e ".[dev]"
uvicorn app.main:app --reloadRequired environment variables (see .env.example):
ANTHROPIC_API_KEYRESEND_API_KEY,RESEND_WEBHOOK_SECRET(only needed for the real inbound webhook)
cd frontend
npm install
npm run devSet NEXT_PUBLIC_API_URL to the backend's URL.
Master-detail layout: a sortable list of processed emails on the left, full detail (items,
original message, a reply-per-category accordion) on the right. A few example emails are
built in for quick testing, and a short onboarding tour runs on first visit (replayable from
the ? button). Responsive down to a single column on mobile.
There are two ways to feed the assistant a message: paste text into the "Nouveau message"
form (calls /intake/manual directly), or send a real email to thermoplus@feraikvra.resend.app.
The second option goes through the actual Resend inbound webhook and shows up in the queue
automatically, no form needed.
| Endpoint | Purpose |
|---|---|
GET /health |
Liveness check |
POST /webhook/resend |
Resend inbound webhook (email.received) |
POST /intake/manual |
Process an email without going through Resend |
GET /requests |
List processed emails and their drafts |
POST /requests/{id}/send |
Send an approved (possibly edited) reply |
DELETE /requests |
Clear all processed emails |
Integration tests run against a deployed instance rather than with local secrets:
BASE_URL=https://<deployed-url> pytest tests/test_intake_live.pyEvery push to main deploys both sides independently.
- Backend: a GitHub Actions workflow (
.github/workflows/backend.yml) builds the image through Azure Container Registry and updates the Azure Container App. No separate build server needed, ACR does the build. One replica is always kept warm to avoid cold starts. - Frontend: Vercel's Git integration builds and deploys the Next.js app automatically, no workflow file needed on this side.
- Email in/out: Resend, with a dedicated inbound address.
- No real ERP connection. Stock is a simulated snapshot (
data/stock.yaml). - No pricing engine. Any request needing a price is routed to a human.
- Training requests are matched against the catalog but session scheduling isn't modeled.
- Storage is in-memory and resets on restart.
- No authentication on any endpoint, including sending a reply or clearing the queue.
- CORS is wide open (
allow_origins=["*"]), needed since frontend and backend are on separate domains, but not a production policy. - No rate limiting, so
/intake/manualand/requests/{id}/sendcould be hit repeatedly and run up API and email costs.



