A full-stack AI tool that helps hiring teams synthesize interview debriefs into structured, evidence-grounded reports — without making hire/no-hire decisions.
Design principle: The tool surfaces and structures evidence. The human decides.
232 tests · TypeScript clean · eval suite passing · Dockerized · GitHub Actions CI
Live demo → interview-feedback-copilot.vercel.app (Frontend on Vercel · Backend on Render free tier — first request after inactivity may take ~30 s to wake up)
Built as a portfolio project to show forward-deployed AI engineering skills:
| Skill | Implementation |
|---|---|
| Structured LLM output | Claude tool_use with forced tool calling; Python computes character offsets from verbatim quotes |
| Pydantic-gated pipelines | Every LLM response is validated before use; HTTPException(422) on failure |
| Evidence grounding | Each claim carries start_char/end_char into the original text; verified by EvidenceVerifier before synthesis |
| Human-in-the-loop | SynthesisReport starts reviewer_approved=False; reviewer PATCH required before sharing |
| Eval suite | Six metrics from the eval plan, measured against gold-labeled synthetic data; release-gated |
| Security | PII scrubbing for logs, security headers middleware, request size limits, env-var-only secrets |
| Persistence | SQLite via SQLAlchemy 1.4 + Alembic migrations; same interface as in-memory store for tests |
| Deployment | Multi-stage Docker builds, docker compose orchestration, GitHub Actions CI |
Hiring managers reviewing many candidates face recurring problems:
- Key concerns are buried in long-form narratives
- Interviewer disagreements are hard to spot across separate documents
- The first debrief read anchors all subsequent interpretations
This tool solves them by:
- Extracting structured signals from each debrief, grounded in verbatim quotes
- Verifying every citation —
quoted_textmust exist at the stated[start:end]position - Mapping evidence to competencies from the role rubric
- Flagging disagreements between interviewers on the same competency
- Identifying coverage gaps — competencies no one assessed
- Generating a reviewable synthesis for the hiring committee
What it deliberately does not do: score candidates, rank candidates, or recommend hire/no-hire.
- Python 3.11+
- Node.js 20+
- (Optional) Docker + Docker Compose
# Clone the repo
git clone <repo-url>
cd interview-feedback-copilot
# Set up environment
cp .env.example .env
# Add ANTHROPIC_API_KEY to .env for LLM extraction (optional)
# Backend
cd backend
pip install -e ".[dev]"
alembic upgrade head # create SQLite tables
uvicorn app.main:app --reload --port 8000
# Frontend (new terminal)
cd frontend
npm install
npm run devOpen http://localhost:3000, click New Analysis, load sample data, and step through the pipeline.
- Frontend: http://localhost:3000
- Backend API: http://localhost:8000
- Interactive API docs: http://localhost:8000/docs
docker compose up --buildmake test # run 232 backend tests
make lint # ruff check + format check
make typecheck # tsc --noEmit (frontend)
make eval # eval suite against gold data
make migrate # alembic upgrade head
make docker-up # docker compose up --buildUpload debriefs + role rubric
↓
Baseline extraction (deterministic, no API key)
LLM extraction (Claude tool_use, verbatim quotes)
↓
Evidence Verifier (100% citation validity gate)
↓
Coverage Map (STRONG / PARTIAL / NOT_COVERED / CONFLICTED)
Disagreement Detector (DIRECTION_CONFLICT / EVIDENCE_ABSENT)
↓
Synthesis Report (no recommendation language)
↓
Human Reviewer (required PATCH before sharing)
Python computes offsets, not Claude. Instead of asking the LLM for start_char/end_char (unreliable), we ask for verbatim quotes and use str.find(). Accuracy is deterministic regardless of which model is used.
Verification is a hard gate. Any signal with a span that can't be located in the debrief fails validation. The synthesizer raises HTTP 422 rather than producing an unverified report.
Schema-first design. Pydantic models are the source of truth. Frontend TypeScript types mirror them manually (could be auto-generated from the OpenAPI spec in a production system).
In-memory store for tests, SQLite for production. InMemoryStore and DatabaseStore implement the same interface. Tests use app.dependency_overrides to inject the in-memory version without hitting disk.
interview-feedback-copilot/
├── backend/
│ ├── app/
│ │ ├── schemas/ Pydantic models — source of truth
│ │ ├── services/ Business logic (extractor, verifier, analyzer, synthesizer)
│ │ ├── routers/ HTTP handlers (thin, delegates to services)
│ │ ├── models/ SQLAlchemy ORM models
│ │ ├── evals/ Evaluation suite
│ │ └── tests/ 232 pytest tests
│ ├── alembic/ Database migrations
│ └── Dockerfile
├── frontend/
│ ├── app/ Next.js App Router pages
│ ├── components/ UI components (SignalCard, CompetencyGrid, etc.)
│ ├── lib/ Typed API client, TypeScript types
│ └── Dockerfile
├── sample_data/
│ ├── debriefs/ 5 synthetic interview debriefs
│ ├── rubrics/ 6 built-in role rubrics (Data Scientist, SWE, MLE, DE, Quant Researcher, UX Researcher)
│ └── gold/ Gold-labeled eval dataset
├── docs/ Product brief, architecture, decisions, eval plan, learning log
├── .github/workflows/ci.yml GitHub Actions CI
├── docker-compose.yml
├── Makefile
└── CLAUDE.md AI pair-programmer operating rules
| Doc | Contents |
|---|---|
docs/product_brief.md |
Problem, users, goals, non-goals |
docs/architecture.md |
System design, data flow, technology choices |
docs/decisions.md |
Architecture decision records |
docs/eval_plan.md |
Evaluation metrics and release gates |
docs/learning_log.md |
Engineering learning notes for each milestone |
CLAUDE.md |
AI pair-programmer operating rules |
All enforced in code, not just documentation:
- No hire/no-hire recommendation —
SynthesisReporthas no such field; a schema change would surface in code review - Every claim cites source text —
EvidenceVerifierblocks synthesis on any invalid span - Human review required — reports start
reviewer_approved=False; no "skip" path exists - PII-aware logging —
scrub_pii_for_log()strips emails, phones, SSNs before any LLM logging - API keys in env only — enforced by
.gitignore+ startup warning if key is missing
| Layer | Tests | What's covered |
|---|---|---|
| Pydantic schemas | 18 | Validation, field constraints |
| API endpoints | 29 | HTTP contracts, status codes |
| Baseline extractor | 37 | Offset accuracy, signal classification |
| LLM extractor | 38 | Mock client, quote location, validation gate |
| Evidence verifier | 26 | Span verification, adversarial inputs |
| Coverage analyzer | 17 | Four coverage states, gap detection |
| Disagreement detector | 13 | Direction conflict, evidence absent |
| Synthesizer + review | 19 | End-to-end pipeline, reviewer workflow |
| Eval suite | 7 | Gold-label metrics, release gates |
| Security | 14 | PII scrubbing, security headers, input limits |
| DatabaseStore | 14 | SQLite persistence, session isolation |
| Total | 232 |
All 18 milestones complete. Built incrementally with an AI pair-programmer following the rules in CLAUDE.md.
This project was designed, scoped, and reviewed by Hsuan “Jimmy” Lo as a portfolio prototype. I used Claude Code as an AI coding assistant for implementation support, debugging, refactoring, and documentation drafts. The core product framing, workflow design, evaluation logic, human-in-the-loop safeguards, and hiring-process analysis are my own.