Skip to content

Repository files navigation

Mohammad Nasim — Portfolio & CV Chatbot

Personal portfolio site at mohammadnasim.com with an embedded AI chatbot that answers visitor questions about Mohammad's background, grounded strictly in his CV via retrieval-augmented generation (RAG).

The repo holds both the static frontend (HTML/CSS/JS deployed to Hostinger) and the Python backend (FastAPI + LangChain on Google Cloud Run).


Architecture at a glance

Portfolio Architecture

Live URLs


Tech stack

Layer Stack
Frontend Vanilla HTML5, CSS3, ES6+ JavaScript (no framework)
Backend API Python 3.11, FastAPI, Uvicorn, Pydantic
RAG LangChain (LCEL), OpenAI embeddings, FAISS vector store, PyPDF
LLM OpenAI gpt-5.4-mini (temp 0.2)
Container Docker (python:3.11-slim base, libgomp1 for FAISS)
Hosting Google Cloud Run (backend), Hostinger FTP (frontend)
CI/CD GitHub Actions (frontend FTP deploy on push to main)
Secrets GCP Secret Manager (OpenAI key bound at deploy time)

Repository layout

.
├── index.html                       # Single-page portfolio site
├── styles.css                       # Site styles (dark/light theme, animations)
├── script.js                        # Chat widget + site behaviors
├── image/                           # Hero photo and static assets
│
├── chatbot_backend.py               # FastAPI app + RAG pipeline
├── requirements.txt                 # Python deps
├── Dockerfile                       # Container build for Cloud Run
├── .dockerignore                    # Excludes frontend assets from image
│
├── cv/
│   └── AgenticAI Architect Mohammad Nasim V5.0_Detailed.pdf
│
├── faiss_index/                     # Pre-built vector store (baked into image)
│   ├── index.faiss
│   └── index.pkl
│
├── .github/workflows/
│   └── deploy-frontend.yml          # Hostinger FTP deploy
│
└── .env                             # Local-only: OPENAI_API_KEY (gitignored ideally)

How the chatbot works

Indexing pipeline (runs once per container build)

  1. LoadPyPDFLoader reads the CV PDF and returns one Document per page.
  2. Annotate — a custom annotate_employer() walks the concatenated text top-to-bottom, detecting employer headers (e.g. "State of Rhode Island Department of Transportation", "Atkins UK", "PAEW") and prepending [Employer: <name>] to every line in that section. This guarantees each chunk carries explicit attribution regardless of where the splitter cuts.
  3. SplitRecursiveCharacterTextSplitter with chunk_size=2000, overlap=400, separators ["\n\n", "\n", ".", " ", ""].
  4. EmbedOpenAIEmbeddings produces vectors for each chunk.
  5. PersistFAISS.save_local() writes index.faiss and index.pkl to disk. These files are baked into the Docker image so cold starts skip the embedding step.

Query pipeline (per request)

  1. POST /chat with {"message": "...", "session_id": "..."}.
  2. Retriever pulls top-k=8 chunks from FAISS by cosine similarity to the question.
  3. The chunks are joined into a CV Context: block and passed to the LLM along with the chat history for that session.
  4. The LLM is instructed to answer strictly from context, refuse fabrication, attribute projects only to employers the context explicitly ties them to, and keep responses under 200 words.
  5. Response is returned and appended to the session history (capped at 6 turn pairs).

Why the employer annotation matters

Without it, an 800-char chunk like "Projects: 1. AI-Powered Bridge Monitoring..." had no client name attached, so when retrieved for a Rhode Island question the LLM would misattribute it to Rhode Island. Tagging every line with [Employer: State of Michigan Department of Transportation] makes the attribution travel with the text into every chunk it lands in.


Local development

Prerequisites

  • Python 3.11+ (3.13 works; 3.11 is what Docker uses)
  • A working OpenAI API key with sufficient quota
  • (Optional) gcloud CLI if you plan to deploy

Setup

# Clone (already done if reading this from the repo)
git clone https://github.com/MohammadNasim/Portfolio2.git
cd Portfolio2

# Create a venv and install deps
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt

# Add your OpenAI key
"OPENAI_API_KEY=sk-proj-..." | Set-Content .env

# Run
python chatbot_backend.py
# → http://localhost:8000/health
# → http://localhost:8000/chat (POST)

On first run the FAISS index is loaded from faiss_index/ on disk (built from V5 PDF). Delete that directory to force a rebuild on next startup.

Frontend

The frontend is a single index.html — open it directly in a browser or serve it locally:

python -m http.server 5500
# → http://localhost:5500/index.html

To point the chat widget at a local backend, edit script.js:

const CHATBOT_API_URL = "http://localhost:8000";

(remember to revert before pushing — production points at the Cloud Run URL).


Production deployment

One-time GCP setup (already done for the production project)

$PROJECT = "prjadkvertexairagservice"
$REGION  = "us-central1"

# Enable APIs
gcloud services enable run.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com secretmanager.googleapis.com --project=$PROJECT

# Artifact Registry repo
gcloud artifacts repositories create chatbot --repository-format=docker --location=$REGION --project=$PROJECT

# Cloud Build runner SA (newer GCP projects lack the legacy default)
gcloud iam service-accounts create cloudbuild-runner --project=$PROJECT
gcloud projects add-iam-policy-binding $PROJECT --member="serviceAccount:cloudbuild-runner@$PROJECT.iam.gserviceaccount.com" --role="roles/cloudbuild.builds.builder"
gcloud projects add-iam-policy-binding $PROJECT --member="serviceAccount:cloudbuild-runner@$PROJECT.iam.gserviceaccount.com" --role="roles/artifactregistry.writer"
gcloud projects add-iam-policy-binding $PROJECT --member="serviceAccount:cloudbuild-runner@$PROJECT.iam.gserviceaccount.com" --role="roles/logging.logWriter"

# Cloud Run runtime SA + secret access
gcloud iam service-accounts create chatbot-runtime --project=$PROJECT
gcloud secrets create OPENAI_API_KEY --replication-policy=automatic --project=$PROJECT
"sk-proj-..." | gcloud secrets versions add OPENAI_API_KEY --data-file=- --project=$PROJECT
gcloud secrets add-iam-policy-binding OPENAI_API_KEY --member="serviceAccount:chatbot-runtime@$PROJECT.iam.gserviceaccount.com" --role="roles/secretmanager.secretAccessor" --project=$PROJECT

Build and deploy (every release)

$IMAGE = "us-central1-docker.pkg.dev/prjadkvertexairagservice/chatbot/cv-chatbot:latest"

# 1. Build image via Cloud Build
gcloud builds submit `
  --tag $IMAGE `
  --project=prjadkvertexairagservice `
  --region=us-central1 `
  --service-account=projects/prjadkvertexairagservice/serviceAccounts/cloudbuild-runner@prjadkvertexairagservice.iam.gserviceaccount.com `
  --default-buckets-behavior=REGIONAL_USER_OWNED_BUCKET

# 2. Roll new Cloud Run revision (env vars + secret bindings persist from prior deploy)
gcloud run deploy cv-chatbot --image $IMAGE --region us-central1 --project=prjadkvertexairagservice

Initial deploy used these flags (only needed once; subsequent deploys inherit them):

gcloud run deploy cv-chatbot `
  --image $IMAGE `
  --region us-central1 `
  --platform managed `
  --allow-unauthenticated `
  --service-account chatbot-runtime@prjadkvertexairagservice.iam.gserviceaccount.com `
  --memory 1Gi --cpu 1 `
  --min-instances 0 --max-instances 3 `
  --timeout 300 `
  "--set-env-vars=^##^ALLOWED_ORIGINS=https://mohammadnasim.com,https://www.mohammadnasim.com" `
  --set-secrets "OPENAI_API_KEY=OPENAI_API_KEY:latest"

The ^##^ delimiter is required because ALLOWED_ORIGINS contains commas — gcloud uses commas as the env-var separator by default.

Frontend deploy (automatic)

Pushing to main triggers .github/workflows/deploy-frontend.yml, which FTPs index.html, styles.css, script.js, and image/** to Hostinger's public_html/. Backend-only changes (chatbot_backend.py, Dockerfile, faiss_index/) do not trigger the workflow — only index.html / script.js / styles.css / image/** paths do.


Common operations

Rebuild FAISS index from updated CV

# Replace the PDF in cv/ first, then:
Remove-Item -Recurse -Force faiss_index
$env:OPENAI_API_KEY = "sk-proj-..."
python -c "import chatbot_backend"   # rebuilds + saves to disk

# Commit, then redeploy (see "Build and deploy" above)
git add faiss_index chatbot_backend.py cv/
git commit -m "Update CV and rebuild FAISS"

Rotate the OpenAI key on Cloud Run

# Add new version to Secret Manager
printf '%s' 'sk-proj-NEW_KEY' | gcloud secrets versions add OPENAI_API_KEY --data-file=- --project=prjadkvertexairagservice

# Pin the service to the new version (this also forces a new revision)
gcloud run services update cv-chatbot --region us-central1 --project=prjadkvertexairagservice --update-secrets "OPENAI_API_KEY=OPENAI_API_KEY:<N>"

Inspect current deployment state

# Active revision and config
gcloud run services describe cv-chatbot --region us-central1 --project=prjadkvertexairagservice

# Image history in Artifact Registry
gcloud artifacts docker images list us-central1-docker.pkg.dev/prjadkvertexairagservice/chatbot/cv-chatbot --include-tags --sort-by=~UPDATE_TIME

# Recent Cloud Build runs
gcloud builds list --project=prjadkvertexairagservice --limit=10

# Live smoke test
$body = @{ message = "Who is Mohammad?"; session_id = "test" } | ConvertTo-Json
Invoke-RestMethod -Uri https://cv-chatbot-679776566774.us-central1.run.app/chat -Method Post -ContentType "application/json" -Body $body

Adjust system prompt or chunking

Edit chatbot_backend.py:

  • SYSTEM_MSG (around line 90) — instructions to the LLM
  • EMPLOYER_ANCHORS and annotate_employer() — attribution preprocessing
  • RecursiveCharacterTextSplitter(...) args — chunk size and overlap
  • vectorstore.as_retriever(search_kwargs={"k": 8}) — number of chunks retrieved

Then rebuild FAISS (delete faiss_index/ and re-run) and redeploy.


API reference

GET /health

{"status": "ok", "chunks_indexed": 23}

POST /chat

Request

{ "message": "What is Mohammad's current role?", "session_id": "user-123" }

Response (200)

{
  "answer": "Mohammad is currently...",
  "session_id": "user-123"
}

Error responses

  • 400 — empty message
  • 500 — upstream LLM/embedding error (passes through OpenAI error detail)

Sessions are in-memory only — they reset when a Cloud Run instance dies or scales to zero. Each session retains the last 6 human/AI turn pairs.

DELETE /session/{session_id}

Clears history for the given session.


Configuration

Environment variables (Cloud Run)

Variable Source Purpose
OPENAI_API_KEY Secret Manager OpenAI API authentication
ALLOWED_ORIGINS --set-env-vars Comma-separated CORS allowlist
PORT Cloud Run Injected automatically; uvicorn binds here

Local .env

OPENAI_API_KEY=sk-proj-...
ALLOWED_ORIGINS=http://localhost:5500

.env is loaded by python-dotenv only — Cloud Run ignores it and uses the Secret Manager binding instead.


Troubleshooting

Symptom Likely cause Fix
Chat widget shows "Could not reach the AI backend" CORS blocked or Cloud Run cold-starting Verify ALLOWED_ORIGINS includes the calling origin; first request after idle takes 5–20s
/chat returns insufficient_quota 500 OpenAI billing or rate limit Top up OpenAI billing or check usage caps
Chatbot says "Architect" instead of "Developer" Old image still deployed Run gcloud builds submit + gcloud run deploy again
gcloud builds submit fails with NOT_FOUND Cloud Build default SA missing Use --service-account=...cloudbuild-runner... + --default-buckets-behavior=REGIONAL_USER_OWNED_BUCKET
Cross-employer hallucination in answers Chunk boundaries split client from project list Verify annotate_employer() is running before splitting; check that anchor strings match the PDF text
Frontend not updating after push Workflow didn't trigger Confirm push touched index.html/script.js/styles.css/image/** (only those paths fire the deploy)

Project resource map

For day-to-day operations, here's where each piece of state lives:

Resource Location
GCP project prjadkvertexairagservice (project number 679776566774)
Cloud Run service cv-chatbot in us-central1
Docker image us-central1-docker.pkg.dev/prjadkvertexairagservice/chatbot/cv-chatbot:latest
Build service account cloudbuild-runner@prjadkvertexairagservice.iam.gserviceaccount.com
Runtime service account chatbot-runtime@prjadkvertexairagservice.iam.gserviceaccount.com
OpenAI key secret OPENAI_API_KEY in Secret Manager
Billing account 0102B6-A64631-55DF6A ("Nasim Portfolio")
Frontend host Hostinger, deployed via GitHub Actions FTP
Source repo https://github.com/MohammadNasim/Portfolio2

License & contact

This is a personal portfolio — code is provided as-is for reference. The CV PDF and likeness are property of Mohammad Nasim.

For questions: mnasimsiddiqui@gmail.com · LinkedIn

About

Portfolio2

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages