An intelligent technical screening tool that conducts a structured interview generated entirely from a candidate's resume and a role-specific knowledge base, using a Retrieval-Augmented Generation (RAG) pipeline. Built for the PG-AGI AI/ML & Backend Engineering Intern assignment.
- Objective
- Problem Statement
- System Architecture
- Technology Stack
- End-to-End System Flow
- AI/ML — RAG Pipeline in Depth
- Backend & API Design
- Key Design Decisions
- Project Structure
- Setup Instructions
- Using the Application
- Possible Extensions
This project evaluates and demonstrates the ability to design and implement a real-world intelligent system that integrates applied AI/ML concepts (specifically RAG), backend system design, frontend interaction, and end-to-end data flow management — with an emphasis on architectural clarity and design maturity, not just feature completeness.
Given a candidate's resume and a target job role, the system must simulate a structured technical interview in which questions are not predefined, but generated dynamically from:
- the candidate's resume,
- the selected job role, and
- a role-specific knowledge base (retrieved via RAG).
The system is composed of three independently deployable layers — a React frontend, a FastAPI backend that owns all orchestration logic, and a data layer combining SQLite (relational, transactional data) with FAISS (vector search). This separation keeps each layer independently testable and replaceable — for example, swapping SQLite for Postgres or FAISS for a managed vector database requires no changes to the frontend or the API contract.
Mermaid source for this diagram (regenerate or edit at mermaid.live):
flowchart TB
U["Recruiter / Candidate<br/>Browser"] -->|HTTPS| FE["Frontend — React + Vite<br/>Setup · Review · Interview · Summary"]
FE -->|REST /api| BE["Backend — FastAPI<br/>Routers · Resume Parser · Interview Engine"]
BE -->|query| RAG["RAG Pipeline"]
subgraph RAG_GROUP[" "]
KB["Knowledge Base<br/>role-specific TOPIC-chunked text"] --> EMB["Embedding Model<br/>sentence-transformers"]
EMB --> IDX["FAISS Vector Index<br/>cosine similarity search"]
IDX --> QG["Question Generator<br/>Groq LLM to template fallback"]
end
RAG --> RAG_GROUP
BE -->|persist| DATA["Data Layer"]
subgraph DATA_GROUP[" "]
DB[("SQLite<br/>Sessions, Questions, Answers")]
VEC[("FAISS index files<br/>one per role")]
end
DATA --> DATA_GROUP
QG -.source chunk + skill.-> DB
| Layer | Technology | Why |
|---|---|---|
| Frontend | React 18 + Vite | Fast dev loop, minimal boilerplate for a 4-stage single-page flow |
| Backend | FastAPI (Python), Uvicorn | Async-native, automatic request validation, self-documenting OpenAPI schema |
| Database | SQLite via SQLAlchemy ORM | Zero-setup persistence; swappable for Postgres by changing one env var |
| Embeddings | sentence-transformers (all-MiniLM-L6-v2) | Small, fast, runs fully locally with no paid API |
| Vector store | FAISS (IndexFlatIP, cosine similarity) | No external service required; sufficient recall at this knowledge-base scale |
| LLM | Groq API (Llama 3.1 8B Instant) | Free tier, fast inference, OpenAI-compatible client |
| Resume parsing | pdfplumber + keyword-based skill matching | Deterministic, dependency-light, no external NLP service |
| Containerization | Docker + docker-compose | One-command reproducible setup |
| Stage | What happens |
|---|---|
| 1. Candidate Entry | Recruiter selects a role and uploads the candidate's resume (PDF or .txt) |
| 2. Resume Processing | pdfplumber extracts raw text; a keyword matcher identifies technical skills |
| 3. Context Construction | Each detected skill plus the role label becomes a natural-language retrieval query |
| 4. Knowledge Retrieval | Each query is embedded and matched against the role's FAISS index; top-k relevant chunks returned |
| 5. Question Generation | Retrieved chunk(s) plus the triggering skill are passed to the LLM (or template engine) to produce one grounded, non-generic question |
| 6. Interactive Interview | The candidate answers in the UI; the system maintains session state across questions |
| 7. Response Handling | Every question, its source chunk, triggering skill, and the candidate's answer are persisted |
| 8. Final Output | A summary screen presents stats, an AI-generated closing insight, and the full traceable transcript |
Knowledge ingestion. Role-specific reference material lives in backend/app/knowledge_base/*.txt,
written as labeled TOPIC: blocks. This is a deliberate chunking strategy: each chunk is a
complete, self-contained concept rather than an arbitrary fixed-token window, which avoids
splitting an idea across a chunk boundary and preserves context. Each chunk is embedded with
all-MiniLM-L6-v2 and stored in a faiss.IndexFlatIP (cosine similarity via normalized inner
product), persisted to disk per role.
Retrieval mechanism. Queries are constructed dynamically — one per resume skill (for example, "FastAPI in the context of Backend Engineer"), plus a small set of general role topics to ensure breadth even when few skills are detected. Each query is embedded and matched against the relevant role's index; the top-k most similar chunks are returned with their similarity scores.
Question generation. The retrieved context is passed to the LLM inside a constrained prompt that instructs it to ground the question only in the supplied material and avoid generic "What is X?" phrasing. If no LLM key is configured, or a call fails, a template engine assembles a question from the same retrieved context — the system is never blocked on external API availability.
Resume utilisation. The resume doesn't just gate access — it directly determines which skills generate queries, which in turn determines topic selection and the direction of the interview. Every question is tagged with the specific skill that triggered it.
Output structuring and traceability. The pipeline stage Context to Query to Retrieval to Question to Answer to Storage is preserved end to end: each stored Question row keeps its source topic, retrieved context, and triggering skill alongside the candidate's answer, so the reasoning behind every question is auditable rather than a black box.
The backend is organized as a set of purpose-built routers, one per stage of the interview lifecycle, rather than a single monolithic file:
| Endpoint | Purpose |
|---|---|
| GET /roles | Lists available screening roles |
| POST /sessions | Accepts a resume + role; returns extracted skills and a session id |
| POST /interview/{session_id}/start | Runs the first retrieval + generation cycle |
| POST /interview/answer | Stores an answer, returns the next adaptively-chosen question, or marks completion |
| GET /sessions/{session_id}/summary | Returns aggregate stats, an AI-generated insight, and the full transcript |
Input validation is handled declaratively through Pydantic schemas; malformed uploads, unknown role ids, and unparsable resumes are rejected early with structured error responses rather than propagating into the pipeline.
- Topic-based chunking over fixed-token windows, so retrieval never splits one idea across two chunks.
- Local embeddings + FAISS instead of a paid vector database — appropriate for this knowledge base's scale, with no external dependency once the model is downloaded once.
- Graceful LLM degradation: a deterministic template fallback means the system is always usable, LLM key or not, and a failed API call never crashes an in-progress interview.
- Adaptive sequencing: the next question is chosen based on the previous answer's length — a short answer routes to another concrete, skill-grounded question; a detailed answer routes to a deeper general-topic question.
- Full traceability: every question carries a reference to the exact knowledge-base chunk and resume skill that produced it.
screener/
backend/
app/
knowledge_base/ role-specific reference text (TOPIC-chunked)
rag/ ingest.py (chunk+embed+index), retrieve.py (query)
routers/ upload.py, interview.py, results.py
config.py environment-driven settings
models.py SQLAlchemy ORM models
schemas.py Pydantic request/response models
resume_parser.py PDF text + skill extraction
interview_engine.py query construction + adaptive sequencing
llm.py Groq client + template fallback
main.py FastAPI app entry point
requirements.txt
.env.example
frontend/
src/
components/ SetupStage, ReviewStage, InterviewStage, SummaryStage
api.js
App.jsx
docker-compose.yml
README.md
- Python 3.10+
- Node.js 18+
- A free Groq API key from console.groq.com/keys (optional — the app runs without one)
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # add your GROQ_API_KEY (optional)
uvicorn app.main:app --reload --port 8000On first run, the app downloads the embedding model (about 90 MB) and builds the FAISS indexes — this requires internet access once, after which everything runs locally.
cd frontend
npm install
npm run devdocker-compose up --build- Select a target role and upload a resume (PDF or .txt).
- Review the automatically extracted skills.
- Answer each generated question — the retrieved source topic and triggering skill are shown alongside it for transparency.
- On completion, view the summary: stats, an AI-generated closing insight, and the full traceable transcript.
- Semantic answer grading via embedding similarity against reference material
- Multi-turn follow-up questioning within a single topic before advancing
- An interviewer-facing dashboard comparing multiple candidates for the same role
- Swapping SQLite/FAISS for Postgres and a managed vector database for multi-user production use
