Case Study — First Deployment

Depot RTB Assistant

How we built a production RAG assistant for veterinary clinic warehouse management in 12 weeks. Every metric measured. Every bug logged.

The problem

Depot RTB serves veterinary clinics in Thailand with warehouse management (products, orders, cold chain, SOPs). Clinic staff needed answers fast:

  • "What's the loading dose for Metacam on a 15kg dog?"
  • "What temperature range for zone B cold storage?"
  • "What documents are needed to order controlled medications?"

Traditional lookup: 3-8 minutes per query across multiple SOP PDFs, product spec sheets, and business process docs. Multiply by 30 queries/day per clinic. Multiply by 20 clinics.

The waste added up. And nobody wanted to memorize 200 pages of SOPs.

What we tried first (and rejected)

Before shipping, we tested three alternatives:

  • ChatGPT plugin. Fast to demo. Broke in production — no citations, hallucinated Thai product names, no cost tracking.
  • Fine-tuned model. $2000+ in training costs, brittle to corpus updates, would break every quarterly SOP change.
  • Simple search UI. Returned documents but not answers. Staff still had to read + interpret.

None of these solved the actual problem: synthesized cited answers, provably correct, with cost transparency.

The approach

Retrieval-augmented generation, but built for production from day one:

  • Retrieval: Voyage AI embeddings (voyage-3-large) into Postgres/pgvector. Semantic + Thai-language aware.
  • Generation: Anthropic Claude Sonnet 4.6 primary, Haiku 4.5 for simple refusals.
  • Guardrails: PII redaction (Thai national ID, phone, email, credit card). Prompt injection detection (2-layer: regex + LLM classifier).
  • Cost optimization: Prompt caching (85% savings on repeated queries). Query complexity classifier routes to cheaper model when possible.
  • Infrastructure: Docker Compose (nginx + php-fpm + Postgres). Streaming SSE responses. Full monitoring with per-query cost tracking.

Everything wired into the existing Laravel app — no separate service, no exotic dependencies.

Results (measured, not estimated)

MetricValueHow measured
Retrieval accuracy 100% hit@1 15-question golden dataset built with domain expert
Cost per query (blended) $0.038 Production data, 5-query sample
Cache hit rate 41.4% Bursty workload pattern (typical clinic session)
Cost savings vs baseline 29% Cache + smart routing combined effect
Latency p50 20s Full markdown answer, complex query
First token latency ~2s Streaming makes response feel immediate
Fresh deployment time <10 min Verified with fresh Docker rebuild + README walkthrough

What broke (and how we caught it)

Transparency matters more than perfection. Bugs we caught during development, before shipping:

  • LLM judge bug. Our quality judge received only document titles, not full content. Result: 50% false-negative pass rate. Fix: pass full document text to judge context. Real quality was ~90%, not 50%.
  • SDK positional args. Anthropic SDK expected named arguments; we passed positional via spread operator. Trace showed createStream(1024, Array, 'sonnet', NULL, ...) — args scrambled. Fix: explicit named arguments in both complete() and streamComplete().
  • Cache API camelCase. Documentation showed cache_control; SDK expected cacheControl. Fix: found via error message + retry.
  • Context assembly regression. A refactor left placeholder variables never populated. Sources labeled [doc_1] but content was from doc_5. LLM refused correct answers. Fix: proper variable assignment in loop + end-to-end test after every refactor.

Every bug: caught, documented in notes, fixed transparently. This discipline is the actual product differentiator — not "AI expertise", but measurement rigor.

Installing it the way you would

Building something that works on my machine and shipping something that works on yours are different problems. In September I stopped adding features and spent three weeks on the second one.

The method was simple: clone the repository from GitHub into an empty directory, follow my own README line by line, and treat anything it failed to mention as a defect. No shortcuts from knowing how the system works.

Five rounds, fifteen blockers

RoundBlockers found
17
23
33
42
50

Round five is the one that mattered. Everything before it was finding out how much I had assumed.

What kept breaking

Things my machine had that yours would not. A stray config file supplying a database password. A dependency directory sitting there since week three. A Docker volume named after an experiment from week five. Each one made something work for me that would fail for everyone else. The install had never actually been tested — it had been tested on a machine where it could not fail.

Labels from my own experiments, baked into the product. The retrieval layer queried a corpus source named week6_fixed — a label from my chunking benchmarks. Index your documents under any other name and retrieval returns nothing.

That one was the worst. The install succeeds. Health checks pass. The corpus loads, 103 chunks, with a cost receipt. A token gets issued. Everything reports green — and then every question comes back with no answer. A client would have had no way to tell what was wrong.

Bugs introduced by fixing other bugs. I wrote a setup script that bypassed the port-conflict workaround I had documented three days earlier. Key generation wrote to a file this deployment does not use. A container restart left the web server pointing at a dead address.

That last category is the useful lesson. Writing a fix and verifying a fix are separate activities, and skipping the second one produced three new blockers in a single afternoon.

What else got tested properly

Backups existed before this. They had never been restored. So I deleted an API token, deleted an entire indexed corpus, and dropped a table — then restored and checked that retrieval was back at 100% accuracy, not merely that the row counts matched.

A backup nobody has restored is a file, not a backup.

The same applied to rollback. Reverting a database migration brought the column back but left it empty across all 103 rows. So the documented way to undo an upgrade is restoring the pre-upgrade backup, which the upgrade command now takes automatically — not running the rollback command, which quietly discards data.

Why this is in a sales page

Because it is the part you cannot verify before buying. Anyone can demo a working system on their own laptop. What you are actually buying is that it installs on yours, that the backups restore, and that the upgrade path does not lose your data.

Fifteen blockers had to be found by someone. It was going to be me or the first client.

What Depot Assistant ships with

Depot RTB deployment produced the product. Everything below ships to every future deployment:

  • Complete Docker Compose stack (nginx + php-fpm + Postgres)
  • 3 safety layers (PII, injection, scope discipline)
  • Prompt caching + smart model routing
  • Analytics DB + cost dashboard endpoint + CLI stats
  • Health check + readiness endpoints
  • README + operations runbook + failure playbook + client onboarding checklist
  • 30 days of post-launch support

The v0.3 code that runs Depot RTB is the same code that ships to your Laravel app.

What's next

Package A pricing: $2,500 flat install fee. You own the code + infrastructure. We install + configure it in your existing Laravel + Postgres stack.

Delivery timeline: 2-3 weeks from kickoff to production.

Book a discovery call →