quentinL52 commited on
Commit
1556508
·
1 Parent(s): 771c0b9
agents_trace.log DELETED
File without changes
src/config/agents.yaml CHANGED
@@ -147,24 +147,19 @@ cv_quality_checker:
147
 
148
  project_analyzer:
149
  role: >
150
- Analyste de Projets Techniques & Conseiller en Mise en Avant
151
  goal: >
152
- Évaluer chaque projet et expérience du CV, fournir une critique objective et complète,
153
- et recommander quels projets mettre en avant pour le poste visé.
154
  backstory: >
155
- Tu es un directeur technique (CTO) qui évalue les projets des candidats.
156
- Pour chaque projet tu analyses :
157
- 1. La cohérence avec le poste visé ET avec le référentiel métier correspondant
158
- 2. La qualité de description (résultats mesurables, métriques techniques spécifiques)
159
- 3. La complexité technique (trivial vs ambitieux, architecture, choix techniques)
160
- 4. L'impact démontré (métriques, utilisateurs, déploiement, performance)
161
- 5. Les technologies utilisées (actuelles et recherchées pour le poste visé ?)
162
- Tu fournis une RECOMMANDATION DE MISE EN AVANT : quels projets le candidat devrait
163
- présenter en priorité pour le poste visé, et comment améliorer leur description.
164
- Pour les profils en reconversion, tu identifies les compétences transférables
165
- démontrées dans les expériences non-tech (gestion, optimisation, leadership, communication).
166
- Tu analyses aussi les EXPÉRIENCES PROFESSIONNELLES pour identifier les compétences
167
- transférables et les relier au poste visé.
168
- Tu donnes des conseils CONCRETS et ACTIONNABLES pour chaque projet.
169
  verbose: false
170
 
 
147
 
148
  project_analyzer:
149
  role: >
150
+ Analyste de Projets Techniques & Evaluateur de Compétences
151
  goal: >
152
+ Évaluer systématiquement les projets du CV selon 7 critères précis, avec une approche purement factuelle.
 
153
  backstory: >
154
+ Tu es un directeur technique (CTO) exigeant qui déteste le bullshit et le name-dropping.
155
+ Tu evaluates chaque projet en examinant :
156
+ 1. Pertinence & Alignement
157
+ 2. Complexité Technique & Architecture
158
+ 3. Stack & Maîtrise
159
+ 4. Innovation & Originalité
160
+ 5. Impact & Résultats Mesurables
161
+ 6. Ownership & Exécution
162
+ 7. Maturité Production & Qualité
163
+ Tu produis un avis CLAIR et CRITIQUE : tu es factuel d'abord (preuve du CV), tu donnes du positif spécifique, tu donnes de la critique constructive et honnête, tu évites le fanboying, et tu termines par un verdict tranché.
 
 
 
 
164
  verbose: false
165
 
src/config/app_config.py CHANGED
@@ -18,6 +18,17 @@ def load_pdf(pdf_path: str) -> str:
18
  return pymupdf4llm.to_markdown(pdf_path)
19
 
20
 
 
 
 
 
 
 
 
 
 
 
 
21
  def load_pdf_first_page_text(pdf_path: str) -> str:
22
  """Extrait le texte brut de la première page en ordre de lecture (haut → bas, gauche → droite).
23
 
@@ -48,9 +59,8 @@ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
48
  def get_big_llm():
49
  """GPT-4o pour les tâches complexes — max_tokens élevé pour éviter la troncature JSON."""
50
  return ChatOpenAI(
51
- model="gpt-4o",
52
  temperature=0.0,
53
- max_tokens=16384,
54
  api_key=OPENAI_API_KEY
55
  )
56
 
@@ -58,18 +68,8 @@ def get_big_llm():
58
  def get_small_llm():
59
  """GPT-4o-mini pour l'extraction rapide."""
60
  return ChatOpenAI(
61
- model="gpt-4o-mini",
62
  temperature=0.0,
63
  max_tokens=1500,
64
  api_key=OPENAI_API_KEY
65
- )
66
-
67
-
68
- def get_fast_llm():
69
- """Groq llama-3.1-8b - Le plus rapide."""
70
- return ChatGroq(
71
- model="groq/llama-3.1-8b-instant",
72
- temperature=0.0,
73
- max_tokens=1500,
74
- groq_api_key=GROQ_API_KEY
75
- )
 
18
  return pymupdf4llm.to_markdown(pdf_path)
19
 
20
 
21
+ def get_pdf_page_count(pdf_path: str) -> int:
22
+ """Retourne le nombre de pages du PDF."""
23
+ try:
24
+ doc = fitz.open(pdf_path)
25
+ count = doc.page_count
26
+ doc.close()
27
+ return count
28
+ except Exception:
29
+ return 1
30
+
31
+
32
  def load_pdf_first_page_text(pdf_path: str) -> str:
33
  """Extrait le texte brut de la première page en ordre de lecture (haut → bas, gauche → droite).
34
 
 
59
  def get_big_llm():
60
  """GPT-4o pour les tâches complexes — max_tokens élevé pour éviter la troncature JSON."""
61
  return ChatOpenAI(
62
+ model="gpt-5.2",
63
  temperature=0.0,
 
64
  api_key=OPENAI_API_KEY
65
  )
66
 
 
68
  def get_small_llm():
69
  """GPT-4o-mini pour l'extraction rapide."""
70
  return ChatOpenAI(
71
+ model="gpt-4o",
72
  temperature=0.0,
73
  max_tokens=1500,
74
  api_key=OPENAI_API_KEY
75
+ )
 
 
 
 
 
 
 
 
 
 
src/config/tasks.yaml CHANGED
@@ -4,12 +4,6 @@ split_cv_task:
4
  description: >
5
  Découpe le CV en sections JSON. Copie le texte brut sans reformuler ni résumer.
6
 
7
- TEXTE PRINCIPAL DU CV (Markdown) :
8
- "{cv_content}"
9
-
10
- TEXTE BRUT PREMIÈRE PAGE (extraction directe, utilise-le si le texte principal manque le header) :
11
- "{cv_raw_start}"
12
-
13
  RÈGLES STRICTES :
14
  1. "header" : Les premières lignes du CV — contient le NOM du candidat, son TITRE/POSTE, ses coordonnées (email, téléphone, LinkedIn, ville). Cherche en priorité dans le TEXTE BRUT PREMIÈRE PAGE car le Markdown peut mal ordonner le header.
15
  2. "experiences" : Uniquement l'historique professionnel (Entreprise, Poste, Dates, missions).
@@ -17,6 +11,12 @@ split_cv_task:
17
  4. "skills" : Listes de compétences, langages, outils.
18
  5. "education" : Diplômes et formations.
19
  6. "languages" : Langues mentionnées avec leur niveau (Français, Anglais, etc.).
 
 
 
 
 
 
20
  expected_output: >
21
  Un objet JSON valide strictement structuré ainsi :
