Architecture
How reveilio is put together, and why it is shaped this way.
Module layout
src/reveilio/
├── __init__.py # public re-exports
├── config.py # ReveilioConfig + configure() singleton
├── models.py # pydantic result models
├── api.py # sync entry points: analyze_*, score, save_*_pdf
├── llm/
│ └── dynamic.py # provider dispatch: Gemini / OpenAI / Azure / Ollama
├── parsing/
│ ├── text_extract.py # PDF / DOCX / DOC / TXT to plain text
│ ├── job_description.py # JobDescription + lazy LLM-parsed .parsed
│ └── resume.py # Resume input wrapper
├── scoring/
│ ├── prompts.py # system + user prompt builders
│ └── scorer.py # calculate_match(): one LLM call, structured output
└── reports/
└── pdf.py # ReportLab-based candidate + batch PDFs
Data flow
(LLM #1)
(LLM #2)
Each call to analyze_resume makes at most two LLM calls:
one to parse the JD (skipped on subsequent resumes because
JobDescription.parsed is cached), and one combined extract-and-score
call for the resume. That second call is the engine. It both extracts structured
candidate data and emits the weighted score in a single round trip.
Why one combined call?
The original recruitment-bot backend did this in two calls: first a resume-parsing agent, then a scoring agent that consumed the parsed output. That works, but it doubles latency and cost, and the scoring agent never sees the raw resume text that the parser dropped.
Reveilio collapses the pair into one prompt with two explicit phases, "PHASE 1: Structured Extraction" and "PHASE 2: Match Analysis". The model returns a single JSON document that contains both outputs. The result is faster, cheaper, and more coherent, because the model sees both the raw text and the weights at the same time.
Why a process-global config?
A class-based Client(config=...) style would arguably be more explicit,
but it penalizes the common case: a script or notebook that picks one provider and
uses it for everything. With a singleton, you call configure() at the
top of your file and forget about it. If you need multiple concurrent configurations,
the lower-level calculate_match and json_completion
functions both accept an explicit config= argument.
What was stripped from the original backend
| Removed | Reason |
|---|---|
| LinkedIn scraping (Apify) | Proprietary, paid, and off-topic for a library. |
| Credly badge verification | Depends on an external account. |
| Geocoding (geopy / Nominatim) | Irrelevant to the core score. Added a network dependency and rate limits. |
| PostgreSQL persistence (asyncpg, sqlalchemy) | A library should not force a database on the caller. The package returns dictionaries, and callers persist as they see fit. |
| FastAPI routes and HashiCorp Vault | These belong in an application built on top of reveilio, not in the library itself. |
| Chat, experience, and multi-turn agents | Out of scope for v0. They may return as opt-in sub-modules. |
Thread safety
analyze_folder uses a ThreadPoolExecutor. This is safe
because:
- The Gemini, OpenAI, and Azure SDK clients are thread-safe.
- The Ollama path creates a fresh
httpx.Clientper call. - The
ReveilioConfigsingleton is read-only afterconfigure(). - Each worker receives its own
Resumeinstance.JobDescription.parsedis computed once on the main thread before the pool starts, so workers only read from it.
Error philosophy
- Configuration errors raise. Missing API keys, unsupported providers, and unreadable file extensions all throw loud exceptions. These are programmer errors worth surfacing immediately.
- Per-resume LLM errors are contained.
calculate_matchwraps the LLM call in atry/exceptand returns a safe "Needs Review" result on failure. In a 500-resume batch, one bad file will not kill the other 499. - No silent recoveries. The fallback result is clearly marked
(low confidence, an explicit error string in
ai_summary) so callers can filter it out.
Extension points
- Custom weights: pass
weights=toconfigure(). - Custom prompts: override the builders in
reveilio.scoring.prompts, or pass your own structured JD dictionary tocalculate_match. - Custom text extraction:
reveilio.parsing.text_extractdispatches on extension. Importextract_textdirectly to reuse it from your own code. - Custom report layout: copy
reports/pdf.pyas a starting point. It is plain ReportLab with no hidden magic.
Version policy
Reveilio follows SemVer. Prior to 1.0, minor versions may break the public API when the shape of a result changes. Always pin the version you test against in production. From 1.0 onwards, breaking changes require a major-version bump.
Continue to Contributing.