Sitemap

Teaching an agent what a custard bucket weighs

7 min readAug 30, 2026

--

I created this piece of content for the purposes of entering the Google Cloud All Things Agentic Hackathon.

Press enter or click to view image in full size

Ask any nutrition app how much rice you bought and it wants a number in kilograms. Ask a Nigerian household the same question and the answer is “one custard bucket.”

That gap is bigger than it looks. It isn’t a units conversion problem — it’s a missing dataset problem. There is no authoritative table anywhere that tells you what a custard bucket of rice weighs. There are market conventions, wholesale bag arithmetic, a scattering of retail listings, and a lot of disagreement.

I spent five days building an agent that goes and figures it out.

This is what I learned.

The actual problem

Nigerian food inflation has made “what do I feed this house this week for ₦20,000” a question people ask themselves out loud, every week. It’s not an optimization anyone enjoys.

Every tool that could help assumes clean structured data. What actually exists looks like this — a real WhatsApp message from a vendor:

Food don cost for market now o oga. 1 custard bucket for rice na around 5k now, beans one painter dae around 5k for iron beans and 8k for brown beans. Yam now dae for around 2k for normal size.

Count the problems. It’s code-switched between Pidgin and English. Prices are hedged — “around”, “na around”. One clause prices two different products (iron beans and brown beans) at a 60% spread. Quantities are in containers nobody outside the country has heard of. And “yam for around 2k for normal size” doesn’t say whether that’s per tuber or for several.

A regex can’t fix any of that. Neither can a schema validator. Resolving it requires judgment — and increasingly, going and finding out.

That’s what makes it an agent problem rather than a script problem.

Extraction that refuses to guess

The first thing I got wrong was confidence.

My extraction prompt asked for a confidence score on every record. Early runs came back with hedged prices — “around 5k” — scored at 0.9. The model was answering a different question than I’d asked. It was scoring “did I parse this correctly”, and it had parsed it perfectly.

But confidence should answer “how likely is this the real price.” Those are not the same number. A perfectly-parsed guess is still a guess.

So I rewrote the calibration to derive from the source rather than the parse:

  • Any hedge word caps confidence at 0.7
  • An unstated unit caps at 0.6
  • Total-versus-unit ambiguity caps at 0.6
  • Multiple flags compound

After that, the same message produced rice at 0.7, beans at 0.65, and yam — hedged and missing a unit — at 0.55. Now the downstream planner can tell which of its own numbers to trust.

The other half of this is flagging. When the agent meets “scumba fish — 1500” on a market banner, no unit is stated. It could be per piece, per kilo, per bundle. A human standing at that stall knows. The agent doesn’t, so it emits:

{
"item": "fish",
"item_raw": "scumba fish",
"grade": "scumba",
"price_ngn": 1500,
"unit": null,
"confidence": 0.55,
"flags": ["unit_not_stated", "total_vs_unit_ambiguous"]
}

A flagged record is useful. A confidently wrong one is worse than nothing. That principle ended up shaping every design decision in the project.

Two other things worth noting, because they’re the difference between a generic extraction demo and something that actually works on this data. “Orobo chicken” is not a species — orobo is Pidgin for a fat one, so it’s a size grade on regular chicken. “Red oil” is palm oil. Local knowledge isn’t decoration here; it’s correctness.

The part that’s actually agentic

Extraction is a prompt. What makes this an agent is what happens when it hits a container it doesn’t recognise.

The flow: it saves the price records, then checks Firestore for a weight mapping for each container it saw. If nothing’s there, it delegates to a research sub-agent that searches the web, then reconciles what comes back.

For a custard bucket of garri, it found seven distinct figures, ranging from 2.2kg to 3.33kg. Not one clean answer — seven, disagreeing. So it wrote this:

{
"container": "custard bucket",
"commodity": "garri",
"weight_kg": 2.5,
"confidence": 0.55,
"conflicting_values": [ 2.2, 2.38, 2.5, 2.6, 2.7, 3, 3.33],
"resolution_note": "Garri is less dense than grains like rice. Wholesale
traders widely calculate 20 custard rubbers to a standard 50kg bag,
giving 2.5kg. Confidence is moderate due to wide variation in moisture
content and bucket sizes."
}

Rice in the same container resolved to 4.0kg at confidence 0.7 — tighter agreement across sources, and a denser commodity. Same bucket, different weight. The mapping key has to be container plus commodity, not container alone. That’s obvious once you’ve thought about it for ten seconds and completely invisible if you haven’t.

