letsautom8
← Lab notes

A domain AI agent for a community organisation: making it trustworthy, not just clever

A general chatbot knows the world and nothing about your organisation. It can't give a manager last month's figures for their area, find the policy that covers a situation, or explain how to fix a wrong entry in their app. And once you give it the database, the hard question arrives: how do you stop it showing someone data they aren't allowed to see?

As a volunteer, I've spent the past year building an AI agent for a community organisation that answers exactly those questions. Its managers use it every day, in English or Tagalog, from a staff portal or a chat widget inside the organisation's own apps.

I came to AI from 25 years of test automation, and that mattered more than any model choice. Most of the work wasn't making the agent clever. It was making it trustworthy.

One front door, specialist rooms behind it

Questions arrive from a staff portal with single sign-on or a widget in each app, are normalised, then classified into one of six specialists: data (generated SQL), documents, research, how-to, app data (the app's API as the person) and support (the report form), each with an example question.
Every question takes one path, so every path can be tested.

It's a LangGraph router, not a free-roaming agent: easier to test, and easier to debug. The widget reuses the login the person already has in the app, so when the agent calls the app's API it does so as them, with their permissions and never more. A fast cloud model answers by default, and the document index re-reads the website every week, re-embedding only the pages that changed.

Answering only what each person may see

The model never decides who sees what. Scope is added to the SQL after the model writes it, at one enforcement point:

The model's SQL passes through clean-up rules, then the person's scope is added, then a read-only check, before reaching the database. The query that actually runs has an extra condition limiting it to the manager's own area. An unknown person, failed lookup or unscopable query gets a refusal. Every allow and deny is audited.
The model writes the question. The guard decides what it's allowed to ask.

It fails closed: anything it can't scope safely is refused. Live testing proved the point by accident. In queries without table aliases, the filter mistook WHERE for an alias, and instead of leaking data, those queries simply failed. (In the widget, the app's own API does this job.)

Checking its own numbers

Nobody double-checks a figure that sounds right, so the agent does it for them:

An illustrative draft answer with two four-digit figures is checked against the query results: both are found, and a two-digit percentage is skipped. If every figure is found the answer is sent; if not, it is rewritten once with a hint; if still wrong, the raw results table is shown instead.
Illustrative numbers. Small ones are skipped: counts and rounded percentages caused constant false alarms.

From complaint to fix

A person fills in a report form, which becomes a ticket; a person approves it with a label; a coding agent writes the fix; a second model with fresh context reviews it; a merge request is opened; a person merges it.
Machines do the work in between. People decide at every gate.

The coding agent once shipped two bugs that passed its own tests: a reordered list that other code read by position, and a "follow the existing pattern" fix that found only one of two parallel implementations. More steps didn't help. A diff tool, two rules written from those failures, the reviewer and real screenshots did. More attempts don't fix checks that can't see the failure.

Three failures worth sharing

The search that quietly stopped searching

Before: the question's embedding call returned 404, the error was caught, an empty list returned, the model was told no knowledge was found, and it answered fluently from general knowledge. Now: a more dependable embedding model and re-embedded documents give answers from the organisation's own documents, and deploys fail if config drifts.
Every question is embedded at query time, so a dead embedding service breaks every search, silently.

The same evening, I found the deploy config still defaulted to the dead endpoint: the next deploy would have quietly undone the fix.

Lesson: graceful degradation can be invisible failure. Treat "no results" from a dead dependency as an error, not an answer. (Making the retrieval path say so out loud is next.)

Memory that remembered too much

Before: an old chat summary saved to the person's profile was mixed into a new, self-contained question by the rewriter, which then named someone unrelated. Now: memory is per session, role and scope are read fresh from the database, and a rewrite that adds a name is thrown away.
About a day after cross-session memory shipped, a brand-new chat picked up a name and a place from an old one.
Lesson: "remember the user" isn't "replay their old conversations". Lasting memory should be a narrow set of explicit facts.

A how-to question that became a ticket

Before: a Tagalog how-to question was classified as a bug and a ticket was filed automatically, although the answer was in the how-to content. Now: a clear routing rule sends it to a how-to answer for the asker's role, and tickets only come from a form.
One conversation, four bugs: the misroute, an English-only acknowledgement, a model control token echoed into the reply, and a how-to answer that pointed a staff member at an admin-only button.
Lesson: never let a classifier's guess trigger something you can't undo. Put a deliberate human action in front of it.

By the numbers

If you're building one

If your organisation has its own documents, data and apps and you'd like an assistant like this, get in touch.

← All lab notesQuestions? Say hi →
Say hi hello@
letsautom8.dev

Write to me about a post, an experiment, or something you'd like me to try in the lab. If you'd like me on your project, testing or automation, that goes through my company, so you get proper terms and a proper invoice.

Work with me through Shiloah →