Case Study

ARIA

Analytical Reporting and Insights Agent — a FinOps AI assistant I built so anyone can ask “what did we spend on GCP last quarter?” in plain English and get an answer traced to live data. It holds no data of its own; it orchestrates two upstream platforms through 30 tools, and it is engineered so that it cannot invent a number.

30 Agent Tools
2 Source Platforms
3 Anti-Fabrication Layers
0 API Keys Stored

The Problem

I had built the dashboards. People still asked me the questions.

Between the FinOps Reporting App and GCP Spotlight, every number a stakeholder could want already existed. But finding it meant knowing which of the two apps to open, which tab to click, and how to filter it. So the questions kept landing in my inbox instead: “what’s HOME tracking at this fiscal year?”, “which subscriptions are over budget?”, “how much are we spending on AI?”

A chatbot was the obvious interface. It was also the obvious risk. A language model that confidently produces a plausible dollar figure is worse than no tool at all — one fabricated number in an executive deck destroys trust in every number that follows it, including the correct ones. The engineering problem was never “can it answer questions?” It was “can it be structurally incapable of making one up?”

Scale

Metric Value
Agent Tools30 (12 FinOps + 18 Spotlight)
Upstream Data Sources2 applications, 0 direct database reads
Max Tool Rounds Per Question10
Extended Thinking Budget10,000 tokens
System Prompt~280 lines, cached between requests
Rate Limit20 questions per 60 seconds, per user
Conversation Retention180 days (audit log retained separately)
InterfacesReact web chat, embeddable widget, Python CLI

Six Systems

An agent loop, the tool layer beneath it, and the guardrails, governance, and interfaces that make it safe to put in front of executives.

The Agent Loop
Claude Sonnet via Vertex AI, streaming
  • Streams the model response, inspects the stop reason, and dispatches any requested tools
  • All tools in a round execute concurrently — they are read-only and independent
  • Loops up to 10 rounds so multi-step questions can chain lookups
  • Extended thinking (10K token budget) forces reasoning over tool data before answering
  • Tracks tools called, tools failed, and per-tool errors for every turn
30-Tool Catalog
12 financial · 18 infrastructure
  • Financial: spend, budget variance, scorecards, vendors, COGS, over-budget, AI and DR spend
  • Infrastructure: GKE workloads and clusters, GCE instances and recommendations, GCS buckets, AI model spend
  • Auto-pagination up to 20 pages of 500 records, invisible to the model
  • Business unit names resolved to IDs through a cached lookup, so users say “HOME” not “14”
  • Every tool failure returns a structured error carrying an explicit instruction not to guess
Fabrication Guard
Three independent layers
  • Prompt layer: eight data-integrity rules that override every other instruction
  • Code layer: responses containing dollar figures are rejected if no tool succeeded
  • Tool layer: errors return “tell the user this source is unavailable, do not fabricate”
  • A blocked response is replaced with the actual tool errors, not a vague apology
  • Pre-computed server-side totals mean the model reads sums rather than doing arithmetic
Tiered Responses
Depth set by the question, not the data
  • Tier 1 (default): one sentence and one drill-down link for a single metric
  • Tier 2: summary plus a table, for comparisons across three or more items
  • Tier 3: monthly breakdown and commentary, only when explicitly requested
  • Ask about one business unit and get one total, even if the tool returns 40 rows
  • Every answer links back into the source app at the exact filtered view
Audit & Feedback
Every question logged and exportable
  • Full audit log: user identity, question, response, tools invoked, tokens, response time
  • Usage dashboard with queries per day, unique users, and top users
  • Thumbs up/down feedback with a satisfaction rate and a recent-negatives queue
  • Query normalization turns raw questions into reusable templates with slot dropdowns
  • Popular questions surface back into the sidebar as one-click suggestions
Three Interfaces
Web, embedded widget, and CLI
  • React chat UI with live tool-call pills, so users watch it fetch rather than wait blindly
  • Embedded chat inside both source apps — ask without leaving the dashboard
  • Python CLI with interactive and one-shot modes for scripted reporting
  • Export any answer to CSV, XLSX, or a print-ready branded document
  • Variance tables auto-render with directional arrows and magnitude bars

