All projects

UET GPT

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

Status
Live
Built with
Next.js, TypeScript, Convex, RAG, Vercel AI SDK, Clerk
The UET GPT home page: campus photographs above the heading “UET Taxila, answered.” and a question box
The UET GPT home page, uetgpt.site.
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

Key decision: Retrieve university content before answering, then use reranker scores to select answer instructions. Refuse on empty retrieval, hedge on low scores, and link sources for checking.

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.

The UET GPT admissions page: the heading “UET Taxila Admissions 2026”, a box with the engineering and computing cutoffs and the tuition per semester, and links to eligibility, ECAT, fees, financial aid and a university comparison
The admissions guide on uetgpt.site: cutoffs and fees first, then eligibility, ECAT, fees and aid, each with its official source.
The UET GPT chat screen: “Ask me anything about UET Taxila”, suggested questions on fees, admissions, departments, transport and hostels, a question box, and a note that answers can be wrong
The assistant itself, with suggested questions above the box and a note under it to verify critical details.

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

  1. University websites
  2. Crawl4AI pipeline (Python): crawl, clean, chunk
  3. Convex vector store (RAG component)
  1. Student question
  2. Next.js app (Clerk auth + Upstash rate limit)
  3. Convex Agent: vector + keyword search, merged with RRF
  4. Rerank: Cohere (local bge fallback)
  5. Answer: gpt-oss-20b on Groq, Gemini 2.5 Flash fallback
  6. Streamed answer with sources
  7. 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 failsWhat the system does
LLM provider down or rate-limitedThe request falls back from Groq to Gemini 2.5 Flash; the answer still streams normally.
Empty retrieval or weak source matchesEmpty 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 volumeRejected by Upstash Redis per-user and global rate limits before reaching an LLM provider.
Unexpected runtime errorCaptured by Sentry for investigation instead of failing silently.

Code

convex/agent.ts
// 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.