PROJECTS

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 Projects

Recommended review order

  1. John's AI Lab: three live Google Gemini agents — streamed tool calls, grounding, structured output. Try it in the browser.
  2. AI Lab on GKE: the same agents on Kubernetes — keyless Vertex AI via Workload Identity, all in Terraform.
  3. Keyless Pipeline: Terraform runs in CI, not on a laptop — plan on PR, apply on merge, live on Cloud Run.
  4. Job Search Agent: agentic RAG + human-in-the-loop, deployed to AWS with Terraform.
  5. RAG Mini Chat: staged build + retrieval logs.
  6. Next-Day Direction Classifier with RAG: time-split evaluation + model insights.
  7. Fake News Learning with ML: baseline -> improvements -> final evaluation.
  8. Finance Spending Coach: transaction analytics + budgeting + coach-style insights.
  9. Sales Inventory Dashboard: dashboarding + KPI reporting.
John's AI Lab — three Gemini agents: App Review Analyst with a streamed tool-call trace, Portfolio Concierge, and Math Mission Maker, live on Vercel

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.
Google Gemini Agentic tool use Streaming Structured output Angular 22 Vercel

  • 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 responseSchema JSON 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 kubernetes provider authenticates from the google provider's token; images push via a _json_key Docker login.
  • Proven, then torn down: "auth":"adc" and a live streamed agent turn captured, then destroy the same day — a cost-controlled demo, not an idle cluster.
GKE Autopilot Workload Identity Terraform Vertex AI (Gemini) Artifact Registry Docker

  • 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-account annotation; the Google SA grants roles/iam.workloadIdentityUser back to PROJECT.svc.id.goog[ai-lab/agent]. Only roles/aiplatform.user is granted — least privilege.
  • Terraform-only Kubernetes: the kubernetes provider is configured from the cluster's endpoint and the google provider's access token, so no kubeconfig, no gcloud, and no kubectl ever 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 global endpoint, 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, /healthz liveness 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 → destroy in 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.admin doesn'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.
Terraform GitHub Actions OIDC Workload Identity Federation Cloud Run GCS remote state Vertex AI (Gemini)

  • Why federation, not a key: GitHub mints a short-lived OIDC token asserting "I am a job in jdoan5/Terraform". The WIF provider's attribute_condition rejects 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.admin sounds 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 init via 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 as ai-lab-runtime. The live endpoint returns {"ok":true,"hasKey":true,"auth":"adc"}.
  • Infrastructure quirk worth knowing: Google's frontend intercepts /healthz on the public run.app URL 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/agent as 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).
Python Claude API Agentic tool use RAG / BM25 Structured outputs Streamlit Docker Terraform AWS ECS

  • 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.
Python RAG Grounding Run logs

  • 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.
Python Retrieval Embeddings Evaluation

  • 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/, then streamlit run app.py to 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.
Python NLP scikit-learn TF-IDF

  • 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.
Python FastAPI scikit-learn Docker RAG / KB

  • 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.

Python Pandas Dashboard BI / Reporting

  • 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.