Skip to content
ahsan876 edited this page Apr 17, 2026 · 1 revision

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

flowchart LR
    A[extract_text] --> B[JobDescription.parsed<br/>LLM #1]
    B --> C[calculate_match<br/>LLM #2]
    C --> D[AnalysisResult]
    D --> E[PDF optional]
Loading

Plain text equivalent:

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

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.Client per call.
  • The ReveilioConfig singleton is read-only after configure().
  • Each worker receives its own Resume instance. JobDescription.parsed is 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_match wraps the LLM call in a try / except and 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= to configure().
  • Custom prompts: override the builders in reveilio.scoring.prompts, or pass your own structured JD dictionary to calculate_match.
  • Custom text extraction: reveilio.parsing.text_extract dispatches on extension. Import extract_text directly to reuse it from your own code.
  • Custom report layout: copy reports/pdf.py as 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.

Clone this wiki locally