Building a Verified AI Tutor: How I Built UniUI with 50+ Academic Engines and Socratic Reasoning A developer built UniUI, a Socratic AI tutor for Nigerian university students that verifies every answer using 50+ academic engines. The system, now used by over 190 students, combines LLMs from Groq and OpenRouter with subject-specific verification tools to correct hallucinated answers and reject vague questions. TL;DR: I built an AI tutor that doesn't guess. It verifies. Here is how I built it with 50+ academic engines, Socratic reasoning, and offline-first PWA. 190+ students are using it. ChatGPT gives you an answer. You trust it. You fail your exam. Why? Because ChatGPT guesses . It doesn't compute. It doesn't verify. It doesn't know Nigerian curricula, engineering thermodynamics, or organic chemistry pathways. Nigerian university students have a broken academic support system. Here's what we're up against: Generic AI tools don't solve this. They make it worse —students trust hallucinated answers. So I built UniUI. UniUI: A strict, Socratic AI tutor for Nigerian university students. Every answer is verified by 50+ academic engines. Wrong answers get corrected. Vague questions get rejected. Live at: app.uniui.com.ng https://app.uniui.com.ng UniUI is built on a modern AI stack designed for reliability, verification, and offline-first accessibility. | Layer | Technology | Purpose | |---|---|---| API | FastAPI Python | Backend logic, routing, LLM orchestration | LLM | Groq primary , OpenRouter fallback | Answer generation | Verification | 50+ academic engines SymPy, ChemPy, PyNiteFEA, etc. | Answer verification | Vector DB | Qdrant Cloud | Semantic search for RAG | Keyword Search | Meilisearch | Hybrid search sparse + dense | Database | Neon PostgreSQL | User data, conversations, notes | Cache | Redis | Rate limiting, session cache | Frontend | Next.js 14 App Router + Tailwind CSS | User interface | Offline | PWA + Service Worker + IndexedDB | Offline-first experience | Encryption | TweetNaCl + localForage | Client-side encrypted storage | Hosting | Hetzner VPS 4 vCPU, 8GB RAM | Self-hosted backend | php flowchart TD A User Question -- B FastAPI Router B -- C{Stage 1: School Notes} C -- |Found| D Qdrant Vector Search C -- |Not Found| E{Stage 2: Curriculum} E -- |Found| D E -- |Not Found| F{Stage 3: References} F -- |Found| D F -- |Not Found| G{Stage 4: Internet} G -- |Found| H LLM + Context G -- |Not Found| I{Stage 5: Direct LLM} D -- H H -- J 50+ Verification Engines J -- K{Verified?} K -- |Yes| L Return Verified Answer K -- |No| M Return Corrected Answer + Explanation This is UniUI's secret weapon . Every AI-generated answer is cross-checked by subject-specific engines before being returned to the student. sympy – Symbolic mathematics calculus, algebra, equations scipy – Numerical computing, linear algebra numpy – Array operations, matrix math mpmath – High-precision arithmetic sage optional – Advanced mathematical computing pydantic – Type validation for mathematical inputs pint – Unit-aware physics calculations sympy.physics – Classical mechanics, quantum, relativity scipy – Differential equation solvers pandas – Data analysis experimental physics openstax – Physics reference data astropy – Astrophysics calculations qiskit – Quantum computing optional chempy – Stoichiometry, equilibrium, thermodynamics rdkit – Molecular fingerprints, SMILES requires Python 3.10-3.12 scipy – Numerical methods for chemistry openbabel optional – Molecular file format conversion pymatgen – Materials science pynitefea – Finite element analysis pandapower – Power systems analysis python-control – Control systems engineering coolprop – Fluid properties thermodynamics py engineers – Structural analysis openmc – Nuclear engineering optional pynt – Medical image reconstruction pypbpk – Physiologically based pharmacokinetic modeling glucostats – Glucose monitoring and analytics biomechanics – Motion analysis opencv – Medical image processing dicom – DICOM file handling dssattools – Crop simulation apsim – Agricultural production systems farmingpy – Precision agriculture geopandas – Geospatial simulation hydropy – Hydrology models pythen – Legal reasoning engine lexnlp – Legal text analysis, entity extraction nltk – NLP for legal documents spacy – Legal text processingHere's an example of how verification works for a math question: python acadermic pipeline/backend/verifiers/math verifier.py import sympy as sp import numpy as np from typing import Dict, Any, Optional def verify math answer question: str, llm answer: str - Dict str, Any : """ Verify a mathematics answer using SymPy. Returns: { "verified": bool, "correct answer": str, "derivation": str, "confidence": float } """ try: Step 1: Parse question using LLM to extract math expression Step 2: Convert to SymPy expression Step 3: Compute the actual answer Step 4: Compare with LLM answer Step 5: Return verification result Example: Calculate integral of x^2 x = sp.Symbol 'x' expression = sp.integrate x 2, x Returns x 3/3 return { "verified": True, "correct answer": str expression , "derivation": "∫x²dx = x³/3", "confidence": 1.0 } except Exception as e: return { "verified": False, "correct answer": None, "error": str e , "confidence": 0.0 } UniUI uses a 5-stage cascade retrieval system to find the most relevant content before generating an answer. python academic pipeline/backend/retrieval/vector search.py def vector search query: str, faculty: Optional str = None - List Dict : """ Search Qdrant for semantically similar content. """ Generate embedding for the query embedding = get embedding query Build filter faculty, course code, etc. filter condition = None if faculty: filter condition = { "must": {"key": "faculty", "match": {"value": faculty}} } Search Qdrant results = qdrant client.search collection name="uniui documents", query vector=embedding, query filter=filter condition, limit=10, score threshold=0.7 return results python academic pipeline/backend/retrieval/keyword search.py def keyword search query: str - List Dict : """ Search Meilisearch for keyword matches. """ results = meilisearch client.index "curriculum" .search query, { "attributesToRetrieve": "title", "content", "course code" , "limit": 10 } return results "hits" python academic pipeline/backend/retrieval/hybrid search.py def hybrid search query: str - List Dict : """ Combine vector + keyword search using Reciprocal Rank Fusion RRF . """ vector results = vector search query keyword results = keyword search query RRF fusion alpha = 60 for best results fused = reciprocal rank fusion vector results, keyword results, alpha=60 return fused :10 python academic pipeline/backend/retrieval/internet search.py def internet search query: str - List Dict : """ Search the internet using Exa or SerpAPI. """ try: Use Exa AI-powered semantic search results = exa client.search query, type="neural", num results=5 return results "results" except Exception: Fallback: use Jina Reader return jina search query python academic pipeline/backend/retrieval/direct llm.py def direct llm query: str - str: """ Fallback: generate answer directly from LLM no context . """ response = groq client.chat.completions.create model="llama-3.3-70b-versatile", messages= {"role": "system", "content": "You are a strict academic tutor. Answer accurately."}, {"role": "user", "content": query} return response.choices 0 .message.content python academic pipeline/backend/routers/ask router.py from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import List, Optional from academic pipeline.backend.verifiers import verify answer from academic pipeline.backend.retrieval import hybrid search from academic pipeline.backend.llm import call llm router = APIRouter class AskRequest BaseModel : question: str faculty: Optional str = None course code: Optional str = None class AskResponse BaseModel : answer: str verified: bool verified answer: Optional str = None sources: List Dict str, str engine used: str confidence: float @router.post "/ask", response model=AskResponse async def ask question request: AskRequest : """ Main endpoint for asking questions. 1. Retrieve context 5-stage cascade 2. Generate LLM answer with context 3. Verify answer using academic engines 4. Return verified result """ Step 1: Retrieve context contexts = hybrid search request.question if not contexts: Stage 5: Direct LLM no context llm answer = await direct llm request.question sources = else: Stage 1-4: LLM with context llm answer, sources = await generate with context request.question, contexts Step 2: Verify answer verification result = await verify answer question=request.question, answer=llm answer, faculty=request.faculty Step 3: Build response return AskResponse answer=verification result.get "answer", llm answer , verified=verification result.get "verified", False , verified answer=verification result.get "correct answer" , sources=sources, engine used=verification result.get "engine used", "groq" , confidence=verification result.get "confidence", 0.0 python academic pipeline/backend/verifiers/ init .py from typing import Dict, Any, Optional import importlib import inspect Registry of all verifiers VERIFIERS = {} def register verifier subject: str : """Decorator to register verifiers.""" def decorator func : VERIFIERS subject = func return func return decorator async def verify answer question: str, answer: str, faculty: Optional str = None - Dict str, Any : """ Verify an answer using the appropriate academic engine. """ Detect subject from question subject = detect subject question, faculty Get the verifier function verifier = VERIFIERS.get subject if not verifier: return { "verified": False, "answer": answer, "engine used": "none", "confidence": 0.0 } Run verification try: result = await verifier question, answer result "engine used" = subject return result except Exception as e: return { "verified": False, "answer": answer, "engine used": subject, "confidence": 0.0, "error": str e } academic pipeline/backend/verifiers/subject detection.py KEYWORDS = { "math": "integrate", "derivative", "calculus", "matrix", "equation" , "physics": "force", "velocity", "energy", "momentum", "gravity" , "chemistry": "molecule", "bond", "reaction", "acid", "base" , "engineering": "beam", "stress", "load", "circuit", "voltage" , "medicine": "cell", "tissue", "disease", "symptom", "blood" , "agriculture": "crop", "soil", "water", "yield", "fertilizer" , "law": "act", "section", "legal", "court", "contract" } def detect subject question: str, faculty: Optional str = None - str: """ Detect the subject of a question using keyword matching. """ if faculty and faculty in KEYWORDS: return faculty question lower = question.lower scores = {} for subject, keywords in KEYWORDS.items : count = sum 1 for kw in keywords if kw in question lower if count 0: scores subject = count if not scores: return "general" Return the subject with the highest keyword score return max scores, key=scores.get UniUI's frontend is built with Next.js 14 and uses a strict, Socratic UI design. js // app/ask/page.tsx 'use client' import { useState } from 'react' import { useRouter } from 'next/navigation' import { api } from '@/lib/api' import { Mascot } from '@/components/Mascot' import { StreamingText } from '@/components/StreamingText' export default function AskPage { const question, setQuestion = useState '' const answer, setAnswer = useState '' const loading, setLoading = useState false const router = useRouter const handleAsk = async e: React.FormEvent = { e.preventDefault if question.trim return setLoading true setAnswer '' try { // Use streaming for real-time answers const response = await api.askStream { question: question, faculty: 'engineering' } // Stream the answer token by token const reader = response.body?.getReader const decoder = new TextDecoder while true { const { done, value } = await reader .read if done break const chunk = decoder.decode value const lines = chunk.split '\n' for const line of lines { if line.startsWith 'data: ' { const data = line.slice 6 if data === ' DONE ' break try { const parsed = JSON.parse data setAnswer prev = prev + parsed.token } catch { // Ignore parse errors } } } } } catch error { console.error 'Error:', error setAnswer 'Sorry, I encountered an error. Please try again.' } finally { setLoading false } } return