← Master Index
Vol. 23 Module 23.1 Lecture

AI Resume Builder

Capstone Projects

How This Lesson Fits the Module & Volume

The clone chats; the PDF chatbot grounds in files. This capstone is structured generation + human approval: a JSON resume as source of truth, an LLM that rewrites (tone, bullets, JD alignment), and an export that must not fire until a human clicks approve. It is Vol. 21 Document AI + Vol. 13 structured output / JSON prompting, with Vol. 15 HITL on the only irreversible step—download / apply.

ATS (applicant tracking systems) are not one algorithm. This lecture teaches caveats, not fake “98% ATS scores.” Vol. 20 privacy (CVs are PII) and fairness / bias apply: do not invent employment, degrees, or metrics. Next: code assistant (diffs + tests as judge).

Learning Objectives

By the end of this lesson, students should be able to:

  • Model a resume as versioned JSON (schema), not as a free-form blob the model owns.
  • Separate extract / rewrite / export stages with validation in code.
  • Require human approve-before-export; never auto-submit to a job portal.
  • State ATS caveats without inventing vendor scores or guaranteed pass rates.
  • Eval factuality (no invented jobs) separately from style/JD keyword coverage.
  • Apply Vol. 20 PII + bias controls to a career document product.
Definition

An AI resume builder is a document product that stores the candidate’s facts in a structured JSON resume, proposes LLM rewrites (summary, bullets, skills grouping) as diffs against that JSON, and exports PDF/DOCX/Markdown only after human approval. It is not an ATS oracle, not a background-check, and not licensed career counseling. Hallucinated employment is a safety failure, not a creative feature.

Problem and Scope

Job seekers need tailored language for a posting without lying. Recruiters and ATS parsers want consistent headings and plain text. The conflict: models love impressive verbs and invented impact numbers. The product job is constrained rewrite plus an audit trail of what the human accepted.

MVP (done when…)Stretch
Source of truthJSON Resume-shaped schema (basics, work[], education[], skills[])Projects, publications, i18n locales
IngestForm editor + optional paste/PDF extract → JSON (HITL confirm extract)LinkedIn export parse; OCR scans
RewriteJD paste → proposed bullet/summary diff; user accept/reject per fieldCover letter draft with same HITL; multi-JD variants
ExportPDF/Markdown from approved JSON onlyDOCX template; shareable link with expiry
Out of scopeNo auto-apply; no fake ATS %; no fabricating metricsJob-board submit APIs (still HITL if ever added)

JSON is canonical

  • Validate with Pydantic / JSON Schema
  • LLM output must parse or retry
  • PDF is a projection, not the store

ATS caveats (honest)

  • No single “the ATS” scoring model
  • Prefer simple headings, selectable text, no text-in-images
  • Keyword stuffing ≠ relevance; humans still read
  • Do not sell a numeric ATS score

Vol. 22 / 18 stack

  • FastAPI + same auth as clone
  • OpenAI-compatible JSON mode or HF + schema repair
  • Postgres JSONB or SQLite
  • WeasyPrint / reportlab / md→PDF for export

Schema + HITL (win)

  • Factuality checkable field-by-field
  • Export is deterministic from JSON
  • Audit: who approved which rewrite

One-shot pretty PDF (lose)

  • Invented dates hide in prose
  • Cannot diff versions
  • ATS + humans both suffer from layout gimmicks

Architecture

Capture

Form / extract → validated JSON v0.

Rewrite

JD + JSON → proposed patch.

HITL

Accept / edit / reject per field.

Export

Render only status=approved.

PlaneResponsibility
UISplit view: JSON/form left, JD + diff right, Approve export disabled until review
API/v1/resumes, /v1/resumes/{id}/rewrite, /v1/resumes/{id}/approve, /v1/resumes/{id}/export
ModelJSON-constrained chat; temperature low; no tools that hit job boards
StorageResume versions + patch proposals + approval events (who, when)
EvalFactuality, schema validity, bias/tone sample, export fidelity

Concrete Stack + Implementation Sketch