Get Henry Okechi’s stories in your inbox

Join Medium for free to get updates from this writer.

Two things about that record matter more than the number. The conflicting_values array preserves the disagreement instead of hiding it. And the confidence is derived from the spread — a 2.2-to-3.33kg range is over 60% variance, which is not a 0.9 situation no matter how good the reasoning is.

Then it caches. Send the same message again and all three containers resolve instantly from Firestore with zero research calls. It learns each container once, permanently.

The bug I nearly shipped

Early runs looked perfect. The agent reported researched container weights, cited plausible ranges, explained its reasoning. I almost moved on.

Then I actually read the event trace. save_prices, lookup_container, save_container. No search calls. None. It had never searched anything — it was answering from model knowledge and presenting it in the shape of research, complete with confident-sounding "conflict ranges" it had invented.

The cause turned out to be a real ADK constraint: built-in tools like google_search don't coexist with custom function tools on the same agent. Mine was being silently ignored.

I had two options. Reframe the feature honestly as “resolved from model knowledge, flagged for verification” — which is defensible and takes ten minutes. Or fix it properly.

I fixed it properly, and the fix turned out to be better architecture anyway. The search lives in a dedicated sub-agent, wrapped as an AgentTool:

research_agent = Agent(
name="research_container",
model="gemini-3.5-flash",
instruction=(
"You research Nigerian market container capacities. Use google_search "
"to find how many kilograms of the given commodity a container holds. "
"Report every distinct figure you find and where it came from. "
"Do NOT average or pick a winner — report the raw disagreement. "
"If you find nothing, say so plainly. Never guess."
),
tools=[google_search],
)
root_agent = Agent(
name="price_agent",
model="gemini-3.5-flash",
instruction=WORKFLOW,
tools=[save_prices, lookup_container, save_container,
AgentTool(agent=research_agent)],
)

Now the root agent can’t search — it can only ask. Search is isolated from writes, the sub-agent reports raw disagreement rather than a pre-digested answer, and reconciliation happens in one place where I can see it.

The lesson isn’t about ADK. It’s that a plausible-sounding output is not evidence that the thing you built is running. Read the traces.

Working while nobody watches

None of the above is the point. The point is that the household shouldn’t have to do anything.

Cloud Scheduler fires a separate planner service every Sunday at 6am Lagos time. It reads every price record and every container mapping out of Firestore and produces the week: seven days of meals, a costed shopping list, and a running total against budget.

A test run came back at ₦19,200 against ₦20,000 — jollof rice, eba with crayfish vegetable soup, beans with palm oil — and included this in its notes:

Prices for vegetable (confidence 0.55) and normal pepper (0.60) are slightly uncertain due to unit variations and market volatility, but they fit within our safety buffer. Iron beans were selected over brown beans to save 3,000 NGN.

It’s explaining its trade-off and telling me which of its own inputs are shaky. That’s the behaviour the confidence work was for.

The stack

  • Gemini 3.5 Flash on Vertex AI — note that Gemini 3.x models serve from the global endpoint, not regional ones. Setting GOOGLE_CLOUD_LOCATION=us-central1 will 404 and the error won't tell you why.
  • Agent Development Kit — root agent, three Firestore tools, one research sub-agent
  • Cloud Run — two services, both scaling to zero
  • Firestore — prices, containers, plans
  • Cloud Scheduler — the weekly trigger

What I’d do next

Voice input, in Pidgin. A market trader is never going to type prices into a form — but they’ll send a voice note without thinking about it. Gemini handles audio natively, so the same extraction prompt should mostly just work. That’s the interface this problem actually wants.

After that: reconciling prices across multiple vendors and markets, and a feedback loop where the household photographs what they actually cooked so the plan adapts to what really gets eaten.

The thing I keep coming back to

The most useful thing this agent does is admit what it doesn’t know.

Every decision that made it better was about surfacing uncertainty instead of smoothing it over — the flags, the confidence rubric, the preserved contradictions, the sub-agent that reports raw disagreement rather than a tidy average.

It would have been easy to build something that always returns a clean number. It would also have been useless, because nobody planning a week’s food on a fixed budget can afford to act on a figure that’s confidently wrong.

ChopChop is open source: github.com/HenryOkechi/chopchop

Demo video: youtu.be/chliYjrooRI

#AllThingsAgenticHackathon

--

--

Henry Okechi
Henry Okechi

Written by Henry Okechi

Medical Doctor writing evidence-based health and aesthetics content, with a background in AI and Decentralized Finance