QuentinL52 commited on
Commit
6c9a9ea
·
verified ·
1 Parent(s): 2e32ddd

Update src/cv_parsing_agents.py

Browse files
Files changed (1) hide show
  1. src/cv_parsing_agents.py +247 -70
src/cv_parsing_agents.py CHANGED
@@ -1,10 +1,41 @@
 
 
 
1
  import os
2
  import json
3
  import logging
4
 
5
  logger = logging.getLogger(__name__)
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  def clean_dict_keys(data):
 
 
 
 
 
 
 
 
 
8
  if isinstance(data, dict):
9
  return {str(key): clean_dict_keys(value) for key, value in data.items()}
10
  elif isinstance(data, list):
@@ -13,102 +44,248 @@ def clean_dict_keys(data):
13
  return data
14
 
15
  class CvParserAgent:
 
 
 
 
 
 
 
16
  def __init__(self, pdf_path: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  self.pdf_path = pdf_path
 
 
 
 
 
 
18
 
19
  def process(self) -> dict:
20
  """
21
- Version sécurisée pour Cloud Run
 
 
 
 
22
  """
23
  logger.info(f"Début du traitement du CV : {self.pdf_path}")
24
 
 
 
 
 
 
 
 
 
 
 
25
  try:
26
- # Import avec gestion d'erreur
27
- from src.config import load_pdf
28
  cv_text_content = load_pdf(self.pdf_path)
29
- logger.info(f"Contenu extrait : {len(cv_text_content)} caractères")
 
 
30
 
31
- # Import sécurisé de crew_pool
32
- try:
33
- from src.crew.crew_pool import analyse_cv
34
- logger.info("Lancement de l'analyse par le crew...")
35
- crew_output = analyse_cv(cv_text_content)
36
- except Exception as crew_error:
37
- logger.error(f"Erreur de permission : {crew_error}")
38
- # Fallback en cas d'erreur CrewAI
39
- return self._create_fallback_response(cv_text_content)
40
 
41
- # Traitement du résultat
42
- if not crew_output:
43
- logger.warning("Crew n'a pas retourné de résultat")
44
- return self._create_fallback_response(cv_text_content)
 
 
45
 
46
- # Si c'est déjà un dictionnaire (cas d'erreur géré)
47
- if isinstance(crew_output, dict):
48
- return clean_dict_keys(crew_output)
49
 
50
- # Si c'est un objet avec .raw
51
- if hasattr(crew_output, 'raw') and crew_output.raw:
52
- raw_string = crew_output.raw.strip()
53
-
54
- # Nettoyage du JSON si nécessaire
55
- if '```' in raw_string:
56
- try:
57
- json_part = raw_string.split('```json')[1].split('```')[0]
58
- raw_string = json_part.strip()
59
- except:
60
- # Si le parsing échoue, utiliser tel quel
61
- pass
62
-
63
- try:
64
- profile_data = json.loads(raw_string)
65
- return clean_dict_keys(profile_data)
66
- except json.JSONDecodeError as e:
67
- logger.error(f"Erreur JSON : {e}")
68
- logger.error(f"Raw data: {raw_string[:500]}...")
69
- return self._create_fallback_response(cv_text_content)
70
 
71
- # Si aucun format reconnu
72
- logger.warning("Format de sortie crew non reconnu")
73
- return self._create_fallback_response(cv_text_content)
74
 
 
 
 
 
 
 
75
  except Exception as e:
76
- logger.error(f"Erreur critique dans CvParserAgent : {e}", exc_info=True)
77
- return self._create_fallback_response("Erreur lors de la lecture du CV")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- def _create_fallback_response(self, cv_content: str) -> dict:
80
- """Crée une réponse de fallback en cas d'erreur"""
 
 
 
 
 
 
81
  return {
82
  "candidat": {
83
  "informations_personnelles": {
84
- "nom": "Extraction automatique échouée",
85
- "email": "Non extrait",
86
- "numero_de_telephone": "Non extrait",
87
- "localisation": "Non extrait"
88
  },
89
  "compétences": {
90
- "hard_skills": ["Analyse manuelle requise"],
91
- "soft_skills": []
92
  },
93
- "expériences": [{
94
- "Poste": "Analyse manuelle requise",
95
- "Entreprise": "Voir CV original",
96
- "start_date": "Non spécifié",
97
- "end_date": "Non spécifié",
98
- "responsabilités": ["Consulter le CV original"]
99
- }],
 
 
100
  "projets": {
101
- "professional": [],
 
 
 
 
 
 
 
102
  "personal": []
103
  },
104
- "formations": [{
105
- "degree": "Analyse manuelle requise",
106
- "institution": "Voir CV original",
107
- "start_date": "Non spécifié",
108
- "end_date": "Non spécifié"
109
- }],
110
- "raw_content_length": len(cv_content),
111
- "status": "fallback_mode",
112
- "message": "L'extraction automatique a échoué. Analyse manuelle recommandée."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module pour le parsing de CV avec CrewAI
3
+ """
4
  import os
5
  import json
6
  import logging
7
 
8
  logger = logging.getLogger(__name__)
9
 
10
+ # Gestion des imports avec fallback
11
+ try:
12
+ from src.crew.crew_pool import analyse_cv
13
+ CREW_POOL_AVAILABLE = True
14
+ logger.info("✅ crew_pool importé avec succès")
15
+ except ImportError as e:
16
+ logger.error(f"❌ Erreur import crew_pool: {e}")
17
+ CREW_POOL_AVAILABLE = False
18
+ analyse_cv = None
19
+
20
+ try:
21
+ from src.config import load_pdf
22
+ CONFIG_AVAILABLE = True
23
+ logger.info("✅ config importé avec succès")
24
+ except ImportError as e:
25
+ logger.error(f"❌ Erreur import config: {e}")
26
+ CONFIG_AVAILABLE = False
27
+ load_pdf = None
28
+
29
  def clean_dict_keys(data):
30
+ """
31
+ Nettoie les clés d'un dictionnaire en les convertissant en string.
32
+
33
+ Args:
34
+ data: Données à nettoyer (dict, list, ou autre)
35
+
36
+ Returns:
37
+ Données nettoyées avec des clés string
38
+ """
39
  if isinstance(data, dict):
40
  return {str(key): clean_dict_keys(value) for key, value in data.items()}
41
  elif isinstance(data, list):
 
44
  return data
45
 
46
  class CvParserAgent:
47
+ """
48
+ Agent de parsing de CV utilisant CrewAI.
49
+
50
+ Cette classe traite un fichier PDF de CV et en extrait les informations
51
+ structurées (compétences, expériences, formations, etc.)
52
+ """
53
+
54
  def __init__(self, pdf_path: str):
55
+ """
56
+ Initialise l'agent de parsing de CV.
57
+
58
+ Args:
59
+ pdf_path (str): Chemin vers le fichier PDF à traiter
60
+
61
+ Raises:
62
+ ValueError: Si le chemin du fichier est invalide
63
+ ImportError: Si les dépendances nécessaires ne sont pas disponibles
64
+ """
65
+ if not pdf_path or not isinstance(pdf_path, str):
66
+ raise ValueError("Le chemin du fichier PDF doit être une chaîne non vide")
67
+
68
  self.pdf_path = pdf_path
69
+
70
+ # Vérifier que les dépendances sont disponibles
71
+ if not CREW_POOL_AVAILABLE:
72
+ logger.warning("CrewAI crew_pool non disponible - mode dégradé")
73
+ if not CONFIG_AVAILABLE:
74
+ logger.warning("Module config non disponible - mode dégradé")
75
 
76
  def process(self) -> dict:
77
  """
78
+ Traite le fichier PDF pour en extraire le contenu sous forme de JSON.
79
+
80
+ Returns:
81
+ dict: Dictionnaire contenant les données extraites du CV,
82
+ ou données de fallback en cas d'erreur
83
  """
84
  logger.info(f"Début du traitement du CV : {self.pdf_path}")
85
 
86
+ # Vérifier que le fichier existe
87
+ if not os.path.exists(self.pdf_path):
88
+ logger.error(f"Fichier PDF non trouvé: {self.pdf_path}")
89
+ return self._create_fallback_data()
90
+
91
+ # Vérifier les dépendances
92
+ if not CREW_POOL_AVAILABLE or not CONFIG_AVAILABLE:
93
+ logger.error("Dépendances manquantes pour le traitement complet")
94
+ return self._create_fallback_data()
95
+
96
  try:
97
+ # Charger le contenu du PDF
 
98
  cv_text_content = load_pdf(self.pdf_path)
99
+ if not cv_text_content or not cv_text_content.strip():
100
+ logger.error("Le PDF semble vide ou illisible")
101
+ return self._create_fallback_data()
102
 
103
+ logger.info(f"PDF chargé, {len(cv_text_content)} caractères extraits")
104
+
105
+ # Analyser avec CrewAI
106
+ crew_output = analyse_cv(cv_text_content)
 
 
 
 
 
107
 
108
+ if not crew_output or not hasattr(crew_output, 'raw') or not crew_output.raw.strip():
109
+ logger.error("L'analyse par le crew n'a pas retourné de résultat.")
110
+ return self._create_fallback_data()
111
+
112
+ raw_string = crew_output.raw
113
+ logger.info(f"Résultat brut du crew: {raw_string[:200]}...")
114
 
115
+ # Nettoyer le JSON si nécessaire
116
+ json_string_cleaned = self._clean_json_string(raw_string)
 
117
 
118
+ # Parser le JSON
119
+ profile_data = json.loads(json_string_cleaned)
120
+ logger.info("Parsing JSON réussi")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ return clean_dict_keys(profile_data)
 
 
123
 
124
+ except json.JSONDecodeError as e:
125
+ logger.error(f"Erreur de décodage JSON : {e}")
126
+ if 'crew_output' in locals():
127
+ logger.error(f"Données brutes reçues : {crew_output.raw}")
128
+ return self._create_fallback_data()
129
+
130
  except Exception as e:
131
+ logger.error(f"Erreur inattendue dans CvParserAgent : {e}", exc_info=True)
132
+ return self._create_fallback_data()
133
+
134
+ def _clean_json_string(self, raw_string: str) -> str:
135
+ """
136
+ Nettoie une chaîne JSON brute en supprimant les blocs de code markdown.
137
+
138
+ Args:
139
+ raw_string (str): Chaîne brute à nettoyer
140
+
141
+ Returns:
142
+ str: Chaîne JSON nettoyée
143
+ """
144
+ json_string_cleaned = raw_string.strip()
145
+
146
+ # Supprimer les blocs de code markdown si présents
147
+ if '```' in raw_string:
148
+ try:
149
+ # Chercher le bloc json
150
+ if '```json' in raw_string:
151
+ json_part = raw_string.split('```json')[1].split('```')[0]
152
+ json_string_cleaned = json_part.strip()
153
+ else:
154
+ # Prendre le premier bloc de code
155
+ parts = raw_string.split('```')
156
+ if len(parts) >= 3:
157
+ json_string_cleaned = parts[1].strip()
158
+ except IndexError:
159
+ logger.warning("Format de code block détecté mais mal formé")
160
+
161
+ return json_string_cleaned
162
 
163
+ def _create_fallback_data(self) -> dict:
164
+ """
165
+ Crée des données de CV de fallback en cas d'erreur de traitement.
166
+
167
+ Returns:
168
+ dict: Structure de données de CV par défaut
169
+ """
170
+ logger.info("Création de données de fallback pour le CV")
171
  return {
172
  "candidat": {
173
  "informations_personnelles": {
174
+ "nom": "Candidat Test",
175
+ "email": "[email protected]",
176
+ "numero_de_telephone": "Non spécifié",
177
+ "localisation": "Non spécifiée"
178
  },
179
  "compétences": {
180
+ "hard_skills": ["Python", "FastAPI", "Data Analysis"],
181
+ "soft_skills": ["Communication", "Travail d'équipe", "Adaptabilité"]
182
  },
183
+ "expériences": [
184
+ {
185
+ "Poste": "Développeur",
186
+ "Entreprise": "Entreprise Test",
187
+ "start_date": "2022",
188
+ "end_date": "Aujourd'hui",
189
+ "responsabilités": ["Développement d'applications", "Maintenance du code"]
190
+ }
191
+ ],
192
  "projets": {
193
+ "professional": [
194
+ {
195
+ "title": "Projet Test",
196
+ "role": "Développeur principal",
197
+ "technologies": ["Python", "FastAPI"],
198
+ "outcomes": ["Application fonctionnelle"]
199
+ }
200
+ ],
201
  "personal": []
202
  },
203
+ "formations": [
204
+ {
205
+ "degree": "Formation en Informatique",
206
+ "institution": "École Test",
207
+ "start_date": "2020",
208
+ "end_date": "2022"
209
+ }
210
+ ],
211
+ "reconversion": {
212
+ "is_reconversion": False,
213
+ "analysis": "Pas de reconversion détectée - données de test"
214
+ }
215
+ }
216
+ }
217
+
218
+ # Fonction utilitaire pour créer des données de fallback
219
+ def create_fallback_cv_data(pdf_path: str = None) -> dict:
220
+ """
221
+ Fonction utilitaire pour créer des données de CV de fallback.
222
+
223
+ Args:
224
+ pdf_path (str, optional): Chemin du fichier PDF (non utilisé dans le fallback)
225
+
226
+ Returns:
227
+ dict: Structure de données de CV par défaut
228
+ """
229
+ return {
230
+ "candidat": {
231
+ "informations_personnelles": {
232
+ "nom": "Candidat Test",
233
+ "email": "[email protected]",
234
+ "numero_de_telephone": "Non spécifié",
235
+ "localisation": "Non spécifiée"
236
+ },
237
+ "compétences": {
238
+ "hard_skills": ["Python", "FastAPI", "Data Analysis"],
239
+ "soft_skills": ["Communication", "Travail d'équipe", "Adaptabilité"]
240
+ },
241
+ "expériences": [
242
+ {
243
+ "Poste": "Développeur",
244
+ "Entreprise": "Entreprise Test",
245
+ "start_date": "2022",
246
+ "end_date": "Aujourd'hui",
247
+ "responsabilités": ["Développement d'applications", "Maintenance du code"]
248
+ }
249
+ ],
250
+ "projets": {
251
+ "professional": [
252
+ {
253
+ "title": "Projet Test",
254
+ "role": "Développeur principal",
255
+ "technologies": ["Python", "FastAPI"],
256
+ "outcomes": ["Application fonctionnelle"]
257
+ }
258
+ ],
259
+ "personal": []
260
+ },
261
+ "formations": [
262
+ {
263
+ "degree": "Formation en Informatique",
264
+ "institution": "École Test",
265
+ "start_date": "2020",
266
+ "end_date": "2022"
267
+ }
268
+ ],
269
+ "reconversion": {
270
+ "is_reconversion": False,
271
+ "analysis": "Pas de reconversion détectée - données de test"
272
  }
273
  }
274
+ }
275
+
276
+ # Test des imports au chargement du module
277
+ if __name__ == "__main__":
278
+ logger.info("Test du module cv_parsing_agents")
279
+ logger.info(f"CREW_POOL_AVAILABLE: {CREW_POOL_AVAILABLE}")
280
+ logger.info(f"CONFIG_AVAILABLE: {CONFIG_AVAILABLE}")
281
+
282
+ # Test de création d'une instance
283
+ try:
284
+ agent = CvParserAgent("/tmp/test.pdf")
285
+ logger.info("✅ CvParserAgent créé avec succès")
286
+ except Exception as e:
287
+ logger.error(f"❌ Erreur création CvParserAgent: {e}")
288
+
289
+ # Test des données de fallback
290
+ fallback_data = create_fallback_cv_data()
291
+ logger.info(f"✅ Données de fallback créées: {len(fallback_data)} clés")