Dokuprint Agent
Summary
In college I ran a small print shop and priced everything by gut feel. Years later I taught a machine to do the pricing (that story lives here). But a pricing machine is not a shop assistant. Nobody walks into a print shop with only numbers on their mind. They ask whether spiral binding fits a 120-page thesis, what laminating costs, whether their file is ready to print.
So I built the assistant on top of the engine. A chat that prices your uploaded PDF and answers questions about the shop. The whole thing rests on one rule: every price in an answer must come from a tool, in that same conversation. Language models invent numbers with a straight face, so mine is not allowed to produce any. It writes sentences around figures that code already computed.
How it works, in plain words
Your message walks a short assembly line. First a check: is this something a print shop can answer, or is it a trick? Then the assistant figures out what you need, looks up the catalog, runs the pricing engine if you uploaded a file, drafts the reply, and finally a guard scans the draft for any price that did not come from a tool. Anything suspicious gets one rewrite. Fail twice and a fixed template answers instead.
Two tools, two kinds of prices. The engine gives the firm quote for your PDF. The catalog gives market ranges, shown plainly as the shop’s list. When the answer says laminating costs Rp 4,500, that number traveled from a catalog row through code into the sentence. You can trace every step of its journey.
New to AI agents? Here is the one idea worth taking home. The model is the least trusted part of this system. The safety lives in plain code around it: a checker that scans for prices, a catalog that admits when it has no match, a rule that says silence beats invention. Most AI demos show you the answers. This one shows you the leash.
The longer version of this story, with the failures included, is on the blog.
Architecture
graph TB
User[User chat + PDF upload] --> API[FastAPI server]
API --> IG[input guard]
IG --> BC[budget check]
BC --> CS[classify scope]
CS --> RT[route]
RT --> KB[retrieve kb from Qdrant]
RT --> PT[call pricing tool]
KB --> AS[assemble draft]
PT --> AS
AS --> OG[output guard]
OG --> OK{prices from tools}
OK -- yes --> User
OK -- no, 1 retry --> AS
IG -- out of scope --> RF[refusal]
RF --> OG
KB -. hybrid dense + TF-IDF .-> QD[(Qdrant catalog)]
PT -. firm quote .-> PE[Pricing engine]
AS -. trace .-> LF[LangFuse]
Demo
Two shorts, recorded against the live demo: asking a shop question, then pricing an uploaded PDF.
Asking a shop question
Pricing an uploaded PDF
Measured
- 362 automated tests (92.78% coverage), CI required on every change
- 47-scenario eval: 100% routing, 100% refusals, zero invented prices
- Retrieval finds the right catalog row 94.7% of the time (hit@5 over 132 queries, hybrid dense + TF-IDF)
- A full conversation turn costs around 1,500 tokens
Closing Thoughts
The catalog is made up. I wrote it as stand-in data and every answer built from it says it is an estimate. The guardrails work, but guardrails are not a shop. Until a real price list replaces my fictional rows, this stays a demo of how to keep an AI honest.