Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

KPI Storytelling Engine

BusinessIntelligence.ai — Accenture Innovation Challenge 2026, Round 2 Prototype

An AI-assisted root-cause analysis engine for business KPIs. When a metric moves, the engine detects whether the move is statistically meaningful, ranks the most likely causes using deterministic logic, and generates a plain-language explanation tailored to who's reading it — all without letting an LLM invent numbers it doesn't have.


Table of Contents

  1. Architecture at a glance
  2. Project structure
  3. Setup
  4. Running the app
  5. Role & Persona — two different controls
  6. The KPI Contract system
  7. API reference
  8. The 4 required demo scenarios
  9. Troubleshooting

Architecture at a glance

Data Sources (3, different grains)
        │
1. Reconciliation Layer   → aligns daily/weekly/real-time data onto one timeline
        │
2. Detection Engine        → flags material KPI moves (statistics, NOT the LLM)
        │
3. Driver Ranking Engine    → ranks likely causes (rules/scoring, NOT the LLM)
        │
4. Confidence Gate          → decides: confident enough to narrate, or abstain?
        │
5. Narrative Layer (LLM)    → turns the structured result into plain language,
        │                      tailored per persona (Regional Manager / CFO / Analyst)
6. Feedback + Telemetry     → logs corrections, tracks latency/tokens/cost

Core design rule: the LLM never calculates anything. Detection, ranking, and confidence scoring are all deterministic Python/pandas logic driven by kpi_contract.json. The LLM's only job is turning that structured result into readable prose — this is intentional and directly addresses the brief's requirement that "the LLM should not be treated as the source of quantitative truth."


Project structure

kpi_analysis/
├── kpi_analysis_backend/
│   ├── main.py                  # FastAPI app, CORS, router registration
│   ├── contract_store.py        # Loads/validates/hot-reloads kpi_contract.json
│   ├── kpi_contract.json        # THE business config — KPIs, drivers, thresholds
│   ├── reconciliation.py        # Aligns the 3 data sources onto one timeline
│   ├── routes/
│   │   ├── detection.py         # /api/detect — deterministic detection + ranking
│   │   ├── narrative.py         # /api/narrative, /api/feedback — LLM layer
│   │   └── contract.py          # /api/contract/* — view/upload/restore contract
│   ├── scripts/generate_data.py # Generates the 3 simulated data files
│   └── data/                    # sales_db.csv, marketing_spend.json, support_tickets.csv
└── UI/
    └── src/
        ├── App.tsx               # Top-level state: selected KPI, role, persona, catalog
        ├── lib/api.ts             # All fetch() calls to the backend
        ├── lib/context.ts         # Shared app context/state definitions
        └── components/
            ├── Sidebar.tsx        # KPI list, role/product selectors, contract upload
            ├── KpiOverview.tsx    # Top row of KPI summary cards
            └── DetailPanel.tsx    # Ranked drivers, recommended action, narrative

Setup

Backend

cd kpi_analysis_backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Create a .env file in kpi_analysis_backend/:

export GEMINI_API_KEY=your-real-gemini-key

Get a key at aistudio.google.com/apikey.

Generate the simulated data (only needed once, or after wiping data/):

python -m kpi_analysis_backend.scripts.generate_data

Frontend

cd UI
npm install

Running the app

Terminal 1 — backend:

cd kpi_analysis_backend
source venv/bin/activate
uvicorn kpi_analysis_backend.main:app --reload --port 8000

Confirm it's alive at http://localhost:8000/docs.

Terminal 2 — frontend:

cd UI
npm run dev

