Inspiration: Harbor Kitchen is a 12-person co-packer. They pack sauces and snacks for other brands. Before a batch ships, a QA lead checks three things by eye: the recipe (spec), the supplier CoA, and a photo of the printed label. If milk is in the recipe but not on the lid, the batch is unsafe. People miss that for months. We wanted a stop gate that holds or releases the lot - not a chatbot that says "please look."

What it does: Label Hold is a fail-closed lot-release gate. QA drops three files. A Google ADK pipeline extracts allergens from each, Python computes undeclared allergens as a set difference the model cannot override, and the system writes held or released to Firestore. Incomplete packets (for example a missing CoA) stay held. Gemma summarizes holds only. The dashboard streams live rows over SSE from a lean read model.

How we built it: Agents: Google ADK Python. Root SequentialAgent (release_pipeline): ParallelAgent ingest (spec / CoA / label), matcher (LLM explains, Python decides), LoopAgent (investigator + critic) for incomplete packets, Gemma on holds, poster writes Firestore. GCP: Two Cloud Run agent/UI services plus a lean-view consumer. Firestore lots-db is the system of record. Eventarc CDC is the only Pub/Sub publisher. lean-db / lots-listen is the read model. Dashboard BFF streams SSE. Frontend: React SPA on Cloud Run with demo presets (multi-allergen hold, full release, incomplete packet). IaC: Terraform for Eventarc, Pub/Sub, identity, and services in asia-southeast1. Challenges we ran into Keeping command and query separate: the poster must write Firestore only; the UI must never publish the bus. Wiring Eventarc CDC so failed publishes retry without double-shipping. Making multimodal ingest reliable (text + PNG/JPEG label photos) while keeping allergen math deterministic after extraction. Fail-closed incomplete packets without inventing a release. Quota: Gemma only on holds so clean releases stay cheap.

Accomplishments that we're proud of: A live Cloud Run demo end to end: hold mutates a row, incomplete packets stay held, Firestore shows the same lot_id / write_id in lots and lots-listen. Overlay contract in EVALS.md: Python decides after extraction - no fake F1. Public repo with smoke scripts and a real Sequential / Parallel / Loop graph, not a stub page.

What we learned: The product is the hold as state, not the extract. Deterministic overlay after LLM extraction beats asking the model for a final verdict. CDC-only publish keeps the browser from lying about the system of record. Small plants need fail-closed gates more than they need chat.

What's next for Harbour Kitchen - Label Hold: More plant fixtures and synonym stress cases. Tighter critic loop telemetry when packets are incomplete. Optional plant SSO and audit export for QA leads. Broader allergen ontology coverage without letting the model override the set difference.

Built With

Share this project:

Updates

Submission history