Kempt answers questions about the documents on your Mac. "What's my IBAN?" "When does my lease end?" "How much did I spend on electricity in July?" It does this with Apple's on-device model, which is private, free to call and fast, and also small enough that you can't let it near a bank number unsupervised.
In the first week, bank details alone produced three bugs.
- Voice search heard "What's my IBAN?" as a question about iPhones, and went looking for those.
- In a rental dossier, a bank statement was offered as the RIB, the French document that actually certifies your account details, because both carry an IBAN.
- Asked for an IBAN by someone with two accounts, the model named one and stopped, as if the question had a single answer.
None of these is exotic. They're what happens when a system takes the first plausible answer and trusts it. This is how the design moved away from that.
The model never owns a fact
Every answer in Kempt is assembled in three steps, and the model only does the last one.
- Retrieval finds the passage. A hybrid search over the documents, with an inverted index and French and English synonyms, returns the pages that could contain the answer.
- Rules extract and validate the value. An IBAN is found by its shape and checked with the ISO 13616 checksum, so a misread number never reaches the screen. A date has to parse. An amount has to sit next to a currency.
- The model phrases it and cites the source. It writes the sentence a person reads, and the answer carries a link to the page the value came from.
If step two finds nothing, there's no step three. The answer says it doesn't know. A small model that's never asked to recall a number can't invent one.
An identifier must follow its own label
Documents are full of numbers that look like the one you want. A payslip has an employee number, a social security number and a bank reference on the same page. One of the most useful rules in Kempt's commit history is a single line: an identifier must follow its own label, and belong to whoever was asked about.
That rule killed a whole class of wrong answers. A string that looks like an IBAN but sits under "Employer reference" isn't your IBAN. A valid IBAN on your partner's payslip isn't yours either, if the question was about you.
The checksum earns its keep in less obvious ways too. On one statement the account number ran straight into the bank code that followed it on the line, and the two were read as a single 34-character number. It failed the checksum, so instead of showing it, Kempt now cuts a match to the length that country's IBANs actually have before checking it.
When there are several legitimate answers, Kempt shows all of them. "What's my IBAN?" lists every account it found, each with its bank and the document it came from. Picking one silently would be a guess.
A price isn't a purchase
The accountant pack collects a month of receipts. Early on it was too generous. A saved web page showing a product and its price got counted as an expense, because it had a merchant, an amount and a date.
The fix was to make the rule about what a document is, not what it mentions: a captured web page counts only if it confirms a payment. Dossiers follow the same principle. A slot that wants a driving licence takes a driving licence, in the owner's name. A receipt that mentions a driving licence doesn't qualify.
This is the part most AI features skip. The model is good at noticing that a document is related to a request. Being related isn't the same as satisfying it, and that distinction has to be written down as a rule.
Measure on labelled real files, report only totals
Every change to extraction runs against a set of documents whose correct answers were checked by hand. For the accountant pack that's 50 verified labels. One batch of fixes on the 14th of October moved one month's results from 14 of 16 inclusions, 3 of 5 amounts and 2 of 5 dates to 16 of 16, 5 of 5 and 5 of 5.
Two rules keep that honest.
The evaluation never prints a document. These are real payslips and bank statements. The scripts print file names and totals, never contents, so a test run can be pasted into a commit message or a review without leaking anything.
The evaluation stays out of the live index. Test documents never end up in the search a person uses, and real documents never quietly become test data.
Without the labelled set, "the answers feel better now" would have been the only signal, and it's the least reliable one there is.
Fail where people can see it
One bug from the accountant pack was worse than any of the IBAN ones, because it was invisible. A model call that failed once had its failure cached as the answer, so the same document kept coming back empty even after the model was fine. Now a failed call is never cached, and a failure says it's a failure.
The same idea runs through the rest of the app. A file Kempt can't classify with confidence goes to Needs a look, where it's never moved, renamed or deleted until a person checks it. Every move is logged with its reason and can be undone. An assistant that admits uncertainty is less impressive in a demo and far more trustworthy on the fifth day of use.
What carries over to bigger models
None of this depends on the model being small. A large API model makes fewer mistakes, but it makes them with more confidence, and on money questions one confident mistake is all it takes.
If I were adding an LLM feature to a bank's back office tomorrow, I'd start in the same place: retrieval for the passage, rules for the value, the model for the sentence, a labelled set before the first prompt, and a visible "I don't know". The build story behind all this is in a signed Mac app in four days, built with AI coding agents.