Back to side projects

Personal build

Portfolio RAG Assistant

Cloudflare Workers Vectorize Workers AI Hybrid retrieval

A recruiter-facing chat on this site that answers from indexed portfolio chunks — not from the model’s memory — and returns source links. The floating assistant on this page is the live system.

Role
Solo developer
Timeline
2026
Stack
Workers AI, Vectorize, KV
Outcome
Cited Q&A from site content
Portfolio RAG Assistant chat panel with cited answers Portfolio RAG Assistant chat panel with cited answers
Portfolio RAG architecture: chat widget, Worker, Vectorize, Workers AI, and indexing pipeline Portfolio RAG architecture: chat widget, Worker, Vectorize, Workers AI, and indexing pipeline

Click diagram to zoom

The Problem

Recruiters and hiring managers will not read every case study, resume PDF, and experience tab. A site-wide chat is useful only if it stays grounded: inventing an employer, a date, or a technology is worse than having no assistant at all.

The goal was a small, edge-hosted RAG that answers from this site’s own content, cites the pages it used, and refuses when the index has nothing relevant — without adding a frontend framework or a database to a static GitHub Pages portfolio.

Design

The site stays static. A separate Cloudflare Worker owns retrieval and generation so Pages never talks to Vectorize or Workers AI directly. Indexing is a local script, not a runtime crawl.

  • DOM-aware extractors: Cheerio pulls timeline items, skill groups, project cards, and case-study sections instead of dumping whole pages into one blob.
  • Structured chunk prefixes: Each chunk is labelled like [Experience | Role (2024)] with Role and Employer fields so the model can cite specifics.
  • Hybrid retrieval: Cosine search is reranked with a light keyword boost so exact names (employers, product titles) still surface when embeddings are close but not first.
  • Single-turn: Each question is independent. No chat history is sent to the LLM, which keeps cost and hallucination surface small.
  • Guarded endpoints: CORS allowlist, KV rate limit (10 requests per IP per minute), and a bearer INDEX_SECRET on /api/index. Turnstile is wired but currently off.

The same Worker, indexer, and widget are documented as a portable copy-paste stack in docs/RAG-INTEGRATION.md so the pattern can move to another app without rewriting the pipeline.

Architecture

A visitor asks a question in chat-widget.js. The widget posts to https://rag.builtbyroger.com/api/chat (with a workers.dev fallback). The Worker rate-limits, embeds the question, queries Vectorize, reranks, and asks the instruct model to answer only from those passages.

  • Embed: @cf/baai/bge-base-en-v1.5 — 768 dimensions, cosine similarity on index portfolio-rag-index.
  • Retrieve: Vectorize TOP_K = 8, keyword boost, drop matches below MIN_MATCH_SCORE = 0.65 (fallback to top 3 if none pass). Top 6 chunks become context; up to 3 citation URLs are returned.
  • Generate: @cf/meta/llama-3.2-3b-instruct, 512 tokens. The system prompt is recruiter-friendly and forbids invented employers, dates, or stack details.
  • Chunking: About 1800 characters with 200 overlap, preferring sentence breaks. PDFs are treated as one section, then split by length — not by page.
  • Sources: Home, academic, enterprise and personal case studies, two resume PDFs, four showcase data files, and two Mermaid architecture diagrams.

Indexing is a separate path: npm run rag:index reads those files, batches 10 chunks to /api/index, and the Worker embeds and upserts into Vectorize. There is no D1 store — metadata lives on the vectors themselves.

Core stack: Vanilla JS chat widget · Cloudflare Worker · Workers AI · Vectorize · KV rate limit · Cheerio + pdf-parse indexer · Wrangler deploy.

Limitations

This is a small, honest RAG — useful for grounded site Q&A, not a general assistant.

  • No conversation memory: Follow-up questions do not see prior turns.
  • No streaming: The Worker returns the full answer; the widget fakes typing on the client.
  • Small model: Llama 3.2 3B is fast and cheap on Workers AI, but weak at multi-hop reasoning.
  • Stale vectors: Re-index upserts by stable chunk IDs. After a page restructure, orphan vectors remain until the Vectorize index is deleted and recreated.
  • PDF chunking: Resumes are length-split, not page-aware, so a citation may point at the whole PDF.
  • No metadata database: Everything retrievable lives in Vectorize metadata (truncated to 8000 characters).
  • Rate limit: 10 requests per IP per minute will block heavy testing.
  • Turnstile off: Bot protection is optional and currently disabled on the widget.
  • Not the Cycle Health RAG: The health Q&A on the Cycle Health PWA is a separate Worker. This page describes only the portfolio assistant.

The assistant is live on every public page of this site. Answers stay short, cite source sections, and fall back to “I don’t have that information in Roger’s portfolio” when the index does not support the question.

Personal project — full stack, architecture, and repository details shared openly.

Enlarged view