Building an Agentic Analytics Loop with Gemini Function Calling
How Brim It lets a finance manager chat with company-card data in plain English without the model ever inventing a number.
Most chat-with-your-data demos are a schema pasted into a prompt and a prayer. That's fine until the numbers are real. Brim It triages actual company-card spend, and a wrong total there isn't bad UX. It's a wrong decision someone makes with real money.
So I built around one rule: the model never does arithmetic. A finance manager asks "how much did we spend on travel in Q2, by region?" and the answer always comes from a query. Never from the model's imagination.
a loop, not a prompt
The core isn't a prompt, it's a loop. Gemini gets a small set of read-only query tools and has to call them. It can't touch raw SQL and it can't make up values. It can only ask the data questions through a typed interface.
Five tools, all boring on purpose:
aggregate— sums/averages over a filtered settime_series— values bucketed by day/week/monthtop_merchants— ranked breakdownlist— raw rows for a filtercompare— two filtered sets side by side
user: "how much on travel in Q2, by region?"
model -> aggregate({ category: "travel", from: "2026-04-01", to: "2026-06-30", groupBy: "region" })
tool -> [{ region: "TX", total: 18230.55 }, { region: "CA", total: 11890.12 }, ...]
model -> renders a bar chart + a one-line summary citing those exact totals
Every number on screen traces back to a tool result. The model translates and presents. That's it.
the chart picks itself
The model also returns a viz hint and the client maps it to a component. Bar for grouped aggregates, line for time series, pie for share-of-total, table for raw rows, a single stat for one scalar. Nobody picks a chart type. The shape of the question already implies it.
follow-ups that actually remember
The part that makes it feel like a colleague instead of a search box: follow-ups edit the previous query instead of starting over.
"now just Texas, monthly"
That keeps the travel category and the Q2 window, swaps the groupBy for a monthly time series, and adds region: "TX". The model isn't re-deriving the whole query, it's diffing the last one. The trick is keeping the previous tool call in context, not just the previous answer.
if I built it again
- Read-only, typed tools. "Never invents a number" is a property of the interface, not of prompting harder.
- Give the model a
vizchannel instead of asking it to format tables in prose. - Persist tool calls, not just messages. Follow-ups are diffs.
The whole thing is one loop and five small functions. Most of the intelligence is in deciding what the model isn't allowed to do.
Related project
Brim It