ARCHITECTURE.md — Sira¶
Project Overview¶
Sira is a multi-agent AI system that analyzes job postings and tailors resumes to match specific job requirements. It ensures authenticity, avoids AI clichés, and optimizes for Applicant Tracking Systems (ATS). The system uses a sequential pipeline of specialized agents orchestrated by pydantic-ai, with built-in quality gates, an inner refinement loop, and SQLite-backed memory for caching and persistence.
Architecture at a Glance¶
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ RESUME │ │ JOB │ │ CV │ │ REVIEWER │ │ AUDITOR │ │ REPORT │
│ PARSER │───▶│ ANALYST │───▶│ WRITER │───▶│ │───▶│ │───▶│GENERATOR │
│ │ │ │ │ │ │ │ │ │ │ │
│ CV JSON │ │ Job JSON │ │ Tailored │ │ Review │ │ Audit │ │ Final │
│ │ │ │ │ CV JSON │ │ Scores │ │ Result │ │ Report │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
▲ ▲ │ │
│ │ │ ◀── RETRY LOOP ── │
│ │ ▼ (--write-attempts) │
│ │ ┌──────────┐ │
│ │ │ QUALITY │◀─────── score < threshold │
│ │ │ GATE │ triggers retry │
│ │ └──────────┘ │
│ │ │
Content- Raw Job │
Hash Cache Posting │
(SQLite) Markdown │
The system runs a 6-stage sequential pipeline in ResumeTailorWorkflow:
- PARSING_RESUME — Parse resume → structured
CV - ANALYZING_JOB — Extract structured
JobAnalysis - WRITING_CV — Tailor CV to match job
- REVIEWING_CV — Score quality, suggest improvements (refinement loop)
- AUDITING_CV — Validate for hallucinations and AI clichés
- GENERATING_REPORT — Compile self-review report
Stages 3-5 form the Write → Review → Audit inner loop: after the initial write, the reviewer assesses quality and the writer refines (up to --review-iterations, default 1). The auditor then checks the final draft. If the audit fails, the entire Write → Review → Audit loop retries (up to --write-attempts, default 2). The Report phase always runs, even on audit failure.
On a cold cache, stages 1 and 2 run concurrently as two DBOS child workflows (see Durable Execution).
Agent Pipeline (Execution Order)¶
0. Job Scraper (job_scraper_agent)¶
Not part of the internal pipeline — called by the CLI before launching the workflow.
- Responsibility: Strip site chrome (navigation, cookie banners, footers) from Markdown that was already extracted deterministically.
- Input / Output:
str→str. The fetch itself is not an agent step:sira/tools/job_scraper.py::fetch_job_markdown()drives Playwright (headless Chromium), converts HTML to Markdown, runs the quality gate (assert_quality), and returns aRawScrape(markdown_raw, source_text, extraction_strategy, injection_indicators). - Tools: none (the former
fetch_webpage/validate_extractiontools were replaced by the deterministic fetch). - Prompt-injection scan:
fetch_job_markdown()also callsdetect_prompt_injection(raw_html, markdown)(regex only, 10 languages) and stores category names inRawScrape.injection_indicators;main.pylogs and prints an advisory warning and continues. An optional local classifier (sira[guard]extra,sira/tools/injection_guard.py, Meta Llama Prompt Guard 2) can add aclassifier_flaggedindicator after a one-time user consent. Both the scraper and analyst prompts state that page text is data, never instructions. - Retries: 3
- Quality Gate: No (the deterministic
assert_qualityruns before the agent)
1. Resume Parser (resume_parser_agent)¶
- Responsibility: Parse Markdown resume text into a structured
CVobject. Extract ALL skills from every section (summary, experience, projects, certifications, education, publications). - Output:
CV(full_name,contact[ContactInfo: email, phone, location, links],summary,skill_groups[SkillGroup: category, skills],experience,education[Education: degree, institution, dates, details],projects[Project: name, description, link],certifications,publications;cv.skillsis a read-only flattened property, not a schema field) - Key Rules: Preserve ALL hyperlinks in
[text](url)format. Never add or modify information. For senior resumes, expect 40+ skills. One entry per skill (no separate version or spelling variants); team, company, product and job-title names are not skills. - Post-processing:
utils/skill_cleanup.py::clean_skill_groupsruns on every parsed CV (before it is cached) and again on the original and tailored CVs inside the workflow. It collapses variants that differ only by case, separators or a trailing version (Python 3.13+→Python) and mergesX (ABC)withXwhen both exist (the acronym form is kept). It never merges aliases (Go/Golang) or word forms — that would need a dictionary and could drop real content. Pure Python, idempotent. - Retries: 2
- Quality Gate: No — the gate was removed for speed; the parse is cached by content hash instead.
_parser_qsstill exists and the workflow reads it as a fallback, but nothing writes to it.
2. Job Analyst (analyst_agent)¶
- Responsibility: Analyze raw job posting text and extract structured job requirements. Identifies core requirements (not "nice-to-haves") and hidden ATS keywords.
- Output:
JobAnalysis(job_title, company_name, summary, hard_skills, soft_skills, key_responsibilities, keywords_to_target) - Retries: 2
- Quality Gate: No — removed for speed, like the parser.
_analyst_qsis read as a fallback but never written.
3. CV Writer (writer_agent)¶
- Responsibility: Rewrite the CV to target the Job Analysis using ONLY content from the original CV. Rephrase and reorganize but never invent skills or experiences. Groups relevant skills at the top.
- Output:
CV - Key Rules: Only use skills/experiences from original CV. Rephrase existing content to align with job keywords. Avoid AI clichés. Preserve ALL hyperlinks.
- Retries: 2
- Quality Gate: Yes — validated by
_validate_writer - Also used: For the refinement loop (review-based improvements)
4. Reviewer (reviewer_agent)¶
- Responsibility: Review the tailored CV against job requirements. Score quality and provide specific improvement suggestions.
- Output:
ReviewResult(quality_score 0-10, needs_improvement bool, specific_suggestions, strengths) - Review Criteria: Keyword optimization, impact & achievements, relevance, clarity & readability, ATS compatibility
- Retries: 5
- Quality Gate: No — reviewer output is used to drive refinement, not gated itself
5. Auditor (auditor_agent)¶
- Responsibility: Compare original vs. tailored CV. Validate: no hallucinations (no new skills/companies/roles), no AI clichés, all hyperlinks preserved, proper job targeting.
- Output:
AuditResult(passed bool, hallucination_score 0-10, ai_cliche_score 0-10, issues list, feedback_summary) - Pass Criteria: Hallucination score ≤ 2, AI cliché score ≤ 3, all hyperlinks preserved
- Retries: 2
- Quality Gate: Yes — validated by
_validate_auditor
6. Skill Matcher (skill_matcher_agent)¶
- Responsibility: Judge, per job skill, whether the original CV shows the same concept even in different words ("mentor to ~30 engineers" covers "Technical leadership and mentorship"). Runs inside the report phase after a pure-Python literal pre-pass, so it only sees skills that did not appear verbatim in the CV.
- Input: The whole CV rendered as plain text (
utils/skill_matching.py::render_cv_text) plus a numbered skill list; the expected skill list also travels asdepsso the validator can check the answer. - Output:
SkillMatchResult— oneSkillMatch(skill, covered, evidence)per skill;evidenceis a CV quote (≤ 200 chars) or empty. - Validator:
_validate_skill_matches—ModelRetryunless exactly the requested skills come back; canonicalises names; blanks evidence for uncovered skills. - Retries: 3 · Tier: fast · Quality Gate: No
- Fallback: on
AgentRunErrorthe undecided skills stay "missing" (pre-semantic behaviour) and a warning is logged. The run never fails because of the matcher.
7. Report Generator (report_agent)¶
- Responsibility: Write the narrative section of the self-review report. Receives the computed match score and verdict plus CVDiff, GapAnalysis (with evidence), AuditResult, ReviewResult, and JobAnalysis as structured JSON.
- Output:
ReportNarrative(suggestions_to_strengthen, audit_summary, recommendation_rationale) - Key Design: every number in the report is computed in Python:
compute_gap_analysis,compute_match_score,compute_recommendationincv_diff.py. The model explains them; it cannot change them. - Retries: 5
- Quality Gate: No
Auxiliary Agents¶
| Agent | Output Type | Quality Gate | Status |
|---|---|---|---|
cover_letter_writer_agent |
str |
Yes | Defined but not wired into the main workflow. retries=2. |
quality_gate_agent |
QualityCheckResult |
N/A | Shared validator; scores any pipeline agent's output 0–10. |
Data Models¶
All models are defined in sira/models/agents/output.py using Pydantic v2.
Core CV & Job Models¶
| Model | Purpose | Key Fields |
|---|---|---|
CV |
Parsed resume structure | full_name, contact (ContactInfo: email, phone, location, links), summary, skill_groups (SkillGroup: category, skills), experience, education (Education: degree, institution, dates, details), projects (Project: name, description, link), certifications, publications; cv.skills is a read-only flattened property |
WorkExperience |
Single job entry | company, role, dates, highlights |
JobAnalysis |
Extracted job requirements | job_title, company_name, summary, hard_skills, soft_skills, key_responsibilities, keywords_to_target |
Audit & Review Models¶
| Model | Purpose | Key Fields |
|---|---|---|
AuditResult |
Hallucination & cliché validation | passed, hallucination_score (0–10), ai_cliche_score (0–10), issues, feedback_summary |
AuditIssue |
Single audit finding | severity ("Critical" / "Minor"), issue, suggestion |
ReviewResult |
Quality review scoring | quality_score (0–10), needs_improvement, specific_suggestions, strengths |
QualityCheckResult |
Quality gate scoring | score (0–10), reasoning, improvements |
Diff & Report Models¶
| Model | Purpose | Key Fields |
|---|---|---|
CVDiff |
Structural diff original vs. tailored | summary_changed, skills_reordered, skills_deprioritized, experience_changes, sections_modified |
ExperienceChange |
Per-role bullet changes | role, company, bullets_rephrased, bullets_unchanged |
SkillMatch |
One job skill judged against the CV | skill, covered, evidence (CV quote, empty when not covered) |
SkillMatchResult |
Skill matcher agent output | matches (list of SkillMatch, one per requested skill) |
GapAnalysis |
Skill/keyword gap metrics | missing_hard_skills, missing_soft_skills, covered_hard_skills, covered_soft_skills, skill_evidence, hard_skill_coverage_percent, soft_skill_coverage_percent, covered_keywords, missing_keywords, keyword_coverage_percent |
ReportNarrative |
Narrative fields written by report_agent |
suggestions_to_strengthen, audit_summary, recommendation_rationale |
FinalReport |
Complete self-review output | job_title, company_name, overall_recommendation (Strong/Partial/Weak Match), match_score (0–100), what_changed, gaps, suggestions_to_strengthen, audit_summary, recommendation_rationale, passed |
Scraping Model¶
| Model | Purpose |
|---|---|
ScrapedJobPosting |
Legacy scraped-job model; the live path uses the RawScrape dataclass in sira/tools/job_scraper.py |
Workflow Result¶
ResumeTailorResult (in models/workflow.py): company_name, job_title, tailored_resume (JSON string), audit_report (dict), passed, final_report (optional FinalReport)
Data Flow¶
1. CLI (main.py)
├── Reads resume file → converts DOCX/PDF to Markdown via InputConverterRegistry
├── fetch_job_markdown(url) (Playwright → Markdown → assert_quality → injection scan)
│ └── job_scraper_agent strips site chrome → job_posting_markdown (str)
└── ResumeMemoryService.aresolve_original_resume (hash-based cache check)
└── Pre-parsed CV if cache hit, None if miss
2. ResumeTailorWorkflow.run()
├── PARSING_RESUME: markdown → resume_parser_agent → CV JSON
│ └── If pre_parsed_cv provided, skip AI parsing
├── ANALYZING_JOB: job markdown → analyst_agent → JobAnalysis JSON
├── WRITING_CV: CV + JobAnalysis → writer_agent → tailored CV JSON
│ └── REVIEWING_CV: tailored CV → reviewer_agent → ReviewResult
│ └── If needs_improvement: writer_agent refines (up to --review-iterations, default 1)
├── AUDITING_CV: original CV + tailored CV → auditor_agent → AuditResult
│ └── If failed: retry WRITING → REVIEWING → AUDITING (up to --write-attempts, default 2)
└── GENERATING_REPORT:
├── compute_cv_diff(original, tailored) → CVDiff (pure Python, no LLM)
├── match_skills(original, job): literal pre-pass → skill_matcher_agent (one call) → {skill: SkillMatch}
├── compute_gap_analysis(original, tailored, job, skill_matches) → GapAnalysis (pure Python)
├── compute_match_score(gap) → 0–100; compute_recommendation(score, gap) → verdict (pure Python)
└── report_agent: score + verdict + diff + gaps + audit + review → ReportNarrative
└── Workflow assembles FinalReport from the computed numbers + narrative
3. CLI post-processing
├── If passed: render_resume (sira/rendering/) → .md + .pdf + .docx output files
├── generate_report_markdown → _report.md
├── Files saved to output/{company_name}-{job_title}/
└── Memory: ResumeMemoryService.save_tailored_resume → SQLite
Key Data Flow Design Decisions¶
- Skill coverage uses one judge call, then everything is pure Python.
skill_matcher_agentdecides which job skills the CV covers (with a CV quote as evidence);compute_gap_analysis,compute_match_scoreandcompute_recommendationincv_diff.pyturn those verdicts into deterministic metrics. Given the judge's answer, every number is reproducible. ATS keyword coverage stays a literal substring check on the tailored CV — that is what an applicant tracking system does. - Match score formula (
compute_match_score):score = round((60·hard% + 20·soft% + 20·keyword%) / 100). A bucket the job lists nothing for is dropped and the remaining weights are rescaled to sum to 100; all buckets empty → 0.roundis Python's built-in (half to even). Verdict (compute_recommendation): Strong Match when score ≥ 75 and hard-skill coverage ≥ 75 % (or the job lists no hard skills); Partial Match when score ≥ 50; otherwise Weak Match. - The report_agent only produces narrative fields (suggestions, audit_summary, rationale). The workflow assembles the
FinalReportfrom the computed score, verdict,CVDiffandGapAnalysisplus that narrative. - The Report phase always runs, even when audit fails or the writer produces no output. This ensures the user always gets feedback.
- Content-hash-based caching in
ResumeMemoryService: if the resume file content hash matches a previously parsed version AND the parser version matches, the cachedCVis reused, skipping AI parsing entirely.
Quality Gate System¶
Architecture¶
- Shared validator: A single
quality_gate_agentscores all gated agents' output, not per-agent custom validation code. - Decorator pattern: Each quality-gated agent has an
@output_validatorasync function that calls the quality gate. - Threshold: advisory. The output is scored once;
ModelRetry(with the gate's improvement list) is raised only when the score is belowQUALITY_GATE_THRESHOLD—--gate-threshold, default 6.--no-quality-gateskips the scoring call entirely. - Fallback:
_QualityStateper-agent holdslast_output. OnUnexpectedModelBehavior(retries exhausted), the fallback is used. - Retry counts (set inline per
Agent(...), not uniform): quality gate, parser, analyst, writer, auditor, cover-letter writerretries=2; reviewer and reportretries=5; job scraper and skill matcherretries=3.
Gated Agents¶
| Agent | Validator Function | Fallback State |
|---|---|---|
writer_agent |
_validate_writer |
_writer_qs |
auditor_agent |
_validate_auditor |
_auditor_qs |
cover_letter_writer_agent |
_validate_cover_letter_writer |
_cover_qs |
_parser_qs and _analyst_qs still exist and are read by the workflow's fallback branches, but no validator writes to them since the parser and analyst gates were removed. Re-adding either gate makes the fallback work again as written.
Ungated Agents¶
resume_parser_agent— gate removed for speed; the content-hash cache makes the parse cheap to repeat.analyst_agent— gate removed for speed.reviewer_agent— Output drives refinement loop; quality is implicitly validated by the auditor later.report_agent— Produces narrative; score, verdict and gaps are computed in Python.skill_matcher_agent— Shape-validated by_validate_skill_matches; a wrong answer degrades to literal matching.job_scraper_agent— Ungated; the deterministicassert_qualityinsira/tools/job_scraper.pyruns before it.
Scoring Criteria by Role¶
| Role | Criteria |
|---|---|
| Resume Parser | Completeness, no data loss, correctly structured fields |
| Job Analyst | Keyword coverage, clear requirement identification, no omissions |
| CV Writer | No hallucinations, ATS keywords naturally incorporated, human tone, no clichés |
| Auditor | Thorough hallucination check, specific cliché identification, actionable feedback |
| Cover Letter Writer | Authentic human voice, no AI clichés, specific to role, concise |
Memory Layer¶
sira/memory/
├── models.py # Data classes: ResolvedOriginalResume, TailoredResumeRecord, etc.
├── repository.py # Abstract interface: ResumeMemoryRepository
├── sqlite_repository.py # SQLite implementation
├── parser.py # PydanticAIResumeParser (adapter over resume_parser_agent)
└── service.py # ResumeMemoryService — single entry point for CLI
Key Behaviors¶
- Database: SQLite at
resume_memory.sqlite3in the per-user data directory (sira/paths.py;SIRA_DATA_DIRoverrides it) - Content-hash caching:
ResumeMemoryService.resolve_original_resume()hashes the resume file content. If the hash matches a previously parsed version AND the parser version matches, the storedCVJSON is deserialized directly — no AI call. - Two variants:
resolve_original_resume(sync) andaresolve_original_resume(async). The CLI uses the async variant since it runs underasyncio. - Source tracking: Every resume source is stored with its absolute path and content hash. Multiple tailored resumes can link back to the same source.
- Job fingerprint: Each tailored resume is keyed by a truncated SHA-256 hash (first 32 hex chars) of
{job_url}:{job_title}to avoid duplicates for the same job.
Tools Layer¶
sira/tools/
├── job_scraper.py # fetch_job_markdown, RawScrape, assert_quality (deterministic, no LLM)
├── job_scraper_helpers.py # parse_html_with_markitdown, parse_html_with_html2text,
│ # detect_placeholder_content, detect_prompt_injection,
│ # clean_job_posting_markdown
└── injection_guard.py # optional local classifier (sira[guard] extra) + consent flow
Job Scraper Architecture¶
fetch_job_markdown(url): Playwright (headless Chromium) → raw HTML → Markdown →assert_quality→detect_prompt_injection→RawScrapeparse_html_with_markitdown(html): Primary parser viamarkitdownlibraryparse_html_with_html2text(html): Fallback parser viahtml2textlibrarydetect_placeholder_content(text): Validates extracted content isn't error/placeholder (checks for<scripttags, "click here", "error loading", "404", minimum 100 chars)detect_prompt_injection(raw_html, extracted_text): Regex scan for instruction overrides, AI-addressing, role/output manipulation, exfiltration requests, invisible Unicode, and phrases present in the HTML but not in the visible text (hidden_content). Advisory; returns category names only.clean_job_posting_markdown(markdown): Normalizes whitespace, collapses blank linesinjection_guard.classify(markdown): Optional second layer — a local discriminative classifier (not a generative LLM), opt-in via theguardextra plus a remembered consent; any failure degrades to regex-only.
Utils Layer¶
sira/utils/
├── cv_diff.py # Pure Python CVDiff + GapAnalysis + match score
├── skill_matching.py # render_cv_text, literal pre-pass
├── skill_cleanup.py # clean_skill_groups: duplicate skill variants → one entry
├── markdown_writer.py # generate_report_markdown
├── resume_converter.py # InputConverterRegistry: DOCX/PDF → Markdown via markitdown
└── validate_inputs.py # Standalone input validation (not used by Typer CLI)
A same-named but different file, sira/workflows/skill_matching.py, holds the one exception to "no model calls in utils/": match_skills orchestration — literal pre-pass → skill_matcher_agent → fallback to literal-only matching on AgentRunError. It lives under workflows/ because it calls a model; utils/skill_matching.py above stays model-free.
Rendering Layer¶
sira/rendering/
├── __init__.py # render_resume(cv, dir, base_name, style) → .md/.pdf/.docx
├── errors.py # RenderError
├── templates.py # TemplateSpec: modern, classic, compact
├── inline.py # inline markdown subset (links, bold, italic, code)
├── html.py + resume.html.j2 # CV → HTML (Jinja2)
├── css.py # TemplateSpec → CSS for the PDF
├── pdf.py # HTML + CSS → PDF (PyMuPDF Story)
├── docx.py # CV + TemplateSpec → DOCX (python-docx)
└── markdown.py # CV → Markdown
render_resume writes the Markdown first — a failure there propagates, since nothing
useful was saved — then the PDF and DOCX, each independently guarded: a failing format
is reported in RenderedResume.errors and its path is None, so one bad format never
blocks the other two. One TemplateSpec per style (modern, classic, compact)
drives both css.py (the PDF stylesheet) and docx.py (the DOCX styler), so the two
outputs cannot drift apart.
Section headings never end a page alone. The DOCX uses Word's keep_with_next on
Heading 2. MuPDF ignores every CSS avoid rule, so pdf.py lays the page out, reads
the recorded element positions (each section is <h2 id="section-<slug>"> plus a
<div id="section-<slug>-body">), and when a body opens on a later page than its
heading it forces page-break-before: always on that heading and lays out again
(capped at four passes; one extra pass is the norm).
Durable Execution¶
Every run is one DBOS workflow (sira.tailor) whose input is a TailorInputs snapshot (resume text, job content, model tiers, quality-gate settings, and the CLI metadata needed for post-processing). Inside it:
sira.tailor (workflow, id = run id)
├─ sira.parse_resume (child workflow) ─ model-request steps (Parser)
├─ sira.analyze_job (child workflow) ─ model-request steps (Analyst)
├─ write → review → audit loop ─ model-request steps (Writer, Reviewer, Auditor, Quality Gate)
├─ sira.human_checkpoint (step) ─ the interactive answer, checkpointed
├─ skill matcher ─ model-request step (Skill Matcher)
└─ report ─ model-request steps (Report)
- Every agent carries pydantic-ai's
DBOSDurabilitycapability: a model request that runs inside the workflow is a checkpointed step (with retries on transient errors). Outside a workflow (the job scraper, the memory cache parser) the capability is transparent. - Parser and Analyst are child workflows because DBOS requires a deterministic step order inside one workflow; each child owns its own sequence, so they may run concurrently.
- DBOS only continues a run under the same executor id and application version. Sira uses a fresh executor id per process, so a new
sira tailornever silently picks up an old run; continuation is explicit (sira resume) and pinned to the installed Sira version. - Continuation (
sira/workflows/continuation.py): interrupted runs are resumed in place; failed runs are forked from the failed step (or the start of a failed child workflow, or the last checkpoint when the user aborted), which creates a new run id with the earlier checkpoints copied. - The system database is SQLite at
dbos.sqlite3in the per-user data directory (sira/paths.py;SIRA_DBOS_DATABASE_URLoverrides it). Post-processing (output files, memory save) stays outside the workflow and is repeated byresume. - Every model request is now made in streaming mode (the durability capability attaches an event-stream handler), including non-interactive runs.
CLI¶
Entry point: sira/main.py — Typer app, console script sira. Five subcommands: tailor, re-tailor, resume, runs, setup.
Subcommands¶
tailor — Full workflow¶
uv run sira tailor JOB_URL RESUME_PATH [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--output-dir |
PATH | ./output |
Output directory |
--model |
TEXT | None |
LLM provider:model override (e.g., anthropic:claude-sonnet-4-5) |
--verbose / -v |
FLAG | False |
Stream agent thinking in real-time |
--debug / -d |
FLAG | False |
Save converted resume, show content hashes |
--output-pattern |
TEXT | {company_name}-{job_title} |
Subdirectory name template |
--resume-name-pattern |
TEXT | {company_name}-{full_name} |
Resume file base name template |
--style |
ENUM | modern |
Resume template for the PDF and DOCX: modern, classic, compact |
--fast |
FLAG | False |
Speed preset: gate threshold 5, mechanical stages on openai:gpt-5-nano, --model (or openai:gpt-5-mini) as the strong tier. Loops stay at the defaults below. |
--write-attempts |
INT | 2 |
Max writer attempts in the write → review → audit loop |
--review-iterations |
INT | 1 |
Max reviewer iterations per write attempt |
--quality-gate / --no-quality-gate |
FLAG | on | Enable the advisory quality gate |
--gate-threshold |
INT | 6 |
Re-run a gated agent only when its score is below this |
--interactive / -i |
FLAG | False |
Pause at quality checkpoints (audit failure, weak match); skipped when stdin is not a TTY |
Template variables: {company_name}, {job_title}, {full_name}, {timestamp}
re-tailor — Re-run with audit feedback¶
uv run sira re-tailor JOB_ID RECOMMENDATIONS [OPTIONS]
All options from tailor plus:
| Option | Type | Default | Description |
|---|---|---|---|
--resume-path |
TEXT | None |
Resume path (uses stored path if omitted) |
Edge case: When the original resume file no longer exists on disk but a source record is stored, the CLI prints an error and instructs the user to re-provide --resume-path.
resume — Continue an interrupted or failed run¶
uv run sira resume RUN_ID [--verbose] [--style …]
Continues the DBOS run named by RUN_ID (printed by tailor / re-tailor; not the Job ID). Interrupted runs resume in place; failed runs are forked into a new run id. Post-processing (output files, memory save) is repeated. See Durable Execution.
runs — List recent runs¶
uv run sira runs [--limit N]
setup — Install the browser¶
uv run sira setup
Runs playwright install chromium with Sira's own interpreter, so it works after uv tool install sira / pipx install sira where the playwright executable is not on PATH.
Execution Flow¶
The commands are synchronous wrappers (def) that call asyncio.run() on async implementation functions:
tailor→asyncio.run(_tailor_impl(...))re_tailor→asyncio.run(_re_tailor_impl(...))resume→asyncio.run(_resume_impl(...))runs→asyncio.run(_runs_impl(...))
Technology Stack¶
| Component | Technology | Version Constraint |
|---|---|---|
| Language | Python | ≥ 3.13 |
| Package manager | uv | latest |
| Agent framework | pydantic-ai | ≥ 2.43, < 3 |
| Data validation | Pydantic v2 | (via pydantic-ai) |
| CLI framework | Typer | ≥ 0.25.1 |
| Web scraping | Playwright | ≥ 1.56.0 |
| Injection guard | transformers + torch | ≥ 4.45 / ≥ 2.2 (opt-in guard extra) |
| HTML→Markdown | html2text | ≥ 2025.4.15 |
| DOCX/PDF→Markdown | markitdown | ≥ 0.1.0 |
| HTML→PDF | pymupdf | ≥ 1.26 |
| HTML templating | jinja2 | ≥ 3.1 |
| DOCX generation | python-docx | ≥ 1.1.0 |
| Rich output | rich | ≥ 14.2.0 |
| Memory | SQLite (stdlib) | — |
| Linting | ruff | ≥ 0.14.6 (dev) |
| Testing | pytest | ≥ 8.0.0 (dev) |
| Releases | commitizen | ≥ 4.15.1 (dev) |
| Build backend | hatchling | — |
Key Design Decisions¶
-
Shared Quality Gate: One
quality_gate_agentscores every gated agent (writer, auditor, and the unwired cover-letter writer) via role-specific scoring criteria, rather than per-agent custom validation code. -
Python-Computed Metrics:
GapAnalysis,match_scoreandoverall_recommendationare computed incv_diff.pyfrom per-skill verdicts. The only model involvement is the skill matcher's yes/no-with-evidence per skill; the report agent only writes prose. -
Always-Run Report Phase: The Report phase executes regardless of audit pass/fail, ensuring users always get actionable feedback.
-
Content-Hash Caching:
ResumeMemoryServicecaches parsed CVs by content hash + parser version. If the resume hasn't changed, AI parsing is skipped entirely. -
Fallback State Pattern: Each quality-gated agent stores its
last_outputin a module-level_QualityStateinstance. On quality gate exhaustion, the fallback is used rather than crashing. -
Inner Loop with Outer Retry: The Write → Review refinement loop (
--review-iterations, default 1) is nested inside the Write → Audit retry loop (--write-attempts, default 2). This allows both fine-tuning and broader corrections. -
Job Fingerprint Dedup: Tailored resumes are keyed by a truncated SHA-256 fingerprint (first 32 hex chars of
{job_url}:{job_title}), preventing duplicate entries for the same job applied multiple times. -
Pre-Parsed CV Bypass: The workflow accepts an optional
pre_parsed_cvparameter. When provided (from cache), the Resume Parser stage is skipped entirely, saving AI calls. -
Streaming via the reporter:
run_agent()emits lifecycle and token events to the activeProgressReporter(sira/reporting/);VerboseReporter(--verbose) prints theTextPartDelta/ThinkingPartDeltastream,LiveDashboard(default) shows a Rich panel. Theverboseparameter onrun_agent()is retained only for call-site compatibility. -
CLI runs under asyncio: All async implementation functions use
asyncio.run()from synchronous Typer command wrappers. -
Durable by default: the pipeline is one DBOS workflow, so every model request is a checkpointed step and a killed or failed run can be continued with
sira resume(see Durable Execution).