Open the printed URL (usually http://localhost:5173).

If the top-right badge says "Backend offline — mock data," the backend isn't running or isn't reachable — check Terminal 1 for errors before anything else.


Role & Persona — two different controls

This trips people up, so it's worth being explicit: there are two separate selectors that do different things.

Control Location What it actually does
Role & Access Left sidebar Controls data access. Regional Manager is restricted to their assigned_region — querying another region returns a 403 Access Denied. CFO and Analyst have full read access.
Persona Top-right of the Detail panel Controls narrative writing style only. It changes how the LLM phrases the explanation (short/action-first for Regional Manager, financial framing for CFO, full transparency for Analyst) — it does not affect what data you can see.

In other words: switching Persona to "Regional Manager" does not restrict access — you must switch the Role dropdown in the sidebar for that. This distinction matters for the role-based security demo scenario (see below).


The KPI Contract system

kpi_contract.json is what makes this engine general-purpose rather than hardcoded to one company. It defines:

  • Which regions and KPIs are valid
  • Materiality thresholds and comparison windows per KPI
  • Which "driver candidates" to check, how to score them, and their evidence templates
  • Confidence/abstention thresholds
  • Optional lever/action/owner mapping for recommended actions

Swap the contract, keep the same code — the detection and ranking engines read everything from this file at runtime (via contract_store.py), not from hardcoded strings. This was verified: a contract with completely different KPI names and driver names produced the same correct math against the same underlying data, unchanged.

Uploading a new contract via the API

curl -X POST http://localhost:8000/api/contract/upload \
  -F "file=@your_new_contract.json"

Takes effect immediately — no server restart needed. If the upload is invalid, it's rejected and your current contract stays active. If you want to undo a successful upload:

curl -X POST http://localhost:8000/api/contract/restore

API reference

Endpoint Method Purpose
/api/detect GET Run detection + driver ranking for a KPI. Params: kpi, period, product, role, assigned_region
/api/narrative POST Generate a persona-specific narrative from a detection result
/api/feedback POST / GET Log or list analyst corrections to a narrative's root-cause call
/api/contract GET View the full active contract
/api/contract/kpis GET Structured list of queryable KPIs (drives the sidebar/overview dynamically)
/api/contract/upload POST Upload and hot-reload a new contract
/api/contract/restore POST Revert to the contract active before the last upload

Full interactive docs: http://localhost:8000/docs


The 4 required demo scenarios

Use these exact combinations to reliably demo each required scenario:

# Scenario How to trigger What you should see
1 Multi-factor movement, high confidence Role: Analyst → KPI: Northeast Revenue → Period: 2026-07-15 Single dominant driver (shipping delays), high confidence, full recommended action
2 Low-confidence / abstain Role: Analyst → KPI: Southeast Revenue → Period: 2026-07-15 "Confidence too low to identify a single cause — recommend manual review," competing drivers labeled as possibilities, not conclusions
3 Sparse history Role: Analyst → Product dropdown: New Product → KPI: Northeast Revenue Flagged as insufficient historical data (sparse_history: true), no false-confidence analysis attempted
4 Role-based security Sidebar Role: Regional Manager, Region: Northeast → click Southeast Revenue Red "Access Denied" panel with a clear explanation and how to get full access

Troubleshooting

externally-managed-environment pip error Your virtualenv isn't activated. Run source venv/bin/activate and confirm with which pip — it should point inside venv/, not /usr/bin/pip.

502 Bad Gateway on /api/narrative Get the real error first:

curl -X POST http://localhost:8000/api/narrative -H "Content-Type: application/json" -d '{"kpi":"Northeast Revenue","period":"2026-07-15","change_pct":-28.91,"is_material":true,"confidence":"high","drivers":[{"factor":"shipping_delay_tickets","score":1.0,"evidence":"159 tickets"}],"abstain":false,"sparse_history":false,"persona":"regional_manager"}'
  • 404 NOT_FOUND mentioning a model name → that model was deprecated for this account; the error message tells you the exact replacement model name Google wants you to use — update MODEL in narrative.py to that name. This project has already hit this twice (gemini-2.5-flashgemini-3.6-flashgemini-2.5-flash-lite → currently gemini-3.5-flash-lite). If it happens again, trust the error message's suggested name over anything written here or found via search — Google's lineup is churning fast this cycle.
  • 429 RESOURCE_EXHAUSTED → you've hit a quota. Read the error's quotaId carefully: it is usually a daily quota (GenerateRequestsPerDayPerProjectPerModel-FreeTier), not per-minute — so retrying immediately won't help, and the quota is scoped to that specific model, not your whole API key. The fix is switching MODEL to a Flash-Lite tier model, which typically carries a much higher free daily quota than flagship Flash/Pro models — not waiting it out.
  • Anything else → the detail field has the specific cause; don't guess, read it

Currently working model: gemini-3.5-flash-lite (as of the last verified test). Check MODEL in narrative.py if this stops working — Gemini free-tier model availability changes without much notice.

Every narrative request seems to fire twice in dev mode This has been observed consistently in local testing (two POST /api/narrative calls per click in the backend logs). Likely React StrictMode double-invoking an effect in development — check if it disappears in a production build (npm run build). Not confirmed fixed as of writing; worth verifying before relying on quota headroom during the live demo, since it roughly doubles narrative-call usage.

Frontend shows "Backend offline — mock data" The backend isn't running, crashed on startup, or CORS is misconfigured. Check Terminal 1 for the actual startup error before assuming it's a frontend problem.

Missing GEMINI_API_KEY crash on startup Environment variables must be loaded (load_dotenv()) before any route file that creates an API client is imported. Check the top of main.py. Confirm your key is actually being read:

python3 -c "from dotenv import load_dotenv; import os; load_dotenv(); print(bool(os.environ.get('GEMINI_API_KEY')))"

Uploaded contract broke the app

curl -X POST http://localhost:8000/api/contract/restore

Every upload is backed up automatically before being applied — you can always revert.


This is a Round 2 prototype for the Accenture Innovation Challenge 2026. Not production-grade; built on simulated data with reasonable assumptions stated throughout.

About

KPI Storytelling Engine — an AI-assisted root-cause analysis system that explains why a business metric moved, ranks the most likely causes using deterministic logic, and generates persona-specific explanations without letting an LLM invent numbers it doesn't have. Built for the Accenture Innovation Challenge 2026, BusinessIntelligence.ai track.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages