Outil d'assistance pour candidater plus vite et plus juste sur le marché de l'emploi français.
SmartApply collecte des offres sur plusieurs sources, filtre localement le bruit, classe les meilleures par pertinence sémantique, analyse les retenues avec un LLM, adapte ton CV + lettre + email à chaque offre, et prépare un brouillon Gmail prêt à envoyer. Rien ne part automatiquement — chaque étape passe par ta validation.
- Cascade de coûts — tout ce qui est déterministe (parsing, dédoublonnage, filtre, scoring) reste local et gratuit ; le LLM n'intervient que là où il apporte une vraie valeur (compréhension d'offre, rédaction CV/lettre/email).
- Anti-hallucination strict — chaque bullet du CV pointe vers un
source_iddu profil et passe un validateur déterministe qui élimine les faits inventés. Le LLM ne peut pas inventer un email d'entreprise à partir du nom de marque. - Contrôle à chaque étape — l'UI Streamlit expose 5 étapes manuelles (Fetch → Scoring → Analyse → Génération → Finalisation). Tu peux désélectionner, surcharger un filtre, récupérer une offre archivée par erreur, ajuster le Top-K.
- Aucun envoi automatique — la brique Gmail crée uniquement un brouillon (scope
gmail.compose). Un test statique AST en CI bloque toute introduction d'appelsend/messages.send/drafts.send.
make install-all # crée .venv, installe UI + PDF + Gmail + dev
cp .env.example .env # renseigne au minimum OPENAI_API_KEY
make init-db
make test # 597 tests, ~6 s, 100 % offline
make run-app # ouvre le dashboard StreamlitVariante allégée sans Streamlit / PDF / Gmail : make install.
| # | Étape | Ce qui se passe | LLM |
|---|---|---|---|
| 1 | Fetch | Recherche multi-sources, filtre local par signaux typés, dédup contre la DB, override manuel possible | — |
| 2 | Scoring | Embeddings + scoring composite, slider Top-K pour la présélection | Embeddings |
| 3 | Analyse | Extraction structurée (rôle, skills, risques, contact, classification du host) | LLM cheap |
| 4 | Génération | CV + lettre + email adaptés à l'offre (un seul appel structuré) | LLM smart |
| 5 | Finalisation | Dry-run preview, puis création du brouillon Gmail OU export .eml |
— |
Deux onglets en haut du Workflow : Recherche contrôlée (tu pilotes chaque étape) ou Autopilot express (un seul bouton pour générer N dossiers en bloc avec quality gate).
| Page | Rôle |
|---|---|
| Workflow (screenshot) | Pipeline 5 étapes manuel ou autopilot |
| Offres (screenshot) | Table searchable, détail par offre, récupération d'une archive |
| Candidatures (screenshot) | Dossiers générés : CV, lettre, email, brouillon Gmail |
| Profil (screenshot) | Lecture du profil candidat, ID des bullets utilisés par le validateur |
| Stats (screenshot) | Entonnoir par étape, coût LLM cumulé, taux de pertinence |
| Autopilot (screenshot) | Run automatique haut volume (génération en bloc, jamais d'envoi) |
- Vraies nouvelles offres au fetch —
max_results=Ncible désormais N offres vraiment nouvelles (pas déjà en DB), pas N offres brutes dont la moitié sont des doublons. Une garde-fou SerpApi refuse le mode--max-results unlimitedqui exploserait le budget. - Localisations FR résolues automatiquement —
Paris→departement=75,Île-de-France→region=11, top villes pinnées offline, fallbackgeo.api.gouv.frpour les 34 945 communes (cache disque versionné). - Champ expérience structuré sur France Travail (
experienceExige+ libellé) et Welcome to the Jungle (experience_minannées) — utilisé en priorité par le filtre avant de retomber sur la description. - Filtre local prudent refactorisé en signaux typés (
contract_signals,location_signals,role_signals,seniority). Corrections P0 contre les faux rejets :apprentissage automatique≠ contrat,institution indépendante≠ freelance,30 ans d'expérience entreprise≠ exigé du candidat,rattaché au directeur≠ poste de management. - Récupération manuelle d'une offre archivée — bouton "Réinjecter avec score maximal" sur la page Offres : l'offre repasse en
SHORTLISTEDavec toutes les composantes à 1.0, audit du rejet conservé. - Slider Top-K interactif en étape 2 pour calibrer combien d'offres iront à l'analyse LLM.
- Aperçu Gmail avant création — étape 5 affiche destinataire / objet / pièces jointes / taille encodée sans aucun appel réseau, puis tu décides.
- Classification des hosts —
company_domain/ats/partner_job_board/application_redirect/unknownpour orienter la stratégie email/formulaire sans inventer un domaine d'entreprise. - Validateur lettre de motivation — 3 paragraphes obligatoires, alias techniques autorisés sourcés du profil, élisions françaises normalisées, acronymes protégés (ML, IA, RAG).
| Source | Mode | Clé requise |
|---|---|---|
| France Travail | API officielle, expérience + localisation structurées, FR uniquement | FRANCETRAVAIL_CLIENT_ID / _SECRET |
| Welcome to the Jungle | Matches personnalisés via ta session, pagination profonde (150 × 50) | WTTJ_COOKIE |
| Google Jobs (SerpApi) | API payante, couverture mondiale, filtre date_posted |
SERPAPI_API_KEY |
| Manuel | URL ou texte collé, refuse les schémas non-HTTP et les IPs privées | — |
Détails par source (paramètres, quirks, fallbacks) : docs/sources/.
| Clé | Pour quoi | Obligatoire ? |
|---|---|---|
OPENAI_API_KEY |
LLM + embeddings | Oui (sauf mode mock) |
FRANCETRAVAIL_CLIENT_ID / _SECRET |
API France Travail | Si francetravail actif |
WTTJ_COOKIE |
Welcome to the Jungle | Si welcometothejungle actif |
SERPAPI_API_KEY |
Google Jobs via SerpApi | Si serpapi actif |
GMAIL_CREDENTIALS_PATH |
Brouillons Gmail (OAuth Desktop client) | Si tu veux les brouillons |
ANYMAILFINDER_API_KEY |
Découverte de contacts RH | Si tu veux l'enrichissement contact |
TOP_K_RANKED |
Défaut du slider Top-K en étape 2 | Non (défaut 25) |
EMBEDDINGS_PROVIDER |
openai / local / mock |
Non (défaut openai) |
Voir .env.example pour la liste complète.
make install-all(installegoogle-api-python-clientvia l'extragmail).- Sur Google Cloud Console : activer Gmail API, créer un client OAuth Desktop app, télécharger le JSON, placer dans
secrets/credentials.json. smartapply gmail-checkvalide la config sans toucher au réseau.- Crée ton premier brouillon depuis l'UI (étape 5) ou via
smartapply apply --job-id N --gmail-draft. Le navigateur s'ouvrira une seule fois pour l'OAuth, puissecrets/token.jsonsera réutilisé.
Seul endpoint Gmail appelé : users().drafts().create. Le test tests/test_email_agent.py::test_gmail_draft_module_has_no_send_calls bloque statiquement toute introduction d'un appel send.
smartapply init-db
smartapply ingest --source francetravail --query "Data Scientist" -l "Paris" --date-posted week
smartapply ingest-url --url https://acme.example/jobs/42
smartapply ingest-text --title "ML Engineer" --company "Acme" --file offer.txt
smartapply process --top-k 20
smartapply apply --job-id 42 --gmail-draft
smartapply pipeline --source francetravail --query "Data Scientist" -l "Paris" --top-apply 5
smartapply autopilot --query "Data Scientist OR ML Engineer" -l "Paris" --target-drafts 25 --gmail-draft
smartapply gmail-check # diagnostic config Gmail, aucun appel réseau
smartapply list-jobs --status analyzed
smartapply list-applications
smartapply update-application --application-id 1 --status sent --notes "Relancer mardi"
smartapply stats # coût LLM + entonnoir par statut--max-results accepte un entier ou none / all / unlimited (sauf pour SerpApi où le mode unbounded est rejeté pour éviter de brûler le budget API).
Scraping (France Travail / WTTJ / SerpApi / Manuel)
│
▼
Parsing + dédoublonnage ───────────────── LOCAL, gratuit
│
▼
Filtre local par signaux typés (contract / location / role / seniority)
│
▼
Scoring sémantique (embeddings, Top-K configurable) ──── ~$0.001
│
▼
Analyse LLM structurée (top-K offres) ───────────────── ~$0.01
│
▼
Génération CV + lettre + email (un appel par offre) ── ~$0.03 / offre
│
▼
Validation anti-hallucination (déterministe)
│
▼
Brouillon Gmail OU export `.eml` ──────── jamais d'envoi
│
▼
DB SQLite + dashboard Streamlit
Modules : scrapers, parsing, dedup, filtering (+ source_facts), ranking, llm (+ analyzer_input, source_metadata), cv (sélecteur + adapter + validateur + générateurs DOCX/PDF), email_agent (Gmail + .eml + contacts), pipeline (+ reports, apply_specs, contact_service), app (Streamlit 7 pages), cli. Chaque module est branché via une interface (Scraper, LLMProvider, EmbeddingsProvider, ContactProvider) et remplaçable indépendamment.
Quatre garde-fous combinés :
- Schéma JSON strict sur tous les appels LLM (
response_format=json_schema), pas de texte libre. - Evidence gate dans le prompt d'analyse — interdit d'utiliser le titre, la metadata scraper ou le slug d'URL comme preuve pour
required_skills,extracted_location,company_context,contact_domain_hint. Pas deCapgemini.cominventé à partir du nom de l'entreprise. - Validateur CV/lettre (
smartapply.cv.validator,smartapply.cv.motivation_validator) — chaque bullet doit pointer vers unsource_iddu profil, les chiffres inventés sont signalés,auto_fixretire les éléments non valides, la lettre doit avoir 3 paragraphes. - Quality gate — un dernier appel LLM cheap relit le dossier avant qu'il soit marqué prêt.
| Usage | Modèle | Coût |
|---|---|---|
| Embeddings 30 offres | text-embedding-3-small |
< $0.001 |
| Analyse 20 offres | gpt-4o-mini (ou gpt-5.4-mini) |
~$0.01 |
| CV + lettre + email × 5 | gpt-4o |
~$0.17 |
| Quality gate × 5 | gpt-4o-mini |
~$0.005 |
| Total | ~$0.18 |
Cache LLM activé par défaut → ré-exécution gratuite. Suivi en temps réel dans la page Stats du dashboard ou via smartapply stats.
make test # 597 tests, ~6 s, 100 % offline
make test-fast # exclut le test d'intégration end-to-endToutes les API externes (SerpApi, France Travail, WTTJ, OpenAI, geo.api.gouv.fr, Gmail) sont mockées. Trois contrats statiques AST verrouillent les invariants critiques : pas de clé manquante dans les dicts UI, pas d'appel send Gmail, pas de bouton "Envoyer" dans l'étape 5.
Côté sécurité : .env, secrets/, data/secrets/, *token*.json, credentials.json sont gitignored ; OPENAI_API_KEY et SERPAPI_API_KEY sont forcés à vide pendant les tests ; le scraper manuel refuse les URLs non-HTTP, les hôtes locaux et les IPs privées (anti-SSRF) ; la brique Gmail ne logue ni destinataire, ni body, ni token.
- France Travail ne couvre que la France (les autres pays passent par SerpApi).
- WTTJ exige une session valide ; pas d'OAuth, juste un cookie copié depuis le navigateur.
- L'autopilot ne soumet jamais un formulaire ATS — il génère le dossier et marque
ready_for_form_submission; la soumission ATS reste manuelle. - Anthropic en provider LLM est cadré côté config mais pas encore branché à 100 %.
- Nouveau scraper : implémenter
smartapply.scrapers.base.Scraper, enregistrer danssmartapply/scrapers/registry.py, ajouter un builder dansfiltering/source_facts.pyetllm/source_metadata.py. - Nouveau LLM provider : implémenter
LLMProvider(voirMockLLMProvider), brancher dans la factory. - Embeddings locaux :
pip install -e '.[local-embeddings]', puisEMBEDDINGS_PROVIDER=local. - Nouveau host de candidature : éditer le catalogue dans
smartapply/email_agent/contact_providers.py+ ajouter un test danstests/test_contact_providers.py.
MIT.

