Portfolio AI Assistant
A retrieval-grounded AI assistant embedded in this site that answers questions about my work without hallucinating. It runs the same RAG + intent-router architecture clients hire me to build.
The problem
Recruiters and clients skim a portfolio with specific questions ("has he shipped Stripe?", "is he available?", "what's the rate?") and bounce when the answer isn't instant. A static page can't field arbitrary questions, and a naive LLM bot would confidently invent a CV.
Ask the assistant
A miniature of the live assistant. Each answer is tagged with the path the real pipeline took: the zero-LLM intent router, BM25 retrieval plus grounded generation, or an honest decline when the answer isn't in the portfolio.
Is Nasim available for work?
Yes, he's open to full-time roles and freelance work, and usually replies within 24 hours. Details are on the contact page.
Intent router · 0 LLM calls
Ask another question:
Architecture
- Three-stage pipeline: a lexical intent router answers 11 factual query types (contact, availability, rates, location, resume, GitHub, LinkedIn, Upwork, booking, work arrangement, ideal role) with zero LLM calls; if none match, a BM25 retriever pulls the most relevant passages from the indexed portfolio; only then does Groq's Llama 3.1 write a grounded answer from that context alone
- The router matches on whole-word boundaries (the query is space-padded so 'rate' can't fire inside 'accurate') and composes answers straight from structured profile and preferences data, so hard facts never pass through the model and can't be reworded or invented
- BM25 lexical retrieval (k1=1.5, b=0.75) over one passage per project section, with stopword filtering and query-side synonym expansion, so a question about a 'mobile app' still surfaces the React Native work without an embedding model or paid vector DB
- A relevance floor gates generation: a question with no real term overlap scores zero everywhere, falls below the threshold, and returns an honest 'I don't have that' pointing to the contact form instead of letting the model improvise
- Strict context-only system prompt at temperature 0.2, with the visitor's question wrapped as untrusted input; the prompt refuses role changes, prompt-leak requests, and any claim the retrieved context doesn't support
- Streamed token-by-token over a Next.js route handler with no SDK: raw fetch against Groq's OpenAI-compatible endpoint piped through a ReadableStream. The widget is lazy-loaded via next/dynamic (ssr: false), so it adds nothing to First Load JS
- Cost-safe by construction: a per-IP burst limit plus a global daily LLM cap. Over budget, with no API key, or if Groq fails, it degrades to an extractive answer from the retrieved passages instead of erroring. Free tier throughout: Groq inference, BM25 instead of a paid vector DB, Vercel Hobby. pgvector is a documented drop-in if the corpus grows
Highlights
Grounded by design, not by luck
The intent router shields every factual question from the model, and a BM25 relevance floor gates the rest. Ask about something not in the portfolio and it declines instead of inventing an answer.
Hardened against prompt injection
The visitor's question reaches the model as explicitly untrusted input; the system prompt refuses any instruction to change role, reveal itself, or assert facts the retrieved context can't support.
Degrades gracefully
With no API key, over the daily budget, or if Groq fails, it still answers extractively from the retrieved passages. It also falls back to seed content when the database is offline, so it works the moment it deploys.
Impact
11
Factual intents answered instantly with zero LLM calls (contact, availability, rates, location, resume, links, booking, arrangement, role fit)
0 KB
Added to First Load JS: the widget is lazy-loaded out of the critical path
$0
Monthly cost: Groq free tier, no vector DB, no new infrastructure
0
Fabricated facts: every answer is grounded in real portfolio content, or it declines instead of guessing