Reuse FastAPI auth/quotas. Store JSONB. Use structured outputs (OpenAI-compatible response_format or tool-call schema). Export is a pure function of approved JSON—no second LLM pass that can re-hallucinate dates.

# app/resume.py — JSON resume + rewrite diff + approve-before-export # pip install fastapi pydantic openai from enum import Enum from pydantic import BaseModel, Field, EmailStr from openai import OpenAI client = OpenAI() REWRITE_SYSTEM = ( "You rewrite resume JSON for a job description. " "You MUST NOT invent employers, titles, dates, degrees, or metrics. " "You MAY rephrase existing bullets and regroup listed skills. " "If the JD asks for a skill not in the resume, omit it or flag in warnings[]; never add it as a fact." ) class WorkItem(BaseModel): company: str position: str start: str end: str | None = None highlights: list[str] = Field(default_factory=list) class ResumeJSON(BaseModel): basics: dict work: list[WorkItem] education: list[dict] = Field(default_factory=list) skills: list[str] = Field(default_factory=list) class RewriteOut(BaseModel): resume: ResumeJSON warnings: list[str] = Field(default_factory=list) changelog: list[str] = Field(default_factory=list) class Status(str, Enum): draft = "draft" pending_review = "pending_review" approved = "approved" def fact_guard(original: ResumeJSON, proposed: ResumeJSON) -> list[str]: """Deterministic checks — do not delegate employment truth to the LLM.""" errors = [] orig_cos = {(w.company.lower(), w.position.lower()) for w in original.work} for w in proposed.work: key = (w.company.lower(), w.position.lower()) if key not in orig_cos and w.company.lower() not in {c[0] for c in orig_cos}: errors.append(f"new_employer_not_allowed:{w.company}") orig_edu = json.dumps(original.education, sort_keys=True) if json.dumps(proposed.education, sort_keys=True) != orig_edu: errors.append("education_mutation_not_allowed") return errors @app.post("/v1/resumes/{rid}/rewrite") def rewrite(rid: str, jd: str, user=Depends(current_user)): row = db.get_resume(rid, owner=user.id) original = ResumeJSON.model_validate(row.json_body) resp = client.chat.completions.create( model=os.environ.get("CHAT_MODEL", "gpt-4.1-mini"), messages=[ {"role": "system", "content": REWRITE_SYSTEM}, {"role": "user", "content": wrap_data("resume_json", original.model_dump_json())}, {"role": "user", "content": wrap_data("job_description", jd[:12_000])}, ], response_format={"type": "json_object"}, temperature=0.3, max_tokens=2_500, ) proposed_raw = RewriteOut.model_validate_json(resp.choices[0].message.content) errors = fact_guard(original, proposed_raw.resume) if errors: raise HTTPException(422, {"code": "factuality_violation", "errors": errors}) pid = db.save_proposal(rid, proposed_raw.model_dump(), status=Status.pending_review) return {"proposal_id": pid, "warnings": proposed_raw.warnings, "changelog": proposed_raw.changelog} @app.post("/v1/resumes/{rid}/approve") def approve(rid: str, proposal_id: str, user=Depends(current_user)): db.apply_proposal(rid, proposal_id, reviewer=user.id) # copies JSON; sets status=approved return {"status": Status.approved} @app.get("/v1/resumes/{rid}/export") def export_pdf(rid: str, user=Depends(current_user)): row = db.get_resume(rid, owner=user.id) if row.status != Status.approved: raise HTTPException(409, "export_requires_approval") pdf = render_pdf(row.json_body) # WeasyPrint/reportlab — no LLM here return StreamingResponse(pdf, media_type="application/pdf")

Acceptance Criteria (“Done When…”)

#Done when…
1User can create/edit JSON resume via form; invalid schema is rejected in API, not silently stored.
2Rewrite against a JD returns a proposal; export endpoint returns 409 until approve.
3A rewrite that inserts a new employer or degree is rejected by fact_guard (422), even if the model emitted it.
4Approve writes an audit row (user, timestamp, proposal id); UI diff is reviewable field-by-field.
5Exported PDF text is selectable (not a screenshot); headings are conventional (Experience, Education, Skills).
6Product copy has no numeric ATS score; a short “ATS caveats” note is visible near export.
7PII: resumes are owner-scoped; delete works; no CV text in vendor logs beyond stated retention.