22
  {{
@@ -32,18 +32,22 @@ split_cv_task:
32
 
33
  skills_task:
34
  description: >
35
- Analyse les sections suivantes pour lister les compétences avec leur contexte d'utilisation.
36
- Expériences : {experiences}
37
- Projets : {projects}
38
- academique : {education}
39
- Skills Explicit : {skills}
40
-
41
  RÈGLES DE CLASSIFICATION :
42
  1. 'hard_skills' : Outils, langages, technos (ex: Python, SQL, Excel, React, FastAPI, LangChain).
43
  IMPORTANT: Inclure TOUTES les technologies mentionnées dans les projets, expériences et académique.
44
  2. 'soft_skills' : Qualités humaines (ex: Leadership, Communication, Rigueur).
45
  3. NE PAS INVENTER. Si ce n'est pas écrit, ne l'ajoute pas.
46
- 4. Pour chaque skill, indique si elle est présente dans une expérience, un projet, academique, sans contexte, si elle est presente dans plusieurs section indique le .
 
 
 
 
 
 
 
 
 
 
47
  expected_output: >
48
  JSON : {{
49
  "hard_skills": ["Python", "SQL", "FastAPI"],
@@ -56,8 +60,7 @@ skills_task:
56
 
57
  experience_task:
58
  description: >
59
- Tu es un extracteur de données strict. Analyse ce texte d'expérience :
60
- "{experiences}"
61
 
62
  Pour CHAQUE poste identifié :
63
  1. Titre du poste
@@ -66,13 +69,14 @@ experience_task:
66
  4. Description (Liste des tâches/responsabilités)
67
 
68
  RÈGLE : Ne confonds PAS les projets scolaires avec des expériences pro. Les stages et alternances SONT des expériences.
 
 
 
69
  expected_output: >
70
  Liste JSON : [{{"Poste": "...", "Entreprise": "...", "start_date": "...", "end_date": "...", "responsabilités": ["task 1", "task 2"]}}]
71
 
72
  project_task:
73
  description: >
74
- Analyse ce texte de projets : "{projects}"
75
-
76
  RÈGLES STRICTES DE STRUCTURE :
77
  Chaque projet DOIT avoir exactement ces clés :
78
  - "title" : Titre du projet
@@ -85,6 +89,10 @@ project_task:
85
  - "personal" : Projets perso, Github, Hackathons, Écoles.
86
 
87
  Si une liste est vide, renvoie [].
 
 
 
 
88
  expected_output: >
89
  JSON : {{
90
  "professional": [
@@ -102,26 +110,30 @@ education_task:
102
 
103
  reconversion_task:
104
  description: >
105
- Analyse les expériences : "{experiences}"
106
- Et "{education}"
107
  Le candidat est-il en reconversion ? (Changement majeur de domaine récent).
108
- indique aussi le contexte (de quelle domaine ou poste vient t'il et quelle est sa réortientation)
109
- pour le contexte de reconversion tu dois prendre en compte les experience et les education.
 
 
 
 
 
110
  expected_output: >
111
- JSON : {{"reconversion_analysis": {{"is_reconversion": true/false, "context": "..."}}
112
 
113
  etudiant_task:
114
  description: >
115
- Analyse la section education : "{education}"
116
- Nous sommes le {current_date}.
117
- Le candidat est-il ACTUELLEMENT étuditant ?
118
  CRITÈRES :
119
  1. Regarde les dates de fin des formations.
120
  2. Identifie la date de fin de la formation la plus récente.
121
- 3. Si cette date est FUTURE par rapport à {current_date} ou si c'est écrit "En cours" / "Présent", alors is_etudiant = true.
122
  4. Récupère explicitement cette date sous le champ 'latest_education_end_date' (format YYYY-MM-DD ou MM/YYYY ou "Present").
123
- 5. indique aussi le niveau d'études (ex: bac+2, bac+5) du diplome le plus proche de {current_date}.
124
  6. indique la spécialité exemple: ingenieur IA, data analyste, devellopeur frontend, etc.
 
 
 
125
  expected_output: >
126
  JSON : {{"etudiant_analysis": {{"is_etudiant": true/false, "niveau_etudes": "bac+5", "specialite": "data analyste", "latest_education_end_date": "YYYY-MM-DD"}}}}
127
 
@@ -129,15 +141,15 @@ language_task:
129
  description: >
130
  Identifie toutes les langues parlées par le candidat.
131
 
132
- SECTION LANGUES (extraite) : "{languages}"
133
-
134
- DÉBUT DU CV (pour détecter la langue de rédaction) : "{cv_raw_start}"
135
-
136
  RÈGLES :
137
  1. Extrais toutes les langues et niveaux présents dans la SECTION LANGUES.
138
  2. Détecte la langue dans laquelle le CV est rédigé à partir du DÉBUT DU CV.
139
  3. Si la langue du CV n'est PAS dans la SECTION LANGUES, ajoute-la avec le niveau "Natif" ou "Langue maternelle".
140
  4. Ne jamais omettre la langue du CV.
 
 
 
 
141
  expected_output: >
142
  JSON : {{"langues": [{{"langue": "Français", "niveau": "Natif"}}, {{"langue": "Anglais", "niveau": "B2"}}]}}
143
 
@@ -145,17 +157,17 @@ identity_task:
145
  description: >
146
  Extrais le prénom du candidat.
147
 
148
- HEADER DU CV (nom, titre, contact) : "{header}"
149
-
150
- TEXTE BRUT DÉBUT DU CV : "{cv_raw_start}"
151
-
152
- NOM DU FICHIER (indice très fiable, souvent au format NOM_PRENOM_...) : "{file_name}"
153
-
154
  RÈGLES :
155
  1. Cherche le prénom dans le HEADER, puis dans le TEXTE BRUT DÉBUT DU CV.
156
  2. Le NOM DU FICHIER est un indice fort : "ANISSA_KACEM_..." → prénom = "Anissa".
157
  3. Ne jamais inventer. Formate avec majuscule initiale.
158
  4. Si impossible à trouver, retourne null.
 
 
 
 
 
 
159
  expected_output: >
160
  JSON : {{"first_name": "..."}}
161
 
@@ -163,15 +175,15 @@ poste_visé_task:
163
  description: >
164
  Extrais le titre de poste visé tel qu'il est écrit dans l'en-tête du CV.
165
 
166
- HEADER DU CV (extrait par le splitter) : "{header}"
167
-
168
- TEXTE BRUT DÉBUT DU CV (fallback si header vide) : "{cv_raw_start}"
169
-
170
  RÈGLES :
171
  1. Le titre de poste se trouve juste après le nom du candidat (ex: "Business Analyst", "Data Engineer").
172
  2. Copie le titre EXACTEMENT tel qu'il est écrit, sans reformuler.
173
  3. Si le header est vide, cherche dans le TEXTE BRUT DÉBUT DU CV.
174
  4. Ne jamais inventer un titre.
 
 
 
 
175
  expected_output: >
176
  JSON : {{
177
  "poste_vise": "Le titre EXACT tel qu'écrit sur le CV",
@@ -184,35 +196,15 @@ metier_matching_task:
184
  description: >
185
  Compare le profil du candidat avec le référentiel de métiers pour recommander les 3 postes les plus adaptés.
186
 
187
- POSTE VISÉ PAR LE CANDIDAT : "{poste_vise}"
188
-
189
- COMPÉTENCES DU CANDIDAT :
190
- Hard Skills : {hard_skills}
191
- Soft Skills : {soft_skills}
192
-
193
- DOMAINES DE COMPÉTENCES IDENTIFIÉS : {skill_domains}
194
-
195
- MÉTHODOLOGIES DU CANDIDAT : {methodologies}
196
-
197
- EXPÉRIENCES : {experiences_summary}
198
-
199
- PROJETS : {projects_summary}
200
-
201
- RECONVERSION : {reconversion_data}
202
-
203
- RÉFÉRENTIEL DE MÉTIERS :
204
- {metiers_reference}
205
-
206
  RÈGLES D'ANALYSE :
207
- IMPORTANT : Tu dois évaluer CHAQUE métier présent dans le RÉFÉRENTIEL DE MÉTIERS ci-dessus,
208
  sans en omettre aucun. Le top 3 final doit être basé sur l'évaluation exhaustive de tous
209
  les métiers listés. Ne jamais présélectionner ou ignorer des métiers a priori.
210
 
211
  1. Pour CHAQUE métier du référentiel, calcule un score de matching (0-100) basé sur :
212
- - Couverture des compétences techniques requises (35%)
213
- - Couverture des outils/technologies (25%)
214
  - Adéquation des expériences et projets (20%)
215
- - Maîtrise des méthodologies de travail : Agile, Scrum, DevOps, CI/CD, TDD, Design Thinking (10%)
216
  - Cohérence avec le niveau d'études et l'expérience requise (10%)
217
  2. Utilise le mapping de domaines pour comprendre les liens implicites (ex: Metabase → BI,
218
  LangChain → LLM Engineering, Power BI → BI Analyst, Scikit-learn → Data Science).
@@ -221,9 +213,26 @@ metier_matching_task:
221
  communication internationale → travail en équipe multiculturelle).
222
  4. Recommande les 3 métiers avec le MEILLEUR score parmi l'ensemble du référentiel évalué.
223
  5. PONDÉRATION TEMPORELLE (CRITIQUE) : Accorde un poids double (x2) aux technologies et compétences issues des expériences et projets les plus récents, ainsi qu'à la formation en cours. Le profil actuel d'un candidat est défini par ce qu'il fait aujourd'hui, pas par son historique lointain.
224
- 6. Pour chaque métier recommandé, liste les compétences matchées, manquantes, et les méthodologies.
225
  7. Si le poste visé par le candidat ne fait pas partie du top 3, explique pourquoi.
226
  8. Fournis une analyse détaillée de l'adéquation du poste visé avec le profil.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  expected_output: >
228
  JSON : {{
229
  "postes_recommandes": [
@@ -240,7 +249,6 @@ metier_matching_task:
240
  }},
241
  "competences_matchees": ["SQL", "Python", "Power BI"],
242
  "competences_manquantes": ["Looker", "dbt"],
243
- "methodologies_matchees": ["Agile", "Scrum"],
244
  "justification": "Le profil couvre 85% des compétences requises..."
245
  }}
246
  ],
@@ -253,22 +261,14 @@ cv_quality_task:
253
  Évalue la qualité globale du CV en appliquant les critères de bonnes pratiques CV tech 2025,
254
  adaptés au niveau de séniorité du candidat.
255
 
256
- CV COMPLET (texte Markdown) : "{cv_full_text}"
257
-
258
- TEXTE BRUT DU CV (première page, pour détecter les URLs et liens) : "{cv_raw_start}"
259
-
260
- COMPÉTENCES EXTRAITES AVEC CONTEXTE : {skills_with_context}
261
- EXPÉRIENCES : {experiences_summary}
262
- PROJETS : {projects_summary}
263
- NIVEAU DE SÉNIORITÉ : "{niveau_seniorite}"
264
- RECONVERSION : {reconversion_data}
265
-
266
  CRITÈRES D'ÉVALUATION (score sur 100 pour chaque) :
267
 
268
  1. COMPATIBILITÉ ATS (20 points) :
269
- - Structure claire avec sections standards ?
270
- - Pas de mise en page complexe qui bloquerait un ATS ?
271
- - Mots-clés techniques bien présents ?
 
 
272
 
273
  2. QUANTIFICATION DES RÉSULTATS (25 points) :
274
  - Les expériences mentionnent-elles des MÉTRIQUES TECHNIQUES SPÉCIFIQUES ?
@@ -281,11 +281,13 @@ cv_quality_task:
281
  en fonction de ses expériences et projets RÉELS (pas des conseils génériques).
282
 
283
  3. STRUCTURE ET LISIBILITÉ (15 points) :
284
- - Le CV tient-il en 1-2 pages ?
285
- - Les sections sont-elles bien séparées et la chronologie claire ?
286
- - STRUCTURATION DES COMPÉTENCES : Les compétences sont-elles regroupées par catégories
287
- logiques (Langages, Frameworks, BDD, DevOps/Cloud, Méthodologies) ou en liste plate ?
288
- Une structuration par catégories est fortement recommandée pour les filtres ATS.
 
 
289
 
290
  4. PRÉSENTATION DES PROJETS (20 points) :
291
  - Chaque projet a-t-il un titre, des technos, et des résultats ?
@@ -294,10 +296,9 @@ cv_quality_task:
294
 
295
  5. PREUVES DE COMPÉTENCES (20 points) :
296
  - RÈGLE CRITIQUE : Une compétence est considérée "sans preuve" UNIQUEMENT si elle
297
- apparaît EXCLUSIVEMENT dans la section Skills/Compétences sans aucune mention dans
298
- les expériences OU les projets. Utilise "skills_with_context" : si le contexte est
299
- "projet", "expérience", "académique" ou "projet+expérience", la compétence EST prouvée
300
- ne la signale pas. Ne signale que les skills dont le contexte est "sans contexte".
301
  - DÉTECTION DES LIENS : Cherche les URLs dans le CV Markdown ET dans le texte brut.
302
  Les liens peuvent apparaître sous forme de Markdown [texte](url), de texte brut
303
  (github.com/..., linkedin.com/...) ou dans le header. Signale les liens PRÉSENTS,
@@ -305,7 +306,7 @@ cv_quality_task:
305
  - Pour les RECONVERSIONS : les compétences transférables (management, optimisation,
306
  communication, gestion budgétaire) sont-elles mises en valeur et reliées au nouveau domaine ?
307
 
308
- ADAPTATION AU NIVEAU DE SÉNIORITÉ "{niveau_seniorite}" :
309
  - Si JUNIOR : valorise les projets personnels, formations, stages bien décrits.
310
  - Si CONFIRMÉ : exige des résultats mesurables, progression, responsabilités.
311
  - Si SENIOR/STAFF : vérifie la présence de choix architecturaux et compromis
@@ -313,11 +314,21 @@ cv_quality_task:
313
  gestion de la scalabilité, impact organisationnel au-delà du code.
314
 
315
  RED FLAGS À DÉTECTER :
316
- - Skills listées UNIQUEMENT dans la section skills sans aucune mention dans expériences/projets
317
  - Trous inexpliqués dans le parcours
318
  - Jargon excessif ou buzzwords sans substance
319
  - Incohérence entre compétences listées et projets/expériences
320
  - Section compétences en liste plate non catégorisée
 
 
 
 
 
 
 
 
 
 
321
  expected_output: >
322
  JSON : {{
323
  "score_global": 72,
@@ -334,49 +345,47 @@ cv_quality_task:
334
 
335
  project_analysis_task:
336
  description: >
337
- Évalue CHAQUE projet du CV et détermine leur pertinence pour le poste visé.
338
-
339
- POSTE VISÉ : "{poste_vise}"
340
-
341
- RÉFÉRENTIEL DU MÉTIER VISÉ (compétences et outils attendus) :
342
- {metier_reference_detail}
343
-
344
- PROJETS PROFESSIONNELS : {professional_projects}
345
- PROJETS PERSONNELS : {personal_projects}
346
-
347
- RECONVERSION : {reconversion_data}
348
-
349
- Pour CHAQUE projet, fournis :
350
- 1. score_coherence (0-100) : cohérence avec le poste visé et le référentiel métier
351
- 2. rang : classement par ORDRE DE PERTINENCE pour le poste (1 = le plus pertinent pour ce poste)
352
- 3. raison : explication CONCISE de pourquoi ce projet doit être mis en avant (ou pas) pour ce poste visé
353
- 4. points_forts : atouts concrets (technologies démontrées, impact, qualité)
354
- 5. points_amelioration : ce qui manque pour convaincre (métriques, résultats, détails techniques)
355
- 6. conseils_description : conseils CONCRETS pour améliorer la description
356
- (métriques à ajouter, aspects techniques à détailler, résultats à valoriser)
357
-
358
- RÈGLES STRICTES :
359
- - N'analyse QUE les projets listés dans PROJETS PROFESSIONNELS et PROJETS PERSONNELS.
360
- - N'invente AUCUN projet à partir des expériences. Les expériences sont un contexte uniquement.
361
- - Si PROJETS PROFESSIONNELS et PROJETS PERSONNELS sont vides, retourne "analyse_projets": [].
362
- - Le nombre d'entrées dans "analyse_projets" doit correspondre EXACTEMENT au nombre de projets fournis.
363
- - Retourne les projets TRIÉS par rang (rang 1 en premier).
364
  expected_output: >
365
  JSON : {{
366
  "analyse_projets": [
367
  {{
368
- "titre": "Dashboard RH",
369
- "rang": 1,
370
- "raison": "Projet BI directement aligné avec les outils et missions du poste visé",
371
- "score_coherence": 90,
372
- "points_forts": ["SQL et Power BI maîtrisés et démontrés", "Impact mesurable sur les décisions RH"],
373
- "points_amelioration": ["Ajouter le volume de données traité", "Mentionner le temps de chargement"],
374
- "conseils_description": ["Préciser le volume de données traité (ex: 500k lignes)", "Ajouter une métrique de performance"]
 
 
 
 
 
 
 
 
375
  }}
376
- ],
377
- "coherence_globale": {{
378
- "score": 85,
379
- "commentaire": "Ensemble de projets cohérent avec le poste visé"
380
- }}
381
  }}
382
 
 
4
  description: >
5
  Découpe le CV en sections JSON. Copie le texte brut sans reformuler ni résumer.
6
 
 
 
 
 
 
 
7
  RÈGLES STRICTES :
8
  1. "header" : Les premières lignes du CV — contient le NOM du candidat, son TITRE/POSTE, ses coordonnées (email, téléphone, LinkedIn, ville). Cherche en priorité dans le TEXTE BRUT PREMIÈRE PAGE car le Markdown peut mal ordonner le header.
9
  2. "experiences" : Uniquement l'historique professionnel (Entreprise, Poste, Dates, missions).
 
11
  4. "skills" : Listes de compétences, langages, outils.
12
  5. "education" : Diplômes et formations.
13
  6. "languages" : Langues mentionnées avec leur niveau (Français, Anglais, etc.).
14
+
15
+ TEXTE PRINCIPAL DU CV (Markdown) :
16
+ "{cv_content}"
17
+
18
+ TEXTE BRUT PREMIÈRE PAGE (extraction directe, utilise-le si le texte principal manque le header) :
19
+ "{cv_raw_start}"
20
  expected_output: >
21
  Un objet JSON valide strictement structuré ainsi :
22
  {{
 
32
 
33
  skills_task:
34
  description: >
 
 
 
 
 
 
35
  RÈGLES DE CLASSIFICATION :
36
  1. 'hard_skills' : Outils, langages, technos (ex: Python, SQL, Excel, React, FastAPI, LangChain).
37
  IMPORTANT: Inclure TOUTES les technologies mentionnées dans les projets, expériences et académique.
38
  2. 'soft_skills' : Qualités humaines (ex: Leadership, Communication, Rigueur).
39
  3. NE PAS INVENTER. Si ce n'est pas écrit, ne l'ajoute pas.
40
+ 4. EXHAUSTIVITÉ ABSOLUE : Traque RIGOUREUSEMENT chaque logiciel, langage, bibliothèque ou concept technique (ex: BeautifulSoup4, Scraping, Pandas, etc.). Ne laisse RIEN passer.
41
+ 5. 'skills_with_context' : Tu DOIS obligatoirement analyser la LISTE COMPLÈTE des 'hard_skills' extraites.
42
+ RÈGLE ABSOLUE : Pour CHAQUE compétence, tu dois EFFECTUER UNE RECHERCHE TEXTUELLE RIGOUREUSE (comme un CTRL+F) dans le texte brut des Expériences, des Projets et de l'Éducation.
43
+ De nombreuses compétences présentes dans les 'Expériences' sont souvent manquées. Prends le temps de lire chaque phrase des expériences !
44
+ Si la compétence est trouvée, précise son contexte (ex: "expérience", "projet", "académique", "expérience, projet").
45
+ Si et SEULEMENT SI tu as lu attentivement tous les autres textes et qu'elle n'est VRAIMENT mentionnée nulle part ailleurs que dans la section Compétences, alors attribue-lui le contexte "sans contexte". Ne mets JAMAIS "sans contexte" par erreur si la compétence se trouve dans l'expérience.
46
+ Analyse les sections suivantes pour lister les compétences avec leur contexte d'utilisation :
47
+ Expériences : {experiences}
48
+ Projets : {projects}
49
+ Education : {education}
50
+ Skills Explicites : {skills}
51
  expected_output: >
52
  JSON : {{
53
  "hard_skills": ["Python", "SQL", "FastAPI"],
 
60
 
61
  experience_task:
62
  description: >
63
+ Tu es un extracteur de données strict.
 
64
 
65
  Pour CHAQUE poste identifié :
66
  1. Titre du poste
 
69
  4. Description (Liste des tâches/responsabilités)
70
 
71
  RÈGLE : Ne confonds PAS les projets scolaires avec des expériences pro. Les stages et alternances SONT des expériences.
72
+
73
+ Analyse ce texte d'expérience :
74
+ "{experiences}"
75
  expected_output: >
76
  Liste JSON : [{{"Poste": "...", "Entreprise": "...", "start_date": "...", "end_date": "...", "responsabilités": ["task 1", "task 2"]}}]
77
 
78
  project_task:
79
  description: >
 
 
80
  RÈGLES STRICTES DE STRUCTURE :
81
  Chaque projet DOIT avoir exactement ces clés :
82
  - "title" : Titre du projet
 
89
  - "personal" : Projets perso, Github, Hackathons, Écoles.
90
 
91
  Si une liste est vide, renvoie [].
92
+
93
+ Analyse ce texte de projets : "{projects}"
94
+
95
+ Si une liste est vide, renvoie [].
96
  expected_output: >
97
  JSON : {{
98
  "professional": [
 
110
 
111
  reconversion_task:
112
  description: >
 
 
113
  Le candidat est-il en reconversion ? (Changement majeur de domaine récent).
114
+ Indique aussi le contexte (de quel domaine ou poste vient-il et quelle est sa réorientation).
115
+ RÈGLE ABSOLUE : Tu DOIS extraire la DATE de début de cette reconversion.
116
+ Cette date correspond au mois et à l'année de début de la formation de reconversion OU du premier emploi dans le nouveau domaine.
117
+ Pour le contexte de reconversion et la date, base-toi sur les expériences et l'éducation.
118
+
119
+ Analyse les expériences : "{experiences}"
120
+ Et l'éducation : "{education}"
121
  expected_output: >
122
+ JSON : {{"reconversion_analysis": {{"is_reconversion": true/false, "context": "...", "date_reconversion": "YYYY-MM ou null"}}}}
123
 
124
  etudiant_task:
125
  description: >
126
+ Le candidat est-il ACTUELLEMENT étudiant ?
 
 
127
  CRITÈRES :
128
  1. Regarde les dates de fin des formations.
129
  2. Identifie la date de fin de la formation la plus récente.
130
+ 3. Si cette date est FUTURE par rapport à la date actuelle ou si c'est écrit "En cours" / "Présent", alors is_etudiant = true.
131
  4. Récupère explicitement cette date sous le champ 'latest_education_end_date' (format YYYY-MM-DD ou MM/YYYY ou "Present").
132
+ 5. indique aussi le niveau d'études (ex: bac+2, bac+5) du diplome le plus proche de la date actuelle.
133
  6. indique la spécialité exemple: ingenieur IA, data analyste, devellopeur frontend, etc.
134
+
135
+ Nous sommes le {current_date}.
136
+ Analyse la section education : "{education}"
137
  expected_output: >
138
  JSON : {{"etudiant_analysis": {{"is_etudiant": true/false, "niveau_etudes": "bac+5", "specialite": "data analyste", "latest_education_end_date": "YYYY-MM-DD"}}}}
139
 
 
141
  description: >
142
  Identifie toutes les langues parlées par le candidat.
143
 
 
 
 
 
144
  RÈGLES :
145
  1. Extrais toutes les langues et niveaux présents dans la SECTION LANGUES.
146
  2. Détecte la langue dans laquelle le CV est rédigé à partir du DÉBUT DU CV.
147
  3. Si la langue du CV n'est PAS dans la SECTION LANGUES, ajoute-la avec le niveau "Natif" ou "Langue maternelle".
148
  4. Ne jamais omettre la langue du CV.
149
+
150
+ SECTION LANGUES (extraite) : "{languages}"
151
+
152
+ DÉBUT DU CV (pour détecter la langue de rédaction) : "{cv_raw_start}"
153
  expected_output: >
154
  JSON : {{"langues": [{{"langue": "Français", "niveau": "Natif"}}, {{"langue": "Anglais", "niveau": "B2"}}]}}
155
 
 
157
  description: >
158
  Extrais le prénom du candidat.
159
 
 
 
 
 
 
 
160
  RÈGLES :
161
  1. Cherche le prénom dans le HEADER, puis dans le TEXTE BRUT DÉBUT DU CV.
162
  2. Le NOM DU FICHIER est un indice fort : "ANISSA_KACEM_..." → prénom = "Anissa".
163
  3. Ne jamais inventer. Formate avec majuscule initiale.
164
  4. Si impossible à trouver, retourne null.
165
+
166
+ HEADER DU CV (nom, titre, contact) : "{header}"
167
+
168
+ TEXTE BRUT DÉBUT DU CV : "{cv_raw_start}"
169
+
170
+ NOM DU FICHIER (indice très fiable, souvent au format NOM_PRENOM_...) : "{file_name}"
171
  expected_output: >
172
  JSON : {{"first_name": "..."}}
173
 
 
175
  description: >
176
  Extrais le titre de poste visé tel qu'il est écrit dans l'en-tête du CV.
177
 
 
 
 
 
178
  RÈGLES :
179
  1. Le titre de poste se trouve juste après le nom du candidat (ex: "Business Analyst", "Data Engineer").
180
  2. Copie le titre EXACTEMENT tel qu'il est écrit, sans reformuler.
181
  3. Si le header est vide, cherche dans le TEXTE BRUT DÉBUT DU CV.
182
  4. Ne jamais inventer un titre.
183
+
184
+ HEADER DU CV (extrait par le splitter) : "{header}"
185
+
186
+ TEXTE BRUT DÉBUT DU CV (fallback si header vide) : "{cv_raw_start}"
187
  expected_output: >
188
  JSON : {{
189
  "poste_vise": "Le titre EXACT tel qu'écrit sur le CV",
 
196
  description: >
197
  Compare le profil du candidat avec le référentiel de métiers pour recommander les 3 postes les plus adaptés.
198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  RÈGLES D'ANALYSE :
200
+ IMPORTANT : Tu dois évaluer CHAQUE métier présent dans le RÉFÉRENTIEL DE MÉTIERS,
201
  sans en omettre aucun. Le top 3 final doit être basé sur l'évaluation exhaustive de tous
202
  les métiers listés. Ne jamais présélectionner ou ignorer des métiers a priori.
203
 
204
  1. Pour CHAQUE métier du référentiel, calcule un score de matching (0-100) basé sur :
205
+ - Couverture des compétences techniques requises (40%)
206
+ - Couverture des outils/technologies (30%)
207
  - Adéquation des expériences et projets (20%)
 
208
  - Cohérence avec le niveau d'études et l'expérience requise (10%)
209
  2. Utilise le mapping de domaines pour comprendre les liens implicites (ex: Metabase → BI,
210
  LangChain → LLM Engineering, Power BI → BI Analyst, Scikit-learn → Data Science).
 
213
  communication internationale → travail en équipe multiculturelle).
214
  4. Recommande les 3 métiers avec le MEILLEUR score parmi l'ensemble du référentiel évalué.
215
  5. PONDÉRATION TEMPORELLE (CRITIQUE) : Accorde un poids double (x2) aux technologies et compétences issues des expériences et projets les plus récents, ainsi qu'à la formation en cours. Le profil actuel d'un candidat est défini par ce qu'il fait aujourd'hui, pas par son historique lointain.
216
+ 6. Pour chaque métier recommandé, liste les compétences matchées et manquantes.
217
  7. Si le poste visé par le candidat ne fait pas partie du top 3, explique pourquoi.
218
  8. Fournis une analyse détaillée de l'adéquation du poste visé avec le profil.
219
+
220
+ RÉFÉRENTIEL DE MÉTIERS :
221
+ {metiers_reference}
222
+
223
+ EN-TÊTE DU CV (pour le poste visé) : "{header}"
224
+
225
+ SECTION COMPÉTENCES :
226
+ {skills}
227
+
228
+ SECTION EXPÉRIENCES :
229
+ {experiences}
230
+
231
+ SECTION PROJETS :
232
+ {projects}
233
+
234
+ SECTION FORMATION (utile pour détecter les reconversions):
235
+ {education}
236
  expected_output: >
237
  JSON : {{
238
  "postes_recommandes": [
 
249
  }},
250
  "competences_matchees": ["SQL", "Python", "Power BI"],
251
  "competences_manquantes": ["Looker", "dbt"],
 
252
  "justification": "Le profil couvre 85% des compétences requises..."
253
  }}
254
  ],
 
261
  Évalue la qualité globale du CV en appliquant les critères de bonnes pratiques CV tech 2025,
262
  adaptés au niveau de séniorité du candidat.
263
 
 
 
 
 
 
 
 
 
 
 
264
  CRITÈRES D'ÉVALUATION (score sur 100 pour chaque) :
265
 
266
  1. COMPATIBILITÉ ATS (20 points) :
267
+ - ÉVALUATION STRICTE ET CRITIQUE : Ne donne jamais un score parfait sans preuve.
268
+ - Les noms des sections principales sont-ils standards (Expériences, Formations, Projets, Compétences) ou atypiques ? Si atypiques, pénalise. (RÈGLE ABSOLUE : N'évalue PAS et ne pénalise JAMAIS les sections d'introduction courtes comme "Profil", "À propos", "Résumé" ou "Profil atypique". Ignore-les totalement pour ce critère).
269
+ - Les mots-clés techniques sont-ils contextuels ou simplement entassés de manière artificielle ?
270
+ - Indique EXPLICITEMENT ce qui ne va pas (ex: "Nom de section 'Mon Parcours' au lieu de 'Expériences'", "Mots-clés empilés sans lien avec les expériences").
271
+ - Fournis des points d'amélioration spécifiques et factuels.
272
 
273
  2. QUANTIFICATION DES RÉSULTATS (25 points) :
274
  - Les expériences mentionnent-elles des MÉTRIQUES TECHNIQUES SPÉCIFIQUES ?
 
281
  en fonction de ses expériences et projets RÉELS (pas des conseils génériques).
282
 
283
  3. STRUCTURE ET LISIBILITÉ (15 points) :
284
+ - ANALYSE CRITIQUE BASÉE SUR LE TEXTE : Ne suppose pas que le CV est beau. Évalue la structure à partir du texte extrait.
285
+ - RÈGLE ABSOLUE SUR LA FORME : N'évalue JAMAIS la présence ou l'absence de listes à puces/tirets. Le parsing supprime souvent la mise en forme, ne fais donc aucune remarque sur des "blocs de texte illisibles".
286
+ - L'ordre chronologique inverse est-il respecté dans les expériences et formations ? Si non, pénalise sévèrement.
287
+ - RÈGLE LONGUEUR : Utilise la donnée `NOMBRE DE PAGES`. Un profil < 7 ans d'expérience devrait tenir sur 1 page. Pénalise si > 2 pages.
288
+ - RÈGLE POSTE VISÉ (CRITIQUE) : Si `POSTE VISÉ` est "Non identifié", c'est une ERREUR MAJEURE. Baisse drastiquement la note de structure et signale-le comme un RED FLAG (le recruteur doit identifier le but du CV en 3 secondes).
289
+ - STRUCTURATION DES COMPÉTENCES : Les compétences sont-elles regroupées par catégories logiques (Langages, Frameworks, BDD, DevOps/Cloud) ou en liste plate ? Une structuration par catégories est fortement recommandée pour les filtres ATS.
290
+ - Fournis des conseils d'amélioration FACTUELS sur la structure.
291
 
292
  4. PRÉSENTATION DES PROJETS (20 points) :
293
  - Chaque projet a-t-il un titre, des technos, et des résultats ?
 
296
 
297
  5. PREUVES DE COMPÉTENCES (20 points) :
298
  - RÈGLE CRITIQUE : Une compétence est considérée "sans preuve" UNIQUEMENT si elle
299
+ apparaît EXCLUSIVEMENT dans la section Compétences sans aucune mention dans
300
+ les expériences OU les projets. Tu dois vérifier par toi-même (par analyse textuelle)
301
+ si les compétences listées apparaissent ailleurs. Ne signale que les skills qui n'ont "aucun contexte".
 
302
  - DÉTECTION DES LIENS : Cherche les URLs dans le CV Markdown ET dans le texte brut.
303
  Les liens peuvent apparaître sous forme de Markdown [texte](url), de texte brut
304
  (github.com/..., linkedin.com/...) ou dans le header. Signale les liens PRÉSENTS,
 
306
  - Pour les RECONVERSIONS : les compétences transférables (management, optimisation,
307
  communication, gestion budgétaire) sont-elles mises en valeur et reliées au nouveau domaine ?
308
 
309
+ ADAPTATION AU NIVEAU DE SÉNIORITÉ :
310
  - Si JUNIOR : valorise les projets personnels, formations, stages bien décrits.
311
  - Si CONFIRMÉ : exige des résultats mesurables, progression, responsabilités.
312
  - Si SENIOR/STAFF : vérifie la présence de choix architecturaux et compromis
 
314
  gestion de la scalabilité, impact organisationnel au-delà du code.
315
 
316
  RED FLAGS À DÉTECTER :
317
+ - DÉTECTION STRICTE DES COMPÉTENCES SANS PREUVE : Tu DOIS t'appuyer uniquement sur ton évaluation de "preuves_competences". S'il y a des compétences listées dans "skills_sans_preuve", ajoute un red flag. Sinon, n'ajoute pas de red flag pour ce sujet.
318
  - Trous inexpliqués dans le parcours
319
  - Jargon excessif ou buzzwords sans substance
320
  - Incohérence entre compétences listées et projets/expériences
321
  - Section compétences en liste plate non catégorisée
322
+
323
+ DONNÉES À ÉVALUER :
324
+ EN-TÊTE DU CV (pour le poste visé et niveau de séniorité) : "{header}"
325
+ NOMBRE DE PAGES : {page_count}
326
+ CV COMPLET (texte Markdown) : "{cv_full_text}"
327
+ TEXTE BRUT DU CV (première page, pour détecter les URLs et liens) : "{cv_raw_start}"
328
+ SECTION COMPÉTENCES : "{skills}"
329
+ SECTION EXPÉRIENCES : "{experiences}"
330
+ SECTION PROJETS : "{projects}"
331
+ SECTION FORMATIONS : "{education}"
332
  expected_output: >
333
  JSON : {{
334
  "score_global": 72,
 
345
 
346
  project_analysis_task:
347
  description: >
348
+ Évalue CHAQUE projet du CV et détermine la qualité technique et l'impact.
349
+
350
+ Pour CHAQUE projet, tu dois scorer sur 10 avec une justification courte et factuelle (preuve tirée du CV) sur les 7 critères suivants :
351
+ 1. Pertinence & Alignement : Correspondance avec le poste visé (multi-agents, RAG, recrutement tech, pipelines, GenAI).
352
+ 2. Complexité Technique & Architecture : Niveau réel d'intégration (7 agents orchestrés ? pipeline complet ingestion→monitoring ? custom vs simple CrewAI/LangChain ?).
353
+ 3. Stack & Maîtrise : Modernité, cohérence, diversité raisonnable vs "tech soup".
354
+ 4. Innovation & Originalité : Valeur ajoutée réelle vs "j'ai collé des libs à la mode" (Ex : scoring multi-dimensionnel + détection fraude = fort ; simple wrapper RAG = faible).
355
+ 5. Impact & Résultats Mesurables : Chiffres concrets (+40 %, 2000 offres, 30 % moins d'étapes...) et crédibilité de la mesure.
356
+ 6. Ownership & Exécution : End-to-end (idea → business plan → dev → déploiement → monitoring). Méthodologies affichées (Design Thinking, Scrum...).
357
+ 7. Maturité Production & Qualité : Monitoring, coût/latence, tests, sécurité, RGPD, scaling, users réels vs démo/prototype.
358
+
359
+ Méthode pour un avis CLAIR et CRITIQUE :
360
+ - Être factuel d'abord (preuve du CV).
361
+ - Donner du positif spécifique.
362
+ - Donner de la critique constructive et honnête (jamais méchant, toujours utile).
363
+ - Éviter le fanboying et le "tout est génial".
364
+ - Terminer par un verdict tranché.
365
+
366
+ DONNÉES DU CANDIDAT :
367
+ EN-TÊTE DU CV (pour déduire le poste visé) : "{header}"
368
+ PROJETS MENTIONNÉS (texte brut) : {projects}
 
 
 
 
 
 
369
  expected_output: >
370
  JSON : {{
371
  "analyse_projets": [
372
  {{
373
+ "titre": "Nom exact du projet",
374
+ "resume": "Description ultra-concise + type (Side Project / SaaS / MVP)",
375
+ "evaluation": {{
376
+ "pertinence": {{"score": 8, "justification": "..."}},
377
+ "complexite": {{"score": 7, "justification": "..."}},
378
+ "stack": {{"score": 9, "justification": "..."}},
379
+ "innovation": {{"score": 6, "justification": "..."}},
380
+ "impact": {{"score": 5, "justification": "Aucun chiffre concret"}},
381
+ "ownership": {{"score": 8, "justification": "..."}},
382
+ "maturite": {{"score": 4, "justification": "Pas de mention de monitoring ou de tests"}}
383
+ }},
384
+ "points_forts": ["...", "..."],
385
+ "points_vigilance": ["Risque d'over-claim sur...", "Manque de visibilité sur tests"],
386
+ "note_globale": 67,
387
+ "verdict_recruteur": "Très bon projet qui démontre une vraie maîtrise agentique... Idéal pour un rôle IA intermédiaire, à creuser en entretien."
388
  }}
389
+ ]
 
 
 
 
390
  }}
391
 
src/data/metiers.json CHANGED
The diff for this file is too large to render. See raw diff
 
src/parser_flow/CV_agent_flow.py CHANGED
@@ -24,16 +24,6 @@ logger = logging.getLogger(__name__)
24
 
25
  #_____________________________________________________________________________________
26
 
27
- # Configuration du logger pour capturer la verbosité dans un fichier
28
- verbose_logger = logging.getLogger("crewai_verbose")
29
- verbose_logger.setLevel(logging.INFO)
30
-
31
- # Création du fichier de log (écrase le précédent à chaque run avec 'w')
32
- file_handler = logging.FileHandler("agents_trace.log", mode='w', encoding='utf-8')
33
- formatter = logging.Formatter('%(asctime)s - %(message)s')
34
- file_handler.setFormatter(formatter)
35
- verbose_logger.addHandler(file_handler)
36
-
37
  class CVAgentOrchestrator:
38
  """Orchestrateur multi-agents pour le parsing et l'analyse de CV."""
39
 
@@ -43,13 +33,8 @@ class CVAgentOrchestrator:
43
  self.agents_config = self._load_yaml("agents.yaml")
44
  self.tasks_config = self._load_yaml("tasks.yaml")
45
  self.metiers_data = self._load_metiers()
46
- self.skill_domain_map = self._load_skill_domain_map()
47
  self._create_agents()
48
 
49
- # ──────────────────────────────────────────────
50
- # Chargement des configurations
51
- # ──────────────────────────────────────────────
52
-
53
  def _load_yaml(self, filename: str) -> Dict:
54
  base_path = os.path.dirname(os.path.dirname(__file__))
55
  config_path = os.path.join(base_path, "config", filename)
@@ -57,29 +42,12 @@ class CVAgentOrchestrator:
57
  return yaml.safe_load(f)
58
 
59
  def _load_metiers(self) -> List[Dict]:
60
- """Charge le référentiel de métiers (sans les embeddings pour économiser la mémoire)."""
61
  base_path = os.path.dirname(os.path.dirname(__file__))
62
  metiers_path = os.path.join(base_path, "data", "metiers.json")
63
  with open(metiers_path, "r", encoding="utf-8") as f:
64
  data = json.load(f)
65
- metiers = []
66
- for m in data.get("metiers", []):
67
- clean = {k: v for k, v in m.items() if k != "embedding"}
68
- metiers.append(clean)
69
- return metiers
70
-
71
- def _load_skill_domain_map(self) -> Dict[str, List[str]]:
72
- """Charge le mapping compétences -> domaines."""
73
- base_path = os.path.dirname(os.path.dirname(__file__))
74
- map_path = os.path.join(base_path, "config", "skill_domain_map.json")
75
- with open(map_path, "r", encoding="utf-8") as f:
76
- return json.load(f)
77
-
78
- # ──────────────────────────────────────────────
79
- # Création des agents
80
- # ──────────────────────────────────────────────
81
-
82
-
83
 
84
  def _create_agents(self):
85
  def make_agent(name, llm_override=None):
@@ -90,12 +58,9 @@ class CVAgentOrchestrator:
90
  verbose=True,
91
  max_iter=1,
92
  respect_context_window=True,
93
- # logs callbackagent
94
- step_callback=lambda step: verbose_logger.info(f"Agent {name} Step: {step}"),
95
  )
96
 
97
- # Phase 2 : Agents d'extraction (existants)
98
- self.cv_splitter = make_agent("cv_splitter", llm_override=self.big_llm)
99
  self.skills_extractor = make_agent("skills_extractor")
100
  self.experience_extractor = make_agent("experience_extractor")
101
  self.project_extractor = make_agent("project_extractor")
@@ -105,11 +70,10 @@ class CVAgentOrchestrator:
105
  self.etudiant_detector = make_agent("etudiant_detector")
106
  self.identity_extractor = make_agent("identity_extractor")
107
 
108
- # Phase 3 : Agents d'analyse et recommandation (nouveaux)
109
- self.header_analyzer = make_agent("header_analyzer", llm_override=self.big_llm)
110
- self.metier_matcher = make_agent("metier_matcher", llm_override=self.big_llm)
111
- self.cv_quality_checker = make_agent("cv_quality_checker")
112
- self.project_analyzer = make_agent("project_analyzer")
113
 
114
  # ──────────────────────────────────────────────
115
  # PHASE 1 : Découpage du CV en sections
@@ -138,25 +102,41 @@ class CVAgentOrchestrator:
138
  return parsed
139
 
140
  # ──────────────────────────────────────────────
141
- # PHASE 2 : Extraction parallèle (8 agents)
142
  # ──────────────────────────────────────────────
143
 
144
- async def extract_all_sections(
145
- self, sections: Dict[str, str], cv_raw_start: str = "", file_name: str = ""
146
  ) -> Dict[str, Any]:
147
- """Exécute les 8 tâches d'extraction en parallèle."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  def create_task_async(task_key, agent, **kwargs):
150
  t_config = self.tasks_config[task_key].copy()
151
  t_description = t_config["description"]
152
-
153
- # Éviter les erreurs de formattage si des clés manquent ou sont mal échappées (ex: accolades dans le texte du CV)
154
  try:
155
- # Utiliser format_map pour plus de flexibilité si besoin, mais format() est standard
156
  t_config["description"] = t_description.format(**kwargs)
157
  except KeyError as e:
158
  logger.warning(f"KeyError formatting task '{task_key}': {e}. Falling back to manual replace.")
159
- # Fallback manuel sécurisé pour les clés présentes
160
  desc = t_description
161
  for k, v in kwargs.items():
162
  placeholder = "{" + k + "}"
@@ -171,69 +151,40 @@ class CVAgentOrchestrator:
171
  return (task_key, c.kickoff_async())
172
 
173
  tasks_def = [
174
- (
175
- "skills_task",
176
- self.skills_extractor,
177
- {
178
- "experiences": sections.get("experiences", ""),
179
- "projects": sections.get("projects", ""),
180
- "skills": sections.get("skills", ""),
181
- "education": sections.get("education", ""),
182
- },
183
- ),
184
- (
185
- "experience_task",
186
- self.experience_extractor,
187
- {"experiences": sections.get("experiences", "")},
188
- ),
189
- (
190
- "project_task",
191
- self.project_extractor,
192
- {"projects": sections.get("projects", "")},
193
- ),
194
- (
195
- "education_task",
196
- self.education_extractor,
197
- {"education": sections.get("education", "")},
198
- ),
199
- (
200
- "reconversion_task",
201
- self.reconversion_detector,
202
- {
203
- "experiences": sections.get("experiences", ""),
204
- "education": sections.get("education", ""),
205
- },
206
- ),
207
- (
208
- "language_task",
209
- self.language_extractor,
210
- {
211
- "languages": sections.get("languages", ""),
212
- "cv_raw_start": cv_raw_start[:500],
213
- },
214
- ),
215
- (
216
- "etudiant_task",
217
- self.etudiant_detector,
218
- {
219
- "education": sections.get("education", ""),
220
- "current_date": datetime.now().strftime("%Y-%m-%d"),
221
- },
222
- ),
223
- (
224
- "identity_task",
225
- self.identity_extractor,
226
- {
227
- "header": sections.get("header", ""),
228
- "cv_raw_start": cv_raw_start[:1500],
229
- "file_name": file_name,
230
- },
231
- ),
232
  ]
233
 
234
- task_coroutines = [
235
- create_task_async(key, agent, **kwargs) for key, agent, kwargs in tasks_def
236
- ]
237
  keys = [t[0] for t in task_coroutines]
238
  coroutines = [t[1] for t in task_coroutines]
239
  results_list = await asyncio.gather(*coroutines, return_exceptions=True)
@@ -245,530 +196,147 @@ class CVAgentOrchestrator:
245
  else:
246
  results_map[key] = result
247
 
248
- return self._aggregate_extraction_results(results_map)
249
-
250
- # ──────────────────────────────────────────────
251
- # PHASE 3a : Analyse d'en-tête (indépendante, tourne en // avec Phase 2)
252
- # ──────────────────────────────────────────────
253
-
254
- async def run_header_analysis(
255
- self,
256
- sections: Dict[str, str],
257
- cv_raw_start: str = "",
258
- cv_full_text: str = "",
259
- ) -> Dict:
260
- """Extrait le poste visé depuis l'en-tête du CV.
261
-
262
- Ne dépend que de Phase 1 (sections) → peut tourner en PARALLÈLE avec Phase 2.
263
- """
264
- header_section = sections.get("header", "")
265
- raw_for_header = cv_raw_start[:2000] if cv_raw_start else cv_full_text[:2000]
266
- safe_cv_raw = raw_for_header.replace("{", "{{").replace("}", "}}")
267
- safe_header = header_section.replace("{", "{{").replace("}", "}}")
268
-
269
- header_data: Dict = {
270
- "poste_vise": "Non identifié",
271
- "niveau_seniorite": "non précisé",
272
- "confiance": 0,
273
- }
274
-
275
- try:
276
- t_config = self.tasks_config["poste_visé_task"].copy()
277
- t_config["description"] = t_config["description"].format(
278
- cv_raw_start=safe_cv_raw,
279
- header=safe_header,
280
- )
281
- task = Task(config=t_config, agent=self.header_analyzer)
282
- crew = Crew(agents=[self.header_analyzer], tasks=[task], verbose=False)
283
- header_result = await crew.kickoff_async()
284
-
285
- if header_result:
286
- header_data = self._parse_json_output(
287
- header_result,
288
- {"poste_vise": "Non identifié", "niveau_seniorite": "non précisé", "confiance": 0},
289
- )
290
- logger.info(
291
- f"Header analyzer : poste_vise='{header_data.get('poste_vise')}', "
292
- f"confiance={header_data.get('confiance')}"
293
- )
294
- except Exception as e:
295
- logger.error(f"Header analyzer failed: {e}", exc_info=True)
296
-
297
- # Fallback programmatique si le LLM n'a pas trouvé le poste
298
- if header_data.get("poste_vise", "Non identifié") == "Non identifié":
299
- logger.warning("Header analyzer 'Non identifié' → fallback programmatique...")
300
- fallback = self._fallback_extract_poste_vise(cv_full_text, header_section)
301
- if fallback:
302
- header_data["poste_vise"] = fallback
303
- header_data["source_detection"] = "fallback_programmatique"
304
- header_data["confiance"] = 70
305
- logger.info(f"Fallback found poste_vise: '{fallback}'")
306
-
307
- return header_data
308
-
309
- # ───────────────────────────��──────────────────
310
- # PHASE 3b : Analyse & Recommandation (3 agents parallèles)
311
- # ──────────────────────────────────────────────
312
-
313
- async def analyze_and_recommend(
314
- self,
315
- cv_full_text: str,
316
- sections: Dict[str, str],
317
- extraction: Dict[str, Any],
318
- cv_raw_start: str = "",
319
- header_data: Dict = None,
320
- ) -> Dict[str, Any]:
321
- """Exécute les 3 tâches d'analyse en parallèle.
322
-
323
- header_data est pré-calculé par run_header_analysis (en // avec Phase 2).
324
- """
325
- if header_data is None:
326
- logger.warning("analyze_and_recommend sans header_data — valeurs par défaut utilisées.")
327
- header_data = {"poste_vise": "Non identifié", "niveau_seniorite": "non précisé", "confiance": 0}
328
-
329
- candidat = extraction.get("candidat", {})
330
- competences = candidat.get("compétences", {})
331
- hard_skills = competences.get("hard_skills", [])
332
- soft_skills = competences.get("soft_skills", [])
333
- skills_with_context = competences.get("skills_with_context", [])
334
- reconversion = candidat.get("reconversion", {})
335
-
336
- skill_domains = self._map_skills_to_domains(hard_skills)
337
- methodologies = self._extract_methodologies(hard_skills, skill_domains)
338
-
339
- experiences_summary = json.dumps(
340
- candidat.get("expériences", []), ensure_ascii=False
341
- )[:3000]
342
- projets = candidat.get("projets", {})
343
- professional_projects = json.dumps(
344
- projets.get("professional", []), ensure_ascii=False
345
- )[:2000]
346
- personal_projects = json.dumps(
347
- projets.get("personal", []), ensure_ascii=False
348
- )[:2000]
349
- projects_summary = f"Pro: {professional_projects}\nPerso: {personal_projects}"
350
- reconversion_data = json.dumps(reconversion, ensure_ascii=False) if reconversion else "{}"
351
-
352
- metiers_reference = self._prepare_metiers_for_prompt()
353
-
354
- poste_vise = header_data.get("poste_vise", "Non identifié")
355
- niveau_seniorite = header_data.get("niveau_seniorite", "non précisé")
356
- metier_reference_detail = self._get_metier_reference_for_poste(poste_vise)
357
-
358
- raw_for_header = cv_raw_start[:2000] if cv_raw_start else cv_full_text[:2000]
359
- safe_cv_raw = raw_for_header.replace("{", "{{").replace("}", "}}")
360
-
361
- def create_task_async(task_key, agent, **kwargs):
362
- t_config = self.tasks_config[task_key].copy()
363
- t_config["description"] = t_config["description"].format(**kwargs)
364
- task = Task(config=t_config, agent=agent)
365
- c = Crew(agents=[agent], tasks=[task], verbose=False)
366
- return (task_key, c.kickoff_async())
367
-
368
- # 3 agents en parallèle (quality + metier matching + project analysis)
369
- parallel_tasks = [
370
- (
371
- "cv_quality_task",
372
- self.cv_quality_checker,
373
- {
374
- "cv_full_text": cv_full_text[:6000],
375
- "cv_raw_start": safe_cv_raw,
376
- "skills_with_context": json.dumps(skills_with_context, ensure_ascii=False)[:2000],
377
- "experiences_summary": experiences_summary,
378
- "projects_summary": projects_summary[:2000],
379
- "niveau_seniorite": niveau_seniorite,
380
- "reconversion_data": reconversion_data,
381
- },
382
- ),
383
- (
384
- "metier_matching_task",
385
- self.metier_matcher,
386
- {
387
- "poste_vise": poste_vise,
388
- "hard_skills": json.dumps(hard_skills, ensure_ascii=False),
389
- "soft_skills": json.dumps(soft_skills, ensure_ascii=False),
390
- "skill_domains": json.dumps(skill_domains, ensure_ascii=False),
391
- "methodologies": json.dumps(methodologies, ensure_ascii=False),
392
- "experiences_summary": experiences_summary,
393
- "projects_summary": projects_summary[:2000],
394
- "reconversion_data": reconversion_data,
395
- "metiers_reference": metiers_reference,
396
- },
397
- ),
398
- (
399
- "project_analysis_task",
400
- self.project_analyzer,
401
- {
402
- "poste_vise": poste_vise,
403
- "metier_reference_detail": metier_reference_detail,
404
- "professional_projects": professional_projects,
405
- "personal_projects": personal_projects,
406
- "reconversion_data": reconversion_data,
407
- },
408
- ),
409
- ]
410
-
411
- task_coroutines = [
412
- create_task_async(key, agent, **kwargs) for key, agent, kwargs in parallel_tasks
413
- ]
414
- keys = [t[0] for t in task_coroutines]
415
- coroutines = [t[1] for t in task_coroutines]
416
- results_list = await asyncio.gather(*coroutines, return_exceptions=True)
417
-
418
- analysis_results = {}
419
- for key, result in zip(keys, results_list):
420
- if isinstance(result, Exception):
421
- logger.error(f"Analysis task '{key}' failed: {result}")
422
- else:
423
- analysis_results[key] = result
424
-
425
- recommendations = self._aggregate_recommendations(analysis_results, header_data)
426
-
427
- # ── Filtre dur : ne garder que les projets issus de la section projets ──
428
- extracted_titles: set[str] = set()
429
- for p in projets.get("professional", []):
430
- if isinstance(p, dict) and p.get("title"):
431
- extracted_titles.add(p["title"].strip().lower())
432
- for p in projets.get("personal", []):
433
- if isinstance(p, dict) and p.get("title"):
434
- extracted_titles.add(p["title"].strip().lower())
435
-
436
- if extracted_titles:
437
- def _is_extracted_project(titre: str) -> bool:
438
- t = titre.strip().lower()
439
- if t in extracted_titles:
440
- return True
441
- return any(t in ref or ref in t for ref in extracted_titles)
442
-
443
- recommendations["analyse_projets"] = [
444
- p for p in recommendations.get("analyse_projets", [])
445
- if isinstance(p, dict) and _is_extracted_project(p.get("titre", ""))
446
- ]
447
- logger.info(
448
- f"Filtre projets : {len(recommendations['analyse_projets'])} projets conservés "
449
- f"sur {len(extracted_titles)} extraits."
450
- )
451
-
452
- return recommendations
453
-
454
- # ──────────────────────────────────────────────
455
- # Mapping compétences -> domaines
456
- # ──────────────────────────────────────────────
457
-
458
- def _map_skills_to_domains(self, hard_skills: List[str]) -> Dict[str, List[str]]:
459
- """Mappe les compétences du candidat à leurs domaines métier."""
460
- result = {}
461
- for skill in hard_skills:
462
- skill_lower = skill.lower().strip()
463
- for domain, domain_skills in self.skill_domain_map.items():
464
- if skill_lower in domain_skills:
465
- if domain not in result:
466
- result[domain] = []
467
- result[domain].append(skill)
468
- break
469
- return result
470
-
471
- def _prepare_metiers_for_prompt(self) -> str:
472
- """Prépare le référentiel métiers COMPLET (30 métiers) pour le prompt."""
473
- lines = []
474
- for m in self.metiers_data:
475
- mid = m.get("id", "?")
476
- nom = m.get("nom", "?")
477
- cat = m.get("categorie", "?")
478
- comp = m.get("competences_techniques", [])
479
- outils = m.get("outils_technologies", [])
480
- soft = m.get("competences_soft", [])
481
- niveau = m.get("niveau_etude", "?")
482
- exp = m.get("experience_requise", "?")
483
- lines.append(
484
- f"[{mid}] {nom} ({cat})\n"
485
- f" Compétences techniques: {', '.join(comp)}\n"
486
- f" Outils: {', '.join(outils)}\n"
487
- f" Soft skills: {', '.join(soft[:3])}\n"
488
- f" Niveau: {niveau} | Expérience: {exp}"
489
- )
490
- return "\n\n".join(lines)
491
-
492
- def _get_metier_reference_for_poste(self, poste_vise: str) -> str:
493
- """Trouve les métiers les plus proches du poste visé pour contextualiser l'analyse de projets."""
494
- if not poste_vise or poste_vise == "Non identifié":
495
- return "Aucun métier de référence spécifique. Analyser les projets selon leur qualité intrinsèque."
496
-
497
- poste_lower = poste_vise.lower()
498
- scored = []
499
-
500
- for m in self.metiers_data:
501
- nom_lower = m.get("nom", "").lower()
502
- id_lower = m.get("id", "").lower()
503
- desc_lower = m.get("description", "").lower()
504
- score = 0
505
-
506
- keywords = [w for w in poste_lower.replace("/", " ").replace("-", " ").split() if len(w) > 2]
507
- for kw in keywords:
508
- if kw in nom_lower:
509
- score += 3
510
- if kw in id_lower:
511
- score += 2
512
- if kw in desc_lower:
513
- score += 1
514
-
515
- nom_keywords = [w for w in nom_lower.replace("/", " ").replace("-", " ").split() if len(w) > 2]
516
- for kw in nom_keywords:
517
- if kw in poste_lower:
518
- score += 3
519
-
520
- if score > 0:
521
- scored.append((score, m))
522
-
523
- scored.sort(key=lambda x: -x[0])
524
-
525
- if not scored:
526
- return "Poste visé non trouvé dans le référentiel. Analyser les projets selon leur qualité intrinsèque."
527
-
528
- lines = ["Métier(s) de référence les plus proches du poste visé :"]
529
- for _, m in scored[:3]:
530
- mid = m.get("id")
531
- nom = m.get("nom")
532
- comp = m.get("competences_techniques", [])
533
- outils = m.get("outils_technologies", [])
534
- missions = m.get("missions_principales", [])
535
- lines.append(
536
- f"\n[{mid}] {nom}\n"
537
- f" Compétences attendues: {', '.join(comp)}\n"
538
- f" Outils attendus: {', '.join(outils)}\n"
539
- f" Missions principales: {'; '.join(missions[:3])}"
540
- )
541
- return "\n".join(lines)
542
-
543
- def _extract_methodologies(self, hard_skills: List[str], skill_domains: Dict[str, List[str]]) -> List[str]:
544
- """Extrait les méthodologies de travail du candidat."""
545
- methodology_keywords = {
546
- "agile", "scrum", "kanban", "devops", "ci/cd", "cicd", "tdd", "bdd",
547
- "design thinking", "lean", "safe", "xp", "pair programming",
548
- "code review", "sprint", "product owner", "scrum master",
549
- "rgpd", "rgaa",
550
- }
551
-
552
- methodologies = []
553
- for skill in hard_skills:
554
- if skill.lower().strip() in methodology_keywords:
555
- methodologies.append(skill)
556
-
557
- if "gestion_projet" in skill_domains:
558
- for skill in skill_domains["gestion_projet"]:
559
- if skill not in methodologies:
560
- methodologies.append(skill)
561
-
562
- if "devops" in skill_domains:
563
- for skill in skill_domains["devops"]:
564
- s = skill.lower()
565
- if any(kw in s for kw in ["ci", "cd", "github actions", "gitlab ci"]):
566
- if skill not in methodologies:
567
- methodologies.append(skill)
568
-
569
- return methodologies
570
-
571
- # ──────────────────────────────────────────────
572
- # Agrégation des résultats d'extraction (Phase 2)
573
- # ──────────────────────────────────────────────
574
-
575
- def _aggregate_extraction_results(self, results_map: Dict[str, Any]) -> Dict[str, Any]:
576
- """Agrège les résultats d'extraction (identique au module existant)."""
577
 
 
 
 
578
  def get_parsed(key, default=None):
579
  if key not in results_map:
580
  return default
581
  return self._parse_json_output(results_map[key], default)
582
 
 
583
  competences = get_parsed("skills_task", {"hard_skills": [], "soft_skills": []})
584
  experiences = get_parsed("experience_task", [])
585
  projets = get_parsed("project_task", {"professional": [], "personal": []})
586
  formations = get_parsed("education_task", [])
587
- reconversion = get_parsed("reconversion_task", {}).get(
588
- "reconversion_analysis", {}
589
- )
590
  etudiant_data = get_parsed("etudiant_task", {}).get("etudiant_analysis", {})
 
591
  latest_end_date = etudiant_data.get("latest_education_end_date")
592
  if latest_end_date:
593
- is_student_by_date = self._is_still_student(latest_end_date)
594
- etudiant_data["is_etudiant"] = is_student_by_date
 
 
 
 
 
 
 
 
595
 
596
  langues_raw = get_parsed("language_task", {})
 
597
 
 
598
  if isinstance(competences, dict):
599
  raw_skills = competences.get("hard_skills", [])
600
  seen = set()
601
  unique_skills = []
602
  for skill in raw_skills:
603
- key = (
604
- str(skill).lower()
605
- if not isinstance(skill, str)
606
- else skill.lower()
607
- )
608
  if key not in seen:
609
  seen.add(key)
610
  unique_skills.append(skill)
611
  competences["hard_skills"] = unique_skills
612
 
613
- identity = get_parsed("identity_task", {})
614
-
615
- return {
616
- "candidat": {
617
- "first_name": (
618
- identity.get("first_name")
619
- if isinstance(identity, dict)
620
- else None
621
- ),
622
- "compétences": competences,
623
- "expériences": experiences,
624
- "reconversion": reconversion,
625
- "projets": projets,
626
- "formations": formations,
627
- "etudiant": etudiant_data,
628
- "langues": (
629
- langues_raw.get("langues", [])
630
- if isinstance(langues_raw, dict)
631
- else []
632
- ),
633
- }
634
  }
635
 
636
- # ──────────────────────────────────────────────
637
- # Agrégation des recommandations (Phase 3)
638
- # ──────────────────────────────────────────────
639
-
640
- def _aggregate_recommendations(
641
- self,
642
- analysis_results: Dict[str, Any],
643
- header_data: Dict,
644
- ) -> Dict[str, Any]:
645
- """Agrège les résultats d'analyse en un objet recommandations structuré."""
646
-
647
- def get_parsed(key, default=None):
648
- if key not in analysis_results:
649
- return default
650
- return self._parse_json_output(analysis_results[key], default)
651
-
652
  metier_data = get_parsed("metier_matching_task", {"postes_recommandes": []})
653
- quality_data = get_parsed(
654
- "cv_quality_task",
655
- {"score_global": 0, "red_flags": [], "conseils_prioritaires": []},
656
- )
657
  project_data = get_parsed("project_analysis_task", {"analyse_projets": []})
658
 
659
- # Conseils d'amélioration : uniquement les conseils qualité CV
660
  conseils = []
661
  if isinstance(quality_data, dict):
662
  conseils.extend(quality_data.get("conseils_prioritaires", []))
663
 
664
- return {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
  "header_analysis": header_data,
666
- "postes_recommandes": (
667
- metier_data.get("postes_recommandes", [])
668
- if isinstance(metier_data, dict)
669
- else []
670
- ),
671
- "analyse_poste_vise": (
672
- metier_data.get("analyse_poste_vise", "")
673
- if isinstance(metier_data, dict)
674
- else ""
675
- ),
676
  "qualite_cv": quality_data,
677
- "analyse_projets": (
678
- project_data.get("analyse_projets", [])
679
- if isinstance(project_data, dict)
680
- else []
681
- ),
682
- "coherence_globale_projets": (
683
- project_data.get("coherence_globale", {})
684
- if isinstance(project_data, dict)
685
- else {}
686
- ),
687
  "conseils_amelioration": conseils,
688
  }
689
 
690
- # ──────────────────────────────────────────────
691
- # Utilitaires
692
- # ──────────────────────────────────────────────
693
-
694
- def _fallback_extract_poste_vise(
695
- self, cv_full_text: str, header_section: str
696
- ) -> str:
697
- """Extraction programmatique du poste visé en fallback.
698
-
699
- Cherche la ligne de titre dans l'en-tête du CV en filtrant les lignes
700
- qui ne sont clairement PAS un titre de poste (email, téléphone, liens,
701
- titres de section, compétences techniques).
702
- """
703
- import re
704
-
705
- # Patterns qui NE sont PAS un titre de poste
706
- skip_patterns = [
707
- r"^#{1,6}\s", # Titres markdown
708
- r"@", # Email
709
- r"^\+?\d[\d\s\-\.]{7,}", # Téléphone
710
- r"^http|^www\.|linkedin|github", # URLs/liens
711
- r"^\*{1,3}[A-Z]", # Bold section headers
712
- r"^(CONTACT|LIENS|STACK|LANGUES|CENTRES|EXPERIENCE|FORMATION|PROJET|COMPÉTENCES|EDUCATION)", # Section headings
713
- r"^(Python|SQL|JavaScript|React|FastAPI|Docker|AWS|Git|CI)", # Skills
714
- r"^(Ile-de-France|Paris|Lyon|Marseille|France)", # Locations
715
- r"^\d{2}\s?\d{2}\s?\d{2}", # Phone numbers
716
- r"^(Français|Anglais|Portugais|Espagnol)", # Languages
717
- r"^(Langages|Frameworks|Analytics|DevOps|Méthodologies|IA &|BI :)", # Skill categories
718
- r"^(Blockchain|Jeux de rôle|Randonnée)", # Interests
719
- r"^\s*$", # Empty lines
720
- r"^[\*\-\|]", # List items and table separators
721
- ]
722
-
723
- # Mots-clés qui INDIQUENT un titre de poste
724
- title_indicators = [
725
- "développeur", "developer", "ingénieur", "engineer", "chef de projet",
726
- "data analyst", "data scientist", "data engineer", "consultant",
727
- "architecte", "manager", "lead", "senior", "junior", "fullstack",
728
- "full-stack", "full stack", "backend", "frontend", "devops",
729
- "product", "project", "spécialiste", "expert", "analyste",
730
- "mlops", "ai", "ia", "machine learning", "nlp", "deep learning",
731
- ]
732
 
733
- def _has_title_indicator(text_lower: str) -> bool:
734
- for indicator in title_indicators:
735
- if len(indicator) <= 3:
736
- if re.search(r"\b" + re.escape(indicator) + r"\b", text_lower):
737
- return True
738
- else:
739
- if indicator in text_lower:
740
- return True
741
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
742
 
743
- def _is_likely_title(line: str) -> bool:
744
- stripped = line.strip().strip("#*_ ")
745
- if len(line.split()) > 10:
746
- return False
747
- for pattern in skip_patterns:
748
- if re.match(pattern, stripped, re.IGNORECASE):
749
- return False
750
- return _has_title_indicator(stripped.lower())
751
-
752
- # Chercher dans toutes les sources, par ordre de priorité
753
- sources = [
754
- ("header", header_section),
755
- ("cv_text", cv_full_text[:3000]),
756
- ]
757
 
758
- for source_name, text in sources:
759
- if not text:
760
- continue
761
- lines = text.split("\n")
762
- for line in lines:
763
- if _is_likely_title(line):
764
- clean = line.strip().strip("#*_ ")
765
- logger.info(f"Fallback: found title in {source_name}: '{clean}'")
766
- return clean
767
 
768
- return ""
 
 
769
 
770
- def _is_still_student(self, date_str: str) -> bool:
771
- """Détermine si le candidat est encore étudiant à partir de la date de fin d'études."""
772
  if not date_str:
773
  return False
774
  date_str = str(date_str).lower().strip()
 
24
 
25
  #_____________________________________________________________________________________
26
 
 
 
 
 
 
 
 
 
 
 
27
  class CVAgentOrchestrator:
28
  """Orchestrateur multi-agents pour le parsing et l'analyse de CV."""
29
 
 
33
  self.agents_config = self._load_yaml("agents.yaml")
34
  self.tasks_config = self._load_yaml("tasks.yaml")
35
  self.metiers_data = self._load_metiers()
 
36
  self._create_agents()
37
 
 
 
 
 
38
  def _load_yaml(self, filename: str) -> Dict:
39
  base_path = os.path.dirname(os.path.dirname(__file__))
40
  config_path = os.path.join(base_path, "config", filename)
 
42
  return yaml.safe_load(f)
43
 
44
  def _load_metiers(self) -> List[Dict]:
45
+ """Charge le référentiel de métiers (avec embeddings)."""
46
  base_path = os.path.dirname(os.path.dirname(__file__))
47
  metiers_path = os.path.join(base_path, "data", "metiers.json")
48
  with open(metiers_path, "r", encoding="utf-8") as f:
49
  data = json.load(f)
50
+ return data.get("metiers", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  def _create_agents(self):
53
  def make_agent(name, llm_override=None):
 
58
  verbose=True,
59
  max_iter=1,
60
  respect_context_window=True,
 
 
61
  )
62
 
63
+ self.cv_splitter = make_agent("cv_splitter")
 
64
  self.skills_extractor = make_agent("skills_extractor")
65
  self.experience_extractor = make_agent("experience_extractor")
66
  self.project_extractor = make_agent("project_extractor")
 
70
  self.etudiant_detector = make_agent("etudiant_detector")
71
  self.identity_extractor = make_agent("identity_extractor")
72
 
73
+ self.header_analyzer = make_agent("header_analyzer")
74
+ self.metier_matcher = make_agent("metier_matcher")
75
+ self.cv_quality_checker = make_agent("cv_quality_checker", llm_override=self.big_llm)
76
+ self.project_analyzer = make_agent("project_analyzer", llm_override=self.big_llm)
 
77
 
78
  # ──────────────────────────────────────────────
79
  # PHASE 1 : Découpage du CV en sections
 
102
  return parsed
103
 
104
  # ──────────────────────────────────────────────
105
+ # PHASE 2 : Extraction et Analyse Parallèles
106
  # ──────────────────────────────────────────────
107
 
108
+ async def run_all_agents(
109
+ self, sections: Dict[str, str], cv_raw_start: str = "", cv_full_text: str = "", file_name: str = "", page_count: int = 1
110
  ) -> Dict[str, Any]:
111
+ """Exécute toutes les tâches d'extraction et d'analyse en parallèle."""
112
+
113
+ raw_header = sections.get("header", "")
114
+ raw_experiences = sections.get("experiences", "")
115
+ raw_projects = sections.get("projects", "")
116
+ raw_skills = sections.get("skills", "")
117
+ raw_education = sections.get("education", "")
118
+ raw_languages = sections.get("languages", "")
119
+ safe_cv_raw = cv_raw_start[:2000].replace("{", "{{").replace("}", "}}")
120
+ safe_header = raw_header.replace("{", "{{").replace("}", "}}")
121
+
122
+ from src.services.metier_pre_filter import get_top_k_metiers
123
+ top_metiers = get_top_k_metiers(
124
+ metiers_data=self.metiers_data,
125
+ experiences_summary=raw_experiences[:2000],
126
+ projects_summary=raw_projects[:2000],
127
+ hard_skills=raw_skills[:2000],
128
+ soft_skills="",
129
+ k=3
130
+ )
131
+ metiers_reference = self._prepare_metiers_for_prompt(top_metiers)
132
 
133
  def create_task_async(task_key, agent, **kwargs):
134
  t_config = self.tasks_config[task_key].copy()
135
  t_description = t_config["description"]
 
 
136
  try:
 
137
  t_config["description"] = t_description.format(**kwargs)
138
  except KeyError as e:
139
  logger.warning(f"KeyError formatting task '{task_key}': {e}. Falling back to manual replace.")
 
140
  desc = t_description
141
  for k, v in kwargs.items():
142
  placeholder = "{" + k + "}"
 
151
  return (task_key, c.kickoff_async())
152
 
153
  tasks_def = [
154
+ ("skills_task", self.skills_extractor, {"experiences": raw_experiences, "projects": raw_projects, "skills": raw_skills, "education": raw_education}),
155
+ ("experience_task", self.experience_extractor, {"experiences": raw_experiences}),
156
+ ("project_task", self.project_extractor, {"projects": raw_projects}),
157
+ ("education_task", self.education_extractor, {"education": raw_education}),
158
+ ("reconversion_task", self.reconversion_detector, {"experiences": raw_experiences, "education": raw_education}),
159
+ ("language_task", self.language_extractor, {"languages": raw_languages, "cv_raw_start": cv_raw_start[:500]}),
160
+ ("etudiant_task", self.etudiant_detector, {"education": raw_education, "current_date": datetime.now().strftime("%Y-%m-%d")}),
161
+ ("identity_task", self.identity_extractor, {"header": raw_header, "cv_raw_start": cv_raw_start[:1500], "file_name": file_name}),
162
+ ("poste_visé_task", self.header_analyzer, {"header": safe_header, "cv_raw_start": safe_cv_raw}),
163
+ ("cv_quality_task", self.cv_quality_checker, {
164
+ "header": safe_header,
165
+ "page_count": page_count,
166
+ "cv_full_text": cv_full_text[:6000],
167
+ "cv_raw_start": safe_cv_raw,
168
+ "skills": raw_skills[:2000],
169
+ "experiences": raw_experiences[:3000],
170
+ "projects": raw_projects[:2000],
171
+ "education": raw_education[:2000],
172
+ }),
173
+ ("metier_matching_task", self.metier_matcher, {
174
+ "header": safe_header,
175
+ "skills": raw_skills[:2000],
176
+ "experiences": raw_experiences[:3000],
177
+ "projects": raw_projects[:2000],
178
+ "education": raw_education[:2000],
179
+ "metiers_reference": metiers_reference,
180
+ }),
181
+ ("project_analysis_task", self.project_analyzer, {
182
+ "header": safe_header,
183
+ "projects": raw_projects[:3000],
184
+ }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  ]
186
 
187
+ task_coroutines = [create_task_async(key, agent, **kwargs) for key, agent, kwargs in tasks_def]
 
 
188
  keys = [t[0] for t in task_coroutines]
189
  coroutines = [t[1] for t in task_coroutines]
190
  results_list = await asyncio.gather(*coroutines, return_exceptions=True)
 
196
  else:
197
  results_map[key] = result
198
 
199
+ return self._build_final_json(results_map)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
+ def _build_final_json(self, results_map: Dict[str, Any]) -> Dict[str, Any]:
202
+ """Agrège les résultats de toutes les tâches en un JSON final."""
203
+
204
  def get_parsed(key, default=None):
205
  if key not in results_map:
206
  return default
207
  return self._parse_json_output(results_map[key], default)
208
 
209
+ # Extraction
210
  competences = get_parsed("skills_task", {"hard_skills": [], "soft_skills": []})
211
  experiences = get_parsed("experience_task", [])
212
  projets = get_parsed("project_task", {"professional": [], "personal": []})
213
  formations = get_parsed("education_task", [])
214
+ reconversion = get_parsed("reconversion_task", {}).get("reconversion_analysis", {})
 
 
215
  etudiant_data = get_parsed("etudiant_task", {}).get("etudiant_analysis", {})
216
+
217
  latest_end_date = etudiant_data.get("latest_education_end_date")
218
  if latest_end_date:
219
+ etudiant_data["is_etudiant"] = self._is_ongoing_date(latest_end_date)
220
+
221
+ is_en_poste = False
222
+ if isinstance(experiences, list):
223
+ for exp in experiences:
224
+ end_date = exp.get("end_date")
225
+ if isinstance(exp, dict) and end_date:
226
+ if self._is_ongoing_date(end_date):
227
+ is_en_poste = True
228
+ break
229
 
230
  langues_raw = get_parsed("language_task", {})
231
+ identity = get_parsed("identity_task", {})
232
 
233
+ # Nettoyage des doublons dans hard_skills (case-insensitive)
234
  if isinstance(competences, dict):
235
  raw_skills = competences.get("hard_skills", [])
236
  seen = set()
237
  unique_skills = []
238
  for skill in raw_skills:
239
+ key = str(skill).lower() if not isinstance(skill, str) else skill.lower()
 
 
 
 
240
  if key not in seen:
241
  seen.add(key)
242
  unique_skills.append(skill)
243
  competences["hard_skills"] = unique_skills
244
 
245
+ candidat = {
246
+ "first_name": identity.get("first_name") if isinstance(identity, dict) else None,
247
+ "langues": langues_raw.get("langues", []) if isinstance(langues_raw, dict) else [],
248
+ "compétences": competences,
249
+ "expériences": experiences,
250
+ "reconversion": reconversion,
251
+ "projets": projets,
252
+ "formations": formations,
253
+ "etudiant": etudiant_data,
254
+ "is_en_poste": is_en_poste,
 
 
 
 
 
 
 
 
 
 
 
255
  }
256
 
257
+ # Analyse
258
+ header_data = get_parsed("poste_visé_task", {"poste_vise": "Non identifié", "confiance": 0})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  metier_data = get_parsed("metier_matching_task", {"postes_recommandes": []})
260
+ quality_data = get_parsed("cv_quality_task", {"score_global": 0, "red_flags": [], "conseils_prioritaires": []})
 
 
 
261
  project_data = get_parsed("project_analysis_task", {"analyse_projets": []})
262
 
 
263
  conseils = []
264
  if isinstance(quality_data, dict):
265
  conseils.extend(quality_data.get("conseils_prioritaires", []))
266
 
267
+ # Filtre de sécurité : ne garder dans l'analyse de projets que ceux issus de l'extraction
268
+ extracted_titles: set[str] = set()
269
+ for p in (projets.get("professional", []) if isinstance(projets, dict) else []):
270
+ if isinstance(p, dict) and p.get("title"):
271
+ extracted_titles.add(p["title"].strip().lower())
272
+ for p in (projets.get("personal", []) if isinstance(projets, dict) else []):
273
+ if isinstance(p, dict) and p.get("title"):
274
+ extracted_titles.add(p["title"].strip().lower())
275
+
276
+ analyse_projets = project_data.get("analyse_projets", []) if isinstance(project_data, dict) else []
277
+ if extracted_titles and isinstance(analyse_projets, list):
278
+ def _is_extracted_project(titre: str) -> bool:
279
+ t = titre.strip().lower()
280
+ return t in extracted_titles or any(t in ref or ref in t for ref in extracted_titles)
281
+
282
+ analyse_projets = [p for p in analyse_projets if isinstance(p, dict) and _is_extracted_project(p.get("titre", ""))]
283
+
284
+ recommandations = {
285
  "header_analysis": header_data,
286
+ "postes_recommandes": metier_data.get("postes_recommandes", []) if isinstance(metier_data, dict) else [],
287
+ "analyse_poste_vise": metier_data.get("analyse_poste_vise", "") if isinstance(metier_data, dict) else "",
 
 
 
 
 
 
 
 
288
  "qualite_cv": quality_data,
289
+ "analyse_projets": analyse_projets,
290
+ "coherence_globale_projets": project_data.get("coherence_globale", {}) if isinstance(project_data, dict) else {},
 
 
 
 
 
 
 
 
291
  "conseils_amelioration": conseils,
292
  }
293
 
294
+ return {
295
+ "candidat": candidat,
296
+ "recommandations": recommandations
297
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
+ def _prepare_metiers_for_prompt(self, metiers: List[Dict] = None) -> str:
300
+ """Prépare le référentiel métiers restreint pour le prompt."""
301
+ if metiers is None:
302
+ metiers = self.metiers_data
303
+
304
+ flat_list = []
305
+ def _flatten(job_list):
306
+ for job in job_list:
307
+ if "metiers" in job:
308
+ _flatten(job["metiers"])
309
+ elif "id" in job:
310
+ flat_list.append(job)
311
+ _flatten(metiers)
312
+
313
+ lines = []
314
+ for m in flat_list:
315
+ mid = m.get("id", "?")
316
+ nom = m.get("nom", "?")
317
+ cat = m.get("categorie", "?")
318
+ comp = m.get("competences_techniques", [])
319
+ outils = m.get("outils_technologies", [])
320
+ soft = m.get("competences_soft", [])
321
+ niveau = m.get("niveau_etude", "?")
322
+ exp = m.get("experience_requise", "?")
323
+ lines.append(
324
+ f"[{mid}] {nom} ({cat})\n"
325
+ f" Compétences techniques: {', '.join(comp)}\n"
326
+ f" Outils: {', '.join(outils)}\n"
327
+ f" Soft skills: {', '.join(soft[:3])}\n"
328
+ f" Niveau: {niveau} | Expérience: {exp}"
329
+ )
330
+ return "\n\n".join(lines)
331
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
 
 
 
 
 
 
 
 
 
333
 
334
+ # ──────────────────────────────────────────────
335
+ # Utilitaires
336
+ # ──────────────────────────────────────────────
337
 
338
+ def _is_ongoing_date(self, date_str: str) -> bool:
339
+ """Détermine si une date (fin d'étude ou fin d'expérience) est dans le futur ou en cours."""
340
  if not date_str:
341
  return False
342
  date_str = str(date_str).lower().strip()
src/scripts/embed_metiers.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import sys
4
+
5
+ # Add src's parent directory to path so we can run this directly if needed
6
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
7
+
8
+ from langchain_openai import OpenAIEmbeddings
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ def get_job_text(job):
14
+ """
15
+ Formats job info using the "Miroir" strategy.
16
+ """
17
+ missions = ", ".join(job.get("missions_principales", []))
18
+ tech = ", ".join(job.get("competences_techniques", []))
19
+ outils = ", ".join(job.get("outils_technologies", []))
20
+ soft = ", ".join(job.get("competences_soft", []))
21
+
22
+ text = f"MISSIONS: {missions}\n"
23
+ text += f"TECH_ET_OUTILS: {tech}, {outils}\n"
24
+ text += f"SOFT_SKILLS: {soft}"
25
+ return text
26
+
27
+ def embed_metiers_file():
28
+ base_path = os.path.dirname(os.path.dirname(__file__))
29
+ metiers_path = os.path.join(base_path, "data", "metiers.json")
30
+
31
+ print(f"Loading {metiers_path}...")
32
+ with open(metiers_path, "r", encoding="utf-8") as f:
33
+ data = json.load(f)
34
+
35
+ embeddings_model = OpenAIEmbeddings(model="text-embedding-3-small")
36
+
37
+ count = 0
38
+ def process_jobs(job_list):
39
+ nonlocal count
40
+ for job in job_list:
41
+ if "metiers" in job:
42
+ process_jobs(job["metiers"])
43
+ elif "id" in job:
44
+ print(f"Embedding {job.get('id')}...")
45
+ text = get_job_text(job)
46
+ emb = embeddings_model.embed_query(text)
47
+ job["embedding"] = emb
48
+ count += 1
49
+
50
+ process_jobs(data.get("metiers", []))
51
+
52
+ print(f"Writing {count} embeddings to {metiers_path}...")
53
+ with open(metiers_path, "w", encoding="utf-8") as f:
54
+ json.dump(data, f, ensure_ascii=False, indent=2)
55
+
56
+ print("Done!")
57
+
58
+ if __name__ == "__main__":
59
+ embed_metiers_file()
src/services/cv_service.py CHANGED
@@ -1,20 +1,16 @@
1
  """
2
  Service de parsing et analyse de CV enrichi.
3
- Pipeline optimisé :
4
- Phase 1 : Découpage en sections
5
- Phase 2 : Extraction parallèle (8 agents) — en // avec Phase 3a
6
- Phase 3a : Analyse d'en-tête (header_analyzer) — en // avec Phase 2
7
- Phase 3b : Analyse & Recommandation (3 agents parallèles)
8
 
9
- Flux : Phase 1 asyncio.gather(Phase 2, Phase 3a) → Phase 3b
10
- Gain estimé : ~5-8 secondes vs pipeline séquentiel précédent.
11
  """
12
 
13
- import asyncio
14
  import logging
15
  from typing import Dict, Any
16
 
17
- from src.config.app_config import load_pdf, load_pdf_first_page_text
18
  from src.parser_flow.CV_agent_flow import CVAgentOrchestrator
19
 
20
  logger = logging.getLogger(__name__)
@@ -22,65 +18,28 @@ logger = logging.getLogger(__name__)
22
 
23
  async def parse_cv(pdf_path: str, file_name: str = "") -> Dict[str, Any]:
24
  """
25
- Parse un CV avec extraction + analyse complète.
26
 
27
  Retourne un JSON en 2 parties :
28
- - candidat : données parsées du CV (identité, compétences, expériences, projets)
29
- - recommandations : analyse critique, matching métiers, qualité CV, header_analysis (poste_vise)
30
  """
31
  orchestrator = CVAgentOrchestrator()
32
-
33
- # Double extraction PDF :
34
- # - cv_text : Markdown (bon pour la structure des sections)
35
- # - cv_raw_start : texte brut ordonné par position (fiable pour le header/nom/titre)
36
  cv_text = load_pdf(pdf_path)
37
  cv_raw_start = load_pdf_first_page_text(pdf_path)
 
38
 
39
- # ── Phase 1 : Découpage du CV en sections (séquentielle, nécessaire pour la suite) ──
40
  logger.info("Phase 1 : Découpage du CV en sections...")
41
  sections = await orchestrator.split_cv_sections(cv_text, cv_raw_start=cv_raw_start)
42
 
43
- # ── Phase 2 + Phase 3a en PARALLÈLE ──────────────────────────────────────────────────
44
- # Phase 2 : 8 agents d'extraction (skills, expériences, projets, etc.)
45
- # Phase 3a : header_analyzer (poste visé) — ne dépend que de sections + cv_raw_start
46
- logger.info("Phase 2 + Phase 3a : Extraction et analyse d'en-tête en parallèle...")
47
- extraction, header_data = await asyncio.gather(
48
- orchestrator.extract_all_sections(
49
- sections, cv_raw_start=cv_raw_start, file_name=file_name
50
- ),
51
- orchestrator.run_header_analysis(
52
- sections, cv_raw_start=cv_raw_start, cv_full_text=cv_text
53
- ),
54
- )
55
-
56
- # ── Phase 3b : 3 agents d'analyse en parallèle ───────────────────────────────────────
57
- logger.info("Phase 3b : Analyse et recommandation...")
58
- recommendations = await orchestrator.analyze_and_recommend(
59
- cv_full_text=cv_text,
60
- sections=sections,
61
- extraction=extraction,
62
  cv_raw_start=cv_raw_start,
63
- header_data=header_data,
 
 
64
  )
65
 
66
- candidat_raw = extraction.get("candidat", {})
67
-
68
- # Assemblage ordonné : identité → langues → compétences → parcours
69
- candidat = {
70
- "first_name": candidat_raw.get("first_name"),
71
- "langues": candidat_raw.get("langues", []),
72
- "compétences": candidat_raw.get("compétences", {}),
73
- "expériences": candidat_raw.get("expériences", []),
74
- "projets": candidat_raw.get("projets", {}),
75
- "formations": candidat_raw.get("formations", []),
76
- "etudiant": candidat_raw.get("etudiant", {}),
77
- "reconversion": candidat_raw.get("reconversion", {}),
78
- }
79
-
80
- result = {
81
- "candidat": candidat,
82
- "recommandations": recommendations,
83
- }
84
-
85
  logger.info("Parsing et analyse terminés.")
86
  return result
 
1
  """
2
  Service de parsing et analyse de CV enrichi.
3
+ Pipeline ultra-optimisé :
4
+ Phase 1 : Découpage en sections (cv_splitter)
5
+ Phase 2 : Extraction et Analyse en STRICT PARALLÈLE (11 agents)
 
 
6
 
7
+ Gain estimé : Temps de traitement grandement réduit.
 
8
  """
9
 
 
10
  import logging
11
  from typing import Dict, Any
12
 
13
+ from src.config.app_config import load_pdf, load_pdf_first_page_text, get_pdf_page_count
14
  from src.parser_flow.CV_agent_flow import CVAgentOrchestrator
15
 
16
  logger = logging.getLogger(__name__)
 
18
 
19
  async def parse_cv(pdf_path: str, file_name: str = "") -> Dict[str, Any]:
20
  """
21
+ Parse un CV avec extraction + analyse complète en 2 phases.
22
 
23
  Retourne un JSON en 2 parties :
24
+ - candidat : données parsées du CV (identité, compétences, expériences, projets...)
25
+ - recommandations : analyse critique (7 critères JSON), matching métiers, qualité CV
26
  """
27
  orchestrator = CVAgentOrchestrator()
 
 
 
 
28
  cv_text = load_pdf(pdf_path)
29
  cv_raw_start = load_pdf_first_page_text(pdf_path)
30
+ page_count = get_pdf_page_count(pdf_path)
31
 
 
32
  logger.info("Phase 1 : Découpage du CV en sections...")
33
  sections = await orchestrator.split_cv_sections(cv_text, cv_raw_start=cv_raw_start)
34
 
35
+ logger.info("Phase 2 : Extraction et Analyse en strict parallèle...")
36
+ result = await orchestrator.run_all_agents(
37
+ sections,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  cv_raw_start=cv_raw_start,
39
+ cv_full_text=cv_text,
40
+ file_name=file_name,
41
+ page_count=page_count
42
  )
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  logger.info("Parsing et analyse terminés.")
45
  return result
src/services/metier_pre_filter.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import logging
3
+ from typing import Dict, List
4
+ from langchain_openai import OpenAIEmbeddings
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ def compute_cosine_similarity(vec1: List[float], vec2: List[float]) -> float:
9
+ """Computes cosine similarity between two vectors without relying on numpy."""
10
+ numerator = sum(a * b for a, b in zip(vec1, vec2))
11
+ norm1 = math.sqrt(sum(a * a for a in vec1))
12
+ norm2 = math.sqrt(sum(b * b for b in vec2))
13
+ if norm1 == 0 or norm2 == 0:
14
+ return 0.0
15
+ return numerator / (norm1 * norm2)
16
+
17
+ def get_top_k_metiers(
18
+ metiers_data: List[Dict],
19
+ experiences_summary: str,
20
+ projects_summary: str,
21
+ hard_skills: str,
22
+ soft_skills: str,
23
+ k: int = 3
24
+ ) -> List[Dict]:
25
+ """
26
+ Filters the job profiles (métiers) by cosine similarity to the candidate's profile.
27
+ Returns the top K job profiles.
28
+ """
29
+ # flatten list to extract nested metiers from the JSON dataset
30
+ flat_list = []
31
+ def _flatten(job_list):
32
+ for job in job_list:
33
+ if "metiers" in job:
34
+ _flatten(job["metiers"])
35
+ elif "id" in job:
36
+ flat_list.append(job)
37
+
38
+ _flatten(metiers_data)
39
+
40
+ try:
41
+ if not flat_list:
42
+ return []
43
+
44
+ embeddings_model = OpenAIEmbeddings(model="text-embedding-3-small")
45
+
46
+ # Stratégie Miroir
47
+ text = f"EXPERIENCES_ET_ACTIONS: {experiences_summary}\n"
48
+ text += f"PROJETS_ET_OUTCOMES: {projects_summary}\n"
49
+ text += f"COMPETENCES_TECH_ET_SOFT: {hard_skills}, {soft_skills}"
50
+
51
+ candidat_emb = embeddings_model.embed_query(text)
52
+
53
+ scored_metiers = []
54
+ for job in flat_list:
55
+ # En cas de manque d'embedding, on met un score de 0
56
+ if "embedding" in job and job["embedding"]:
57
+ sim = compute_cosine_similarity(candidat_emb, job["embedding"])
58
+ else:
59
+ sim = 0.0
60
+ scored_metiers.append((sim, job))
61
+
62
+ # Sort by similarity in descending order
63
+ scored_metiers.sort(key=lambda x: x[0], reverse=True)
64
+ top_k = [item[1] for item in scored_metiers[:k]]
65
+
66
+ logger.info(f"Top {k} métiers sélectionnés par embedding: {[m.get('id') for m in top_k]}")
67
+ return top_k
68
+
69
+ except Exception as e:
70
+ logger.error(f"Erreur lors du pré-filtrage des métiers: {e}", exc_info=True)
71
+ # En cas d'erreur de clé d'API ou autre, on renvoie une liste par défaut (fallback)
72
+ return flat_list[:k]
test_api.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import traceback
4
+ from src.services.cv_service import parse_cv
5
+
6
+ async def main():
7
+ pdf_path = "c:\\Users\\quent\\Documents\\Projets\\devellopement_AIRH - Copie\\CV - Quentin Loumeau - 2026.pdf"
8
+ print(f"Testing CV Parser with file: {pdf_path}")
9
+
10
+ try:
11
+ # Define minimum metier data for the metier_matching_task
12
+ metiers_reference = {
13
+ "chef_projet_data_ia": {
14
+ "nom": "Chef de Projet Data / IA",
15
+ "competences": ["Python", "SQL", "Gestion de projet", "IA", "Machine Learning"]
16
+ }
17
+ }
18
+
19
+ result = await parse_cv(pdf_path, "Chef de Projet Data / IA")
20
+ print("Success! Output saved to test_result.json")
21
+ with open("test_result.json", "w", encoding="utf-8") as f:
22
+ json.dump(result, f, indent=2, ensure_ascii=False)
23
+
24
+ except Exception as e:
25
+ print(f"Error occurred: {e}")
26
+ traceback.print_exc()
27
+
28
+ if __name__ == "__main__":
29
+ asyncio.run(main())
test_result.json ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "candidat": {
3
+ "first_name": "Quentin",
4
+ "langues": [
5
+ {
6
+ "langue": "Français",
7
+ "niveau": "Natif"
8
+ },
9
+ {
10
+ "langue": "Anglais",
11
+ "niveau": "Courant (C1)"
12
+ },
13
+ {
14
+ "langue": "Portugais",
15
+ "niveau": "Courant"
16
+ }
17
+ ],
18
+ "compétences": {
19
+ "hard_skills": [
20
+ "Python",
21
+ "SQL",
22
+ "Excel",
23
+ "React.js",
24
+ "FastAPI",
25
+ "LangChain",
26
+ "LangGraph",
27
+ "Scikit-learn",
28
+ "RAG",
29
+ "Pandas",
30
+ "PySpark",
31
+ "DBT",
32
+ "Dataiku",
33
+ "PostgreSQL",
34
+ "MongoDB",
35
+ "MinIO",
36
+ "Pinecone",
37
+ "Metabase",
38
+ "PowerBI",
39
+ "Mage.ai",
40
+ "AWS",
41
+ "Docker",
42
+ "Git",
43
+ "CI/CD",
44
+ "Selenium",
45
+ "BeautifulSoup4",
46
+ "N8n",
47
+ "Langflow",
48
+ "Gumloop"
49
+ ],
50
+ "soft_skills": [
51
+ "Leadership",
52
+ "Communication",
53
+ "Adaptation",
54
+ "Rigueur"
55
+ ],
56
+ "skills_with_context": [
57
+ {
58
+ "skill": "Python",
59
+ "context": "projet, académique"
60
+ },
61
+ {
62
+ "skill": "SQL",
63
+ "context": "académique"
64
+ },
65
+ {
66
+ "skill": "Excel",
67
+ "context": "sans contexte"
68
+ },
69
+ {
70
+ "skill": "React.js",
71
+ "context": "projet"
72
+ },
73
+ {
74
+ "skill": "FastAPI",
75
+ "context": "projet"
76
+ },
77
+ {
78
+ "skill": "LangChain",
79
+ "context": "projet"
80
+ },
81
+ {
82
+ "skill": "LangGraph",
83
+ "context": "projet"
84
+ },
85
+ {
86
+ "skill": "Scikit-learn",
87
+ "context": "académique"
88
+ },
89
+ {
90
+ "skill": "RAG",
91
+ "context": "expérience, projet"
92
+ },
93
+ {
94
+ "skill": "Pandas",
95
+ "context": "académique"
96
+ },
97
+ {
98
+ "skill": "PySpark",
99
+ "context": "certification"
100
+ },
101
+ {
102
+ "skill": "DBT",
103
+ "context": "certification"
104
+ },
105
+ {
106
+ "skill": "Dataiku",
107
+ "context": "expérience"
108
+ },
109
+ {
110
+ "skill": "PostgreSQL",
111
+ "context": "projet"
112
+ },
113
+ {
114
+ "skill": "MongoDB",
115
+ "context": "projet"
116
+ },
117
+ {
118
+ "skill": "MinIO",
119
+ "context": "projet"
120
+ },
121
+ {
122
+ "skill": "Pinecone",
123
+ "context": "projet"
124
+ },
125
+ {
126
+ "skill": "Metabase",
127
+ "context": "projet"
128
+ },
129
+ {
130
+ "skill": "PowerBI",
131
+ "context": "expérience, académique"
132
+ },
133
+ {
134
+ "skill": "Mage.ai",
135
+ "context": "projet"
136
+ },
137
+ {
138
+ "skill": "AWS",
139
+ "context": "sans contexte"
140
+ },
141
+ {
142
+ "skill": "Docker",
143
+ "context": "sans contexte"
144
+ },
145
+ {
146
+ "skill": "Git",
147
+ "context": "sans contexte"
148
+ },
149
+ {
150
+ "skill": "CI/CD",
151
+ "context": "sans contexte"
152
+ },
153
+ {
154
+ "skill": "Selenium",
155
+ "context": "projet"
156
+ },
157
+ {
158
+ "skill": "BeautifulSoup4",
159
+ "context": "sans contexte"
160
+ },
161
+ {
162
+ "skill": "N8n",
163
+ "context": "sans contexte"
164
+ },
165
+ {
166
+ "skill": "Langflow",
167
+ "context": "sans contexte"
168
+ },
169
+ {
170
+ "skill": "Gumloop",
171
+ "context": "sans contexte"
172
+ }
173
+ ]
174
+ },
175
+ "expériences": [
176
+ {
177
+ "Poste": "Alternant Data Analyste / Chef de Projet IA",
178
+ "Entreprise": "Enedis - Direction Audit Contrôle Interne Risques (DACIR)",
179
+ "start_date": "Déc. 2024",
180
+ "end_date": "Déc. 2025",
181
+ "responsabilités": [
182
+ "Développement de 5 tableaux de bords PowerBI à destination des 150 collaborateurs du contrôle interne",
183
+ "Paramétrage d'une solution RAG en test pour le Risk manager, ingestion de 40 documents techniques, travail du prompt et du flow agentique, évaluation du MRR",
184
+ "Refactorisation et optimisation de 10 flows dataiku de traitement des données d’analyses, réduction de 30% des étapes de traitement et création de documentation des process",
185
+ "Cadrage des besoins d'analyse de fraude et d'anomalies sur 6 projets majeurs, en animant des réunions avec une dizaine de collaborateurs",
186
+ "Animation d’ateliers de montée en compétence data mining auprès de 150 auditeurs"
187
+ ]
188
+ },
189
+ {
190
+ "Poste": "Pâtissier - Manager & Responsable Production",
191
+ "Entreprise": "Parcours international (UK, Suède, Australie)",
192
+ "start_date": "2009",
193
+ "end_date": "2023",
194
+ "responsabilités": [
195
+ "14 ans d’expérience : gestion d’équipes (jusqu’à 8 pers.), planification de production, relation client",
196
+ "Optimisation des processus de production, réduisant les pertes matières de ~20%",
197
+ "3 pays, 3 cultures : adaptation, communication en anglais au quotidien",
198
+ "Analyse des ventes et gestion budgétaire : suivi des marges, adaptation de la production en fonction des ventes"
199
+ ]
200
+ }
201
+ ],
202
+ "reconversion": {
203
+ "is_reconversion": true,
204
+ "context": "Le candidat a effectué une reconversion professionnelle en passant du domaine de la pâtisserie, où il a travaillé comme Manager & Responsable Production pendant 14 ans, à un domaine technologique en tant que Data Analyste et Chef de Projet IA. Il a suivi une formation en Data Analyst de février 2024 à juillet 2024, puis a poursuivi avec une formation Chef de Projet IA de novembre 2024 à décembre 2025.",
205
+ "date_reconversion": "2024-02"
206
+ },
207
+ "projets": {
208
+ "professional": [],
209
+ "personal": [
210
+ {
211
+ "title": "AIRH - Plateforme SaaS de recrutement augmenté (système multi-agents)",
212
+ "technologies": [
213
+ "React.js",
214
+ "FastAPI",
215
+ "CrewAI",
216
+ "LangGraph",
217
+ "PostgreSQL",
218
+ "MongoDB",
219
+ "MinIO",
220
+ "Metabase",
221
+ "Mage.ai"
222
+ ],
223
+ "outcomes": [
224
+ "Architecture d'un système multi-agents orchestrant l'analyse automatisée de CV et la simulation d'entretien dynamique",
225
+ "Moteur de scoring multi-dimensionnel avec détection de fraude automatisée",
226
+ "Pipeline de données complet orchestré avec Mage.ai",
227
+ "Pilotage de A à Z : Design Thinking, business plan, développement full-stack, déploiement"
228
+ ],
229
+ "domaine metier": "recrutement/RH"
230
+ },
231
+ {
232
+ "title": "FounderOS - Dashboard tout-en-un pour fondateurs de startups",
233
+ "technologies": [
234
+ "Next.js",
235
+ "TypeScript",
236
+ "Tailwind",
237
+ "API Gemini"
238
+ ],
239
+ "outcomes": [
240
+ "Outil complet : Lean Canvas, CRM Lite, veille stratégique, gestion financière, pipeline de contenu",
241
+ "Module d'intelligence concurrentielle avec scoring automatisé"
242
+ ],
243
+ "domaine metier": "entrepreneuriat/startups"
244
+ },
245
+ {
246
+ "title": "Data Jobs - Moteur de recommandation d'emploi RAG",
247
+ "technologies": [
248
+ "Python",
249
+ "LangChain",
250
+ "Pinecone",
251
+ "Selenium",
252
+ "API Groq"
253
+ ],
254
+ "outcomes": [
255
+ "Scraping et vectorisation de +2 000 offres d'emploi tech",
256
+ "Pertinence des recommandations améliorée de +40% via Prompt Engineering iteratif",
257
+ "Pilotage des sprints en tant que Scrum Master"
258
+ ],
259
+ "domaine metier": "recrutement/RH"
260
+ }
261
+ ]
262
+ },
263
+ "formations": [
264
+ {
265
+ "degree": "Chef de Projet IA - RNCP Niveau 7 (Bac+5)",
266
+ "institution": "Wild Code School",
267
+ "start_date": "Nov. 2024",
268
+ "end_date": "Déc. 2025"
269
+ },
270
+ {
271
+ "degree": "Data Analyst - RNCP Niveau 6 (Bac+3)",
272
+ "institution": "Wild Code School",
273
+ "start_date": "Fév. 2024",
274
+ "end_date": "Juil. 2024"
275
+ }
276
+ ],
277
+ "etudiant": {
278
+ "is_etudiant": false,
279
+ "niveau_etudes": "bac+5",
280
+ "specialite": "Chef de Projet IA",
281
+ "latest_education_end_date": "2025-12-31"
282
+ },
283
+ "is_en_poste": false
284
+ },
285
+ "recommandations": {
286
+ "header_analysis": {
287
+ "poste_vise": "Chef de Projet Data / IA - Spécialiste Systèmes Multi-Agents & Recrutement Tech",
288
+ "confiance": 90
289
+ },
290
+ "postes_recommandes": [
291
+ {
292
+ "metier_id": "data_analyst",
293
+ "nom": "Data Analyst",
294
+ "categorie": "Noyau data & analytique",
295
+ "score_matching": 88,
296
+ "detail_scores": {
297
+ "competences_techniques": 85,
298
+ "outils_technologies": 90,
299
+ "experiences_projets": 85,
300
+ "methodologies": 80
301
+ },
302
+ "competences_matchees": [
303
+ "SQL",
304
+ "Python",
305
+ "Power BI",
306
+ "Metabase"
307
+ ],
308
+ "competences_manquantes": [
309
+ "Looker",
310
+ "Qlik"
311
+ ],
312
+ "justification": "Le profil couvre 88% des compétences requises grâce à une solide maîtrise de SQL et Python, ainsi qu'une expérience significative avec Power BI et Metabase. Les méthodologies Agile et Scrum sont bien intégrées, ce qui est crucial pour ce rôle."
313
+ },
314
+ {
315
+ "metier_id": "data_engineer",
316
+ "nom": "Data Engineer",
317
+ "categorie": "Ingénierie data & IA",
318
+ "score_matching": 82,
319
+ "detail_scores": {
320
+ "competences_techniques": 80,
321
+ "outils_technologies": 85,
322
+ "experiences_projets": 80,
323
+ "methodologies": 75
324
+ },
325
+ "competences_matchees": [
326
+ "Python",
327
+ "SQL",
328
+ "PySpark",
329
+ "DBT",
330
+ "AWS",
331
+ "Docker"
332
+ ],
333
+ "competences_manquantes": [
334
+ "Apache Kafka",
335
+ "Flink"
336
+ ],
337
+ "justification": "Le candidat possède une bonne maîtrise des outils de transformation et de cloud, ainsi qu'une expérience en DevOps, ce qui est essentiel pour un Data Engineer. Cependant, l'expérience avec des outils de streaming comme Kafka est limitée."
338
+ },
339
+ {
340
+ "metier_id": "data_scientist",
341
+ "nom": "Data Scientist",
342
+ "categorie": "Noyau data & analytique",
343
+ "score_matching": 80,
344
+ "detail_scores": {
345
+ "competences_techniques": 75,
346
+ "outils_technologies": 80,
347
+ "experiences_projets": 85,
348
+ "methodologies": 70
349
+ },
350
+ "competences_matchees": [
351
+ "Python",
352
+ "Scikit-learn",
353
+ "LangChain",
354
+ "RAG"
355
+ ],
356
+ "competences_manquantes": [
357
+ "Deep learning",
358
+ "Spark"
359
+ ],
360
+ "justification": "Le profil est bien aligné avec les compétences en machine learning et en IA, notamment avec l'utilisation de LangChain et Scikit-learn. Cependant, une expérience plus approfondie en deep learning et Spark serait bénéfique."
361
+ }
362
+ ],
363
+ "analyse_poste_vise": "Le poste de Data Analyst est bien aligné avec le profil du candidat, qui possède une forte expérience en SQL, Python, et Power BI, ainsi qu'une bonne compréhension des méthodologies Agile et Scrum. Les compétences manquantes comme Looker et Qlik peuvent être acquises rapidement grâce à l'expérience existante avec d'autres outils BI.",
364
+ "qualite_cv": {
365
+ "score_global": 76,
366
+ "compatibilite_ats": {
367
+ "score": 83,
368
+ "details": "Sections principales standard et bien nommées : « EXPERIENCE PROFESSIONNELLE », « PROJETS CLES », « FORMATION », « STACK TECHNIQUE », « LANGUES ». Bon point ATS. Les mots-clés techniques sont majoritairement contextualisés via les projets (AIRH/FounderOS/Data Jobs) et l’alternance (PowerBI, Dataiku, RAG). Points perfectibles : 1) « STACK TECHNIQUE » est OK mais certains ATS attendent « COMPETENCES » (mineur). 2) Présence de sous-intitulés atypiques « de base », « Transformation », « BI » qui peuvent être moins bien parsés selon ATS (mieux en listes/catégories simples). 3) Des outils très nombreux (Langflow, Gumloop, N8n, Langfuse…) peuvent être perçus comme empilement si non reliés à une expérience/projet précis (à relier explicitement). 4) Les liens sont présents mais libellés (LinkedIn / AIRH / FounderOS) sans URL en clair dans le texte brut : certains ATS ne cliquent pas/extraient mal les ancres (préférer afficher l’URL en toutes lettres)."
369
+ },
370
+ "quantification_resultats": {
371
+ "score": 62,
372
+ "details": "Bon niveau pour un profil en reconversion/early-confirmé Data : plusieurs chiffres concrets existent (5 dashboards, 150 collaborateurs, 40 documents, 10 flows Dataiku, -30% d’étapes, 6 projets, +2000 offres, +40% pertinence, 7 agents, équipe jusqu’à 8, -20% pertes matière). En revanche, il manque des métriques techniques ‘2025’ attendues côté Data/IA/Produit : latence/temps de réponse, volumétrie (Go, lignes, requêtes/jour), coûts, performance des pipelines, qualité modèle (précision, recall), temps de déploiement, monitoring, couverture de tests, SLA. Sur Enedis et AIRH, les résultats restent partiellement orientés ‘activité’ plutôt que ‘impact’ (gain de temps, adoption, fiabilité, incidents évités).",
373
+ "metriques_suggerees": [
374
+ "Enedis/PowerBI : temps moyen de chargement des dashboards (ex. -40% vs avant), nombre de vues mensuelles, taux d’adoption (% des 150 utilisateurs actifs), réduction du temps de production de reporting (heures/sem).",
375
+ "Enedis/RAG : taux de réponses utiles (ex. % évalué), précision@k / recall@k, taux d’hallucination, temps moyen de réponse (ms/s), coût par requête (€, tokens), nombre de requêtes/semaine en test.",
376
+ "Enedis/Dataiku : volume de données traité par flow (Go/jour, nb de tables), durée d’exécution avant/après (ex. 45 min → 28 min), taux d’échec des jobs (incidents/mois), dette technique réduite (nb de recettes/variables standardisées).",
377
+ "AIRH : temps de parsing CV (ms), throughput (CV/min), coût d’inférence par entretien simulé, latence end-to-end multi-agents, taux de succès des runs (sans erreurs), nombre de candidats/recruteurs beta testeurs.",
378
+ "AIRH : métriques qualité scoring (corrélation avec décisions humaines, calibration, taux de faux positifs ‘fraude’), couverture de tests backend (%), temps de déploiement (CI/CD) et fréquence de release.",
379
+ "Data Jobs : NDCG@k / MRR (si déjà mesuré), taux de clic ou satisfaction utilisateur, temps de vectorisation (2k offres en X min), coût Pinecone (€/mois) et optimisation (index, dimension, batch).",
380
+ "FounderOS : performances web (LCP, TTFB), taille bundle, temps de build/deploy, nombre d’utilisateurs actifs, réduction du churn ou du temps de préparation (Lean Canvas, veille) via l’outil."
381
+ ]
382
+ },
383
+ "structure_lisibilite": {
384
+ "score": 78,
385
+ "details": "CV 1 page : conforme et très bon pour un profil en montée en compétence Data. Poste visé clairement indiqué dès l’en-tête (« Chef de Projet Data / IA ») : bon signal. Chronologie : expériences en ordre globalement inverse côté Data (2024-2025 puis 2009-2023) OK ; formations listées 2024-2025 puis 2024 OK. Point à améliorer : la section « LIENS » et « STACK TECHNIQUE » apparaissent aussi dans le texte brut au milieu des expériences (probable effet de mise en page/colonnes) : risque réel de parsing ATS et de lecture recruteur (les liens/skills semblent ‘couper’ l’expérience). Il faut sécuriser une structure linéaire : En-tête > Compétences > Expériences > Projets > Formation (ou Projets avant Expériences si objectif produit).",
386
+ "structuration_competences": "Compétences bien catégorisées (Langages/Frameworks, IA/ML, Analytics & Data Engineering, DevOps & Automatisation, Méthodologies). Recommandation : renommer « STACK TECHNIQUE » en « COMPETENCES TECHNIQUES » et éviter des micro-sous-blocs (« de base ») au profit de catégories ATS plus standards."
387
+ },
388
+ "presentation_projets": {
389
+ "score": 81,
390
+ "details": "Projets bien présentés : chaque projet a un nom, une stack, et des éléments concrets (AIRH : 7 agents, pipeline complet, scoring pondéré ; Data Jobs : +2000 offres, +40% pertinence ; FounderOS : modules identifiés). Très pertinent pour le poste visé (Data/IA + produit). À renforcer : ajouter systématiquement 1) le rôle exact (solo vs équipe), 2) l’état (MVP en prod ? beta ? utilisateurs ?), 3) des métriques d’exploitation (latence, coût, usage), 4) un lien direct par projet vers repo/démo (URL en clair) pour preuve immédiate. Sur AIRH, détailler les choix d’architecture (monolithe vs microservices, orchestrateur, storage, stratégie d’indexation vecteur, observabilité) et les compromis."
391
+ },
392
+ "preuves_competences": {
393
+ "score": 74,
394
+ "details": "Bon niveau de ‘preuves’ car beaucoup de compétences sont démontrées dans expériences/projets : PowerBI et Dataiku (Enedis), RAG/LangChain/Pinecone/Selenium (Data Jobs), React/FastAPI/PostgreSQL/MongoDB/MinIO/Metabase/Mage.ai (AIRH), Next.js/TypeScript/Tailwind/Gemini (FounderOS), Scrum Master mentionné (Data Jobs), management (pâtissier manager) utile pour Chef de Projet. Points faibles : plusieurs outils listés en compétences ne sont pas explicitement rattachés à un projet/expérience (risque de “skills shelf”). Aussi, AWS/Docker/Git/CI/CD sont cités mais sans exemple concret de pipeline, déploiement, registry, infra as code, etc.",
395
+ "skills_sans_preuve": [
396
+ "PySpark",
397
+ "DBT",
398
+ "AWS",
399
+ "Docker",
400
+ "CI/CD",
401
+ "BeautifulSoup4 (BS4)",
402
+ "N8n",
403
+ "Langflow",
404
+ "Gumloop",
405
+ "Langfuse",
406
+ "Design Thinking (usage mentionné mais sans livrables/atelier concret hors AIRH)",
407
+ "RGAA (mention conformité mais pas d’action vérifiable : audit, critères appliqués, corrections)"
408
+ ],
409
+ "liens_detectes": [
410
+ "https://www.linkedin.com/in/loumeau-quentin/",
411
+ "https://www.airh.online/",
412
+ "https://founderdashboard.vercel.app/"
413
+ ]
414
+ },
415
+ "red_flags": [
416
+ "Compétences listées sans preuve explicite dans expériences/projets : PySpark, DBT, AWS, Docker, CI/CD, BS4, N8n, Langflow, Gumloop, Langfuse, RGAA (à contextualiser ou retirer).",
417
+ "Risque de parsing/lecture : dans le texte brut, les sections LIENS/STACK TECHNIQUE semblent interrompre la section EXPÉRIENCE (probable mise en page en colonnes)."
418
+ ],
419
+ "points_forts": [
420
+ "Positionnement clair et différenciant (Chef de Projet Data/IA + multi-agents + recrutement tech).",
421
+ "Projets très pertinents et actuels (RAG, multi-agents, vector DB, scraping, full-stack).",
422
+ "Déjà plusieurs quantifications utiles (utilisateurs, volumes, gains, nombre de flows, +40% pertinence).",
423
+ "Valorisation cohérente de la reconversion avec compétences transférables (management, optimisation, international, animation d’ateliers).",
424
+ "Liens vers LinkedIn et projets live présents (preuve produit)."
425
+ ],
426
+ "conseils_prioritaires": [
427
+ "Sécuriser la lisibilité ATS : sortir d’une mise en page en colonnes et garantir un flux linéaire (les sections LIENS/COMPÉTENCES ne doivent pas apparaître au milieu de l’EXPERIENCE dans l’extraction texte). Si vous gardez les ancres, ajoutez aussi les URL en clair (ex. linkedin.com/in/… ; airh.online ; founderdashboard.vercel.app).",
428
+ "Ajouter 6–10 métriques techniques ‘signature’ sur Enedis + AIRH : temps d’exécution avant/après des flows Dataiku, volume de données, temps de réponse RAG, coût par requête, adoption (utilisateurs actifs), fiabilité (taux d’échec), et côté AIRH latence multi-agents + coût inference + taux de succès des runs.",
429
+ "Nettoyer la section compétences : soit retirer les outils non prouvés, soit ajouter une ligne de preuve par outil (ex. ‘DBT : modèle X, 12 tests, docs’, ‘Docker : image FastAPI, docker-compose’, ‘CI/CD : GitHub Actions avec lint+tests+deploy’). Objectif : zéro skill “hors contexte”.",
430
+ "Sur AIRH, expliciter 3–5 décisions d’architecture et leurs compromis (ex. choix Postgres vs Mongo, stockage MinIO, orchestration Mage.ai, stratégie d’indexation/search, observabilité/trace Langfuse) + ce que vous feriez différemment à l’échelle (10k CV/jour, multi-tenant, files/queues)."
431
+ ],
432
+ "adaptation_seniorite": "Profil ‘reconversion vers Data/IA’ avec expérience pro longue mais expérience tech récente (alternance 2024–2025 + projets). Attentes calibrées : comme un junior/confirmé côté Data/IA, les projets sont un atout majeur et sont bien exploités. Pour viser ‘Chef de Projet Data/IA’ (confirmé), il manque encore des marqueurs d’impact mesurable (SLA, gains de temps/coûts, adoption, qualité IA) et des preuves de delivery (déploiement, run, monitoring, CI/CD). La partie compétences transférables (management, optimisation, animation) est bien présente ; elle gagnerait à être reliée à des livrables tech/projet (roadmap, RACI, KPI, gestion des risques, conduite du changement sur les 150 utilisateurs)."
433
+ },
434
+ "analyse_projets": [
435
+ {
436
+ "titre": "AIRH - Plateforme SaaS de recrutement augmenté (système multi-agents)",
437
+ "resume": "SaaS / Projet live de recrutement augmenté : multi-agents (7) pour analyse CV + simulation d’entretien + pipeline data end-to-end.",
438
+ "evaluation": {
439
+ "pertinence": {
440
+ "score": 10,
441
+ "justification": "Cœur du poste visé: 'système multi-agents (7 agents)', 'recrutement augmenté', scoring candidats, et pipeline data (ingestion→monitoring)."
442
+ },
443
+ "complexite": {
444
+ "score": 8,
445
+ "justification": "Architecture multi-agents annoncée (CrewAI + LangGraph) + pipeline complet (parsing, stockage objet MinIO, PostgreSQL+MongoDB, orchestration Mage.ai). Pas de détails sur patterns de résilience, gestion d’état, évaluation offline/online."
446
+ },
447
+ "stack": {
448
+ "score": 8,
449
+ "justification": "Stack cohérente SaaS data/IA: React, FastAPI, PostgreSQL/MongoDB, MinIO, Metabase, Mage.ai, + frameworks agents. L’ensemble est moderne mais assez large (double DB relationnel/NoSQL + orchestration + observabilité) sans justification explicite."
450
+ },
451
+ "innovation": {
452
+ "score": 8,
453
+ "justification": "Au-delà d’un wrapper RAG: scoring multi-dimensionnel pondéré (40/30/20/10) + 'détection de fraude automatisée' + 'simulation d’entretien dynamique'. Innovation plausible, mais le CV ne décrit pas la méthode de fraude (règles, ML, signaux) ni l’évaluation."
454
+ },
455
+ "impact": {
456
+ "score": 5,
457
+ "justification": "Aucun KPI chiffré (temps de recrutement, qualité shortlist, taux de conversion, coûts/latence). Projet 'live' mentionné, mais sans métriques d’adoption/usage."
458
+ },
459
+ "ownership": {
460
+ "score": 10,
461
+ "justification": "Ownership explicitement end-to-end: 'Pilotage de A à Z : Design Thinking, business plan, développement full-stack, déploiement' + mémoire de fin d’études sur le sujet."
462
+ },
463
+ "maturite": {
464
+ "score": 6,
465
+ "justification": "Mention de monitoring qualité via Metabase + orchestration Mage.ai. En revanche: pas de preuve de tests, CI/CD, sécurité, RGPD, gestion des prompts/versions, observabilité LLM (traces, coûts), SLA/latence, ou scalabilité."
466
+ }
467
+ },
468
+ "points_forts": [
469
+ "Alignement parfait avec multi-agents + recrutement tech: 7 agents orchestrés et simulation d’entretien.",
470
+ "Vrai scope produit: full-stack + data pipeline + stockage objet + monitoring (Metabase) + orchestration (Mage.ai).",
471
+ "Ownership rare sur un projet IA: business plan + déploiement + projet live (donc contrainte produit réelle)."
472
+ ],
473
+ "points_vigilance": [
474
+ "Risque d’over-claim sur la 'détection de fraude' et la 'simulation dynamique' sans description de méthode ni métriques d’efficacité (FP/FN, protocoles d’évaluation).",
475
+ "Maturité production incomplète dans le CV: pas de mention de tests, CI/CD, sécurité, RGPD (données CV = sensibles), monitoring LLM (coût/latence), ni gestion des erreurs/timeout.",
476
+ "Complexité stack: PostgreSQL + MongoDB + MinIO + Mage + Metabase + CrewAI/LangGraph; il faut justifier les choix et la cohérence opérationnelle."
477
+ ],
478
+ "note_globale": 79,
479
+ "verdict_recruteur": "Projet le plus solide et le plus aligné: vraie construction agentique + pipeline data + produit SaaS. Techniquement crédible, mais je le considère 'production-lean': pour valider un niveau senior/CTO-like, il faut des preuves chiffrées (impact) et des éléments de qualité prod (sécurité/RGPD, tests, observabilité LLM, perf/coûts). À creuser en entretien."
480
+ },
481
+ {
482
+ "titre": "FounderOS - Dashboard tout-en-un pour fondateurs de startups",
483
+ "resume": "Side project / MVP produit: dashboard Next.js pour tâches founder (Lean Canvas, CRM lite, veille, finance, contenu) + module d’intelligence concurrentielle scoré.",
484
+ "evaluation": {
485
+ "pertinence": {
486
+ "score": 5,
487
+ "justification": "Pertinent côté produit/PM et un peu GenAI (API Gemini), mais moins aligné sur multi-agents/RAG/recrutement/pipelines."
488
+ },
489
+ "complexite": {
490
+ "score": 4,
491
+ "justification": "Le CV décrit des modules fonctionnels et un scoring concurrentiel, mais pas d’architecture IA (agents, RAG, ingestion), ni complexité backend/data. Semble surtout front + appels API."
492
+ },
493
+ "stack": {
494
+ "score": 7,
495
+ "justification": "Next.js + TypeScript + Tailwind = stack moderne et cohérente. 'API Gemini' intégrée, mais absence d’info sur backend, persistance, auth, ou pipeline données."
496
+ },
497
+ "innovation": {
498
+ "score": 6,
499
+ "justification": "Module d’intelligence concurrentielle avec 'Health Score', 'Radar 6 axes', SWOT intelligente: idée intéressante. Mais on ne voit pas ce qui est réellement automatisé (sources, ingestion, critères) vs simple prompting."
500
+ },
501
+ "impact": {
502
+ "score": 2,
503
+ "justification": "Aucun chiffre (utilisateurs, adoption, gain de temps, taux d’activation), pas de preuve de projet live."
504
+ },
505
+ "ownership": {
506
+ "score": 6,
507
+ "justification": "On suppose une réalisation personnelle (projet listé), mais contrairement à AIRH, pas de mention explicite de déploiement, pilotage A→Z, ou méthodes."
508
+ },
509
+ "maturite": {
510
+ "score": 3,
511
+ "justification": "Aucune mention de monitoring, tests, sécurité, contrôle coût/latence Gemini, ni gestion des données. Semble MVP/démo."
512
+ }
513
+ },
514
+ "points_forts": [
515
+ "Bonne culture produit: regrouper des besoins founders (canvas, CRM, veille, finance) dans un même outil.",
516
+ "Stack front moderne (Next.js/TS) et intégration GenAI (Gemini) utile pour des analyses structurées."
517
+ ],
518
+ "points_vigilance": [
519
+ "Projet peu probant pour évaluer la compétence 'multi-agents/RAG/pipeline' (pas décrit).",
520
+ "Innovation difficile à distinguer d’un 'prompt + UI' sans description de données, métriques, ou protocole d’évaluation.",
521
+ "Aucun signal de maturité production (auth, rôles, stockage, observabilité, coûts)."
522
+ ],
523
+ "note_globale": 47,
524
+ "verdict_recruteur": "Bon projet produit/UI, mais techniquement trop peu étayé côté data/IA pour le poste visé. Je le vois comme un bonus (sens produit), pas comme une preuve de maîtrise agentique ou pipeline. À ne pas sur-vendre."
525
+ },
526
+ {
527
+ "titre": "Data Jobs - Moteur de recommandation d'emploi RAG",
528
+ "resume": "Projet IA / prototype RAG: scraping d’offres, vectorisation Pinecone, recommandation via LangChain + Groq, amélioration de pertinence annoncée.",
529
+ "evaluation": {
530
+ "pertinence": {
531
+ "score": 8,
532
+ "justification": "RAG + pipeline (scraping→vectorisation) appliqué à l’emploi tech (proche recrutement). Moins multi-agents mais très aligné GenAI/pipelines."
533
+ },
534
+ "complexite": {
535
+ "score": 6,
536
+ "justification": "Scraping Selenium + vector store Pinecone + LangChain + LLM Groq = pipeline RAG classique. Pas de mention d’ingestion robuste (dédup, scheduling), évaluation IR (precision@k), ni serving/monitoring."
537
+ },
538
+ "stack": {
539
+ "score": 7,
540
+ "justification": "Python + LangChain + Pinecone + Selenium + Groq: stack standard RAG. Cohérente, mais dépendante de services externes; pas d’info sur persistance, API, ou déploiement."
541
+ },
542
+ "innovation": {
543
+ "score": 5,
544
+ "justification": "Amélioration via 'Prompt Engineering itératif' = approche courante. Pas de signal d’innovation type reranking, hybrid search, feedback loop, ou évaluation systématique."
545
+ },
546
+ "impact": {
547
+ "score": 6,
548
+ "justification": "+2 000 offres scrapées et '+40% pertinence' annoncés. Mais la mesure n’est pas définie (metric, baseline, protocole, dataset de test), donc crédibilité partielle."
549
+ },
550
+ "ownership": {
551
+ "score": 7,
552
+ "justification": "Réalisation technique décrite + 'pilotage des sprints en tant que Scrum Master'. Ownership delivery/orga visible, mais pas explicitement end-to-end produit (déploiement, users)."
553
+ },
554
+ "maturite": {
555
+ "score": 4,
556
+ "justification": "Pas de mention de monitoring, tests, gestion anti-bot/robustesse scraping, conformité (TOS scraping), ni optimisation coût/latence. On est sur un prototype solide."
557
+ }
558
+ },
559
+ "points_forts": [
560
+ "Bon cas d’usage RAG appliqué au recrutement: ingestion (scraping) + indexation (Pinecone) + recommandation.",
561
+ "Chiffres minimaux présents (+2 000 offres, +40% pertinence), ce qui est rare dans un CV.",
562
+ "Dimension delivery: rôle Scrum Master mentionné (capacité à structurer l’exécution)."
563
+ ],
564
+ "points_vigilance": [
565
+ "Le '+40% pertinence' est invérifiable sans métrique (NDCG@k, precision@k, taux de clic) ni protocole d’évaluation; à challenger en entretien.",
566
+ "RAG très 'standard LangChain + vector DB' sans éléments avancés (reranker, hybrid search, chunking strategy, eval harness, feedback loop).",
567
+ "Maturité et conformité: scraping Selenium sans mention de robustesse, légal/TOS, ni industrialisation (scheduler, retries, monitoring)."
568
+ ],
569
+ "note_globale": 61,
570
+ "verdict_recruteur": "Bon projet RAG pragmatique et utile, mais techniquement assez standard. Je le valorise comme preuve de compréhension des briques RAG et d’un mini-pipeline, pas comme démonstration d’architecture avancée. Si le candidat peut expliquer clairement la métrique du '+40%' et une stratégie d’industrialisation, la note remonte nettement."
571
+ }
572
+ ],
573
+ "coherence_globale_projets": {},
574
+ "conseils_amelioration": [
575
+ "Sécuriser la lisibilité ATS : sortir d’une mise en page en colonnes et garantir un flux linéaire (les sections LIENS/COMPÉTENCES ne doivent pas apparaître au milieu de l’EXPERIENCE dans l’extraction texte). Si vous gardez les ancres, ajoutez aussi les URL en clair (ex. linkedin.com/in/… ; airh.online ; founderdashboard.vercel.app).",
576
+ "Ajouter 6–10 métriques techniques ‘signature’ sur Enedis + AIRH : temps d’exécution avant/après des flows Dataiku, volume de données, temps de réponse RAG, coût par requête, adoption (utilisateurs actifs), fiabilité (taux d’échec), et côté AIRH latence multi-agents + coût inference + taux de succès des runs.",
577
+ "Nettoyer la section compétences : soit retirer les outils non prouvés, soit ajouter une ligne de preuve par outil (ex. ‘DBT : modèle X, 12 tests, docs’, ‘Docker : image FastAPI, docker-compose’, ‘CI/CD : GitHub Actions avec lint+tests+deploy’). Objectif : zéro skill “hors contexte”.",
578
+ "Sur AIRH, expliciter 3–5 décisions d’architecture et leurs compromis (ex. choix Postgres vs Mongo, stockage MinIO, orchestration Mage.ai, stratégie d’indexation/search, observabilité/trace Langfuse) + ce que vous feriez différemment à l’échelle (10k CV/jour, multi-tenant, files/queues)."
579
+ ]
580
+ }
581
+ }