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

extract_text()
JobDescription.parsed
(LLM #1)
calculate_match()
(LLM #2)
AnalysisResult
PDF (optional)

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

RemovedReason
LinkedIn scraping (Apify)Proprietary, paid, and off-topic for a library.
Credly badge verificationDepends 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 VaultThese belong in an application built on top of reveilio, not in the library itself.
Chat, experience, and multi-turn agentsOut of scope for v0. They may return as opt-in sub-modules.

Thread safety

analyze_folder uses a ThreadPoolExecutor. This is safe because:

Error philosophy

Extension points

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.