Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🤖 Personal AI Assistant Portfolio

live site CI status python 3.12 FastAPI AWS Lambda monthly cost $0.00-$0.05 413 tests passing MIT license

The portfolio of Mohamed Amine Arous — AI Engineer / Generative AI Engineer — left exactly as it's always been, plus a floating assistant on top that answers questions about his actual work. Grounded in his own career data. Citations back to the source. No vibes-based hallucinating.

Ask it "what GenAI work has he shipped?" or "what did he do at CEA?" and get an answer sourced from real data — not generic recruiter-bait copy.

Note

Status: 🟢 live. The backend runs on AWS Lambda, the widget is deployed on destivano.github.io, and both are verified end-to-end against the real model — not just "should work locally." Deployment is always a manual step the author runs from docs/deployment.md; no AI agent has ever touched AWS or pushed to the live portfolio repo.

Jump to: How it's built · Repo layout · Run it locally · Quality, measured · Cost · Security · Deployment · Limitations


🧱 Two layers, deliberately

Layer 1 — the portfolio. Plain static HTML/CSS/JS. No build step, no framework, no dependency to go stale. This is the site that's always lived at destivano.github.io, restored verbatim after an earlier iteration of this project rewrote it in Next.js — see ADR 0008 for why that detour got reverted.

Layer 2 — the assistant. A floating widget (assistant-widget.js/css) bolted on top, calling a separately deployed AWS backend over HTTPS. Delete the three widget files and their references in index.html, and the site goes right back to what it was before — the portfolio never depends on the assistant existing.

  • 🔎 Ask it about his background. It retrieves the relevant facts, cites them as chips that jump straight to the source on the page, and refuses rather than guesses when the corpus doesn't support an answer.
  • 💸 Cheap on purpose. Structured lookups ("how do I contact him?") are answered from data with zero LLM calls. Everything else goes through hybrid retrieval before one bounded LLM call — and the model, not a keyword score, decides whether it can actually answer.

🏗️ How it's built

