Skip to content

Repository files navigation

ApplyForge Logo

ApplyForge - Apply Pipeline: Discover, Tailor, Apply, Track


Version Python License LinkedIn

فارسی

What it does: You send a job posting link — the system reads it, picks the right CV, writes a personalised cover letter, and delivers two ready-to-send PDFs. It also tracks every PhD/job application end-to-end: Gmail sync, a terminal TUI, and a local web dashboard, all backed by a SQLite tracker (see Application Tracker).


First-Time Setup

Step 1 — Run the installer (zero-touch, run once)

./install.sh

This detects your OS, installs uv + LaTeX (+ optional tesseract OCR), runs uv sync, links the shared agent constitution (a central clone — not a submodule), registers its skills into ~/.claude/skills/, and creates your personal-data vault at ~/.local/share/agent-projects/applyforge/ with a seeded secrets/.env. Personal data never lives in the repo.

Step 2 — Create your personal profile (in the vault)

cp examples/master_cv.example.json \
   ~/.local/share/agent-projects/applyforge/data/master_cv.json

Edit it with your real name, contact info, work history, and skills. It lives in the vault, outside the repo — it can never be committed.

Step 3 — Add your API keys (in the vault)

The installer already seeded ~/.local/share/agent-projects/applyforge/secrets/.env from .env.example. Open it and fill in:

  • CV_OWNER_SLUG — your name slug used in all output filenames (e.g. Firstname_LASTNAME)
  • LLM_MODEL — LLM model identifier (e.g. deepseek-v4-flash for DeepSeek, or gpt-4o for OpenAI)
  • OPENAI_API_KEY or DEEPSEEK_API_KEY — depending on your chosen LLM provider
  • TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID — only needed for the Telegram bot (see docs/bot-setup.md)

Step 4 — Verify everything is working

uv run main.py test

Generating an Application

Quickstart (minimal)

If you want to try the tool quickly, run these commands from the repository root:

# 1) Install Python deps

uv sync
# 2) Copy env and set your OpenAI key

cp .env.example .env
# Edit .env and set OPENAI_API_KEY (and TELEGRAM_* if using the bot)
# 3) Create the internal profile (accepts .tex, .pdf, .jpg/.png)
Examples:

uv run main.py init-profile --cv templates/lato/CV_AI_en.tex
uv run main.py init-profile --cv path/to/my_cv.pdf
uv run main.py init-profile --cv path/to/photo_of_cv.jpg
# 4) Generate an application from a job URL (optional: pass --cv to override)

uv run main.py apply https://company.com/jobs/12345
uv run main.py apply https://company.com/jobs/12345 --cv path/to/my_cv.pdf
# 5) Or start the Telegram bot and use /apply from your phone

uv run main.py bot
# 6) Quick health check

uv run main.py test

Notes:

  • The --cv flag for init-profile and apply accepts .tex, .pdf, and common image formats (.jpg, .jpeg, .png, .webp).
  • PDF extraction and image OCR require additional system/software:
    • pdfminer.six, pytesseract, and Pillow are Python dependencies (declared in pyproject.toml).
    • tesseract OCR engine must be installed on your system for OCR to work. On macOS: brew install tesseract.
  • If you only use a LaTeX .tex source, you do NOT need tesseract or the OCR Python packages — the .tex path works without extra system deps.
  • The --template flag for apply supports altacv (default) and lato.
  • The --licence flag for apply forces inclusion of the conditional electronics degree.
  • The pipeline automatically blocks applications that require: Permis B, fonctionnaire status, French nationality, or Secret Défense clearance.
  • For Technicien-tier postings (catégorie B, Bac+2/3 keywords), the pipeline automatically drops the DU degree, filters Master honors to Réseaux/Systèmes modules only, and normalizes experience titles from "Ingénieur" to "Technicien" — no LLM involved.
  • Bilingual master CV: data/master_cv_en.json (English source of truth) and data/master_cv_fr.json (French). When --lang fr is used, the pipeline loads CV_<Role>_fr_source.json and generates it from master_cv_fr.json if absent — experience titles, degrees, and certifications are French from the start, no manual translation needed.

Option A — Telegram (easiest)

Start the bot:

uv run main.py bot

Then in Telegram, send:

/apply https://company.com/jobs/your-job-link

The bot will:

  1. Read the job posting
  2. Generate a tailored CV + cover letter (takes ~30 seconds)
  3. Send you both PDFs to review
  4. Ask Approve or Reject
    • Approve → saves both files to your private archive channel
    • Reject → nothing is saved

Option B — Terminal

uv run main.py apply https://company.com/jobs/your-job-link