Eval Rubric + HITL / Safety

GateWhat you measureHook
FactualityNo new employers/degrees/metrics vs source JSONCode guard + Vol. 19 hallucination tests
Schema validity100% parse rate after one retryVol. 13 structured output
JD alignment (human)Bullets relevant without stuffing; 5 raters on a rubricHuman eval
Bias / toneNo gendered self-sabotage prompts; no “fix your ethnicity” featuresVol. 20 bias / fairness
HITL completenessExport impossible without approve; no job-board side effectsVol. 15 HITL
PrivacyOwner isolation, TTL/export/delete, minimize JD+CV in logsVol. 20 privacy

HITL is the product: the human is the author of record. The model is a copy editor with a fact fence. Auto-applying to Greenhouse/Lever is out of scope; if a later team adds it, that is an irreversible tool and needs a second confirmation.

Related Lectures

LectureRole
JSON prompting / structured outputSchema discipline
Document AI / HITLExtract + approve pattern
FastAPI / DockerServing
Privacy / biasCVs are sensitive
PDF chatbot / Code assistant / Interview assistantSiblings
Common Misconception

“If the PDF looks premium, ATS will rank it #1.” Many parsers want simple text; we do not invent scores. Second: the model may add a plausible internship “to help the student.” That is a factuality fail. Third: export can run on pending_review to “save a click.” Fourth: keyword stuffing is the same as JD alignment. Fifth: cover letters can auto-send. Sixth: JSON is optional if the markdown looks good.

Knowledge Check

  1. Short Answer: What is the source of truth in this product? Answer: Versioned structured JSON resume, not the PDF blob.
  2. True/False: Export may run on a pending rewrite to speed up users. Answer: False—409 until human approve.
  3. Multiple Choice: Inventing a new employer in a rewrite should be: (a) rejected in code (fact_guard), (b) allowed if the JD requires it, (c) scored as +ATS points. Answer: (a).
  4. Short Answer: Name one honest ATS caveat. Answer: No single ATS score; prefer selectable text/simple headings; keyword stuffing ≠ relevance; humans still read (any valid).
  5. True/False: This lecture publishes a fake 98% ATS pass rate. Answer: False.
  6. Multiple Choice: PDF rendering should: (a) be a pure function of approved JSON, (b) call the LLM again to “polish dates,” (c) screenshot a canvas. Answer: (a).
  7. Short Answer: Why are CVs a Vol. 20 privacy topic? Answer: They contain PII (identity, employment, often phone/address); owner-scope, delete, minimize logs.
  8. True/False: Education fields may be freely rewritten including new degrees. Answer: False—education mutation is blocked in the sketch.
  9. Multiple Choice: Next sibling capstone is: (a) AI Code Assistant, (b) Midjourney, (c) n8n. Answer: (a).
  10. Short Answer: Where does HITL sit in this architecture? Answer: On approve-before-export (and on confirming extracted facts); no unsupervised job-board submit.

Key Takeaways

  • Resume AI is schema + constrained rewrite + human approve, not a magic PDF.
  • Fact fences live in code; the model cannot become the employment oracle.
  • ATS advice is qualitative; never invent scores or guaranteed ranks.
  • Export is a projection of approved JSON; audit who accepted which patch.
  • Next: repo diffs with tests as judge—AI Code Assistant.
Trainer’s Guide

Lab: Students use a fictional resume (no real SSN/phone). Implement schema, rewrite, fact_guard, approve, Markdown or PDF export. Red-team: prompt the model to add “Google intern, Summer 2024” and show the 422. Deliverable: eval of 8 rewrites (factuality Y/N, schema Y/N, human style 1–5) + ATS caveats paragraph with zero invented percentages.

Exit ticket: “Marketing wants a circular gauge that says ATS Match 94%. What do you ship instead?”

Recap: The resume builder capstone stores facts in JSON, rewrites under a fact fence, and exports only after HITL—with honest ATS caveats. Continue to AI Code Assistant.