{"slug": "kpiassembler-stop-hand-picking-kpis-let-ai-propose-them", "title": "KPIAssembler: stop hand-picking KPIs, let AI propose them", "summary": "Developer Akshat Srivastava released KPIAssembler, an open-source Ruby gem that uses an LLM to propose KPI metric SQL recipes while deterministic Ruby code certifies which ones are safe to publish. The tool inspects a live database schema, runs candidates through EXPLAIN and sample-period replay checks, and flags failures such as unconstrained joins or unsafe division, with heuristic candidates as a fallback when no model is available. \"The LLM proposes. Code certifies,\" the project states.", "body_md": "Text-to-SQL looks great in a demo. Then someone ships a conversion rate that `JOIN` s without `ON`, a revenue figure that double-counts through a fan-out, or a \"tenant-safe\" query that forgot `account_id`.\n\nThe chart library was never the hard part. **Metric definition** is.\n\nI built **KPIAssembler** for that gap: inspect the live schema, let a model *propose* KPI recipes, and let **deterministic Ruby** decide what is certified. The model never gets a vote on publication.\n\nGem: [kpi_assembler](https://rubygems.org/gems/kpi_assembler)\n\nSource: [github.com/Akshatsrivastava700/kpi_assembler](https://github.com/Akshatsrivastava700/kpi_assembler)\n\nThe LLM proposes. Code certifies.\n\nCertification is boring on purpose. Each accepted candidate must:\n\n`SELECT` (or `WITH` … `JOIN` without `ON`\n`NULLIF` / `CASE`)` EXPLAIN` and a sample-period replay\nFail any check and the KPI is **draft**, with reasons. Draft is not \"you rejected it in the UI.\" Rejected candidates never reach this step. Draft means you *accepted* it and the engine still refused to publish the number.\n\nThat distinction is the whole product.\n\nHeuristics still run as a backfill. If Gemini is down or Ollama has the wrong model pulled, you get schema-driven candidates instead of an empty screen. The UI says whether the proposer was `llm` or `heuristics`.\n\n```\n# Gemfile\ngem \"kpi_assembler\", \"~> 0.5\"\nbundle install\nbin/rails generate kpi_assembler:install\n# config/routes.rb — inside the same auth scope as the rest of the app\nmount KPIAssembler::Engine => \"/kpi-assembler\"\n```\n\nPoint the initializer at a **read-only** pool. Certification executes candidate SQL. Do not hang this off the write primary.\n\n```\nKPIAssembler.configure do |config|\n  config.connection_provider = lambda do |_controller|\n    ApplicationRecord.connected_to(role: :reading) do\n      ApplicationRecord.connection_pool\n    end\n  end\n\n  config.tenant_column = \"account_id\"\n  config.tenant_id_resolver = ->(controller) { controller.send(:current_account).id }\n\n  config.authorize_with = lambda do |controller|\n    controller.send(:authenticate_user!)\n    controller.send(:current_account).present?\n  end\n\n  config.llm_provider = :gemini\n  config.gemini_api_key = ENV[\"GEMINI_API_KEY\"]\nend\n```\n\nIn `.env`:\n\n```\nKPI_LLM_PROVIDER=gemini\nGEMINI_API_KEY=your-key\nKPI_GEMINI_MODEL=gemini-2.0-flash\n```\n\nNo Gemini URL to set. Restart, sign in, open `/kpi-assembler`, click **Discover metrics**.\n\nPrefer local models? `KPI_LLM_PROVIDER=ollama` and a running `ollama serve`. Prefer no model? `KPI_USE_LLM=false`.\n\nWalkthrough and troubleshooting: [setup guide](https://github.com/Akshatsrivastava700/kpi_assembler/blob/main/docs/setup.md).\n\nThe sample catalog includes a metric that is *supposed* to fail: revenue per lead via an unconstrained join. It is accepted on purpose so you can watch certification refuse it.\n\nYou should see reasons like:\n\n`JOIN without ON — unconstrained join / cartesian risk`` Unsafe division without NULLIF or CASE`\nThat is the demo I care about — not a green dashboard.\n\nOther drafts you will hit with a real LLM: missing `account_id = 123`, invented table names, or `Sample-period replay returned NULL` when `NULLIF` did its job on an empty window. Those are honest failures. Empty last-30-days is not the same as bad SQL, but the engine currently treats a NULL replay as unpublished. Read the `reasons` array before you rewrite the query.\n\nIt is not Looker, Metabase, or a warehouse. It does not persist packs to your database yet: the engine keeps the latest pack **in memory per tenant**. Restart the process and it is gone. `GET /kpi-assembler/api/v1/pack` is the JSON to save yourself if you need it durable.\n\nIt is not \"AI analytics.\" It is a gated compiler for metric SQL.\n\n```\ngem \"kpi_assembler\", \"~> 0.5\"\n```\n\nIssues and PRs: [Akshatsrivastava700/kpi_assembler](https://github.com/Akshatsrivastava700/kpi_assembler).\n\nIf you already generate KPIs with a chatbot, run one of those queries through a join-without-`ON` check before you put it on a slide. That is the same instinct this gem encodes.", "url": "https://wpnews.pro/news/kpiassembler-stop-hand-picking-kpis-let-ai-propose-them", "canonical_source": "https://dev.to/akshat_srivastava_1930291/kpiassembler-stop-hand-picking-kpis-let-ai-propose-them-kbo", "published_at": "2026-09-11 10:17:11+00:00", "updated_at": "2026-09-11 10:33:20.744348+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "large-language-models", "ai-products"], "entities": ["KPIAssembler", "Akshat Srivastava", "Ruby", "Gemini", "Ollama", "Looker", "Metabase"], "alternates": {"html": "https://wpnews.pro/news/kpiassembler-stop-hand-picking-kpis-let-ai-propose-them", "markdown": "https://wpnews.pro/news/kpiassembler-stop-hand-picking-kpis-let-ai-propose-them.md", "text": "https://wpnews.pro/news/kpiassembler-stop-hand-picking-kpis-let-ai-propose-them.txt", "jsonld": "https://wpnews.pro/news/kpiassembler-stop-hand-picking-kpis-let-ai-propose-them.jsonld"}}