Software Engineer
AI & ML Projects
End-to-end ML/RAG work with reproducible runs: classification baselines, retrieval-augmented workflows, evaluation artifacts, and clear “what to review” paths.
Back to ProjectsRecommended review order
- John's AI Lab: three live Google Gemini agents — streamed tool calls, grounding, structured output. Try it in the browser.
- AI Lab on GKE: the same agents on Kubernetes — keyless Vertex AI via Workload Identity, all in Terraform.
- Keyless Pipeline: Terraform runs in CI, not on a laptop — plan on PR, apply on merge, live on Cloud Run.
- Job Search Agent: agentic RAG + human-in-the-loop, deployed to AWS with Terraform.
- RAG Mini Chat: staged build + retrieval logs.
- Next-Day Direction Classifier with RAG: time-split evaluation + model insights.
- Fake News Learning with ML: baseline -> improvements -> final evaluation.
- Finance Spending Coach: transaction analytics + budgeting + coach-style insights.
- Sales Inventory Dashboard: dashboarding + KPI reporting.
John's AI Lab Live
Three Google Gemini agent patterns, live on Vercel: a tool-calling review analyst with its tool calls streamed to the UI as they run, a grounded portfolio concierge, and schema-validated playable math missions.
- Stage 1 — App Review Analyst: a function-calling loop over live App Store data (iTunes Search & RSS); every tool call and the answer stream to the browser as NDJSON events.
- Stage 2 — Portfolio Concierge: grounded Q&A with per-agent tool scoping and a curated profile tool — recommends the right app for a visitor.
- Stage 3 — Math Mission Maker:
responseSchema-constrained JSON, re-validated server-side (the math itself is checked), rendered as a playable Cosmic Cadets-style mission.
- Key stays server-side: the Gemini key lives only in a Vercel
env var / local
.env.local; the browser receives answers and a tool-call trace, never credentials. - Streaming protocol: the serverless agent emits NDJSON events
(
tool-start/tool-end/delta/done) so the UI shows each tool call the moment it runs, then types the answer out live. - Per-agent tool kits: each agent declares its own tool subset (enforced server-side); tools hit Apple's public iTunes Search & RSS APIs — live data, no auth, no scraping.
- Validated generation: missions come back as
responseSchemaJSON and are re-validated in code — including evaluating machine-checkable equations against the marked answer, with one corrective retry fed by the validator's errors. - Hardened for a public demo: per-IP rate limiting, request caps, prompt-injection boundary for untrusted review text, and client-disconnect aborts so abandoned requests stop spending quota.
- Review process: three multi-agent adversarial review passes (independent reviewers, refute-by-default verification) — 32 confirmed findings fixed, including a schema bug that would have broken the first production call.
- $0 stack: Gemini API free tier + Vercel Hobby + public Apple APIs — no billing account anywhere.
AI Lab on GKE IaC
The same Gemini agents, redeployed the way an enterprise would run them: a GKE Autopilot cluster where the pod calls Vertex AI through Workload Identity — no API key in the image, the manifests, or the Terraform. Provisioned, verified, and destroyed in one session.
- Keyless auth chain: pod → annotated Kubernetes SA → Google SA (
ai-lab-runtime, "No keys") →roles/aiplatform.user→ Gemini. - All Terraform: APIs, Artifact Registry, Autopilot cluster, IAM bindings, namespace, deployment and LoadBalancer — 13 resources from one
apply. - No gcloud, no kubectl: the
kubernetesprovider authenticates from thegoogleprovider's token; images push via a_json_keyDocker login. - Proven, then torn down:
"auth":"adc"and a live streamed agent turn captured, thendestroythe same day — a cost-controlled demo, not an idle cluster.
- Why keyless matters: the app supports three auth modes
(AI Studio key, Vertex key, and ADC). In the cluster it runs ADC, so the
credential is a short-lived token from the metadata server — nothing to
leak, rotate, or commit.
"auth":"adc"in the health response is the runtime proof. - Identity chain: the Kubernetes SA carries an
iam.gke.io/gcp-service-accountannotation; the Google SA grantsroles/iam.workloadIdentityUserback toPROJECT.svc.id.goog[ai-lab/agent]. Onlyroles/aiplatform.useris granted — least privilege. - Terraform-only Kubernetes: the
kubernetesprovider is configured from the cluster's endpoint and thegoogleprovider's access token, so no kubeconfig, nogcloud, and nokubectlever run. - Ordering the review caught: the Workload Identity pool
only exists once the first cluster does, so the IAM binding needs an
explicit
depends_on— otherwise a fresh-project apply fails with "Identity Pool does not exist". - Endpoint gotcha: Vertex serves the Gemini-3 family from
the
globalendpoint, so the location is a separate variable from the infrastructure region — a regional pin returns "Publisher Model not found". - Container: Node 22 slim, production deps only, non-root,
/healthzliveness and readiness probes, SIGTERM drain, and a request-scoped error guard so a client abort can't take the process down. - Cost discipline: Autopilot bills per pod, so the whole
exercise was build → verify → screenshot →
destroyin one session, for well under a dollar. The artifact is the code, not a running cluster.
Keyless Pipeline CI/CD Live
Project 2 (AI Lab on GKE) removed the API key from the container. This removes it from the laptop: Terraform runs in GitHub Actions, authenticated by a short-lived OIDC token instead of a service-account key — plan on every PR, apply on merge to main.
- Trust chain, no stored secret: CI job → OIDC token → WIF pool gated on
assertion.repository→github-deployer→apply. GitHub holds no secret; the workflow references none. - Reviewed like code: the PR check posts the plan to the job summary; merging applies it and prints
service_url. No terminal in the loop. - Least privilege, and it cost something: four narrow grants instead of Project 2's
Editor. The first apply failed —run.admindoesn't cover reading the image, because Cloud Run validates it as the calling principal. - Designed to stay up: Cloud Run scales to zero, so idle is ~$0 — a clickable endpoint instead of screenshots of something deleted.
- Why federation, not a key: GitHub mints a short-lived OIDC
token asserting "I am a job in
jdoan5/Terraform". The WIF provider'sattribute_conditionrejects tokens from any other repository at the door, so a forked or copied workflow cannot deploy. - Split ownership:
bootstrap/is applied once by a human and owns the identity plane — state bucket, registry, WIF, both service accounts. CI owns exactly one thing: the Cloud Run service. The deployer can never grant itself IAM. - The failure that taught the most: the first apply died with
artifactregistry.repositories.downloadArtifacts denied.run.adminsounds sufficient for "deploy a Cloud Run service" but isn't — two principals need registry access: the deployer (Cloud Run validates the image as the caller) and the Cloud Run service agent (which actually pulls it). Project 2 never hit this: GKE nodes pull with a different identity. Both grants are in the Terraform, scoped to one repository. - Remote state: Projects 1 and 2 kept state next to the code.
Here it lives in a versioned GCS bucket with locking, injected at
initvia partial backend config rather than hardcoded — so CI and laptop share one source of truth. - No key anywhere, verified: bootstrap ran on ADC, Docker
authenticated via
gcloud auth configure-docker, CI runs on OIDC, and the container reaches Gemini asai-lab-runtime. The live endpoint returns{"ok":true,"hasKey":true,"auth":"adc"}. - Infrastructure quirk worth knowing: Google's frontend
intercepts
/healthzon the publicrun.appURL and returns its own 404 — while the startup probe on that same path passes, because Cloud Run runs probes inside the container network. Use/api/agentas the public liveness check. - Cost model, inverted from Project 2: no cluster and no load balancer, so there is nothing to bill while idle. Cloud Run scales to zero; the bucket and registry are pennies a month. This one is meant to stay up.
Job Search Agent
Agent that reads a job posting, retrieves matching evidence from a canonical inventory of my real experience, and drafts tailored resume bullets — with a human approval gate before anything is saved. Grounding is enforced in code: a validator rejects any bullet citing evidence retrieval never returned, so the agent cannot invent experience.
- Stage 1 — Pipeline + CLI: posting extraction, an agentic BM25 evidence loop, grounded drafting, and a human approval gate (33 unit tests).
- Stage 2 — Browser UI: Streamlit Approve/Reject replaces the terminal prompt; nothing is saved before approval.
- Stage 3 — Tracker: approved drafts ingest into SQLite with a status board and a recurring-gap ranking.
- Stage 4 — Docker + auth: both apps behind a fail-closed password gate in a non-root image.
- Stage 5 — Terraform on AWS: ECR, ECS Fargate, ALB, Secrets Manager, and least-privilege IAM — deployed live, verified, then destroyed on purpose: IaC recreates it in minutes, and an idle demo shouldn't bill by the hour.
- Stage 6 — Live demo: the same portal re-platformed to Streamlit Community Cloud — $0 hosting behind the fail-closed password gate (demo password available on request).
- Stage 1 – Pipeline: four stages — structured extraction of posting requirements, an agentic evidence loop where the model drives a BM25 search tool (written from scratch, stdlib only), grounded drafting, then a human approval gate. 33 unit tests.
- Anti-fabrication by construction:
_validate_grounding()rejects any drafted bullet citing evidence IDs retrieval never returned — prompts request behavior, validators guarantee it. - Stage 2 – Browser UI: Streamlit front-end where Approve/Reject buttons replace the terminal prompt; nothing is written to disk before approval.
- Stage 3 – Tracker: ingests each approved draft's JSON sidecar into SQLite (the filesystem is the integration contract — zero coupling to Stages 1–2), with a status board and a recurring-gap ranking that answers “what should I learn next?”. 13 unit tests.
- Stage 4 – Container: both apps behind a password gate that fails closed, non-root image, state on a mounted volume, Docker Compose + Fly.io configs.
- Stage 5 – Infrastructure as code: Terraform provisioning ECR,
ECS Fargate, an Application Load Balancer, Secrets Manager and least-privilege IAM
in
us-east-1— deployed live on AWS. - Review process: every stage went through multi-agent adversarial review (independent reviewers, then refute-by-default verification). 100+ confirmed defects fixed pre-deploy — including catching that AWS App Runner lacks the WebSocket support Streamlit requires, before any infrastructure was paid for.
RAG Mini Chat
Staged RAG build (single-doc → multi-doc → logging) with grounding and run artifacts so answers are debuggable.
- Stage 1 — Single-Doc RAG: chunk one document, retrieve, and answer with grounded prompting so replies stay inside the retrieved context.
- Stage 2 — Multi-Doc RAG: index multiple documents with top-k retrieval across sources and smarter context packing.
- Stage 3 — Logging & Polish: run logs trace query → retrieved chunks → context → answer, so every reply is debuggable.
- Stage 1 – Single-Doc RAG: document chunking + basic retrieval over one source, grounded prompting, and a simple Q&A loop to confirm answers stay within the retrieved context.
- Stage 2 – Multi-Doc RAG: indexes multiple documents with top-k retrieval across sources and more robust context packing, improving coverage over the Stage 1 baseline.
- Stage 3 – Logging & Polish: run logs capturing query → retrieved chunks → selected context → final answer, so a reviewer can see why the model answered as it did.
- Re-run flow:
python ingest.py→python chat.py(each stage folder has exact commands).
Next-Day Prediction Classifier with RAG
Direction classifier that augments features with retrieved context and logs training/evaluation outputs for traceability and debugging.
- Stage 1 — Baseline Training + Evaluation: next-day direction classifier on trend/volatility/volume features with time-based splits (no shuffling), saved run artifacts, and a Streamlit dashboard.
- Stage 2 — RAG Augmentation + Insights: three models compared across a ticker list (best picked by macro-F1), with a TF-IDF “run assistant” that answers questions from the saved evaluation artifacts.
- Stage 1 – Trend-based baseline: single-ticker next-day direction classifier on
Yahoo Finance OHLCV data with trend/volatility/volume features, time-based train/val/test splits
(no shuffling), saved run artifacts (
model.joblib, metrics, report), a local RAG stub that answers questions from run files, and a Streamlit dashboard. - Stage 2 – Multi-model, multi-stock: trains Logistic Regression, Random Forest, and Gradient Boosting across a ticker list, selects the best by macro-F1, and writes per-run evaluation artifacts (val/test metrics, confusion matrix, classification report). A TF-IDF RAG “run assistant” retrieves over those artifacts, with a dashboard for run browsing + interpretability.
- Educational project — not investment advice; emphasis is on honest evaluation and traceability.
- Re-run flow: train/evaluate from
src/, thenstreamlit run app.pyto browse runs.
Fake News Learning with ML
Three-stage NLP build: baseline pipeline → improved modeling → final evaluation and error review, with saved artifacts and reproducible runs. Models are trained on the Kaggle Fake News Detection dataset.
- Stage 1 — Baseline Pipeline: text cleaning + TF-IDF + a baseline classifier with first saved metrics.
- Stage 2 — Model Comparison: n-grams, regularization, and model comparisons to beat the baseline.
- Stage 3 — Final Evaluation: threshold selection, confusion matrix, and error-review artifacts.
- Dataset: Kaggle — Fake News Detection.
- Stage 1: text cleaning + TF-IDF + baseline classifier.
- Stage 2: tuning/iterations (n-grams, regularization, model comparisons).
- Stage 3: final metrics + confusion matrix + error review artifacts.
- Re-run flow:
python train.py→python evaluate.py(update to match repo).
Finance Spending Coach
Spending analytics that grow into a grounded AI coach — a staged FastAPI + ML build that ends with RAG-backed budgeting tips.
- Stage 1 — Scoring API: FastAPI endpoint serving model scores with Swagger/OpenAPI docs.
- Stage 2 — Front-end + Docker: a UI on top of the API, packaged as a Docker image.
- Stage 3 — AI Coach (RAG): grounded tips from a local Markdown knowledge base + embeddings.
- Stage 1 – ML service: FastAPI + scikit-learn logistic regression that scores a single monthly spending profile and returns overspending risk + rule-based suggestions.
- Stage 2 – Front-end: lightweight HTML/CSS UI that posts JSON to the API, visualizes risk level, and shows “coach-style” explanations on top of the score.
- Stage 3 – Local KB & RAG: markdown budgeting notes are embedded, indexed on disk, and retrieved to ground additional advice in concrete best-practice snippets.
- DevOps: Dockerized service with a one-command build/run flow and auto-generated Swagger docs for all stages.
- Privacy & reproducibility: uses synthetic transactions by default so the project can be demoed safely in a portfolio showcase.
Sales Inventory Dashboard
End-to-end sales + inventory reporting on a synthetic retail CSV dataset created for learning purposes: curated tables, KPI rollups, and trend views with filters for fast stakeholder-style review. Includes reproducible refresh steps and validation checks, with no real customer data used.
- Synthetic CSV dataset (store, item, daily sales, and stock) designed for analysis and learning — no production data.
- ETL/cleanup into curated tables (sales, inventory, product, calendar/time).
- Metrics layer: KPIs (revenue, units, margin, stock coverage) plus trend + breakdown views.
- Dashboard layer with filters and drilldowns via Streamlit.
- Saved outputs: KPI summary tables, charts, and refresh/validation instructions for repeatable runs.