Architecture

A thin agent over two existing platforms. ARIA owns no cost data — it owns the reasoning, the guardrails, and the audit trail.

 React Chat UI
Full page + embedded widget
 Python CLI
Interactive + one-shot
↓ Server-sent events (streaming), 25s heartbeat
 FastAPI Backend
Python 3.11 · chat, audit, feedback, suggestions, export routes · SQLite (WAL) for history
 Agent Loop — Claude Sonnet on Vertex AI
Extended thinking · cached system prompt · Workload Identity auth · up to 10 tool rounds
↓ 30 tools, executed concurrently per round
 FinOps Reporting App
12 tools · spend, budgets, scorecards, vendors
 GCP Spotlight
18 tools · GKE, GCE, GCS, AI efficiency
Ternary API  ·  GCP APIs  ·  BigQuery Billing Export

Why no direct database access? The upstream apps already handle collection, alias mapping, COGS classification, and cost calculation. Querying their databases directly would have duplicated that logic in a third place and guaranteed the two would eventually disagree. Going through their APIs means ARIA can never contradict the dashboards it links to.

Tech Stack

LayerTechnology
Agent APIPython 3.11, FastAPI, async httpx with connection pooling and retries
ModelClaude Sonnet via Vertex AI, extended thinking, ephemeral prompt caching
FrontendReact 18, TypeScript, Vite, Tailwind CSS, react-markdown
CLIPython, Click, Rich (markdown rendering in the terminal)
DatabaseSQLite with WAL mode — conversations, messages, audit log, feedback, query patterns
AuthGCP Workload Identity for Vertex AI; Okta SSO at the platform edge
DeploymentMulti-stage Docker (Node build → Python runtime), Kubernetes, ArgoCD

Key Technical Decisions

The “why” behind each major design choice — not just what was built, but the reasoning and trade-offs considered.

Prompt instructions alone are not a control. If the FinOps API is down and the model still produces “$1.2M”, no amount of “please don’t hallucinate” in the system prompt saved you. So the guard exists at three independent levels:

  • Prompt — a Data Integrity section of eight absolute rules that override every other instruction: every number traces to a tool result, empty results are stated as empty, uncertainty is disclosed, no confidence bluffing.
  • Code — after the final response, the agent scans for currency patterns. If the answer contains dollar figures but every tool errored (or no tool ran at all), the response is discarded and replaced with the specific tool errors.
  • Tool — each failure returns a structured payload whose instruction field reads: “STOP. Tell the user this data source is unavailable. Do NOT fabricate numbers.” The model sees the guardrail inside its own context.

The detector deliberately exempts greetings and off-topic refusals so a canned response mentioning a dollar amount doesn’t trip the wire. The trade-off: a legitimate answer could in principle be blocked. I’d take that trade every time — a false “I can’t reach the data” costs a retry, a false number costs the project.

Early on, spend questions returned raw records and the model summed them. It was right most of the time — which is the worst possible failure mode, because “mostly right” is indistinguishable from “right” until someone checks.

Every financial tool now returns a summary object computed server-side: actual total, budget total, forecast, delta, COGS and non-COGS splits. The system prompt instructs the model to read summary.actual_total and never to add up rows itself. Aggregation happens in Python, where it is deterministic and testable; the model’s job is narrowed to selecting the right tool and explaining the result.

The same principle drives the grading and variance tools — scorecard weights, budget-adherence bands, and coefficient-of-variation stability scores are all calculated in code, not reasoned about in prose.

The first version answered everything with a wall of analysis. Ask what one subscription spent and you got a monthly breakdown, a trend commentary, and three caveats. Executives stopped reading at line two.

TierTriggered ByResponse Shape
Tier 1 (default)A single-metric lookupOne sentence + one drill-down link
Tier 2“compare”, “which”, 3+ itemsSummary sentence + table (5 columns, 10 rows max)
Tier 3“monthly”, “trend”, “breakdown”Full breakdown + analyst commentary

The critical rule: the tier is set by the user’s words, not by how much data came back. A tool returning 40 rows for a one-entity question means the agent aggregates to a single total and stays at Tier 1. Volume of available data is not a reason to spend the reader’s attention.

ARIA calls Claude through Vertex AI rather than the direct API. That single choice removed a whole category of operational risk:

  • No API key exists — the pod authenticates as a GCP service account through Workload Identity. There is no secret to rotate, leak, or accidentally commit.
  • Least privilege — a custom IAM role granting only aiplatform.endpoints.predict. The agent can call the model and do nothing else in the project.
  • Data residency and governance — inference runs inside the organization’s own GCP project and region, which is what made this approvable for corporate financial data.
  • Local development — the same code path works with application default credentials, so there is no “dev uses keys, prod uses identity” divergence.

Trade-off accepted: Vertex model identifiers lag the direct API slightly, and some newer features arrive later. For a corporate deployment handling financial data, credential elimination was worth more than being first to a model release.

Two model features doing opposite things to the bill, deliberately paired:

  • Extended thinking (10,000 token budget) makes the model reason through tool results before writing. It measurably reduced the “grabbed the wrong field from the JSON” class of error. Enabling it forces temperature to 1 and requires the output ceiling to cover both thinking and the answer — a detail that silently truncates responses if you miss it. The budget is configurable, so thinking can be switched off entirely without a code change.
  • Ephemeral prompt caching on the ~280-line system prompt. It is identical on every request, so paying full input price for it each time is pure waste. Cache reads are tracked per request in the audit log alongside token counts, so the savings are measurable rather than assumed.

Net effect: better reasoning on the part that varies, near-free on the part that doesn’t.

  • Concurrent execution — when the model requests four tools in one round, all four run at once. This is only safe because every tool is read-only and independent; there is no ordering constraint to violate.
  • Bounded rounds — a hard ceiling of 10 tool rounds. A confused agent burns budget, not the afternoon.
  • Server-sent events — tool calls stream to the UI as they happen, so a 20-second answer shows “Fetching: get_budget_variance” instead of a spinner. Perceived latency collapses when users can see the work.
  • 25-second heartbeat — the service mesh proxy closes idle connections. A background task emits a keepalive event so long multi-round answers survive the trip. This was found in production, not in design.
  • Connection pooling with retries — upstream calls use a shared async client with keepalive connections, two retries on timeout or connection error, and no retry on HTTP errors (a 404 will still be a 404).

ARIA runs as a single pod behind an SSO proxy on an internal network. That context justified two deliberately unfashionable choices:

  • SQLite over PostgreSQL — WAL mode gives concurrent reads during writes and a 30-second busy timeout absorbs contention. The honest cost is that it pins the app to one replica; if this ever needs horizontal scaling, the database is the migration. For the actual usage pattern, running a database server would have been infrastructure for its own sake.
  • No login — the platform already enforces Okta SSO at the edge, so a second auth layer would add friction without adding security. Conversation IDs live in browser local storage, so each person sees their own history; user identity is read from proxy headers for the audit log only.

Both are reversible decisions documented as such — which is the point. Choosing the smaller thing is only defensible when you know exactly what would force you to change it.

ARIA answers cloud cost questions. Ask it to write code, explain the weather, or opine on security posture and it declines. That constraint is load-bearing: a tool that will attempt anything is a tool nobody can trust on the thing it’s actually for.

  • Clearly off-topic questions get a rotating refusal drawn from a pool of eleven — movie quotes and Victorian-era demurrals, cycled per conversation so the same user never sees a repeat until they’ve seen them all.
  • Adjacent questions — a real FinOps topic ARIA has no data for — get a redirect explaining what it can answer, not a joke.
  • The refusal path is handled server-side: the model emits a marker, the API swaps in the canned text and increments the rotation counter.

The humor was not decoration. A blunt “I cannot answer that” reads as broken; a wink reads as designed, and it made people comfortable testing the edges — which is exactly the behavior you want during rollout.

Features at a Glance

 Conversation & Analysis
  • Natural-language questions across spend, budgets, vendors, and infrastructure efficiency
  • Live tool-call pills showing exactly which data source is being queried
  • Three follow-up suggestions after every answer, each one level deeper
  • Variance tables rendered with color coding, directional arrows, and magnitude bars
  • Fiscal-year aware by default — July through June, with no date needed in the question
  • Deep links back into the source app at the exact filtered view
 Governance & Trust
  • Full audit log with user, question, response, tools invoked, tokens, and latency
  • Usage dashboard: queries per day, unique users, average response time, top users
  • CSV and JSON audit export for compliance review
  • Thumbs up/down feedback with satisfaction rate and a recent-negatives review queue
  • Deep health check that verifies both upstream apps and model access before reporting ready
  • Per-user rate limiting, keyed to SSO identity rather than IP address
 UX & Operations
  • Sidebar with quick commands, popular questions, worked examples, and an in-app FAQ
  • Popular questions auto-generated from real usage, with dropdowns for the variable parts
  • Export any answer to CSV, XLSX, or a print-ready branded document
  • Dark mode with system preference detection
  • Conversation history with delete, plus automatic 180-day cleanup
  • Error boundary and graceful degradation when a single data source is unreachable

Security Posture

No Model API Keys
Vertex AI access through Workload Identity. No key is created, stored, or rotated.
Least-Privilege IAM
A custom role granting model prediction only — no broader AI platform permissions.
Read-Only by Construction
Every one of the 30 tools is a GET. ARIA cannot change a budget, a resource, or a record.
Rate Limiting & Input Bounds
20 questions per minute per identity; messages capped at 2,000 characters; every query parameter validated.
Internal Traffic Only
Upstream calls use cluster-internal service addresses. Public access sits behind SSO on the corporate network.
Attributable Audit Trail
Every question is recorded against the SSO identity that asked it, with the tools used to answer.

What I Learned

What Worked Well
  • Layering the fabrication guard across prompt, code, and tool responses — no single layer is trusted to hold on its own
  • Moving all arithmetic server-side into pre-computed summaries removed an entire class of “subtly wrong” answers
  • Building on the two existing apps’ APIs instead of their databases — ARIA can never disagree with the dashboards it links to
  • Streaming tool calls to the UI turned a slow answer into a visibly working one; perceived latency dropped without a millisecond of real optimization
  • Tiered response depth made it usable for executives, who want one sentence, not a report
  • Vertex AI plus Workload Identity made the security review straightforward — there was no credential to discuss
  • Rotating refusals with actual personality made people comfortable probing the boundaries during rollout
Challenges
  • Prompt engineering has no compiler — a rule added to fix one behavior can quietly change three others, and only usage reveals it
  • Service mesh proxies silently killing idle streaming connections; the heartbeat fix came from production traffic, not from testing
  • Two tools legitimately named “AI spend” — one infrastructure-level, one business-unit-attributed — required explicit disambiguation rules to stop the agent picking the wrong one
  • Getting the thinking budget and output ceiling relationship wrong truncates answers with no obvious error
  • Balancing refusal against helpfulness: too strict and it declines real FinOps questions, too loose and scope creeps back in
  • Tool schemas are a UX surface — vague parameter descriptions produce confidently wrong tool calls
Would Do Differently
  • Build an evaluation suite of question/expected-answer pairs from day one, so prompt changes can be regression-tested instead of eyeballed
  • Version the system prompt alongside the audit log, so a past answer can be explained by the rules in force when it was given
  • Capture the full tool inputs and outputs per message from the start — the schema has the columns, but early answers can’t be fully replayed
  • Design the negative-feedback loop to link directly to the audit entry, turning thumbs-down into a one-click investigation
  • Plan for multi-replica deployment earlier — SQLite is right for today and is the one thing that would need to change first

The lesson that generalizes beyond this project: with financial data, an AI assistant’s credibility is set by its worst answer, not its average one. Most of the engineering here went into the paths where something has gone wrong — a source is down, a tool returns nothing, a question falls outside scope. Getting those right is what let people rely on the answers when everything worked, and it is why the interesting work was never the conversation. It was the constraints around it.

ARIA is the conversational layer over two platforms I built — both are case studies of their own:

 FinOps Reporting Platform  GCP Spotlight
 Back to Projects