An AI copilot that is not allowed to guess
Adding language-model features to a Power BI-style dashboard builder, where every column the model names is checked against the data model before anything runs.
- React
- TypeScript
- C# / ASP.NET Core
- EF Core
- SSAS / DAX
- Claude and OpenAI APIs

Outcome
- Every column or measure a model proposes is checked against the real data model before anything runs
- Claude, OpenAI and an internal model behind one gateway, with fallback and usage tracking
- Sixteen visual types on one shared query engine, covered by 571 backend tests
The problem
Farakav’s dashboard builder lets analysts build Power BI-style reports on SSAS models. I am rewriting it in phases: a React and TypeScript front end with sixteen visual types on a drag-and-drop canvas, and an ASP.NET Core service that reads the model’s metadata, generates DAX and runs the queries.
The brief also asked for AI: describe a chart or a measure, in Persian or English, and get it built. A language model will happily produce a plausible column name that does not exist. In a reporting tool that is worse than an error, because people trust the chart that comes out of it.
What I did
The model never writes a query and never reaches SSAS. In the dashboard copilot it can do three things: propose a query definition, propose a DAX measure, or ask a clarifying question. Its prompt carries the model’s real metadata, with hidden fields left out.
Every column and measure id it proposes is checked against that metadata before the proposal reaches the screen. If one does not exist, the proposal becomes a clarifying question. An accepted proposal runs through the same query engine and measure validation as a hand-built visual, so the AI adds no second path to the data.
Later features follow the same rule: a natural-language measure builder, a DAX copilot that explains, repairs and generates expressions, and an analyst that answers questions about a dashboard by running validated queries. All of them call one AI gateway that routes by task and data-sensitivity policy, falls back to a second provider when the first fails, and logs usage and cost. Moving between Claude, OpenAI and the company’s internal model is a configuration change.
The back end was new to me. I came to it as a TypeScript developer and wrote the C# with an AI coding agent, reviewing each phase, checking it against a real SSAS model, and keeping the reasoning behind each design decision in the repository’s docs.
What I would do differently
The first version of the AI gateway was built from a summary of the requirements instead of the specification itself, and it came out as a third hard-coded provider. The real spec asked for a model-agnostic gateway, so that work was redone. I now read the source document before building, however clear the summary sounds.
I wrote about the design behind this in more depth: Letting a language model build dashboards without trusting it.