Today I built the first version of a system that sorts my Gmail inbox automatically. Every few hours, a Python script looks at incoming messages and decides which of the categories I already use should be applied.
The labels are Actioned, Awaiting Reply, Cold Email, FYI, Marketing, Meeting Update, and To Respond. I also added Not classified for messages the system cannot decide with enough confidence. That last category matters. A classifier shouldn't be forced to guess.
The problem
My inbox contains newsletters, sales offers, meeting invitations, automated notifications, work conversations, and messages that need an answer. The hard part isn't opening each email. It's deciding what kind of attention it needs.
To Respond means I need to reply or do something. Awaiting Reply means I already did my part and am waiting for somebody else. Actioned means the matter is closed. Marketing and Cold Email look similar until you use them: one is usually a mass mailing, while the other is a direct attempt to start a business conversation.
The differences look small until you turn them into rules. Then the awkward cases arrive: an airline offering to change a flight, a LinkedIn job alert, or a technical newsletter that includes a webinar invitation.
The flow
Gmail → gog → OmniRoute → Kev → gog → Gmail label
gog reads the messages and applies labels. The Python script accepts the maximum number of emails to process:
python3 scripts/gmail_kev_classifier.py 5
First, it searches the inbox for messages that don't already have a classification label or Not classified. It then gets the message body.
The full body does not go straight to Kev. It first goes through an OmniRoute instance running on the gateway. OmniRoute produces a factual summary with the sender, purpose, exact request, dates, conversation state, and signs of marketing or a newsletter.
That extra step became necessary because some emails are long. Sending a full email to a small CPU model made some requests take more than twenty seconds and end in a timeout. The summary removes HTML, signatures, and visual noise while keeping the information that matters for the decision.
Kev then receives the message metadata and the summary. It chooses one of the seven categories. If its confidence is below the configured threshold, the script applies Not classified instead. Future runs exclude that email.
Technical failures are handled differently. If OmniRoute or Kev times out, the script doesn't mark the email as Not classified. It can be retried when the service is available again.
Why Kev instead of a general LLM?
A conversational LLM can classify emails. It can also summarise them, draft a reply, translate them, and explain its reasoning. That flexibility helps when the problem is open-ended.
Classifying an email into a closed list is a smaller problem. I need a category, or an abstention. I don't need a new essay every time.
Kev is built for structured decisions. Instead of asking for a paragraph and then trying to extract a label from it, the program expects a choice and a confidence value. The result can go straight into the next step.
A local general-purpose model such as Qwen, Llama, or Mistral can run on the same kind of infrastructure and offers much broader capabilities. It can understand a complex thread and write a complete response. The trade-off is usually more memory, more generated tokens, and more work to force a reliable structured output.
Kev is aimed at a narrower job. The instance I'm testing runs on the gateway using CPU, without an NVIDIA GPU. It uses jaredpalmer/kev-0.8b, based on Qwen3.5-0.8B-Base. That doesn't make it automatically better than a general model. It makes it worth measuring whether a specialised decision model can solve this task with less infrastructure.
A general LLM may still be the next step after classification: understand a long thread, summarise it in detail, or draft a reply. Kev can be the small local gate in front of that more expensive work.
What the first tests showed
The first version sent long email bodies directly to Kev. On CPU, some messages took more than twenty seconds and timed out. That led to the OmniRoute summary step.
The first summaries were too short. They left out exactly the details Kev needed: whether the message asked for a reply, whether it was a sales offer, or whether it belonged to an existing conversation. I expanded the summary prompt to preserve those signals.
In a sample of five emails, a confidence threshold of 0.60 classified only one. At 0.40, three of five were classified. That reduces the backlog, but it also increases the risk of accepting a weak decision. For now, 0.40 is a test value, not a scientifically calibrated number.
Kev's confidence values still need to be compared with manually labelled examples. A score of 0.60 should not automatically be read as a real 60% chance of being correct. Over the next few days I want to see whether the scores are useful for ranking decisions or whether the policy needs more work.
Running it automatically
The script now runs every five hours through cron. Each run processes up to one hundred messages that have no classification label and no Not classified label.
There is also a safe manual mode:
python3 scripts/gmail_kev_classifier.py --dry-run 5
In dry-run mode the system reads, summarises, and classifies the messages without changing Gmail. A normal run uses gog to apply the selected label to the message thread.
The automatic wrapper gets the Gmail credentials and the OmniRoute token from Bitwarden. The secrets aren't written into the script or the crontab.
What I will measure next
Over the next few days I want to check whether the assigned categories match my own judgement, how many messages end up as Not classified, how long the complete CPU-based flow takes, and how it compares with a general local LLM making the same decision.
The boundary cases will tell me more than an obvious demo: Marketing versus Cold Email, FYI versus To Respond, and Awaiting Reply versus Actioned.
This is not a finished product. It's a small, local, measurable automation. I can change the prompt, the summary, the threshold, or the labels, run another test, and compare the result without turning the whole thing into a black box.
The goal isn't for Kev to decide everything. The goal is for it to handle the small decisions well and step aside when it isn't sure.