Building an Agentic Analytics Loop with Gemini Function Calling
How Brim It lets a non-technical finance manager chat with company-card data in plain English — a function-calling loop over read-only query tools that never invents a number.
Most "chat with your data" demos cheat. They paste a schema into the prompt, ask the model to "answer using the data," and hope it doesn't hallucinate a number. For a finance tool that triages real company-card spending, hope is not a strategy. A wrong total isn't a bad UX — it's a wrong decision.
For Brim It I wanted a non-technical finance manager to ask things like "how much did we spend on travel in Q2, by region?" and get an answer that is always grounded in an actual query — never the model's imagination.
The shape: a function-calling loop, not a prompt
The core isn't a prompt — it's a loop. Gemini is given a small set of read-only query tools and asked to call them. It can't touch raw SQL or invent values; it can only ask the data questions through a typed interface.
There are five tools, deliberately boring:
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's job is translation and presentation, not arithmetic.
Auto-picking the visualization
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 list, a single stat for one scalar. The user never picks a chart type; the shape of the question implies it.
Multi-turn follow-ups that actually reuse context
The part that makes it feel like a colleague rather than a search box: follow-ups reuse the prior filters.
"now just Texas, monthly"
That carries forward category: "travel" and the Q2 window, swaps groupBy for a monthly time_series, and adds region: "TX". The model isn't re-deriving the whole query — it's editing the last one. Keeping the previous tool call in context (not just the previous answer) is what makes this reliable.
What I'd tell my past self
- Make the tools read-only and typed. The safety property ("never invents a number") is a consequence of the interface, not of prompting harder.
- Return data the UI can render directly. Don't ask the model to format tables in prose; give it a
vizchannel. - Persist tool calls, not just messages. Follow-ups are diffs over the last query.
The whole thing is one loop and five small functions. Most of the "intelligence" is in constraining what the model is allowed to do.
Related project
Brim It