AICTE-2026 / IBM SkillsBuild
Stack: IBM Cloud Lite Β· IBM Granite (via watsonx.ai) Β· RAG (ChromaDB) Β· FastAPI Β· WebSocket
An AI-powered mock interview coach that parses your rΓ©sumΓ©, retrieves role-specific material from a vector knowledge base, conducts a natural conversational interview with live token streaming, evaluates each answer with evidence-based scoring, and produces an end-of-session performance report.
- Architecture
- Project structure
- Quick start
- Environment variables
- REST & WebSocket API reference
- WebSocket live simulation
- Running tests
- Design decisions
- Troubleshooting
- Contributing
- License
User βββΊ Orchestrator βββ¬βββΊ Profile Parser (LLM temp 0.1)
ββββΊ Query Formulator βββΊ ChromaDB (RAG)
ββββΊ Question Generator (LLM temp 0.65)
ββββΊ Conversationalist ββββΊ (multi-turn, streams tokens)
ββββΊ Answer Evaluator (LLM temp 0.25, dual-call)
ββββΊ Session Summarizer (LLM temp 0.25)
ββββΊ Hint Generator (on-demand Socratic nudge)
AWAITING_PROFILE β PROFILE_PARSED β QUESTION_READY β INTERVIEWING
β ANSWER_RECEIVED β FEEDBACK_GIVEN β (loop N questions)
β SESSION_COMPLETE
| # | Component | Temperature | Output contract |
|---|---|---|---|
| 3.1 | Orchestrator | 0.1 | {"action": "...", "reasoning": "..."} |
| 3.2 | Profile Parser | 0.1 | ParsedProfile JSON |
| 3.3 | Query Formulator | 0.1 | {"query": "..."} for vector store |
| 3.4 | Question Generator | 0.65 | QuestionRecord JSON + internal ideal-answer pointers |
| 3.5 | Conversationalist | 0.75 | Plain conversational text (streamed token-by-token) |
| 3.6 | Answer Evaluator | 0.25 | QuestionEvaluation with scores & feedback |
| 3.7 | Session Summarizer | 0.25 | SessionSummary JSON |
| β | Hint Generator | 0.6 | One-sentence Socratic nudge (on-demand) |
interview_trainer/
βββ __init__.py
βββ config.py β env-var configuration (no hardcoded secrets)
βββ models.py β Pydantic schemas for all data contracts
βββ llm_client.py β IBM Granite API wrapper + defensive JSON parsing
βββ vector_store.py β ChromaDB + sentence-transformers RAG layer
βββ session_manager.py β State machine, guardrails, streaming generator
βββ ingest.py β PDF / TXT / MD ingestion (CLI + importable)
βββ app.py β FastAPI REST + WebSocket + SSE endpoints
βββ cli.py β Interactive terminal interface
βββ components/
βββ orchestrator.py β Section 3.1 β routing controller
βββ profile_parser.py β Section 3.2 β rΓ©sumΓ© β ParsedProfile
βββ query_formulator.py β Section 3.3 β profile β vector-store query
βββ question_generator.py β Section 3.4 β context + profile β QuestionRecord
βββ conversationalist.py β Section 3.5 β live interviewer dialogue
βββ evaluator.py β Section 3.6 β answer scoring
βββ summarizer.py β Section 3.7 β session summary
βββ hint_generator.py β Socratic mid-answer hints (on-demand)
data/
βββ seed_knowledge_base.txt β Sample knowledge base (ingest before first run)
tests/
βββ test_agent.py β 77 unit + integration tests (no real API calls)
git clone https://github.com/MithunKumarRajak/Interview-trainer-agent.git
cd interview-trainer-agent
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtPython version: 3.10 or later required (uses
X | Yunion syntax andmatchexpressions).
cp .env.example .envOpen .env and fill in your IBM Cloud credentials:
WATSONX_API_KEY=<your IBM Cloud API key>
WATSONX_PROJECT_ID=<your watsonx.ai project ID>
WATSONX_URL=https://us-south.ml.cloud.ibm.com # adjust region if neededSee Environment variables for the full list of settings.
The RAG layer needs at least a few documents before it can generate grounded questions.
# Ingest the sample corpus that ships with the repo (recommended for first run)
python -m interview_trainer.ingest --file data/seed_knowledge_base.txt
# Add your own documents (PDF, TXT, or MD)
python -m interview_trainer.ingest --file path/to/job_description.pdf
python -m interview_trainer.ingest --dir path/to/question_bank/python -m interview_trainer.cli # mixed questions (default)
python -m interview_trainer.cli --type technical # technical questions only
python -m interview_trainer.cli --type behavioral # behavioral questions only
python -m interview_trainer.cli --verbose # enable debug logginguvicorn interview_trainer.app:app --reload --port 8000- Interactive API docs: http://localhost:8000/docs
- Frontend UI: http://localhost:8000/ (served from
interview_trainer/static/index.html)
All variables are optional except the three IBM credentials when making real API calls.
| Variable | Default | Description |
|---|---|---|
WATSONX_API_KEY |
(required) | IBM Cloud API key |
WATSONX_PROJECT_ID |
(required) | watsonx.ai project ID |
WATSONX_URL |
https://us-south.ml.cloud.ibm.com |
watsonx.ai regional endpoint |
GRANITE_MODEL_ID |
ibm/granite-3-3-8b-instruct |
IBM Granite model to use |
CHROMA_DB_PATH |
./data/chroma_db |
Directory for the ChromaDB persistent store |
CHROMA_COLLECTION |
interview_knowledge_base |
Collection name inside ChromaDB |
EMBEDDING_MODEL |
all-MiniLM-L6-v2 |
sentence-transformers model for embeddings |
MAX_QUESTIONS_PER_SESSION |
5 |
Questions asked before the session ends |
MAX_FOLLOW_UPS_PER_QUESTION |
2 |
Max follow-up turns per question |
MAX_CLARIFY_RETRIES |
2 |
Off-topic messages before a forced re-engagement reply |
MAX_INJECTION_ATTEMPTS |
3 |
Prompt-injection attempts before the session is suspended |
ADMIN_API_KEY |
(required for /admin/ingest) |
Bearer token protecting the admin ingest endpoint |
CORS_ORIGINS |
* |
Comma-separated allowed CORS origins; use * for development |
| Method | Path | Request body | Response | Description |
|---|---|---|---|---|
POST |
/sessions |
{"question_type": "mixed"} |
{session_id, message} |
Create a new session. question_type: technical, behavioral, or mixed. |
GET |
/sessions/{id} |
β | {session_id, state, questions_completed, profile_role, profile_level} |
Inspect current session state. |
DELETE |
/sessions/{id} |
β | 204 No Content | Delete a session and free its memory. |
| Method | Path | Request body | Response | Description |
|---|---|---|---|---|
POST |
/sessions/{id}/message |
{"message": "..."} |
{reply, session_state} |
Send a text message; returns the full reply synchronously. |
POST |
/sessions/{id}/message/stream |
{"message": "..."} |
SSE text/event-stream |
Streaming variant β emits the same JSON event envelope as WebSocket (see below). |
WS |
/sessions/{id}/live |
{"message": "...", "resume_text": null} |
JSON text frames | WebSocket live simulation mode β token-by-token streaming. |
| Method | Path | Request body | Response | Description |
|---|---|---|---|---|
POST |
/sessions/{id}/upload-resume |
multipart/form-data (file, question_type) |
{reply, session_state} |
Upload a PDF, TXT, or MD rΓ©sumΓ© file. |
POST |
/sessions/{id}/hint |
β | {hint} |
Request a Socratic mid-answer hint for the current active question. |
| Method | Path | Response | Description |
|---|---|---|---|
GET |
/sessions/{id}/heatmap |
{dimensions, question_labels} |
Per-dimension score arrays for the confidence heatmap chart. |
GET |
/sessions/{id}/history |
{session_id, profile_role, total_questions, questions[]} |
Full question-by-question history with scores and feedback. |
| Method | Path | Auth | Response | Description |
|---|---|---|---|---|
GET |
/health |
β | {status, vector_store_docs} |
Liveness probe. |
POST |
/admin/ingest |
Bearer ADMIN_API_KEY |
{chunks_ingested, collection_size} |
Ingest a PDF / TXT / MD document into the vector store. |
# 1. Create a session
SESSION=$(curl -s -X POST http://localhost:8000/sessions \
-H "Content-Type: application/json" \
-d '{"question_type": "mixed"}' \
| python -c "import sys,json; print(json.load(sys.stdin)['session_id'])")
echo "Session: $SESSION"
# 2. Provide your profile / rΓ©sumΓ©
curl -s -X POST "http://localhost:8000/sessions/$SESSION/message" \
-H "Content-Type: application/json" \
-d '{"message": "I am a mid-level Python backend engineer with 3 years experience in Django and PostgreSQL."}'
# 3. Start the interview
curl -s -X POST "http://localhost:8000/sessions/$SESSION/message" \
-H "Content-Type: application/json" \
-d '{"message": "start"}'
# 4. Answer questions (repeat as needed)
# 5. Request the session summary
curl -s -X POST "http://localhost:8000/sessions/$SESSION/message" \
-H "Content-Type: application/json" \
-d '{"message": "summary"}'
# 6. View the performance heatmap
curl -s "http://localhost:8000/sessions/$SESSION/heatmap"
# 7. Clean up
curl -s -X DELETE "http://localhost:8000/sessions/$SESSION"The WebSocket endpoint (WS /sessions/{id}/live) delivers token-by-token streaming so the interviewer's response appears character-by-character in real time. Both WebSocket and SSE transports emit structurally identical JSON events.
| Event | Fields | When emitted |
|---|---|---|
connected |
session_id |
WS only β handshake confirmed |
thinking |
β | Pipeline started; disable user input |
ttft |
β | First token imminent; create the streaming bubble |
token |
delta: str |
Each LLM chunk (conversationalist only) |
done |
reply: str, session_state: str, evaluation: obj|null |
Turn complete; re-enable input |
error |
message: str |
Pipeline error or invalid session |
import asyncio, json, websockets
async def live_demo():
session_id = "<your-session-id>" # create via POST /sessions first
uri = f"ws://localhost:8000/sessions/{session_id}/live"
async with websockets.connect(uri) as ws:
# Wait for the 'connected' confirmation
print(json.loads(await ws.recv()))
# Send a message
await ws.send(json.dumps({"message": "start", "resume_text": None}))
# Collect events until 'done'
while True:
evt = json.loads(await ws.recv())
if evt["type"] == "token":
print(evt["delta"], end="", flush=True)
elif evt["type"] == "done":
print("\n--- turn complete ---")
break
asyncio.run(live_demo())pytest tests/ -vAll 77 tests mock the IBM Granite API β no real credentials required. The suite covers:
- Model schemas and Pydantic validation
- Every pipeline component with success + fallback paths
- Injection guard (8 known attack vectors + 5 legitimate-answer false-positive checks)
- Dual-call evaluator averaging and borderline-score detection
- Orchestrator routing and off-topic flood protection
- Session manager streaming handler (
handle_message_stream) - WebSocket endpoint: connect, invalid-session protocol, full turn, disconnect preservation
- SSE endpoint: content-type, event shape, evaluation key presence
- Admin auth: missing key (503), wrong key (403), correct key (pass)
Each component has its own system prompt, temperature, and output contract. This makes components independently debuggable β you can unit-test the evaluator in isolation by mocking granite_chat_json, without needing the rest of the pipeline. It also makes the architecture transparent in a project review ("here's the routing logic, here's why evaluation uses a lower temperature than generation").
Both run locally with no external service β ideal for IBM Cloud Lite tier. The embedding model (all-MiniLM-L6-v2, ~80 MB) produces strong semantic similarity for technical and HR text without needing a separate embedding API call.
- Off-topic flood protection: after
MAX_CLARIFY_RETRIESconsecutiveclarifyrouting decisions, the pipeline bypasses the LLM and returns a hardcoded re-engagement message, preventing runaway API spend. - Sparse profile guard:
is_profile_too_sparse()counts null fields; if β₯ 2 are missing the pipeline stops and prompts the user for more detail rather than guessing and generating irrelevant questions. - JSON parsing failures:
_extract_json()inllm_client.pystrips prose wrappers with a regex fallback;granite_chat_json()retries once with a stricter JSON-only reminder, then raisesValueError. - Context window management: only the conversation for the current question (not the full session transcript) is passed to the conversationalist and evaluator. The summarizer receives compact
QuestionEvaluationJSON objects, not raw turns. - Injection / jailbreak guard:
sanitize_candidate_input()pattern-matches 13 known injection vectors before any LLM call. AfterMAX_INJECTION_ATTEMPTSflagged messages the session is suspended and transitioned toSESSION_COMPLETE. - Anti-sycophancy evaluator: calibration anchors in the evaluator system prompt prevent the model from inflating scores for confident-sounding but content-free answers. For borderline scores (any dimension in 3β4), a second evaluator call is made and the scores are averaged.
The current implementation stores sessions in a plain Python dict for simplicity. For multi-instance or persistent deployments, replace _sessions in session_manager.py with a Redis-backed store. The InterviewSession Pydantic model is fully serialisable (model_dump_json() / model_validate_json()), so the swap is straightforward.
- Uses the
chatendpoint with separatesystemandusermessage roles. - There is no native "JSON mode" β the
"Respond with ONLY this JSON"instruction plus_extract_json()defensive parsing is the safety net. - Default model ID:
ibm/granite-3-3-8b-instruct(override withGRANITE_MODEL_IDenv var).
pip install --upgrade chromadb sentence-transformersOn Apple Silicon (M-series) you may need:
pip install --upgrade torch --index-url https://download.pytorch.org/whl/cpu- Verify
WATSONX_API_KEYandWATSONX_PROJECT_IDin your.envfile. - Ensure your IBM Cloud account has access to the watsonx.ai service and the model ID you configured.
- Check that
WATSONX_URLmatches the region where your project lives (us-south,eu-de,jp-tok, etc.).
The agent falls back to general best-practice questions when the vector store returns no results. Run the ingestion step:
python -m interview_trainer.ingest --file data/seed_knowledge_base.txt- Ensure the server is running with
uvicorn(not a different ASGI server that may not support WebSocket). - If you're behind a reverse proxy (nginx, Caddy), confirm WebSocket upgrade headers are forwarded (
Upgrade: websocket,Connection: Upgrade). - The frontend degrades silently to SSE if the WebSocket handshake fails β check the browser console for
[LiveSession]log lines.
Set the key in .env:
ADMIN_API_KEY=<strong random secret> # e.g. openssl rand -hex 32Then pass it as a Bearer token:
curl -X POST http://localhost:8000/admin/ingest \
-H "Authorization: Bearer <your-key>" \
-F "file=@path/to/doc.pdf"Run tests from the repo root so the package is importable:
cd interview-trainer-agent
pytest tests/ -v- Fork the repository and create a feature branch from
main. - Install dev dependencies β the project has no separate dev-requirements file;
requirements.txtis sufficient. - Run the test suite before pushing:
pytest tests/ -v
- Add tests for any new behaviour β aim for a test per public function and per guardrail path.
- Keep commits focused β one logical change per commit, present-tense subject line (e.g.
add session expiry TTL,fix evaluator borderline threshold). - Open a pull request against
mainwith a description of what changed and why.
# Single file
python -m interview_trainer.ingest --file docs/system_design_questions.pdf
# Whole directory
python -m interview_trainer.ingest --dir docs/The admin REST endpoint can also be used programmatically β see the Admin API reference and the ADMIN_API_KEY troubleshooting note for the Bearer-token curl invocation.
MIT