AI Business Mail Assistant
Senior backend work on an internal AI assistant that runs in the background of an inbox, collecting business contacts and conversations that could lead to collaboration. LLM extraction, data freshness, and per-user observability.
An internal product I worked on was built as an assistant that does business development work in the background while you get on with your day. Connect a Gmail account and it reads mail as it arrives and digs through history: it collects the people you correspond with, their companies, roles, and ways to reach them, and it pulls out the conversations that look like the start of a collaboration. Instead of a CRM you have to feed, you get a contact base that fills itself and a shortlist of threads worth your attention.
At Sirin Software I was the senior backend engineer on that pipeline, and I mentored the project’s other developer: reviewing the extraction work, setting the approach to validation and merging, and unblocking him on the Django and Celery sides. The stack was Django and DRF, Celery, Postgres, Redis, Qdrant, ArangoDB, and Loki with Grafana. The project was shelved before launch, so what follows is engineering rather than adoption.
A pipeline that runs without the user
The product only works if it is useful before anyone opens it. Mail processing ran as two Celery workloads: an initial pass over the mailbox and a periodic pass that keeps up with new mail. Per-user settings controlled how much history to read and which addresses to ignore, and follow-up jobs filled in organization and job title after the first extraction pass.
I worked on the periodic side of this. A worker that re-reads a mailbox unattended has to be idempotent: it must not create a second copy of a contact, and it must not treat an already-processed thread as new. That constraint shaped most of the data model decisions below.
Making LLM extraction trustworthy
The extraction path was shared work, and a lot of my review comments ended up as the cleanup rules I then implemented myself: rejecting email addresses that don’t belong to the contact, detecting impersonal and noreply addresses, normalizing addresses, removing duplicated names parsed out of an email address, and handling responses that come back in an unexpected shape. I also lowered top-k and top-p to make the output more deterministic.
An assistant that recommends business contacts lives or dies on this. A contact base that confidently invents a phone number or merges two people is worse than an empty one, because the user stops trusting the suggestions and goes back to their inbox.
Newer data wins
Re-parsing a mailbox creates a nasty failure mode: an old message can overwrite what a newer one said. I added per-field timestamps to contact records and the merge logic around them, so periodic parsing updates organization and job title only when it has a newer, non-empty value, and parsing walks mail from recent to old.
That turned periodic parsing from a risk into the feature it was supposed to be: a contact base that improves as a relationship develops, instead of one that randomly regresses to how things looked two years ago.
Summaries that help you triage
I wrote the email summarization path. The LLM call runs at temperature zero, the response is validated for structure before it’s shown, and an invalid or failed call falls back to a local extractive summarizer.
The point was triage: you should be able to tell whether a long thread is a real opportunity or a newsletter thread you can ignore, without reading all of it. The fallback means that answer is available even when the model misbehaves or the API is down.
Observability per user
I added the Loki logging setup with a handler that tags entries with the user’s email, so a support question turns into a filtered query in Grafana instead of a grep across a shared log file. I also built staff-only test pages for running the parser over a chosen subset of a real inbox and for trying a prompt against a specific message.
An assistant that acts on someone’s private mailbox has to be explainable. When a user says the assistant got their contacts wrong, you need to replay exactly what it saw, not guess.
Running and debugging it
I maintained the Docker Compose configurations for production, debug, test-server, and multi-worker setups, and the nginx and certbot configuration for the test server. I also set up memory profiling with memray, pympler, and the Django debug toolbar to chase growth in the long-running parsing workers.
The test server gave the team a stable place to demo, and the profiling work was the start of making a worker that parses thousands of messages survivable in memory.