UET GPT
An unofficial assistant for UET Taxila students, using retrieval from the university's published pages.

What this demonstrates for your project
- Problem
- University admissions, fee schedules, and course policies were scattered across dozens of department pages, PDFs, and notice boards.
- What was built
- A production RAG assistant built on Next.js 15 and Convex vector search, with Clerk authentication and a Crawl4AI scraping pipeline.
- What it proves
- Multi-provider routing (gpt-oss-20b on Groq with Gemini 2.5 Flash fallback), Cohere rerank with local bge fallback, and refusal instructions for empty retrieval. No published answer-accuracy figure.
- Status & context
- Live in production at uetgpt.site. Unofficial independent project; not affiliated with or endorsed by UET Taxila.
Overview
Problem
University information lives scattered across dozens of pages, PDFs, and notice boards: students have to check several sources for answers about admissions, fees, and courses.
UET GPT is a ChatGPT-style assistant for University of Engineering and Technology, Taxila students and applicants. Ask it a question and it answers from university pages collected by its own crawler. Empty retrieval selects refusal instructions; low scores select hedging. These instructions do not guarantee a correct answer. It is live at uetgpt.site. It is an independent project, not affiliated with or endorsed by the university.
What I built
The full production stack: a Next.js frontend, a Convex backend using its Agent, RAG, and Workflow components, sign-in with Clerk, rate limiting with Upstash Redis, and error monitoring with Sentry. A separate Python crawler (Crawl4AI) collects university content and feeds it into the knowledge base. Answers come from gpt-oss-20b on Groq, with Gemini 2.5 Flash taking over when Groq is down or rate-limited; Gemini embeddings index the pages and Cohere reranks what retrieval finds, with a local model as its fallback.
Retrieval is hybrid: a vector search and a keyword search run side by side and their rankings are merged with reciprocal rank fusion, so an exact program name or fee code is not lost to a fuzzy semantic match. When the reranked results look weak, a relevance check runs before the model answers. The same data is open to other AI tools through a public MCP server at uetgpt.site/mcp, with five typed tools (merit aggregate, fee estimate, program lookup, program list and site search) that need no sign-in.


Impact / Learnings
My most complete system to date: auth, rate limiting, observability, automated testing (Playwright + Vitest), and a data-ingestion pipeline.
A stale crawl or poorly chosen chunk can give the model the wrong context, so the crawler and index deserve as much engineering as the chat loop.
Architecture
System Design
- University websites
- Crawl4AI pipeline (Python): crawl, clean, chunk
- Convex vector store (RAG component)
- Student question
- Next.js app (Clerk auth + Upstash rate limit)
- Convex Agent: vector + keyword search, merged with RRF
- Rerank: Cohere (local bge fallback)
- Answer: gpt-oss-20b on Groq, Gemini 2.5 Flash fallback
- Streamed answer with sources
- Sentry monitoring + Convex Workflow for background jobs
Production Hardening
- Auth
- Clerk (sign-in, per-user history)
- Rate limiting
- Upstash Redis: per-user + global caps
- Fallbacks
- Groq -> Gemini for answers, Cohere -> local model for reranking
- Monitoring
- Sentry error tracking
- Testing
- Playwright end-to-end + Vitest unit tests
- Docs
- ARCHITECTURE.md, THREAT_MODEL.md, PRODUCT.md
When it goes wrong
| What fails | What the system does |
|---|---|
| LLM provider down or rate-limited | The request falls back from Groq to Gemini 2.5 Flash; the answer still streams normally. |
| Empty retrieval or weak source matches | Empty retrieval selects refusal instructions. Scores below 0.4 select hedging; scores from 0.4 to below 0.6 require citations. These instructions can still produce wrong answers. |
| Abusive or excessive request volume | Rejected by Upstash Redis per-user and global rate limits before reaching an LLM provider. |
| Unexpected runtime error | Captured by Sentry for investigation instead of failing silently. |
Code
// convex/agent.ts (simplified) - RAG answer flow
// question -> embed -> retrieve university docs -> grounded answer
import { components } from "./_generated/api";
import { Agent } from "@convex-dev/agent";
import { rag } from "./rag";
export const uetAgent = new Agent(components.agent, {
name: "UET GPT",
instructions:
"Answer using ONLY the retrieved university context. " +
"If the context does not cover it, say so and point to the source page.",
});
export async function answer(ctx: ActionCtx, question: string) {
// 1. Retrieve the most relevant chunks of crawled university content
const context = await rag.search(ctx, {
namespace: "uet",
query: question,
limit: 8,
});
// 2. Answer with gpt-oss-20b on Groq, falling back to Gemini 2.5 Flash
const model = pickProvider({ preferFast: true });
// 3. Stream a grounded answer with sources attached
return uetAgent.streamText(ctx, {
model,
prompt: question,
context: context.entries,
});
}Need an AI assistant grounded in your own docs or data?
Send a brief →Need something like this? See AI Assistants & Production RAG Systems.