Objectif produit
CV Critic & Strategy Agent est un workflow interne d'audit du CV et du positionnement professionnel. Objectif volontairement opposé à SAIF-IA : challenger le discours au lieu de vendre.
- Analyser le CV, le portfolio et le chatbot avec un regard critique externe
- Produire une critique globale et une critique spécifique du CV imprimable
- Transformer ces critiques en stratégie d'amélioration priorisée
- Versionner chaque run pour suivre l'évolution du positionnement
- DĂ©montrer une migration technique complĂšte : Node.js manuel â CrewAI script â CrewAI native autonome
Ce workflow ne remplace pas la dĂ©cision humaine. Il sert d'outil d'aide Ă l'itĂ©ration â un Ćil externe simulĂ© sur un dispositif qu'on ne peut plus voir objectivement.
Architecture globale
Flux 3 agents â critiques indĂ©pendantes + stratĂ©gie contextualisĂ©e · identique Node.js et CrewAI
L'architecture métier reste stable dans les trois implémentations. Ce qui change : le niveau d'abstraction, la formalisation des rÎles, le passage de contexte et la maintenabilité du runner.
Agents & séparation des rÎles
Simule un recruteur qui découvre le portfolio complet : CV public, chatbot SAIF-IA, fiches techniques, knowledge serveur. Ne lit pas le contexte interne. Son regard doit rester indépendant et dur. Produit global.md.
Simule un recruteur qui ouvre uniquement le CV imprimable/téléchargeable. Il juge la lisibilité A4, la clarté en 10 secondes, la cohérence titre/preuves, la compatibilité ATS. Ne lit pas le contexte interne. Produit printable-cv.md.
Seul agent à lire context.md (décisions internes validées). Lit les deux critiques et produit un plan d'action priorisé P0/P1/P2. Dans la version CrewAI, reçoit les outputs des deux autres agents via injection automatique (context=[task_global, task_cv]). Produit strategy.md.
Séparation des sources
| Agent | Lit sources portfolio | Lit context.md | Reçoit outputs autres agents |
|---|---|---|---|
| Global Critic | â Portfolio complet (6 fichiers) | â Non | â Non |
| Printable CV Critic | â CV seul (3 fichiers) | â Non | â Non â intentionnel |
| Strategy Agent | â Non (lit les critiques) | â Oui | â Oui (global + CV) |
Raison de l'indépendance des critics : chaque critic simule un regard externe naïf. Si le CV Critic lisait la critique globale, son verdict serait biaisé. La séparation est une rÚgle métier, pas une contrainte technique.
Séquences d'un run
Trois modes sont conservĂ©s en parallĂšle. MĂȘme contrat de sortie, trois niveaux d'abstraction (SDK direct â script CrewAI â CrewAI native).
Ătape 1 â Node.js legacy, orchestration manuelle
npm run critique-cv
â
ââ charge .env.local (ANTHROPIC_API_KEY)
ââ prĂ©pare reports/<timestamp>/
â
ââ [1] Global Critic â Anthropic SDK direct
â lit : data.ts · chatbot-knowledge · system-prompt · tools · /cv · fiches
â Ă©crit : global.md via writeFileSync
â
ââ [2] Printable CV Critic â indĂ©pendant, sans contexte du [1]
â lit : data.ts · /cv · fiches techniques
â Ă©crit : printable-cv.md
â
ââ [3] Strategy Agent
â lit : global.md + printable-cv.md + context.md (manuellement)
â Ă©crit : strategy.md
â
ââ summary.md + copie dans reports/latest/
Cette version contrĂŽle explicitement chaque Ă©tape : prompts, sĂ©quence, passage de contexte, Ă©criture des rapports â orchestration custom sans framework.
Ătape 2 â Script Python CrewAI de migration
npm run critique-cv-crew
â
ââ charge .env.local via python-dotenv
ââ prĂ©pare reports/<timestamp>/
â
ââ Crew.kickoff() â Process.sequential
â
ââ Task 1 : global_critic (External Portfolio Critic)
â role/goal/backstory formalisĂ©s · LLM via LiteLLM anthropic/claude-haiku-*
â context=[] â intentionnellement vide
â output_file â global.md (Ă©crit automatiquement par CrewAI)
â
ââ Task 2 : cv_critic (Printable CV & ATS Specialist)
â indĂ©pendant â context=[] intentionnellement vide
â output_file â printable-cv.md
â
ââ Task 3 : strategy_agent (CV Strategy Synthesizer)
â context=[task_global, task_cv] â injection automatique des outputs
â context.md injectĂ© dans la description de la tĂąche
â output_file â strategy.md
â
ââ write_report() + write_summary() â latest/
Ătape 3 â CrewAI native autonome
PYTHONPATH=src python -m cv_critic_agent.main
â
ââ charge .env.local / .env
ââ sĂ©lectionne le provider LLM
â par dĂ©faut : CV_CRITIC_PROVIDER=mistral
â
ââ CVCriticCrew
â ââ Global Critic
â ââ Printable CV Critic
â ââ Strategy Agent
â
ââ reports/<timestamp>/ + reports/latest/
La version cible vit dans un repo séparé cv-critic-agent avec un package Python, une CLI, une configuration CrewAI native, des tests et un provider Mistral par défaut.
Durée typique : 60 à 120 secondes (3 appels LLM séquentiels, contexte large).
Migration Node.js â CrewAI
Trois implémentations maintenues cÎte à cÎte. La migration explicite les responsabilités : agents, tùches, prompts, sources, providers LLM, rapports et tests.
Orchestration custom
- 3 appels SDK Anthropic directs
- Agents nommés "CV Critic Agent" (pas de rÎle distinct)
- Passage de contexte manuel (lire fichier â injecter dans prompt)
- Ăcriture des fichiers via
writeFileSync - Retry/erreurs gérés manuellement
- ~330 lignes de plumbing orchestration
Repo agentique autonome
- Agent/Task/Crew API déclarative
- RÎles, goals et backstories formalisés par agent
- Passage de contexte automatique via
context=[task] - Helpers partagés pour prompts, sources et rapports
- Provider Mistral par défaut, Anthropic en fallback
- Tests mock sans appel API réel
Pourquoi CrewAI
Les 3 agents (Global Critic â Printable CV Critic â Strategy) correspondent au modĂšle Agent/Task/Crew : rĂŽles distincts, sĂ©quence fixe, un seul agent lit les outputs des autres. L'orchestration manuelle Node.js a Ă©tĂ© conservĂ©e en miroir pour permettre la comparaison directe entre les deux niveaux d'abstraction.
Ce qui ne change pas
- L'architecture métier est identique (3 agents, 2 critics indépendants, 1 stratÚge)
- L'indĂ©pendance des critics est prĂ©servĂ©e â
context=[]intentionnellement vide sur les deux tasks critics - Les fichiers sources lus sont exactement les mĂȘmes
- Le format de sortie (Markdown, dossiers horodatés, latest/) est identique
Provider LLM paramétrable
CV_CRITIC_PROVIDER=mistral
CV_CRITIC_MODEL=mistral-medium-latest
MISTRAL_API_KEY=...
# fallback de comparaison
CV_CRITIC_PROVIDER=anthropic
CV_CRITIC_MODEL=claude-haiku-4-5-20251001
ANTHROPIC_API_KEY=...
Le provider n'est plus figé dans le code. Le mode mock reste le standard pour les tests, Mistral devient le choix par défaut pour les runs réels à coût réduit.
Ătapes techniques de refactoring
| Avant | Refactoring | Résultat |
|---|---|---|
| Script Node.js monolithique | Extraction sources, prompts, rapports | Unit tests possibles sans LLM |
| Prompts couplés au runner | prompts.py + sources.py | Contrat lisible, partagé par 3 modes |
| Ăcriture fichiers dupliquĂ©e | reports.py | reports/<timestamp> et latest/ centralisĂ©s |
| Provider Anthropic figé | llm.py provider-agnostic | Mistral par défaut, Anthropic en fallback |
| Mock LLM heuristique fragile | MockLLM(responses=, fallback=) injectable | Tests qui ne dépendent plus des prompts internes |
| Agent caché dans le portfolio | Repo cv-critic-agent public sur GitHub | Projet autonome, présentable et déployable |
Indices magiques REPORT_SPECS[0/1] | spec_by_slug("global") / spec_by_slug("printable-cv") | Ajout/réorganisation de specs sans casser crew.py |
Logique Mistral mĂȘlĂ©e Ă llm.py | Module mistral_utils.py (clean messages + extraction) | SĂ©paration des responsabilitĂ©s, importable |
| Sources copiées à la main | scripts/sync_sources.py --check | Sync reviewable + détection drift CI |
| Pas de CI | .github/workflows/ci.yml : Ruff + pytest | Lint et 16 tests mock verts Ă chaque push |
CV_CRITIC_PROVIDER mal validé | Raise explicite sur valeur inconnue | Erreur claire au lieu de comportement silencieux |
Bug : context.md ignoré en mode CrewAI native | build_strategy_prompt_header() + test régression | Contrat éditorial préservé dans les 3 implémentations |
YAML CrewAI scaffold (config/agents.yaml, tasks.yaml) en double avec prompts.py | Définition code-first des Agent/Task en Python | Source unique de vérité, prompts dynamiques (injection context.md runtime), YAML statique impossible |
| Pas de visibilité temps réel des runs | EventBus (sync pub/sub) + FastAPI/SSE + Living Agent Graph React Flow | Run mock en CLI rich, run en API streamé en SSE, run dans UI Vercel animé |
Token statique X-API-Token partagĂ© en clair pour les runs rĂ©els | Phase 5 â gate HMAC + Fernet PII + Turnstile + Telegram approval + budget cap + UI Next.js (S1âS7) | Real runs gardĂ©s derriĂšre approbation humaine, PII chiffrĂ©es at rest, dĂ©gradation silencieuse au cap journalier, 288 tests verts |
Tests automatisés (288 tests, 0 appel API)
- Séparation critics vs strategy : les critics ne lisent pas
context.md - Parité des sorties entre CrewAI native, script CrewAI et Node.js legacy
- Mock LLM sans appel API rĂ©el â avec injection custom de
responses - Régression
context.md: strategy l'injecte dans les 3 implémentations - Nettoyage messages Mistral :
clean_mistral_messagesstripcache_breakpoint - API FastAPI testée avec Mock LLM : runs, rapports, sources, graph
- Phase 5 S1âS4 : crypto HMAC (signature timing-safe, replay, expiry), PII Fernet (round-trip, tampering), logging filter (mask emails), security middleware (Turnstile, rate limit, headers), notifier Telegram + Resend (network errors, malformed JSON, owner-id check), access-request model (state machine + IP binding + quotas), store (fcntl concurrency, lazy TTL, PII at rest), endpoints Sprint 4 (49 tests : honeypot, captcha, HMAC decide, idempotence, Telegram webhook, admin magic-link, PATCH approve/reject/revoke)
- Phase 5 S5âS7 : real-run gate (verify_session sig + exp + approved + IP-bind + quota),
Store.atomic_consume_run(locked read+validate+bind+consume+write),budget/tracker.py(concurrence multi-thread, alertes 80/100 % idempotentes, race buffer Python corrigée parfh.flush()avantLOCK_UN),budget/wiring.py(record_real_run alert dispatcher, fail-soft, factory injectable),cron_router.py(authhmac.compare_digest, idempotence, dégrade gracefully si stores non configurés),BudgetTracker.cleanup_old_days(rétention configurable, dates lexicographiques UTC)
Gestion du contexte interne
Le fichier context.md contient les décisions internes déjà validées que le Strategy Agent doit respecter.
Exemples de décisions dans context.md
- Ne pas prétendre avoir implémenté le service AI/RAG Sense (délégué à une équipe dédiée)
- Présenter Resell Radar comme veille automatisée d'annonces publiques (pas "scraper")
- Séparer SAIF-IA (vend) et CV Critic Agent (challenge)
- Garder le CV imprimable lisible sans le surcharger
Le contexte sert Ă ne pas recommander des actions dĂ©jĂ Ă©cartĂ©es avec raison. Il Ă©vite les allers-retours sur des dĂ©cisions stabilisĂ©es. Les critics ne le lisent pas â leur regard doit rester externe.
Sorties & versioning
Chaque run gĂ©nĂšre un dossier horodatĂ© et une copie latest pour accĂšs rapide. Les trois implĂ©mentations produisent la mĂȘme structure.
reports/
2026-06-04T19-16-50Z/ # run CrewAI native
global.md # critique globale (252 lignes)
printable-cv.md # critique CV imprimable (252 lignes)
strategy.md # plan d'action P0/P1/P2 (180 lignes)
summary.md # résumé exécutif
2026-05-14T21-52-40Z/ # run Node.js
global.md
printable-cv.md
strategy.md
summary.md
latest/ # dernier run (mis Ă jour automatiquement)
global.md
printable-cv.md
strategy.md
summary.md
Comparer deux runs : diff reports/<run1>/strategy.md reports/<run2>/strategy.md â permet de mesurer si les recommandations ont Ă©voluĂ© aprĂšs des mises Ă jour du portfolio.
Plan de déploiement CrewAI native
Phase 1 â CLI locale
- Commande smoke test :
PYTHONPATH=src python -m cv_critic_agent.main --mock - Commande réelle :
PYTHONPATH=src python -m cv_critic_agent.main - Configuration via
.env.local: provider, modÚle, clé Mistral - Sortie attendue :
reports/latest/*.md
Phase 2 â GitHub Actions CI
- Workflow
.github/workflows/ci.ymlactif - Lint Ruff sur
src,tests,scripts - Tests mock sans appel LLM réel
- ContrĂŽle de drift des sources prĂȘt Ă brancher sur
cv-portfolio
Phase 3 â API + UI locale
- FastAPI :
POST /api/runs,GET /api/runs/{id},GET /api/runs/{id}/events,GET /api/runs/{id}/reports/{slug} - UI Next.js : landing, lancement mock, graphe Live Agent, panneau sources et rapports
- Authentification simple pour les runs réels via
CV_CRITIC_API_TOKEN - CORS configurable avec
CV_CRITIC_ALLOWED_ORIGINS
Phase 4 â DĂ©ploiement (livrĂ©)
- API Python sur Render free tier (région Frankfurt) via Infrastructure-as-Code
render.yaml - URL API :
https://cv-critic-agent-api.onrender.com - UI Next.js sur Vercel avec domaine custom : https://agentic-ai.saifallah.dev (CNAME Cloudflare vers
cname.vercel-dns.com) - Variable UI :
NEXT_PUBLIC_API_URL=https://cv-critic-agent-api.onrender.com - Secrets serveur sur Render dashboard :
MISTRAL_API_KEY,CV_CRITIC_API_TOKEN - CORS restreint :
CV_CRITIC_ALLOWED_ORIGINS=https://agentic-ai.saifallah.dev,https://cv-critic-agent.vercel.app - Runs mock publics gratuits, runs réels protégés par token
X-API-Token
Choix Render plutÎt que Vercel pour l'API : le backend est statefull (RunManager en RAM, threads, écriture fichiers reports/<timestamp>/, SSE long-lived 60-90s). Le modÚle serverless de Vercel Functions imposerait une réécriture vers Redis + queue + Blob storage. Render free tier accepte le design tel quel (1 container always-on, disk attaché, cold start 30s acceptable pour une démo).
Phase 5 â Access gate avec validation humaine
Ouvre les real runs au public derriĂšre un flow d'approbation human-in-the-loop. Plan complet et threat model : docs/PHASE5_ACCESS_GATE.md. 7 sprints, S1âS7 livrĂ©s au 9 juin 2026 (288 tests verts + UI 8 routes), reste sync portfolio + smoke prod.
Flow utilisateur cible
- Demandeur remplit un formulaire (nom, société, email, motif) protégé par Cloudflare Turnstile (siteverify server-side, score < 0.5 rejeté).
- Owner reçoit la demande sur Telegram (inline buttons Approve / Reject) et par email (liens HMAC signés, accept encodé dans le token pour bloquer le tampering URL).
- Owner approuve â session token HMAC envoyĂ© par email au demandeur, liĂ© Ă son email, liĂ© Ă son IP Ă la 1Ăšre utilisation, valide 24 h, quota 3 runs.
- Budget cap quotidien (
MAX_TOKENS_PER_DAY=200000) avec alertes Telegram à 80 % et 100 %, dégradation vers mock au-delà .
Threat model
6 adversaires (bot crawler, curieux, concurrent hostile, attaquant ciblé, canal owner compromis, insider futur) à 6 surfaces (formulaire public, décision owner, session requester, OAuth optionnel V2, stockage PII, exécution run). Décisions principales : Cloudflare Turnstile, magic-link admin (pas de token en URL), Fernet pour PII at rest, IP-binding au lieu de cookies cross-origin, rétention 7 j pending / 90 j décidées avec endpoint RGPD DELETE.
Sprints livrés
| Sprint | Livré | Modules |
|---|---|---|
| S1 â Security foundations | 2026-06-05 | security/crypto.py (HMAC sign/verify timing-safe, expiry encodĂ©), security/pii.py (Fernet), security/logging_filter.py (mask emails), security/security_middleware.py (Turnstile + slowapi + CSP/HSTS headers) |
| S2 â AccessRequest storage | 2026-06-06 | access_requests/models.py (state machine PENDING/APPROVED/REJECTED/CONSUMED/EXPIRED/REVOKED + IP binding + quotas), access_requests/store.py (JSON one-file-per-request, fcntl advisory locking, PII Fernet at rest, lazy TTL purge) |
| S3 â Notifier layer | 2026-06-06 | notifier/telegram.py (owner pending + budget alert + is_owner_callback), notifier/email.py (Resend approved + rejected + admin magic link, HTML + plain bundles). Fail-soft, http_client_factory injectable. |
| S4 â Decision endpoints | 2026-06-08 | POST /api/access-requests (honeypot + Turnstile + 3/h rate limit), GET .../{id}/status, GET .../{id}/decide?token= (HMAC idempotent, accept dans la signature), POST /api/telegram/webhook (owner chat_id + callback_query + /cmd), admin magic-link login â session 24 h, GET /api/admin/requests, PATCH /api/admin/requests/{id} |
| S5 â Real run gate | 2026-06-08 | POST /api/runs accepte X-Session-Token (HMAC sub=session+req), verify_session (sig + exp + approved + IP-bind + quota), Store.atomic_consume_run (locked read+validate+bind+consume+write), budget/tracker.py (fichier JSON par jour UTC, alertes 80/100 % idempotentes), dĂ©gradation silencieuse realâmock si cap atteint sans consommer de slot. +62 tests. |
| S6 â UI Next.js 16 | 2026-06-08 | 8 routes : /access-request (form + Turnstile widget + honeypot) â /access-request/[id]/status (poll 5s, 6 statuts) â /access-granted/[token] (POST run avec X-Session-Token, gĂšre degraded:true). Admin : /admin/login (magic link) â /admin/session/[token] (redeem â localStorage) â /admin/requests (panel filtrĂ©, actions approve/reject/revoke). Home : deux CTAs Mock/Real. |
| S7.1 â Wiring budget runtime | 2026-06-09 | budget/wiring.py record_real_run (fail-soft, factory injectable) + hook depuis run_manager._execute() via budget_callback (closure asyncio.run) + helper _build_budget_callback cĂŽtĂ© api.py lisant app.state. Estimation pessimiste 15 000 tokens/run (3 agents Ă 5 000 max_tokens). +9 tests. |
| S7.2 â Cron cleanup TTL | 2026-06-09 | POST /api/cron/cleanup (header X-Cleanup-Secret, hmac.compare_digest) â AccessRequestStore.cleanup_expired + BudgetTracker.cleanup_old_days(retain_days=90). 503 si secret non configurĂ©, 401 si header faux, 200 + counts sinon. Fix bonus : race buffer Python sur add_tokens corrigĂ©e par fh.flush() avant LOCK_UN. +12 tests. |
| S7.3 â Docs + sync portfolio | 2026-06-09 | README cv-critic-agent : section Phase 5 (flow, token taxonomy, budget cap, where modules live), arborescence Ă jour, env de dĂ©ploiement complĂšte. Sync cv-portfolio : highlight Phase 5 + cette fiche technique. |
Conventions code (Phase 5)
- Pas d'env read dans les modules mĂ©tier â secrets (bot_token, fernet_key, hmac_secret, api_key) passĂ©s explicitement,
api.pyseul litos.environau startup. - Fail-soft sur I/O externe â Telegram / Resend / Turnstile retournent
Falsesur erreur réseau, jamais d'exception au caller. - Tests
unittestnegative-first â un test par concern, mock async_RecordingClientqui capture url / json / headers, factory injectĂ©e viahttp_client_factory=. - PII jamais en clair sur disque ni dans les logs â Fernet at rest pour name / company / email / motive, IP et status restent indexables en clair, filter logging mask les emails.
Limites & suite
Limites actuelles
- Pas d'application automatique des recommandations
- Pas de comparaison automatique entre deux runs
- Pas de scoring stable par grille mathématique
- DĂ©pend d'un LLM externe â les rĂ©sultats peuvent varier d'un run Ă l'autre
- Les décisions finales restent humaines
Suite directe
- Phase 5 â smoke test prod (Telegram + Resend + Turnstile en conditions rĂ©elles, vĂ©rifier que l'alerte 80 % tombe et que le degrade realâmock s'enclenche au cap)
- Fichier
action-plan.mdcocheable - Comparateur de runs pour suivre l'évolution entre versions
- Grille de scoring fixe et reproductible