The two PDFs are saved in the Applied/ folder.


Where Are My Files?

What Where
Generated applications (CV + cover letter per job) Applied/YYYY-MM-DD_Company_Role/
Spontaneous applications (no company) Applied/YYYY-MM-DD_Spontannee_Role_lang/

Output Filenames: All generated CVs and cover letters follow the standardized naming pattern:

{CV_OWNER_SLUG}-{DocumentType}_{Role}_{Language}.{ext}

The slug is set via CV_OWNER_SLUG in your .env file. Examples:

  • Firstname_LASTNAME-CV_IT_fr.pdf — IT infrastructure CV (French)
  • Firstname_LASTNAME-LettreMotivation_AI_fr.pdf — AI cover letter (French)
  • Firstname_LASTNAME-CV_PhD_en.pdf — PhD application CV (English)
  • Firstname_LASTNAME-LettreMotivation_Courte_AI_fr.txt — plain-text short cover letter (≤1500 chars), generated alongside every PDF letter for pasting into character-limited application portals (e.g. France Travail)

This naming convention makes it easy to identify document type and role at a glance.


Updating Your CVs

Change your contact info (name, email, phone, location)

Edit one file only:

templates/shared/personal_data.tex

All CVs pull from this file — you never need to update the same detail in multiple places.

Change CV content (work experience, skills, etc.)

Templates are organized by style family:

Filenames follow CV_<Label>_<lang>.tex, matching the role labels in config/roles.yaml and the profile JSON files (…-CV_<Label>_source.json).

templates/altacv/ — AltaCV style (xelatex), used for spontaneous applications:

File Role Use for
CV_AI_fr.tex / CV_AI_en.tex ai AI / MLOps roles
CV_IngenieurEtude_fr.tex ingenieur_etude Ingénieur d'études / IGE (placeholder copy of AI)
CV_DevOpsAlternance_fr.tex devops, devops_alternance DevOps (alternance layout)
CV_Polyvalent_fr.tex polyvalent Polyvalent / interim agency (French)
CV_Python_fr.tex python Generalist Python (placeholder copy of Polyvalent)

templates/lato/ — Lato/article style (pdflatex):

File Role Use for
CV_AI_en.tex ai AI / Data Science / Python roles (English)
CV_Support_fr.tex support IT Support / Network technicien (French)
CV_Technicien_fr.tex technicien Intérim / technicien-tier — pre-down-levelled (French)
CV_PhD_en.tex phd PhD / Research applications (English)

templates/classic/ — ModernCV banking style (pdflatex), 16 role variants.

After editing a lato or classic template, rebuild the PDF:

./compile.sh ai        # CV_AI_en
./compile.sh support   # CV_Support_fr
./compile.sh phd       # CV_PhD_en
./compile.sh all       # rebuild all CV_*.tex across all template folders

Spontaneous Applications

Generate a pre-written CV without LLM — no job URL needed:

uv run main.py spontaneous python                # Generalist Python (French)
uv run main.py spontaneous ai                    # AI / MLOps (French)
uv run main.py spontaneous ai-en                 # AI / MLOps (English)
uv run main.py spontaneous devops                # DevOps (French)
uv run main.py spontaneous devops_alternance     # DevOps alternance / work-study (French)
uv run main.py spontaneous support               # Network / support technicien (French)
uv run main.py spontaneous phd                   # PhD / Research (English)
uv run main.py spontaneous ingenieur_etude       # Ingénieur d'études / IGE (French)
uv run main.py spontaneous polyvalent            # Polyvalent / interim (French)
uv run main.py spontaneous technicien            # Intérim technicien-tier, no DU/honours (French)

# Add --city to select Montpellier vs City Name automatically:
uv run main.py spontaneous ai --city montpellier

Output goes to Applied/YYYY-MM-DD_Spontannee_{role}_{lang}/.


Roles Registry (config/roles.yaml)

All CV tracks are defined in one file: config/roles.yaml. The role classifier, filename labels, CV + spontaneous template maps, cover-letter variant, and per-role skill ordering are all derived from it — nothing is hardcoded elsewhere.

Canonical roles: general, devops, ai, phd. Each entry lists aliases that route fuzzy or legacy inputs to a canonical role (e.g. it, networkdevops; python, mlopsai; polyvalentgeneral). Append -en/-fr to a spontaneous role to override its language (ai-en).

Add a new role by adding an entry under roles: — or auto-scaffold one (clones a base template stub + registers the entry):

from src.core import roles
roles.scaffold_role("cloud", base="devops", lang="fr")

Run the registry contract tests with:

python tests/test_roles.py        # or: pytest tests/test_roles.py

Updating Your Cover Letter

