An end-to-end walkthrough of the whole repository β from the raw SQLite database and the multi-agent Mistral pipeline, through file uploads and indexing, to the live HTTPS deployment behind nginx.
The School AI Analytics Dashboard is a self-contained web application that lets school administrators ask plain-English questions about their students, marks, fees, hostel and staff data and get instant, formatted answers. There is no SQL to write and no IT ticket to file β a principal simply types "How many students are yet to pay hostel fees?" and the system routes the request, reads the relevant database tables, asks a Large Language Model (Mistral) to analyse them, and returns conversational Markdown with charts and rankings.
It is built from three layers that all live in one folder:
school.db) holding 13 relational tables with 500 generated
students, plus a ChromaDB vector store that indexes the schema for semantic lookups.
server.py) that routes questions to tables and calls the
Mistral API through a paced, retry-protected client.
index.html) with Overview, AI Assistant,
Data Tables and file-upload views β no build step, no framework.
| File | Role |
|---|---|
server.py | The heart of the app. FastAPI server defining every API route, the Mistral client, the question-to-table router, the multi-agent prompt, and the file-upload / indexing engine. |
setup_db.py | Builds school.db from scratch β creates the 13 tables and generates 500 students, marks, fees, hostel, transport, staff and event records. Also writes the schema reference files. |
index.py | Builds the ChromaDB vector database (vector_db/) by chunking the schema reference and embedding it into a school_context collection. |
index.html | The entire front-end: layout, styling, and all client-side JavaScript that calls the API endpoints. |
how_it_works.html | This report. |
start.py / stop.py | Cross-platform launch/stop helpers. start.py frees port 8000, runs the DB + index builders, then launches uvicorn with live reload. (See the deployment note in Β§8.) |
2_start.ps1 / 3_stop.ps1 | Windows PowerShell equivalents of the start/stop scripts. |
api.txt | Holds the line MISTRAL_API_KEY=β¦. Read at request time to authenticate with Mistral. |
requirements.txt | Python dependencies: fastapi, uvicorn, requests, chromadb, sentence-transformers, pandas, openpyxl, pypdf, python-multipart, etc. |
db_schema_reference.txtdb_schema_mermaid.txt | Auto-generated schema maps (one human-readable, one Mermaid ER diagram) produced by setup_db.py and consumed by index.py. |
setup_db.py wipes any old school.db and rebuilds a fully relational schema, then seeds it
with deterministic synthetic data so every install looks identical. The 13 core tables are:
| Table | Holds |
|---|---|
grades | The four grade levels (Grade 5, Middle 8, High 10, Senior 12). |
students | 500 students β name, gender, DOB, address, day-scholar vs hostel flag, grade. |
Subjects_Grade | Subjects mapped to each grade. |
Marks | Two subject scores per student (the basis of all leaderboards and averages). |
AdmissionFees / HostelDetails | Tuition and hostel fee records. A null FeesPaid means "not yet paid". |
TransportOpted / TransportFeesDetails | Bus route opt-ins and transport fee payments. |
Staff / Staff_Subject | Faculty members and the subjects they teach. |
PTM_Report | Parent-teacher meeting comments and ratings. |
Events | School events with dates. |
index.py reads db_schema_reference.txt, splits it on the word Table: into
per-table chunks, and stores them in a persistent ChromaDB collection called school_context under
vector_db/. This gives the system a semantic memory of the schema's structure that can scale into
true retrieval-augmented generation later.
When you type a question in the AI Assistant tab, the front-end POSTs it to
/api/chat. The backend then runs an agentic pipeline that mimics a human analyst:
mistral-small-latest). The system prompt instructs it to write clean Markdown, auto-correct casual typos (hospitalβhostel, boysβMale), treat null hostel fees as unpaid, and draw text bar-charts with β blocks when a chart is requested.marked.js, while a diagnostics object (routed tables, upload flag, status) is logged to the F12 console.Every call goes through ask_general_mistral(), which is deliberately defensive about the free-tier
API limits:
ERROR_SIGNAL sentinel, which the pipeline converts into the
friendly "system is pacing requests, please retry" message instead of a crash.The dashboard lets you upload a file and ask the AI about it. Uploads are not just attached to a
single message β they are indexed into the same school.db so they become part of the
analytics workspace. The handling depends on the file type:
| Type | How it is extracted & indexed |
|---|---|
| CSV / Excel | Parsed with pandas into a real, queryable SQLite table named upload_<id>_<name>. Excel workbooks with multiple sheets are merged with a __sheet column. |
Text extracted page-by-page with pypdf and stored as document text. | |
| Text / Markdown / JSON / log | Decoded and stored directly. |
| Images (png, jpg, webp, gifβ¦) | Sent as base64 to Mistral's Pixtral vision model (pixtral-12b-2409), which extracts all text, tables and numbers from the picture. |
A registry table, UploadedDocuments, tracks every upload (filename, type, linked table, extracted
text, timestamp). On each chat request, gather_uploaded_context() pulls the most recent uploads (within
a character budget) into the model's context. Deleting an upload also drops its generated data table.
setup_db.py rebuilds school.db from scratch and therefore clears
all uploaded files. Normal restarts keep them.index.html is a single self-contained page β no framework, no bundler. On load it fires four fetches
(loadStats, loadTables, loadQuickQuestions, loadUploads) and
renders three switchable panels:
/api/stats.marked.js./api/tables.All routes are served under the base path /ai-azure-table-query.
| Endpoint | Purpose |
|---|---|
GET/api/stats | Totals, class average, top student, leaderboard and subject averages for the Overview tab. |
GET/api/tables | Every table with its rows and columns for the Data Tables tab. |
POST/api/chat | The main Q&A endpoint β runs the routing + Mistral pipeline. |
GET/api/quick-insights | The list of suggested "Quick" questions. |
GET/api/ai-status | Live health of every AI provider (key valid? online / rate-limited / offline) β powers the AI Status tab. Pings each provider's /models endpoint, so it costs no tokens. |
POST/api/upload | Accepts a file (β€25 MB), extracts and indexes it. |
GET/api/uploads | Lists indexed uploads. |
DELETE/api/uploads/{id} | Removes an upload and drops its data table. |
GET/ · /how-it-works | Serve the dashboard and this report. |
This instance runs as a managed background service behind nginx with SSL, rather than via the repo's
start.py (which is geared toward local foreground development and binds port 8000):
server:app, managed by a systemd service (ai-school.service) that auto-restarts on failure and starts on boot.# status / logs / restart
sudo systemctl status ai-school
sudo journalctl -u ai-school -f
sudo systemctl restart ai-school
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
echo "MISTRAL_API_KEY=your_key_here" > api.txt
python3 start.py # builds DB + index, launches uvicorn
ask_llm() / ask_vision()) β the routing, data layer and front-end stay the same.gpt-oss-120b) first, then Groq
(llama-3.3-70b-versatile), then Google Gemini (gemini-2.5-flash).
ask_llm() walks this chain provider-by-provider β if one returns a rate-limit (429) or
too-large (413) error, it instantly switches to the next β so a single provider running out of free
quota never takes the dashboard down. The order is configured by LLM_FALLBACK_ORDER in
api.txt. Image uploads are read by Gemini's vision model, falling back to
Mistral Pixtral (Cerebras and Groq have no vision model).provider_attempts field.The app currently calls Mistral, but because the pipeline is model-agnostic you can point it at Claude instead. First, an important billing distinction that trips a lot of people up:
sk-ant-api03-β¦.| Model | Model ID | Input /1M | Output /1M | Best for |
|---|---|---|---|---|
| Claude Opus 4.8 | claude-opus-4-8 | $5 | $25 | Most capable |
| Claude Sonnet 4.6 | claude-sonnet-4-6 | $3 | $15 | Balanced (good default here) |
| Claude Haiku 4.5 | claude-haiku-4-5 | $1 | $5 | Cheapest / fastest |
For a lightweight analytics Q&A like this, the token volume per question is small, so real-world cost is a few cents regardless of model.
Install the official SDK and store the key alongside the existing one:
# add to requirements.txt and install
pip install anthropic
# api.txt β add this line next to MISTRAL_API_KEY
ANTHROPIC_API_KEY=sk-ant-api03-...
Then add a Claude version of the LLM call in server.py and use it wherever
ask_general_mistral() is called today:
import anthropic
client = anthropic.Anthropic(api_key=get_anthropic_key())
def ask_general_claude(messages_payload):
# split the system prompt out of the message list
system = ""
msgs = []
for m in messages_payload:
if m["role"] == "system":
system = m["content"]
else:
msgs.append(m)
resp = client.messages.create(
model="claude-opus-4-8", # or claude-sonnet-4-6 / claude-haiku-4-5
max_tokens=4096,
system=system,
messages=msgs,
)
return "".join(b.text for b in resp.content if b.type == "text")
ask_general_mistral() isn't needed on the Claude path. Note that the latest Opus/Sonnet
models don't accept a temperature parameter β just omit it (the Mistral code's temperature: 0.1
has no Claude equivalent here).Images: the upload feature's vision step (currently Pixtral) can also move to Claude β it reads
images natively. Send the picture as an image content block:
messages=[{"role": "user", "content": [
{"type": "image", "source": {
"type": "base64", "media_type": "image/png", "data": image_b64}},
{"type": "text", "text": "Extract all text and data from this image."}
]}]
If you'd rather not pay at all, many providers offer a genuinely free tier you can use in place
of Mistral or Claude. Because this app already calls an OpenAI-style /chat/completions
endpoint, switching to most of them means changing only three things: the URL, the
model name, and the API key. The expanded list below is drawn from the community
awesome-free-llm-apis
directory.
| Provider | Where to get a key | Example free model(s) | Free limits (approx.) |
|---|---|---|---|
| Google Gemini | aistudio.google.com | gemini-2.5-flash, gemini-2.0-flash | 15 RPM / 1,500 per day (Flash) β very generous |
| Groq | console.groq.com | llama-3.3-70b-versatile, Llama 4, Kimi K2 | 30 RPM / 14,400 per day; ~6k tokens/min |
| Cerebras | cloud.cerebras.ai | gpt-oss-120b, Qwen3 235B, Llama 3.3 70B | 30 RPM / 14,400 per day; 60k tokens/min β fastest |
| NVIDIA NIM β | build.nvidia.com | Llama 3.3 70B, Qwen3 235B, DeepSeek-R1 | 40 RPM (replenishing credits) β top-tier large models |
| OpenRouter | openrouter.ai | 30+ models ending :free (DeepSeek R1, GPT-OSSβ¦) | 20 RPM / 200 per day β biggest model catalog |
| Zhipu AI (GLM) | open.bigmodel.cn | glm-4.5-flash, glm-4.6v-flash (vision) | Free "Flash" tier; OpenAI-compatible |
| Cloudflare Workers AI | dash.cloudflare.com | Llama 3.3 70B, Qwen QwQ 32B, +47 more | ~10,000 "neurons"/day β generous daily quota |
| GitHub Models | github.com/marketplace/models | gpt-4o, Llama 3.3 70B, DeepSeek-R1, Phi-4 | 10β15 RPM / 50β150 per day β uses your GitHub token |
| Hugging Face | huggingface.co | Llama 3.3 70B, Qwen2.5 72B, Mistral 7B | ~$0.10 credits/month (auto-refresh) |
| Pollinations AI β | pollinations.ai | openai, gemini, mistral + image/audio/video | No signup needed; per-IP hourly limit |
| Mistral | console.mistral.ai | mistral-small-latest | 1 req/sec; 1B tokens/month β app's current vision fallback |
| Ollama (local) | ollama.com | llama3.2, qwen2.5 | Unlimited β runs on your machine, no key, no internet |
Groq is OpenAI-compatible, so the change mirrors the existing ask_general_mistral() almost exactly β
a new URL, model, and key:
# api.txt
GROQ_API_KEY=gsk_...
# server.py β same shape as ask_general_mistral(), three values changed
def ask_general_groq(messages_payload):
api_key = get_key("GROQ_API_KEY")
payload = {
"model": "llama-3.3-70b-versatile",
"messages": messages_payload,
"temperature": 0.1,
}
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
r = requests.post("https://api.groq.com/openai/v1/chat/completions",
headers=headers, json=payload, timeout=60)
return r.json()["choices"][0]["message"]["content"]
The same function works for OpenRouter (https://openrouter.ai/api/v1/chat/completions),
Cerebras (https://api.cerebras.ai/v1/chat/completions), and Gemini's
OpenAI-compatible endpoint
(https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) β just swap the URL,
model name, and key.
api.txt and drop its name into the
LLM_FALLBACK_ORDER chain. For full privacy with no usage limits, run Ollama locally
and point the app at http://localhost:11434.The AI Assistant doesn't just answer questions β it can also generate images, audio and video right inside the chat, powered by Pollinations AI. Just describe what you want in plain English:
Every chat message first passes through detect_generation(), which scans for media keywords
(image / picture / photo / logo / draw → image; audio / speech / voice / say / tts / song
→ audio; video / animation / clip / gif → video). If a media request is detected the message is
sent to Pollinations instead of the analytics pipeline; otherwise it flows to the normal data-analysis path.
Words like chart and graph are deliberately not triggers β those stay as text bar-charts in
the analytics engine.
| Type | Pollinations endpoint | Model | Returned as |
|---|---|---|---|
| Image | /v1/images/generations | flux | base64 JPEG → saved & served |
| Audio | /v1/chat/completions | openai-audio | base64 MP3 + transcript |
| Video | /v1/images/generations | ltx-2 | MP4 |
The backend (generate_media()) calls Pollinations with the POLLINATIONS_API_KEY from
api.txt, decodes the result, writes it to the generated/ folder, and returns a URL the
browser renders as an <img>, <audio> or <video> element
inside the AI's reply.