A recruiter's desk usually ends up with resumes scattered across a downloads folder and an overflowing inbox, and matching any of them to an open role means reading each one by hand. Recruit Assistant reads both places, figures out who's who even if someone applied twice, and uses AI to score and explain how well each person fits a job — entirely on your own computer, with nothing sent anywhere you haven't explicitly connected.
Three layers. You use a normal web page in your browser; that page talks to one program running on your computer; that program is the only thing that ever touches your data, your files, or the outside world.
Nothing in the bottom row talks to anything else in the bottom row — the backend is the only thing that ever coordinates between them.
Follow a single resume from the moment it lands to the moment it shows up as a color-coded match. Every stage below is a real step the software takes — nothing skipped, nothing simplified away. Steps tagged you decide are places a human makes the call; everything else runs automatically.
You either point the app at a folder of resumes already on your computer, or let it check a mailbox you've connected (read-only — it can only look, never send or delete). From this point on, it makes no difference which door a resume came through; both are treated identically for everything downstream.
scanning/folder_ingestor.py
scanning/email_ingestor.py
— one shared output shape either way (IngestedResume)
The simpler default for an integration like this is to request broad mailbox access "just in case." Instead, a connected mailbox is granted read-only permission from the start — the app is structurally incapable of sending or deleting anything, even if something went wrong.
The text is pulled out of the PDF, Word document, or plain text file. If it's a scanned image with no real text underneath it — a photocopy of a resume, say — the software falls back to optical character recognition (OCR), essentially "reading" the picture the same way a person would, rather than giving up.
scanning/parser.py
— pdfplumber/pypdf/python-docx first, OCR (pytesseract) only if that comes back too thin
An AI model reads the extracted text and pulls out the structured facts a recruiter actually cares about: legal name, contact info, skills, years of experience, education, work-authorization status. Anything it's not confident about is left blank rather than guessed — a blank field is a prompt to double-check, a wrong guess is a silent mistake.
scanning/parser.py
— structured LLM extraction, cross-checked against pattern-matched email/phone/LinkedIn/GitHub
Before saving anything, the system asks: have we seen this person before? It checks by email address first, then by name-plus-phone as a fallback. If yes, this new resume updates their existing profile — new skills get added, a longer work history overwrites a shorter one — instead of creating a confusing second entry for the same human being.
scanning/identity_resolution.py
— the reason someone who applies again six months later, with a stronger resume, stays one person, not two
The easy path is to keep email-sourced and folder-sourced resumes as two separate lists, since they arrive through two different doors. That would defeat the point — a candidate is a person, not a source — so every resume is checked against everyone already on file before it's ever saved.
The resume itself, a short AI-written summary, and a small metadata note all get saved to an actual folder on your disk, organized by date — so you can find and open the real file later, even offline, even years from now. The structured facts also get saved to a local database so the app can search and filter across everyone.
scanning/mirror_writer.py
— data/candidates/<date>/<name>/resume.pdf + summary + a local SQLite database
Starting with a cloud database is the modern default for most software — but that would mean your candidates' data lives on someone else's server from day one. Instead, storage sits behind one clean interface: local now, with a door deliberately left open for a company server or cloud backend later, as a plug-in, not a rewrite.
You write or paste a job description and click "Run matching." Behind the scenes, this is where things get interesting — the next three steps only happen when you ask for them, and they're what actually turn a pile of resumes into a ranked shortlist.
Reading every single resume in depth with AI, for every job, would be slow and expensive at real volume. So a fast, inexpensive pass compares the meaning of the job description against everyone's profile using something called an embedding — a mathematical fingerprint of what a piece of text is actually about — and keeps only the strongest handful of candidates for the next, more careful step.
matching/embeddings.py
— cosine similarity over the whole pool, computed once per candidate and reused, not recalculated every time
The most thorough-sounding approach is to have the AI carefully read and score every resume in the pool. At real volume that's slow and expensive for no real benefit — most candidates were never going to be close matches. Match cheaply first, think carefully second keeps both wait time and AI cost bounded, without losing quality where it actually matters.
Only the narrowed-down shortlist gets the expensive, careful treatment: a stronger AI model reads the actual job description against the actual resume and produces a 0-100 score, a plain-language list of what matched, what's missing, and any information that's still unclear — never just a bare number with no explanation.
matching/matcher.py
— deep_score(), run concurrently across the shortlist rather than one at a time
Any score that lands in an ambiguous middle range (not clearly a great fit, not clearly a poor one) gets reviewed a second time by an independent AI pass acting as a judge — it can agree with the original score, or correct it. This is the "AI checking AI" step, aimed at exactly the calls a human reviewer would also want to look at twice.
matching/matcher.py
— judge_score(), triggered only for scores between 40 and 70
A single AI opinion can be confidently wrong, especially on close calls. Rather than trust one score all the way through, the same ambiguous middle range a human reviewer would double-check gets an independent second AI opinion too — cheap insurance, spent only where it's actually needed.
Great, Good, Average, or Poor — plus a red-flag override for anything that should never be buried in the middle of a list regardless of score. Every result carries its reasoning, so you're reviewing a case, not trusting a black box. From here you can flag a candidate, or generate a draft outreach email pre-filled with the actual reasons this person is a good fit.
matching/matcher.py (score_to_tier)
email_draft/generator.py
Six pieces do essentially all of the work. Each one solves exactly one problem, and none of them know or care about the internal details of the others — which is what lets any one of them change later without the rest breaking.
Folder scanning and email scanning are built as two interchangeable "readers" that both hand off the same shape of information to everything downstream. Adding a third source later — a LinkedIn export, an applicant-tracking-system integration — means writing one more reader, not touching anything else.
scanning/ingestor_base.py — one shared interface, several implementationsThe cheap-filter → deep-score → judge-review pipeline described above, all in one place. It's deliberately staged rather than one giant AI call, so cost and speed stay predictable even with a pool of tens of thousands of candidates.
matching/ — embeddings, matcher, judge, prompts, in one moduleEverything lives in a local database and a local folder of files — no server, no account, no subscription required to keep using your own data. It's built behind one swappable interface, so a future move to a shared company server or the cloud is a new implementation of that interface, not a rewrite of the app.
storage/base.py → storage/local.py (SQLite + files)Beyond the AI's own read of a resume, you can turn on specific weighted criteria per job — years of experience, location, visa eligibility, certifications — and re-run matching against them without re-scanning every source from scratch.
criteria/builtin.py, criteria/service.pyYou can mark specific folders or mailboxes to be re-scanned automatically overnight — but nothing runs on a schedule unless you explicitly turn it on, source by source. No background behavior is sprung on you.
scheduler/ — off by default (SCHEDULER_ENABLED=false)A strong match doesn't just sit in a list — one click drafts a personalized outreach email, pre-filled using the actual reasons the match scored the way it did, ready for you to edit and send yourself.
email_draft/generator.pyThis handles real people's names, contact details, and work history, so the safeguards below aren't an afterthought — they're load-bearing.
Candidate data and resumes stay in a local database and local files, unless you explicitly connect an email account or an LLM provider.
Your login password is run through a one-way hashing process (PBKDF2) before it's saved — the app itself never keeps a readable copy.
A connected email account can only be scanned for attachments — never sent from, deleted from, or otherwise changed.
Not in a database file, not in a config file — in the same secure credential store your operating system already uses for other saved passwords.
Automatic overnight re-scanning exists, but is switched off by default and only ever touches sources you've explicitly opted in, one at a time.
A built-in generator can create thousands of realistic fake resumes for trying the app out, so you never have to test with real candidates' data.
A per-candidate delete removes their database record and every resume file on disk — not a soft "hide," a real, honest erasure for when someone asks to be forgotten.
The first time you switch on real (non-mock) AI scoring, a plain-language dialog explains exactly what leaves the machine before you confirm — not buried in a settings toggle.
The server listens on localhost only, not the whole network — someone on the same office Wi-Fi can't reach it, and repeated failed logins lock out automatically.
For the more technically curious — the actual tools this is built with, and how the code is laid out.
The full pipeline above — folder + email intake, identity resolution, two-stage matching with judge review, criteria, outreach drafting, and a personal dashboard.
Performance hardening — matching and scanning run concurrently instead of one item at a time, candidate search and similarity ranking stay fast at real volume (10,000+ candidates).
Real-inbox readiness — connected accounts show their real address and refresh their own access automatically instead of quietly expiring.
OCR fallback and cleaner multi-attachment handling — scanned resumes still get read, and a resume-plus-cover-letter email no longer confuses the two.
Opt-in nightly scanning, Docker packaging, and a public repository with continuous testing on every change.
Security hardening — the server only listens on this machine by default, not the whole network, and repeated failed logins lock out automatically. Followed a four-perspective stakeholder review that read the app as a recruitment analyst, a product owner, a data-governance lead, and a CEO, each independently.
Pipeline stage tracking — where a candidate actually stands in the hiring process (sourced through placed or declined), tracked separately from match quality, on Match Results, Candidate Detail, and the dashboard.
Real per-candidate deletion — an honest right-to-erasure path, distinct from the existing "clear everything" reset, that removes one person's database record and every file on disk.
A one-time consent step before real AI scoring — asked once, remembered for this installation, explaining plainly what leaves the machine.
Faster scans and matches — the mailbox-scanning and AI-matching pipelines are fully mapped for speed (parallelizing per-resume processing, smarter use of existing AI concurrency, shrinking what gets sent per scoring call) but not yet built.
A locally-hosted, recruiting-specific AI model — long-term: a small open-source model, fine-tuned specifically for resume/job scoring and run entirely on the recruiter's own machine, to cut real-mode AI cost toward zero rather than paying a cloud provider per scoring call indefinitely. Not yet scoped — needs a training-data plan, a model-size-vs-laptop-hardware decision, and a quality bar to clear before it could replace today's cloud models as the default.
A genuinely shared/cloud version — the storage interface is already built to support this; it isn't built out yet because nothing has needed it.
Reading team culture/energy fit from email tone — deliberately deferred; a more sensitive feature that deserves its own careful design pass.