Open the relevant template in cover_letters/:

File Language
Cover_Letter_Template_Fr.tex French
Cover_Letter_Template_En.tex English

Only edit the stock paragraphs (the text that describes your experience). The personalisation variables at the top (\CompanyName, \PositionTitle, etc.) are filled automatically for each application — do not touch them.

Cover letter body is now fully LLM-generated. \CLIntro and \CLBody are written by the LLM based on the actual job posting — no hardcoded variant blocks. The LLM adapts tone and content: technical for IT/AI roles, transferable-skills focused for maintenance or industrial roles.


Data Scraping (France Travail)

Scrape job-market stats for data roles

node scripts/data_jobs_scraper.mjs

Reads ROME codes from docs/it_rome_codes.json, visits candidat.francetravail.fr/metierscope/fiche-metier/{CODE}/ for each role, extracts national offer count and candidate count, saves to docs/data_jobs_stats.json.

Key ROME codes for data jobs:

Code Métier
M1405 Data Scientist
M1419 Data Analyst
M1811 Data Engineer
M1889 Ingénieur IA
M1872 Consultant BI

Note: France Travail uses non-breaking spaces as thousands separators ("3 830"). The scraper strips all non-digit characters before parsing numbers.


Application Tracker

src/apply_tracker/ tracks every PhD/job application from first sighting to reply, in a SQLite DB (tracker.db) mirrored to per-track tracking.json files. It's CLI-agnostic — the same src/apply_tracker/service.py business layer backs three interfaces, invoked here directly with uv run python -m:

# Web dashboard (localhost:8765, auto-reload)
uv run python -m src.apply_tracker.web ~/@-Amir/Apply/2026-2027 8765

# Terminal TUI (arrow-key navigation)
uv run python -m src.apply_tracker.tui ~/@-Amir/Apply/2026-2027 phd

# Gmail sync — pulls [AMIR-SYNC] tagged position emails into the tracker
uv run python -c "
from pathlib import Path
from src.apply_tracker.gmail_sync import fetch_and_process
print(fetch_and_process(Path.home() / '@-Amir/Apply/2026-2027'))
"

# Quick stats
uv run python -m src.apply_tracker.stats_cli ~/@-Amir/Apply/2026-2027

The amir CLI (separate repo, amir-cli) wraps these as amir apply web|tui|sync|stats|alert and amir phd/amir job — same wrap pattern as amir apply <url> forwarding to main.py apply in this repo. The application data itself (tracking.json, tracker.db, found/applied position files) lives outside this repo, in the personal-data vault (~/@-Amir/Apply/2026-2027 by default, override with APPLY_BASE_DIR) — never committed.


Coherence Gate (main.py check)

A generated application is a set of documents that must tell one story: the CV, the letter PDF and the two .txt letters all restate the same degrees, the same figures and the same identity. Editing one of them by hand — which is the normal workflow — silently desynchronises the rest. The gate audits a finished folder for exactly that:

uv run main.py check "$APPLY_BASE_DIR/Job-Search/applied/2026-08-03_Acme_Technicien_fr"

It reads whatever is on disk right now (not the pipeline's in-memory bundle), so hand-edits are covered. Layer 1 is eight deterministic rules — R1 stale artifact (mtime + SHA sidecar), R2/R3 degree support and contradiction, R4 figures backed by the CV on the same subject, R5 identity, R6 completeness and letter format, R7 one language per document, R8 leftover placeholders. Layer 2 is a single free agy flash call that catches wording contradictions the rules cannot see; it runs by default, is non-blocking, and no-ops when agy is not installed.

uv run main.py check <dir> --no-semantic          # Layer 1 only, fully offline
uv run main.py check <dir> --report /tmp/out.md   # write the report elsewhere

COHERENCE.md is written into the folder only when the target sits inside this repo; for folders in the personal vault the report goes to stdout unless --report is given.


Technical Documentation

For developers, AI agents, or anyone who wants to understand the internals:

Topic File
How the pipeline works (scraping, LLM, compilation) docs/architecture.md
LaTeX macros, template structure, adding new styles docs/latex-templates.md
Telegram bot setup, AUTO_APPLY, source files docs/bot-setup.md
Git workflow, commit conventions, tracked files docs/git-workflow.md

Contact

Contact details live in the personal-data vault outside the repo (~/.local/share/agent-projects/applyforge/shared/personal_data.tex, override with APPLYFORGE_DATA_DIR) — private, never committed.

About

End-to-end job application automation in Python: scrapes job boards, filters offers against hard rules, picks the right CV variant, and generates tailored cover letters with LLMs (LangChain/Gemini/OpenAI).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages