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.
- Architecture at a glance
- Project structure
- Setup
- Running the app
- Role & Persona — two different controls
- The KPI Contract system
- API reference
- The 4 required demo scenarios
- Troubleshooting
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."
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
cd kpi_analysis_backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtCreate 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_datacd UI
npm installTerminal 1 — backend:
cd kpi_analysis_backend
source venv/bin/activate
uvicorn kpi_analysis_backend.main:app --reload --port 8000Confirm it's alive at http://localhost:8000/docs.
Terminal 2 — frontend:
cd UI
npm run devOpen 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.
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).
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.
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| 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
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 |
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_FOUNDmentioning 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 — updateMODELinnarrative.pyto that name. This project has already hit this twice (gemini-2.5-flash→gemini-3.6-flash→gemini-2.5-flash-lite→ currentlygemini-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'squotaIdcarefully: 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 switchingMODELto 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
detailfield 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/restoreEvery 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.