Layer Choice Why
Portfolio Static HTML/CSS/JS, no build step GitHub Pages-deployable exactly as it always was — ADR 0008
Widget Vanilla JS, no framework, no dependencies One small file talking to one REST endpoint doesn't need a build pipeline
Assistant corpus data/*.json, one canonical source The assistant can't contradict itself — ADR 0001
Retrieval Hybrid: BM25 (from scratch) + static embeddings, fused with RRF p50 0.55 ms, no vector DB — ADR 0004
Embeddings model2vec potion-base-8M, reimplemented in ~40 lines of numpy No torch/onnxruntime shipped, verified 1.000000 cosine vs. reference — ADR 0003
Understanding The LLM reads the question; retrieval only supplies evidence A BM25 gate deciding scope refused "what projects did he do" — ADR 0012
Routing A small deterministic layer for exactly-repeatable answers $0 for greetings and canonical lookups, and it never refuses — ADR 0007
Generation Groq (openai/gpt-oss-120b), behind a provider interface Swappable by config, not code — ADR 0005
Backend FastAPI, deployed as a Lambda .zip behind a Function URL 103 MB unzipped, no container image, no VPC — ADR 0002
Hosting Portfolio on GitHub Pages, backend on Lambda — two origins No CloudFront, no S3; CORS makes the boundary explicit

Full write-up → docs/architecture.md

📁 Repo layout

index.html, styles.css, script.js      the portfolio — hand-authored, unchanged
assistant-widget.js/css                the floating assistant widget
assistant-config.js                    the one config value: backend API URL
data/                                   canonical content for the ASSISTANT
backend/                                FastAPI app: RAG, LLM providers, tools, router, quota
evaluation/                             hand-labelled golden set + two evaluation harnesses
scripts/                                build_index.py, package_lambda.sh, optimize_images.py
docs/                                   architecture, evaluation, security, cost-control, deployment, ADRs
archive/                                superseded iterations, not served — including the
                                         retired Next.js frontend

🚀 Run it locally

# Terminal 1 — backend
docker compose up --build              # :8000, defaults to an offline LLM stub

# Terminal 2 — portfolio
python -m http.server 5500             # http://localhost:5500

No API key required — the backend defaults to a deterministic offline LLM stub, so the whole stack runs end-to-end with zero network dependency. Drop a real GROQ_API_KEY in a root .env (see .env.example) for real generation.

Full setup, how to edit content, and how to add a new question type → docs/development.md

📊 Quality, measured

Two harnesses, because they answer different questions. run_eval.py is free and proves the structural pieces — retrieval, routing, guards — never regress. run_live_eval.py costs real Groq tokens and proves the model's own answers are actually good. Every number below is from a real run, not an estimate — full detail and the ablations behind each design decision → docs/evaluation.md.

🟢 Live — real questions, real model, real answers
Metric Result
Legitimate questions refused 0 / 21 — the number that matters most
Answer accuracy 15/15 (100%)
Grounded-answer accuracy (≥1 valid citation) 15/15 (100%)
Hallucination rate 0 / 18 (0%)
Honest "that's not in my sources" 3/3 (100%)
Polite scope reply on unrelated questions 4/4, zero general-knowledge leakage
Prompt-injection success rate 0/5 (0%)
Zero-cost responses (no LLM call needed) 6/33 (18%)
⚪ Structural — free, runs in CI, every commit
Metric Result
Legitimate questions incorrectly refused 0 / 99
Real-world paraphrases handled on topic 25/25 (100%)
RAG recall@5 / @10 0.961 / 1.000
Structured-tool routing / tool-selection accuracy 14/14 (100%)
Identity routing (Mohamed vs. the assistant itself) 13/13 (100%)
Abuse guards (injection + misuse) 11/11
Conversational messages, all answered at $0 15/15
Retrieval latency (p50 / p95) 0.55 ms / 0.95 ms
Backend tests 413 passing, ruff clean
Lambda package 103 MB unzipped / 43 MB zipped (under the 250 MB .zip limit, verified on Linux)

💰 Cost

$0.00–$0.05/month, expected. No always-on compute, no VPC (so no NAT Gateway — the single biggest cost trap for a Lambda-behind-a-database design), no managed database, no ECR, no CloudFront, no S3 — hosting the portfolio costs $0 because GitHub Pages hosts it, not AWS. Full breakdown, the risks that could change that, and the monthly check commands → docs/cost-control.md

🔐 Security

Prompt-injection and off-purpose-use guards, server-enforced quotas (session / per-IP-minute / per-IP-day / global-daily), citation verification that drops fabricated markers, salted+hashed IPs with no raw address ever logged or stored, and CORS scoped to the exact portfolio origin (the two really are different origins — see ADR 0008) → docs/security.md

📦 Deployment

Deployed manually by the author, never by an AI agent. Two independent steps — see docs/deployment.md:

  1. Backend → AWS: budget alert → DynamoDB table → IAM role → Lambda .zip → Function URL → verification checklist → teardown.
  2. Portfolio → GitHub Pages: point assistant-config.js at the deployed Function URL, then copy the files to Destivano/destivano.github.io and push — the same publishing step that repository has always used.

🔗 Relationship to the original portfolio

This project started as a copy of destivano.github.io — files and git history. That repository is read-only source material and has never been modified by work here; the two repositories evolve independently, enforced by a pre-push hook that blocks any push whose remote URL contains destivano.github.io. index.html, styles.css and script.js in this repository are that original site, restored to the root after a detour through a Next.js rewrite now archived at archive/nextjs-frontend-retired/ — see ADR 0008.

⚠️ Known limitations

  • Citation precision degrades in three places. Because the restored static page predates data/*.json and wasn't generated from it, publications (no dedicated section exists — anchors point to where each paper is actually mentioned) and skill categories (the page groups more categories into one row than the data model defines) fall back to a less precise anchor than experience/project/volunteering citations get. Honest degradation, not a bug — see ADR 0008.
  • No response streaming. The Lambda Function URL / Mangum combination doesn't support it cleanly; answers appear in one paint after a "thinking…" indicator. See ADR 0006.
  • Answer quality isn't scored automatically. Retrieval, routing, gating and guards are measured against ground truth; free-text answer quality needs human judgement or an LLM-as-judge with no established validity at this project's size, so it isn't claimed as a number. Grounding is instead enforced structurally — see Hallucination controls.
  • No persistent visitor conversations, by design — sessions are client-generated and exist only for the browser tab (sessionStorage, not a server-side store).
  • The corpus is immutable per deployment. A content edit needs a rebuild and redeploy, not a live update — acceptable for a portfolio that changes a few times a year.

📄 License

MIT for the code. Portfolio content, images and institutional logos are not covered — they belong to their respective owners.

About

AI portfolio assistant — RAG over real career data, cited answers, serverless on AWS for pennies a month

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages