Skip to content

Case study · Reliability & Observability

A text-to-SQL agent 2,000 people could actually trust with HR data

A natural-language analytics agent over sensitive workforce data, serving 2,000 concurrent users across isolated tenants. Reporting went from 2–3 days to under 30 seconds — but only after we stopped trusting the model with the boundary.

Reporting turnaround
2–3 days → 30s
My role
Technical Lead — architecture, AI engineering, delivery
Stack
PythonFastAPIAWS BedrockLangChain AgentsPostgreSQLVector embeddings / RAGscikit-learnDocker

What broke

The agent generated a syntactically perfect query that would have read another tenant's data. The SQL was valid, the intent was innocent, and the isolation existed only in the prompt.

HR teams sat on a workforce database they could not ask questions of. Every “what was attrition in the Bangalore engineering team last quarter, split by tenure?” became a ticket, and the ticket took two to three days because a human had to write the query, sanity-check it, and format the result.

The obvious answer was a natural-language interface over the warehouse. The obvious answer is also the one that quietly leaks data across tenants, so most of the engineering went into the parts that are not the language model.

What it had to do

Multiple client organisations shared the platform, each seeing strictly their own data. Users asked questions in plain English and got numbers, tables, and charts. Beyond descriptive reporting it had to support diagnostic questions — why did attrition rise — and predictive ones: attrition risk, time-to-fill, time-to-start.

Architecture

Question to answer, with the boundary outside the model

The sequence matters more than the components. Authorisation is resolved before the model is involved and re-checked after it responds, because a language model is not an access-control mechanism.

Select a stage to see the decision made there.

01 Question intakeFastAPI

The request arrives with an authenticated session. Tenant identity is taken from that session and never from anything the user or the model says later. This sounds obvious and is the single most important line in the system.

Tradeoff: It means the agent cannot support legitimate cross-tenant questions for platform administrators. We accepted that: a separate, explicitly authorised path is safer than a model deciding when crossing the boundary is acceptable.

What broke

During testing, a user in one tenant asked a question phrased around a department name that also existed in another tenant. The model generated valid SQL that resolved that department by name, without a tenant predicate — because the tenant predicate lived in the system prompt as an instruction, and instructions are advisory.

Nothing leaked; it was caught in a staging environment against synthetic data. But the failure was not “the model hallucinated.” The model did something reasonable given ambiguous input. The defect was architectural: we had put a security boundary inside a probabilistic system.

That is when the query gate got built. Tenant isolation moved out of the prompt and into an AST rewrite that we control, backed by a database role that physically cannot read across tenants. The prompt still mentions the convention — it produces better first drafts — but nothing depends on the model honouring it.

What it cost and what it took

  • 2,000 concurrent chatbot users with per-tenant isolation maintained under load.
  • Reporting turnaround from 2–3 days to under 30 seconds for the large majority of requests.
  • ~75% forecasting accuracy on attrition and time-to-fill, reported with the uncertainty attached.
  • The query gate was roughly two weeks of work and is the reason the project shipped at all.

What I would do differently

I would build the eval suite before the second feature rather than after the fifth. We had good instincts about failure modes and no systematic way to know whether a prompt change made retrieval better or worse. Every improvement was argued rather than measured, which is slower and occasionally wrong.

I would also have measured schema retrieval separately from end-to-end answer quality from day one. When answers were wrong, we spent time tuning generation prompts when the actual defect was upstream in what got retrieved — a mistake I now look for first.