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
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.
| 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
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
# Terminal 1 — backend
docker compose up --build # :8000, defaults to an offline LLM stub
# Terminal 2 — portfolio
python -m http.server 5500 # http://localhost:5500No 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
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) |
$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
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
Deployed manually by the author, never by an AI agent. Two independent steps — see docs/deployment.md:
- Backend → AWS: budget alert → DynamoDB table → IAM role → Lambda
.zip→ Function URL → verification checklist → teardown. - Portfolio → GitHub Pages: point
assistant-config.jsat the deployed Function URL, then copy the files toDestivano/destivano.github.ioand push — the same publishing step that repository has always used.
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.
- Citation precision degrades in three places. Because the restored
static page predates
data/*.jsonand 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.
MIT for the code. Portfolio content, images and institutional logos are not covered — they belong to their respective owners.