Files changed (37) hide show
  1. Dockerfile +17 -12
  2. main.py +236 -39
  3. prompts/rag_prompt_old.txt +35 -89
  4. requirements.txt +1 -2
  5. src/agents/cv_agents.py +0 -251
  6. src/agents/scoring_agent.py +0 -183
  7. src/core/rag_handler.py +0 -130
  8. src/{agents → crew}/__init__.py +0 -0
  9. src/crew/__pycache__/__init__.cpython-311.pyc +0 -0
  10. src/crew/__pycache__/__init__.cpython-312.pyc +0 -0
  11. src/crew/__pycache__/__init__.cpython-312.pycZone.Identifier +2 -0
  12. src/crew/__pycache__/agents.cpython-311.pyc +0 -0
  13. src/crew/__pycache__/agents.cpython-312.pyc +0 -0
  14. src/crew/__pycache__/agents.cpython-312.pycZone.Identifier +2 -0
  15. src/crew/__pycache__/analysis_crew.cpython-312.pyc +0 -0
  16. src/crew/__pycache__/analysis_crew.cpython-312.pycZone.Identifier +2 -0
  17. src/crew/__pycache__/crew_pool.cpython-311.pyc +0 -0
  18. src/crew/__pycache__/crew_pool.cpython-312.pyc +0 -0
  19. src/crew/__pycache__/crew_pool.cpython-312.pycZone.Identifier +2 -0
  20. src/crew/__pycache__/tasks.cpython-312.pyc +0 -0
  21. src/crew/__pycache__/tasks.cpython-312.pycZone.Identifier +2 -0
  22. src/crew/agents.py +75 -0
  23. src/crew/crew_pool.py +81 -0
  24. src/crew/tasks.py +184 -0
  25. src/cv_parsing_agents.py +291 -0
  26. src/{core/deep_learning_analyzer.py → deep_learning_analyzer.py} +57 -56
  27. src/{core → interview_simulator}/__init__.py +0 -0
  28. src/interview_simulator/__pycache__/__init__.cpython-312.pyc +0 -0
  29. src/interview_simulator/__pycache__/entretient_version_prod.cpython-312.pyc +0 -0
  30. src/{services/interview_service.py → interview_simulator/entretient_version_prod.py} +42 -50
  31. src/models.py +0 -37
  32. src/rag_handler.py +202 -0
  33. src/scoring_engine.py +102 -0
  34. src/services/__init__.py +0 -0
  35. src/services/analysis_service.py +0 -97
  36. src/services/graph_service.py +0 -193
  37. tools/analysis_tools.py +0 -56
Dockerfile CHANGED
@@ -1,39 +1,44 @@
1
  FROM python:3.11-slim
2
 
 
3
  ENV PYTHONUNBUFFERED=1 \
4
  PYTHONDONTWRITEBYTECODE=1 \
5
  PIP_NO_CACHE_DIR=1 \
6
  PIP_DISABLE_PIP_VERSION_CHECK=1 \
7
- PYTHONPATH=/app \
8
- HOME=/app \
9
- HF_HOME=/tmp/cache \
 
10
  TRANSFORMERS_CACHE=/tmp/cache \
11
  HF_HUB_CACHE=/tmp/cache/hub \
12
- SENTENCE_TRANSFORMERS_HOME=/tmp/cache/sentence_transformers \
13
- XDG_DATA_HOME="/app/.local/share" \
14
- CREWAI_TELEMETRY_OPT_OUT=true
15
 
 
16
  WORKDIR /app
17
 
 
18
  RUN apt-get update && apt-get install -y \
19
  curl \
20
  && rm -rf /var/lib/apt/lists/*
21
 
 
22
  RUN pip install uv
23
 
 
24
  COPY requirements.txt .
25
  RUN uv pip install --system --no-cache -r requirements.txt
26
 
 
27
  COPY . .
28
 
 
29
  RUN mkdir -p /tmp/cache/hub \
30
  /tmp/cache/sentence_transformers \
31
- /tmp/vector_store \
32
- /app/.local/share/crewai \
33
- /app/.chroma
34
-
35
-
36
- RUN chmod -R 777 /tmp /app
37
 
 
38
  EXPOSE 7860
 
 
39
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
  FROM python:3.11-slim
2
 
3
+ # Variables d'environnement
4
  ENV PYTHONUNBUFFERED=1 \
5
  PYTHONDONTWRITEBYTECODE=1 \
6
  PIP_NO_CACHE_DIR=1 \
7
  PIP_DISABLE_PIP_VERSION_CHECK=1 \
8
+ PYTHONPATH=/app
9
+
10
+ # Variables pour les modèles ML - utilisation de /tmp (toujours writable)
11
+ ENV HF_HOME=/tmp/cache \
12
  TRANSFORMERS_CACHE=/tmp/cache \
13
  HF_HUB_CACHE=/tmp/cache/hub \
14
+ SENTENCE_TRANSFORMERS_HOME=/tmp/cache/sentence_transformers
 
 
15
 
16
+ # Répertoire de travail
17
  WORKDIR /app
18
 
19
+ # Installer les dépendances système
20
  RUN apt-get update && apt-get install -y \
21
  curl \
22
  && rm -rf /var/lib/apt/lists/*
23
 
24
+ # Installer uv
25
  RUN pip install uv
26
 
27
+ # Copier et installer les dépendances Python
28
  COPY requirements.txt .
29
  RUN uv pip install --system --no-cache -r requirements.txt
30
 
31
+ # Copier le code source
32
  COPY . .
33
 
34
+ # Créer les répertoires de cache dans /tmp (toujours writable)
35
  RUN mkdir -p /tmp/cache/hub \
36
  /tmp/cache/sentence_transformers \
37
+ /tmp/vector_store && \
38
+ chmod -R 777 /tmp
 
 
 
 
39
 
40
+ # Exposer le port HF Spaces
41
  EXPOSE 7860
42
+
43
+ # Commande de démarrage
44
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
main.py CHANGED
@@ -1,27 +1,56 @@
 
 
1
  import os
2
  import logging
3
- from fastapi import FastAPI, Request, HTTPException
4
- from fastapi.responses import JSONResponse
5
  from fastapi.middleware.cors import CORSMiddleware
6
- from pydantic import BaseModel
7
-
8
- from src.services.graph_service import GraphInterviewProcessor
 
 
 
9
 
10
- from langtrace_python_sdk import langtrace
 
 
 
 
 
 
 
 
 
11
 
12
- langtrace.init(api_key=os.getenv("LANGTRACE_API_KEY"))
 
 
 
 
 
 
 
13
 
14
- logging.basicConfig(level=logging.INFO)
15
- logger = logging.getLogger(__name__)
 
 
 
 
 
 
16
 
 
17
  app = FastAPI(
18
- title="Interview Simulation API",
19
- description="API for interview simulations.",
20
- version="1.0.0",
21
  docs_url="/docs",
22
  redoc_url="/redoc"
23
  )
24
 
 
25
  app.add_middleware(
26
  CORSMiddleware,
27
  allow_origins=["*"],
@@ -30,44 +59,212 @@ app.add_middleware(
30
  allow_headers=["*"],
31
  )
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  class HealthCheck(BaseModel):
34
  status: str = "ok"
 
 
 
35
 
 
36
  @app.get("/", response_model=HealthCheck, tags=["Status"])
37
  async def health_check():
38
- return HealthCheck()
39
-
40
- @app.post("/simulate-interview/")
41
- async def simulate_interview(request: Request):
42
- """
43
- This endpoint receives the interview data, instantiates the graph processor
44
- and starts the conversation.
45
- """
46
- logger = logging.getLogger(__name__)
47
  try:
48
- payload = await request.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
- if not all(k in payload for k in ["user_id", "job_offer_id", "cv_document", "job_offer"]):
51
- raise HTTPException(status_code=400, detail="Missing data in payload (user_id, job_offer_id, cv_document, job_offer).")
52
-
53
- logger.info(f"Starting simulation for user: {payload['user_id']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- processor = GraphInterviewProcessor(payload)
56
- result = processor.invoke(payload.get("messages", []))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- return JSONResponse(content=result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- except ValueError as ve:
61
- logger.error(f"Data validation error: {ve}", exc_info=True)
62
- return JSONResponse(content={"error": str(ve)}, status_code=400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  except Exception as e:
64
- logger.error(f"Internal error in simulate-interview endpoint: {e}", exc_info=True)
65
- return JSONResponse(
66
- content={"error": "An internal error occurred on the assistant's server."},
67
- status_code=500
68
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
  if __name__ == "__main__":
71
  import uvicorn
72
- port = int(os.getenv("PORT", 8002)) # Use PORT environment variable, default to 8002
73
- uvicorn.run(app, host="0.0.0.0", port=port)
 
1
+ import tempfile
2
+ import requests
3
  import os
4
  import logging
5
+ from fastapi import FastAPI, UploadFile, File, HTTPException
6
+ from fastapi.concurrency import run_in_threadpool
7
  from fastapi.middleware.cors import CORSMiddleware
8
+ from pydantic import BaseModel, Field
9
+ from typing import List, Dict, Any, Optional
10
+ os.environ['HOME'] = '/tmp'
11
+ # Configuration du logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
 
15
+ # Imports avec gestion d'erreurs robuste
16
+ try:
17
+ from src.cv_parsing_agents import CvParserAgent, create_fallback_cv_data
18
+ CV_PARSING_AVAILABLE = True
19
+ logger.info("✅ CV Parsing disponible")
20
+ except Exception as e:
21
+ logger.error(f"❌ CV Parsing indisponible: {e}")
22
+ CV_PARSING_AVAILABLE = False
23
+ CvParserAgent = None
24
+ create_fallback_cv_data = None
25
 
26
+ try:
27
+ from src.interview_simulator.entretient_version_prod import InterviewProcessor
28
+ INTERVIEW_AVAILABLE = True
29
+ logger.info("✅ Interview Simulator disponible")
30
+ except Exception as e:
31
+ logger.error(f"❌ Interview Simulator indisponible: {e}")
32
+ INTERVIEW_AVAILABLE = False
33
+ InterviewProcessor = None
34
 
35
+ try:
36
+ from src.scoring_engine import ContextualScoringEngine
37
+ SCORING_AVAILABLE = True
38
+ logger.info("✅ Scoring Engine disponible")
39
+ except Exception as e:
40
+ logger.error(f"❌ Scoring Engine indisponible: {e}")
41
+ SCORING_AVAILABLE = False
42
+ ContextualScoringEngine = None
43
 
44
+ # Application FastAPI
45
  app = FastAPI(
46
+ title="AIrh Interview Assistant",
47
+ description="API pour l'analyse de CV et la simulation d'entretiens d'embauche",
48
+ version="1.3.0",
49
  docs_url="/docs",
50
  redoc_url="/redoc"
51
  )
52
 
53
+ # Configuration CORS pour HF Spaces
54
  app.add_middleware(
55
  CORSMiddleware,
56
  allow_origins=["*"],
 
59
  allow_headers=["*"],
60
  )
61
 
62
+ # Configuration API Celery
63
+ CELERY_API_URL = os.getenv("CELERY_API_URL", "https://celery-7as1.onrender.com")
64
+
65
+ # Modèles Pydantic
66
+ class InterviewRequest(BaseModel):
67
+ user_id: str = Field(..., example="user_12345")
68
+ job_offer_id: str = Field(..., example="job_offer_abcde")
69
+ cv_document: Dict[str, Any]
70
+ job_offer: Dict[str, Any]
71
+ messages: List[Dict[str, Any]]
72
+ conversation_history: List[Dict[str, Any]]
73
+
74
+ class AnalysisRequest(BaseModel):
75
+ conversation_history: List[Dict[str, Any]]
76
+ job_description_text: str
77
+ candidate_id: Optional[str] = None
78
+
79
+ class TaskResponse(BaseModel):
80
+ task_id: str
81
+ status: str
82
+ result: Any = None
83
+ message: Optional[str] = None
84
+
85
  class HealthCheck(BaseModel):
86
  status: str = "ok"
87
+ celery_api_status: Optional[str] = None
88
+ services: Dict[str, bool] = Field(default_factory=dict)
89
+ message: str = "API AIrh fonctionnelle"
90
 
91
+ # Endpoints
92
  @app.get("/", response_model=HealthCheck, tags=["Status"])
93
  async def health_check():
94
+ """Health check de l'API avec test de connectivité Celery."""
95
+
96
+ # Test connexion Celery
97
+ celery_status = "unknown"
 
 
 
 
 
98
  try:
99
+ response = requests.get(f"{CELERY_API_URL}/", timeout=5)
100
+ celery_status = "connected" if response.status_code == 200 else "error"
101
+ except Exception:
102
+ celery_status = "disconnected"
103
+
104
+ services = {
105
+ "cv_parsing": CV_PARSING_AVAILABLE,
106
+ "interview_simulation": INTERVIEW_AVAILABLE,
107
+ "scoring_engine": SCORING_AVAILABLE,
108
+ "celery_api": celery_status == "connected"
109
+ }
110
+
111
+ return HealthCheck(
112
+ celery_api_status=celery_status,
113
+ services=services
114
+ )
115
 
116
+ @app.post("/parse-cv/", tags=["CV Parsing"])
117
+ async def parse_cv(file: UploadFile = File(...)):
118
+ """Analyse un CV PDF et extrait les informations structurées."""
119
+
120
+ if not CV_PARSING_AVAILABLE:
121
+ # Fallback si le parsing n'est pas disponible
122
+ return create_fallback_cv_data() if create_fallback_cv_data else {
123
+ "error": "Service de parsing de CV temporairement indisponible",
124
+ "candidat": {
125
+ "informations_personnelles": {"nom": "Test User"},
126
+ "compétences": {"hard_skills": [], "soft_skills": []}
127
+ }
128
+ }
129
+
130
+ if file.content_type != "application/pdf":
131
+ raise HTTPException(status_code=400, detail="Fichier PDF requis")
132
+
133
+ tmp_path = None
134
+ try:
135
+ # Sauvegarder le fichier temporairement
136
+ contents = await file.read()
137
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
138
+ tmp.write(contents)
139
+ tmp_path = tmp.name
140
+
141
+ # Traiter le CV
142
+ cv_agent = CvParserAgent(pdf_path=tmp_path)
143
+ parsed_data = await run_in_threadpool(cv_agent.process)
144
+
145
+ if not parsed_data and create_fallback_cv_data:
146
+ parsed_data = create_fallback_cv_data(tmp_path)
147
+
148
+ # Scoring si disponible
149
+ if SCORING_AVAILABLE and ContextualScoringEngine and parsed_data:
150
+ try:
151
+ scoring_engine = ContextualScoringEngine(parsed_data)
152
+ scored_data = await run_in_threadpool(scoring_engine.calculate_scores)
153
+ if parsed_data.get("candidat"):
154
+ parsed_data["candidat"].update(scored_data)
155
+ except Exception as e:
156
+ logger.warning(f"Scoring échoué: {e}")
157
+
158
+ return parsed_data
159
+
160
+ except Exception as e:
161
+ logger.error(f"Erreur parsing CV: {e}")
162
+ if create_fallback_cv_data:
163
+ return create_fallback_cv_data(tmp_path)
164
+ raise HTTPException(status_code=500, detail=str(e))
165
+
166
+ finally:
167
+ if tmp_path and os.path.exists(tmp_path):
168
+ try:
169
+ os.remove(tmp_path)
170
+ except Exception:
171
+ pass
172
 
173
+ @app.post("/simulate-interview/", tags=["Interview"])
174
+ async def simulate_interview(request: InterviewRequest):
175
+ """Gère une conversation d'entretien d'embauche."""
176
+
177
+ if not INTERVIEW_AVAILABLE:
178
+ raise HTTPException(
179
+ status_code=503,
180
+ detail="Service de simulation d'entretien indisponible"
181
+ )
182
+
183
+ try:
184
+ processor = InterviewProcessor(
185
+ cv_document=request.cv_document,
186
+ job_offer=request.job_offer,
187
+ conversation_history=request.conversation_history
188
+ )
189
+
190
+ result = await run_in_threadpool(processor.run, messages=request.messages)
191
+ return {"response": result["messages"][-1].content}
192
+
193
+ except Exception as e:
194
+ logger.error(f"Erreur simulation entretien: {e}")
195
+ raise HTTPException(status_code=500, detail=str(e))
196
 
197
+ @app.post("/trigger-analysis/", response_model=TaskResponse, status_code=202, tags=["Analysis"])
198
+ async def trigger_analysis(request: AnalysisRequest):
199
+ """Déclenche une analyse asynchrone via l'API Celery."""
200
+
201
+ try:
202
+ response = requests.post(
203
+ f"{CELERY_API_URL}/trigger-analysis",
204
+ json=request.dict(),
205
+ headers={"Content-Type": "application/json"},
206
+ timeout=30
207
+ )
208
+
209
+ if response.status_code == 202:
210
+ data = response.json()
211
+ return TaskResponse(
212
+ task_id=data["task_id"],
213
+ status=data["status"],
214
+ message="Analyse démarrée"
215
+ )
216
+ else:
217
+ raise HTTPException(status_code=503, detail="Service d'analyse indisponible")
218
+
219
+ except requests.RequestException:
220
+ raise HTTPException(status_code=503, detail="API Celery inaccessible")
221
+ except Exception as e:
222
+ raise HTTPException(status_code=500, detail=str(e))
223
 
224
+ @app.get("/analysis-status/{task_id}", response_model=TaskResponse, tags=["Analysis"])
225
+ async def get_analysis_status(task_id: str):
226
+ """Récupère le statut d'une analyse."""
227
+
228
+ try:
229
+ response = requests.get(f"{CELERY_API_URL}/task-status/{task_id}", timeout=10)
230
+
231
+ if response.status_code == 200:
232
+ data = response.json()
233
+ return TaskResponse(
234
+ task_id=task_id,
235
+ status=data["status"],
236
+ result=data.get("result"),
237
+ message=data.get("progress", "Statut récupéré")
238
+ )
239
+ else:
240
+ raise HTTPException(status_code=503, detail="Service d'analyse indisponible")
241
+
242
+ except requests.RequestException:
243
+ raise HTTPException(status_code=503, detail="API Celery inaccessible")
244
  except Exception as e:
245
+ raise HTTPException(status_code=500, detail=str(e))
246
+
247
+ # Endpoint de debug pour HF Spaces
248
+ @app.get("/debug", tags=["Debug"])
249
+ async def debug_info():
250
+ """Informations de debug pour le déploiement."""
251
+ return {
252
+ "environment": {
253
+ "HF_HOME": os.getenv("HF_HOME"),
254
+ "CELERY_API_URL": CELERY_API_URL,
255
+ "PYTHONPATH": os.getenv("PYTHONPATH")
256
+ },
257
+ "services": {
258
+ "cv_parsing": CV_PARSING_AVAILABLE,
259
+ "interview_simulation": INTERVIEW_AVAILABLE,
260
+ "scoring_engine": SCORING_AVAILABLE
261
+ },
262
+ "cache_dirs": {
263
+ "/tmp/cache": os.path.exists("/tmp/cache"),
264
+ "/app/cache": os.path.exists("/app/cache")
265
+ }
266
+ }
267
 
268
  if __name__ == "__main__":
269
  import uvicorn
270
+ uvicorn.run(app, host="0.0.0.0", port=7860)
 
prompts/rag_prompt_old.txt CHANGED
@@ -1,89 +1,35 @@
1
- Tu es un recruteur expert, menant un premier entretien de qualification. Ton ton est professionnel mais engageant.
2
- Ta mission est d'évaluer l'adéquation d'un candidat pour un poste.
3
- CONTEXTE DE L'ENTRETIEN
4
- Tu dois baser ta conversation sur les informations suivantes :
5
-
6
- 1. Informations à utiliser activement dans la conversation :
7
- Entreprise : {entreprise}
8
- Intitulé du poste : {poste}
9
- Équipe / Pôle : {pole}
10
- Missions principales : {mission}
11
-
12
- 2. Informations pour guider tes questions :
13
- Profil recherché : {profil_recherche}
14
- Compétences clés attendues : {competences}
15
-
16
- 3. Informations sur le candidat :
17
- Les données de son CV sont : {cv}
18
-
19
- 4. ANALYSE DES COMPÉTENCES DU CANDIDAT :
20
- {skills_analysis}
21
-
22
- 5. ANALYSE DE RECONVERSION (si applicable) :
23
- {reconversion_analysis}
24
-
25
- IMPORTANT: La description complète du poste à utiliser pour l'analyse est la suivante:
26
- {job_description}
27
-
28
- DIRECTIVES D'ADAPTATION SELON LE PROFIL
29
-
30
- 1. Adaptation aux niveaux de compétences :
31
- - Pour les compétences "expert" : Pose des questions techniques approfondies, demande des exemples concrets d'optimisation, de leadership technique, ou d'innovation
32
- - Pour les compétences "avance" : Explore les défis rencontrés, la résolution de problèmes complexes, et l'autonomie
33
- - Pour les compétences "intermediaire" : Vérifie la compréhension pratique, demande des exemples d'application
34
- - Pour les compétences "debutant" : Teste les connaissances de base, évalue la motivation à apprendre
35
-
36
- 2. Prise en compte de la reconversion (si détectée) :
37
- - Explore les motivations du changement de carrière
38
- - Identifie les compétences transférables
39
- - Évalue l'engagement dans la nouvelle voie
40
- - Rassure sur la valorisation de l'expérience passée
41
-
42
- 3. Questions adaptées au contexte :
43
- - Si le candidat a de l'expérience dans le domaine : Questions sur l'évolution, les projets marquants
44
- - Si le candidat est en reconversion : Questions sur la transition, l'apprentissage, l'adaptation
45
-
46
- 4. Pour le candidat qui a des projets :
47
- - Essaye de creuser les skills utilisé pour ce projet
48
- - n'hésite pas a poser des questions technique sur les skills d'un projet
49
-
50
- DÉROULEMENT DE L'ENTRETIEN
51
-
52
- 1. Déroulement de l'entretien :
53
- Introduction : Commence par te présenter en utilisant le prénom Roni.
54
- Présente l'entreprise ({entreprise}) et le contexte du recrutement en t'appuyant sur l'intitulé du poste ({poste}) et les missions ({mission}).
55
-
56
-
57
- 2. Présentation du candidat : Ta première question doit inviter le candidat à se présenter.
58
-
59
- 3. Questions adaptées au profil :
60
- - Utilise l'analyse des compétences pour calibrer tes questions
61
- - Creuse les compétences avec des questions techniques
62
- - Explore les motivations si reconversion détectée
63
- - Adapte ton niveau de questionnement au profil
64
-
65
- 4. Une question à la fois : Pose une seule question à la fois et attends la réponse complète.
66
-
67
- STYLE ET COMPORTEMENT
68
-
69
- - Personnalisation : Appelle toujours le candidat par son nom
70
- - Langage Naturel : Évite le jargon RH, utilise des formulations fluides
71
- - Écoute active : Montre que tu écoutes avec des relances appropriées
72
- - Évaluation subtile : Adapte tes questions au niveau détecté sans le mentionner explicitement
73
- - Conserve toujours une intéraction la plus naturelle et professionelle possible
74
-
75
- --- CONTEXTE TECHNIQUE POUR L'AGENT (ne pas mentionner à l'utilisateur) ---
76
- L'ID de l'utilisateur actuel est : {user_id}
77
- L'ID de l'offre d'emploi actuelle est : {job_offer_id}
78
-
79
- Quand tu appelleras l'outil 'trigger_interview_analysis', tu devras OBLIGATOIREMENT utiliser ces IDs exacts.
80
- Tu devras aussi OBLIGATOIREMENT fournir l'argument 'conversation_history' en utilisant l'historique complet des messages de la conversation en cours.
81
- --- FIN DU CONTEXTE TECHNIQUE ---
82
-
83
- COMMANDE DE DÉVELOPPEMENT
84
- Si le dernier message de l'utilisateur est EXACTEMENT la phrase "LANCEMENT_ANALYSE_DEV", tu dois ignorer toutes les autres instructions de conversation, conclure brièvement et immédiatement appeler l'outil `trigger_interview_analysis`.
85
-
86
- CONCLUSION
87
-
88
- Quand tu estimes avoir assez d'informations, conclus l'échange de manière positive.
89
- Action finale OBLIGATOIRE : Appelle l'outil `trigger_interview_analysis` pour lancer l'analyse finale. C'est ta seule et dernière action. Ne réponds rien d'autre.
 
1
+ Tu es un recruteur expert, menant un premier entretien de qualification. Ton ton est professionnel mais engageant. Ta mission est d'évaluer l'adéquation d'un candidat pour un poste.
2
+
3
+ CONTEXTE DE L'ENTRETIEN
4
+ Tu dois baser ta conversation sur les informations suivantes :
5
+
6
+ 1. Informations à utiliser activement dans la conversation :
7
+ Entreprise : {entreprise}
8
+ Intitulé du poste : {poste}
9
+ Équipe / Pôle : {pole}
10
+ Missions principales : {mission}
11
+
12
+ 2. Informations pour guider tes questions (à ne PAS mentionner directement) :
13
+ Profil recherché : {profil_recherche}
14
+ Compétences clés attendues : {competences}
15
+ (Utilise ces deux points comme une grille d'analyse interne pour formuler des questions pertinentes. Tes questions doivent permettre de vérifier si le candidat possède ces compétences et correspond au profil.)
16
+ 3. Informations sur le candidat :
17
+ Les données de son CV sont : {cv}
18
+
19
+ DIRECTIVES PRÉCISES
20
+
21
+ 1. Déroulement de l'entretien :
22
+ Introduction : Commence par te présenter avec un prénom (ex: Camille, Thomas...). Présente l'entreprise ({entreprise}) et le contexte du recrutement en t'appuyant sur l'intitulé du poste ({poste}) et les missions ({mission}).
23
+ Présentation du candidat : Ta toute première question doit inviter le candidat à se présenter. Par exemple : "Pour commencer, parlez-moi un peu de votre parcours."
24
+ Questions ciblées : En te basant sur les compétences et le profil recherché (que tu gardes en tête), pose des questions ouvertes pour évaluer le candidat. Fais des liens entre ses expériences ({cv}) et les missions du poste ({mission}). Par exemple, si une compétence attendue est "l'analyse de données", demande au candidat de décrire un projet où il a dû analyser un ensemble de données complexe.
25
+ Une question à la fois : Pose une seule question à la fois et attends la réponse complète du candidat avant de poursuivre.
26
+
27
+ 2. Style et Comportement :
28
+ Personnalisation : Appelle toujours le candidat par son nom (présent dans le CV).
29
+ Langage Naturel : Évite le jargon RH. Utilise des formulations fluides comme "J'ai noté dans votre CV que...", "Racontez-moi l'expérience chez...". Montre que tu écoutes avec des relances comme "D'accord, je vois.", "C'est intéressant.".
30
+ Évaluation subtile : Ne dis jamais "la compétence requise est...". À la place, évalue la compétence à travers des questions situationnelles ou comportementales.
31
+
32
+ 3. Conclusion de l'entretien :
33
+ Quand tu estimes avoir assez d'informations, conclus l'échange de manière positive.
34
+ Termine par une phrase de politesse.
35
+ Action finale OBLIGATOIRE : Ta toute dernière phrase, après la politesse, doit être exactement : "nous allons maintenant passer a l'analyse". Juste après, tu dois utiliser l'outil interview_analyser.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -18,9 +18,8 @@ sentencepiece
18
  accelerate
19
  pypdf
20
  python-dotenv
21
- pymongo
22
  requests
23
  faiss-cpu
24
- langtrace-python-sdk
25
 
26
  httpx==0.28.1
 
18
  accelerate
19
  pypdf
20
  python-dotenv
21
+
22
  requests
23
  faiss-cpu
 
24
 
25
  httpx==0.28.1
src/agents/cv_agents.py DELETED
@@ -1,251 +0,0 @@
1
- import json
2
- import logging
3
- from typing import Dict, Any, List
4
- from crewai import Agent, Task, Crew, Process
5
-
6
- logger = logging.getLogger(__name__)
7
-
8
- class CVAgentOrchestrator:
9
- def __init__(self, llm):
10
- self.llm = llm
11
- self._create_agents()
12
-
13
- def _create_agents(self):
14
- self.section_splitter = Agent(
15
- role="Analyseur de Structure de CV",
16
- goal="Découper intelligemment un CV en sections thématiques",
17
- backstory="Expert en analyse documentaire spécialisé dans la reconnaissance de structures de CV.",
18
- verbose=False,
19
- llm=self.llm
20
- )
21
-
22
- self.contact_extractor = Agent(
23
- role="Extracteur d'informations de contact",
24
- goal="Extraire les coordonnées du candidat",
25
- backstory="Expert en extraction d'informations de contact avec précision.",
26
- verbose=False,
27
- llm=self.llm
28
- )
29
-
30
- self.skills_extractor = Agent(
31
- role="Extracteur de compétences",
32
- goal="Identifier hard skills et soft skills",
33
- backstory="Spécialiste en identification de compétences techniques et comportementales.",
34
- verbose=False,
35
- llm=self.llm
36
- )
37
-
38
- self.experience_extractor = Agent(
39
- role="Extracteur d'expériences",
40
- goal="Extraire les expériences professionnelles",
41
- backstory="Expert en analyse de parcours professionnels.",
42
- verbose=False,
43
- llm=self.llm
44
- )
45
-
46
- self.project_extractor = Agent(
47
- role="Extracteur de projets",
48
- goal="Identifier projets professionnels et personnels",
49
- backstory="Spécialiste en identification de projets significatifs.",
50
- verbose=False,
51
- llm=self.llm
52
- )
53
-
54
- self.education_extractor = Agent(
55
- role="Extracteur de formations",
56
- goal="Extraire formations et diplômes",
57
- backstory="Expert en analyse de parcours académiques.",
58
- verbose=False,
59
- llm=self.llm
60
- )
61
-
62
- self.reconversion_detector = Agent(
63
- role="Détecteur de reconversion",
64
- goal="Analyser les changements de carrière",
65
- backstory="Conseiller d'orientation expert en transitions de carrière.",
66
- verbose=False,
67
- llm=self.llm
68
- )
69
-
70
- self.profile_builder = Agent(
71
- role="Constructeur de profil",
72
- goal="Assembler le profil candidat final",
73
- backstory="Expert en structuration de données JSON.",
74
- verbose=False,
75
- llm=self.llm
76
- )
77
-
78
- def split_cv_sections(self, cv_content: str) -> Dict[str, str]:
79
- task = Task(
80
- description=f"Analyser ce CV et l'organiser en sections: {cv_content}",
81
- expected_output="""JSON avec sections: contact, experiences, projects, education, skills, other""",
82
- agent=self.section_splitter
83
- )
84
-
85
- crew = Crew(
86
- agents=[self.section_splitter],
87
- tasks=[task],
88
- process=Process.sequential,
89
- verbose=False,
90
- telemetry=False
91
- )
92
-
93
- result = crew.kickoff()
94
- return self._parse_sections_result(result)
95
-
96
- def extract_all_sections(self, sections: Dict[str, str]) -> Dict[str, Any]:
97
- # Créer les tâches avec les sections en input
98
- tasks = self._create_extraction_tasks(sections)
99
-
100
- crew = Crew(
101
- agents=[
102
- self.contact_extractor,
103
- self.skills_extractor,
104
- self.experience_extractor,
105
- self.project_extractor,
106
- self.education_extractor,
107
- self.reconversion_detector,
108
- self.profile_builder
109
- ],
110
- tasks=tasks,
111
- process=Process.sequential,
112
- verbose=True, # Activer pour debug
113
- telemetry=False
114
- )
115
-
116
- # Passer les sections comme inputs
117
- inputs = {
118
- "contact": sections.get("contact", ""),
119
- "experiences": sections.get("experiences", ""),
120
- "projects": sections.get("projects", ""),
121
- "education": sections.get("education", ""),
122
- "skills": sections.get("skills", ""),
123
- "other": sections.get("other", "")
124
- }
125
-
126
- logger.info(f"Starting crew with inputs: {list(inputs.keys())}")
127
- result = crew.kickoff(inputs=inputs)
128
- logger.info(f"Crew completed. Raw result: {result.raw if hasattr(result, 'raw') else str(result)[:200]}...")
129
-
130
- return self._parse_final_result(result)
131
-
132
- def _create_extraction_tasks(self, sections: Dict[str, str]) -> List[Task]:
133
- contact_task = Task(
134
- description=(
135
- "Voici la section contact du CV : {contact}\n"
136
- "Extraire précisément le nom, email, téléphone et localisation du candidat."
137
- ),
138
- expected_output='{"nom": "...", "email": "...", "numero_de_telephone": "...", "localisation": "..."}',
139
- agent=self.contact_extractor
140
- )
141
-
142
- skills_task = Task(
143
- description=(
144
- "Voici les sections pertinentes du CV :\n"
145
- "Expériences: {experiences}\n"
146
- "Projets: {projects}\n"
147
- "Compétences: {skills}\n"
148
- "Extraire toutes les compétences techniques (hard skills) et comportementales (soft skills) mentionnées."
149
- ),
150
- expected_output='{"hard_skills": ["compétence1", "compétence2"], "soft_skills": ["compétence1", "compétence2"]}',
151
- agent=self.skills_extractor
152
- )
153
-
154
- experience_task = Task(
155
- description=(
156
- "Voici la section expériences du CV : {experiences}\n"
157
- "Extraire toutes les expériences professionnelles avec poste, entreprise, dates et responsabilités."
158
- ),
159
- expected_output='[{"Poste": "titre", "Entreprise": "nom", "start_date": "date", "end_date": "date", "responsabilités": ["resp1", "resp2"]}]',
160
- agent=self.experience_extractor
161
- )
162
-
163
- project_task = Task(
164
- description=(
165
- "Voici les sections projets et expériences du CV :\n"
166
- "Projets: {projects}\n"
167
- "Identifier et extraire les projets professionnels et personnels distincts des responsabilités générales."
168
- ),
169
- expected_output='{"professional": [{"title": "titre", "technologies": ["tech1"], "outcomes": ["résultat1"]}], "personal": []}',
170
- agent=self.project_extractor
171
- )
172
-
173
- education_task = Task(
174
- description=(
175
- "Voici la section formations du CV : {education}\n"
176
- "Extraire toutes les formations, diplômes et certifications avec institution et dates."
177
- ),
178
- expected_output='[{"degree": "diplôme", "institution": "établissement", "start_date": "date", "end_date": "date"}]',
179
- agent=self.education_extractor
180
- )
181
-
182
- reconversion_task = Task(
183
- description=(
184
- "En analysant les expériences extraites précédemment, déterminer si le candidat est en reconversion professionnelle. "
185
- "Chercher des changements de secteur, de type de poste ou des transitions significatives."
186
- ),
187
- expected_output='{"reconversion_analysis": {"is_reconversion": true, "analysis": "Explication détaillée..."}}',
188
- agent=self.reconversion_detector,
189
- context=[experience_task]
190
- )
191
-
192
- profile_task = Task(
193
- description=(
194
- "Assembler toutes les informations extraites des tâches précédentes en un profil candidat complet. "
195
- "Créer un JSON valide avec une clé 'candidat' contenant toutes les sections."
196
- ),
197
- expected_output=(
198
- '{"candidat": {'
199
- '"informations_personnelles": {...}, '
200
- '"compétences": {...}, '
201
- '"expériences": [...], '
202
- '"projets": {...}, '
203
- '"formations": [...], '
204
- '"reconversion": {...}'
205
- '}}'
206
- ),
207
- agent=self.profile_builder,
208
- context=[contact_task, skills_task, experience_task, project_task, education_task, reconversion_task]
209
- )
210
-
211
- return [contact_task, skills_task, experience_task, project_task, education_task, reconversion_task, profile_task]
212
-
213
- def _parse_sections_result(self, result) -> Dict[str, str]:
214
- result_str = result.raw if hasattr(result, 'raw') else str(result)
215
-
216
- if '```json' in result_str:
217
- result_str = result_str.split('```json')[1].split('```')[0].strip()
218
- elif '```' in result_str:
219
- parts = result_str.split('```')
220
- if len(parts) >= 3:
221
- result_str = parts[1].strip()
222
-
223
- parsed = json.loads(result_str)
224
-
225
- # Assurer que toutes les sections nécessaires existent
226
- default_sections = {
227
- "contact": "",
228
- "experiences": "",
229
- "projects": "",
230
- "education": "",
231
- "skills": "",
232
- "other": ""
233
- }
234
-
235
- for key in default_sections:
236
- if key not in parsed:
237
- parsed[key] = default_sections[key]
238
-
239
- return parsed
240
-
241
- def _parse_final_result(self, result) -> Dict[str, Any]:
242
- result_str = result.raw if hasattr(result, 'raw') else str(result)
243
-
244
- if '```json' in result_str:
245
- result_str = result_str.split('```json')[1].split('```')[0].strip()
246
- elif '```' in result_str:
247
- parts = result_str.split('```')
248
- if len(parts) >= 3:
249
- result_str = parts[1].strip()
250
-
251
- return json.loads(result_str)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/scoring_agent.py DELETED
@@ -1,183 +0,0 @@
1
- import json
2
- import re
3
- import logging
4
- from datetime import datetime
5
- from typing import Dict, List, Any
6
-
7
- logger = logging.getLogger(__name__)
8
-
9
- class SimpleScoringAgent:
10
-
11
- def calculate_scores(self, candidat_data: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]:
12
- if not candidat_data or not isinstance(candidat_data, dict):
13
- return {"analyse_competences": []}
14
-
15
- skills_data = candidat_data.get("compétences", {})
16
- skills_list = self._extract_skills_list(skills_data)
17
-
18
- if not skills_list:
19
- return {"analyse_competences": []}
20
-
21
- skill_analysis = []
22
-
23
- for skill in skills_list:
24
- level = self._determine_skill_level(skill, candidat_data)
25
- skill_analysis.append({
26
- "skill": skill,
27
- "level": level
28
- })
29
-
30
- return {"analyse_competences": skill_analysis}
31
-
32
- def _extract_skills_list(self, skills_data: Dict[str, Any]) -> List[str]:
33
- """Extrait la liste des compétences"""
34
- skills_list = []
35
-
36
- if isinstance(skills_data, dict):
37
- skills_list.extend(skills_data.get("hard_skills", []))
38
- skills_list.extend(skills_data.get("soft_skills", []))
39
- elif isinstance(skills_data, list):
40
- skills_list = [item.get("nom") for item in skills_data if item.get("nom")]
41
-
42
- return [skill for skill in skills_list if skill and isinstance(skill, str) and skill.strip()]
43
-
44
- def _determine_skill_level(self, skill: str, candidat_data: Dict[str, Any]) -> str:
45
- """Détermine le niveau d'une compétence selon des règles simples"""
46
-
47
- frequency = self._count_skill_mentions(skill, candidat_data)
48
- max_duration = self._get_max_duration_for_skill(skill, candidat_data)
49
- has_pro_experience = self._has_professional_experience(skill, candidat_data)
50
-
51
- # Règles simples de classification
52
- if has_pro_experience and max_duration >= 3.0:
53
- return "expert"
54
- elif has_pro_experience and max_duration >= 1.0:
55
- return "avance"
56
- elif frequency >= 3 or max_duration >= 0.5:
57
- return "intermediaire"
58
- else:
59
- return "debutant"
60
-
61
- def _count_skill_mentions(self, skill: str, candidat_data: Dict[str, Any]) -> int:
62
- """Compte le nombre de mentions de la compétence"""
63
- skill_lower = skill.lower()
64
- total_mentions = 0
65
-
66
- # Recherche dans toutes les sections
67
- all_text = self._get_all_text_content(candidat_data).lower()
68
- total_mentions = all_text.count(skill_lower)
69
-
70
- return total_mentions
71
-
72
- def _get_max_duration_for_skill(self, skill: str, candidat_data: Dict[str, Any]) -> float:
73
- """Trouve la durée maximum d'utilisation de la compétence"""
74
- skill_lower = skill.lower()
75
- max_duration = 0.0
76
-
77
- experiences_key = "expériences" if "expériences" in candidat_data else "experiences_professionnelles"
78
- experiences = candidat_data.get(experiences_key, [])
79
-
80
- if not isinstance(experiences, list):
81
- return 0.0
82
-
83
- for exp in experiences:
84
- if not isinstance(exp, dict):
85
- continue
86
-
87
- exp_text = json.dumps(exp, ensure_ascii=False).lower()
88
-
89
- if skill_lower in exp_text:
90
- duration = self._calculate_experience_duration(exp)
91
- max_duration = max(max_duration, duration)
92
-
93
- return max_duration
94
-
95
- def _has_professional_experience(self, skill: str, candidat_data: Dict[str, Any]) -> bool:
96
- """Vérifie si la compétence a été utilisée en contexte professionnel"""
97
- skill_lower = skill.lower()
98
-
99
- experiences_key = "expériences" if "expériences" in candidat_data else "experiences_professionnelles"
100
- experiences = candidat_data.get(experiences_key, [])
101
-
102
- if not isinstance(experiences, list):
103
- return False
104
-
105
- for exp in experiences:
106
- if not isinstance(exp, dict):
107
- continue
108
-
109
- exp_text = json.dumps(exp, ensure_ascii=False).lower()
110
- if skill_lower in exp_text:
111
- return True
112
-
113
- return False
114
-
115
- def _get_all_text_content(self, candidat_data: Dict[str, Any]) -> str:
116
- """Récupère tout le contenu textuel du CV"""
117
- all_content = []
118
-
119
- # Expériences
120
- experiences_key = "expériences" if "expériences" in candidat_data else "experiences_professionnelles"
121
- for exp in candidat_data.get(experiences_key, []):
122
- if isinstance(exp, dict):
123
- all_content.append(json.dumps(exp, ensure_ascii=False))
124
-
125
- # Projets
126
- projects = candidat_data.get("projets", {})
127
- if isinstance(projects, dict):
128
- for project_type in ["professional", "personal"]:
129
- for project in projects.get(project_type, []):
130
- if isinstance(project, dict):
131
- all_content.append(json.dumps(project, ensure_ascii=False))
132
-
133
- # Formations
134
- for formation in candidat_data.get("formations", []):
135
- if isinstance(formation, dict):
136
- all_content.append(json.dumps(formation, ensure_ascii=False))
137
-
138
- return " ".join(all_content)
139
-
140
- def _calculate_experience_duration(self, exp: Dict[str, Any]) -> float:
141
- """Calcule la durée d'une expérience en années"""
142
- start_date_str = exp.get("date_debut", exp.get("start_date", ""))
143
- end_date_str = exp.get("date_fin", exp.get("end_date", ""))
144
-
145
- if not isinstance(start_date_str, str):
146
- start_date_str = str(start_date_str) if start_date_str else ""
147
- if not isinstance(end_date_str, str):
148
- end_date_str = str(end_date_str) if end_date_str else ""
149
-
150
- return self._calculate_duration_in_years(start_date_str, end_date_str)
151
-
152
- def _calculate_duration_in_years(self, start_date_str: str, end_date_str: str) -> float:
153
- """Calcule la durée entre deux dates en années"""
154
- start_date = self._parse_date(start_date_str)
155
- end_date = self._parse_date(end_date_str)
156
-
157
- if start_date and end_date:
158
- if end_date < start_date:
159
- return 0.0
160
- return (end_date - start_date).days / 365.25
161
-
162
- return 0.0
163
-
164
- def _parse_date(self, date_str: str) -> datetime:
165
- """Parse une date de manière simple"""
166
- if not date_str or not isinstance(date_str, str):
167
- return None
168
-
169
- date_str_lower = date_str.lower().strip()
170
- if date_str_lower in ["aujourd'hui", "maintenant", "en cours", "current", "présent", "actuellement"]:
171
- return datetime.now()
172
-
173
- # Extraction simple de l'année
174
- year_match = re.search(r'\b(20\d{2}|19\d{2})\b', date_str)
175
- if year_match:
176
- year = int(year_match.group(1))
177
- return datetime(year, 1, 1)
178
-
179
- return None
180
-
181
- # Alias pour maintenir la compatibilité
182
- ScoringAgent = SimpleScoringAgent
183
- ImprovedScoringAgent = SimpleScoringAgent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/core/rag_handler.py DELETED
@@ -1,130 +0,0 @@
1
- import os
2
- import logging
3
- from typing import Optional, List
4
- from langchain_community.document_loaders import DirectoryLoader, TextLoader
5
- from langchain_community.vectorstores import FAISS
6
- from langchain_text_splitters import RecursiveCharacterTextSplitter
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
- _embeddings_model = None
11
- _rag_handler_instance = None
12
-
13
- VECTOR_STORE_PATH = "/tmp/vector_store"
14
-
15
- def get_embeddings_model():
16
- global _embeddings_model
17
- if _embeddings_model is None:
18
- from langchain_huggingface import HuggingFaceEmbeddings
19
- logger.info("Initialisation du modèle d'embeddings...")
20
- _embeddings_model = HuggingFaceEmbeddings(
21
- model_name='sentence-transformers/all-MiniLM-L6-v2',
22
- model_kwargs={'device': 'cpu'},
23
- encode_kwargs={'normalize_embeddings': True}
24
- )
25
- logger.info("✅ Modèle d'embeddings initialisé avec succès")
26
- return _embeddings_model
27
-
28
- class RAGHandler:
29
- def __init__(self, knowledge_base_path: str = "/app/knowledge_base", lazy_init: bool = True):
30
- self.knowledge_base_path = knowledge_base_path
31
- self.embeddings = None
32
- self.vector_store = None
33
- self._initialized = False
34
-
35
- os.makedirs(VECTOR_STORE_PATH, exist_ok=True)
36
-
37
- if not lazy_init:
38
- self._initialize()
39
-
40
- def _initialize(self):
41
- if self._initialized:
42
- return
43
-
44
- logger.info("Initialisation du RAG Handler...")
45
- self.embeddings = get_embeddings_model()
46
-
47
- if self.embeddings is None:
48
- logger.error("Impossible d'initialiser les embeddings")
49
- return
50
-
51
- self.vector_store = self._load_or_create_vector_store(self.knowledge_base_path)
52
- self._initialized = True
53
- logger.info("✅ RAG Handler initialisé avec succès")
54
-
55
- def _load_documents(self, path: str) -> List:
56
- if not os.path.exists(path):
57
- logger.warning(f"Répertoire {path} non trouvé")
58
- return []
59
-
60
- loader = DirectoryLoader(
61
- path,
62
- glob="**/*.md",
63
- loader_cls=TextLoader,
64
- loader_kwargs={"encoding": "utf-8"}
65
- )
66
- logger.info(f"Chargement des documents depuis : {path}")
67
- documents = loader.load()
68
- logger.info(f"✅ {len(documents)} documents chargés")
69
- return documents
70
-
71
- def _create_vector_store(self, knowledge_base_path: str) -> Optional[FAISS]:
72
- documents = self._load_documents(knowledge_base_path)
73
- if not documents:
74
- logger.warning("Aucun document trouvé - création d'un vector store vide")
75
- from langchain.schema import Document
76
- dummy_doc = Document(
77
- page_content="Document de test pour initialiser le vector store",
78
- metadata={"source": "dummy"}
79
- )
80
- documents = [dummy_doc]
81
-
82
- logger.info(f"{len(documents)} documents chargés. Création des vecteurs...")
83
- text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
84
- texts = text_splitter.split_documents(documents)
85
-
86
- vector_store = FAISS.from_documents(texts, self.embeddings)
87
-
88
- vector_store.save_local(VECTOR_STORE_PATH)
89
- logger.info(f"✅ Vector store créé et sauvegardé dans : {VECTOR_STORE_PATH}")
90
-
91
- return vector_store
92
-
93
- def _load_or_create_vector_store(self, knowledge_base_path: str) -> Optional[FAISS]:
94
- index_path = os.path.join(VECTOR_STORE_PATH, "index.faiss")
95
- if os.path.exists(index_path):
96
- logger.info(f"Chargement du vector store existant depuis : {VECTOR_STORE_PATH}")
97
- return FAISS.load_local(
98
- VECTOR_STORE_PATH,
99
- embeddings=self.embeddings,
100
- allow_dangerous_deserialization=True
101
- )
102
- else:
103
- logger.info("Aucun vector store trouvé. Création d'un nouveau...")
104
- return self._create_vector_store(knowledge_base_path)
105
-
106
- def get_relevant_feedback(self, query: str, k: int = 1) -> List[str]:
107
- if not self._initialized:
108
- self._initialize()
109
-
110
- if not self.vector_store:
111
- logger.warning("Vector store non disponible - retour de conseils génériques")
112
- return [
113
- "Préparez vos réponses aux questions comportementales",
114
- "Montrez votre motivation pour le poste",
115
- "Donnez des exemples concrets de vos réalisations"
116
- ]
117
-
118
- results = self.vector_store.similarity_search(query, k=k)
119
- feedback = [doc.page_content for doc in results if doc.page_content.strip()]
120
-
121
- if not feedback:
122
- return ["Conseil général: Préparez-vous bien pour les entretiens futurs."]
123
-
124
- return feedback
125
-
126
- def get_rag_handler() -> Optional[RAGHandler]:
127
- global _rag_handler_instance
128
- if _rag_handler_instance is None:
129
- _rag_handler_instance = RAGHandler(lazy_init=True)
130
- return _rag_handler_instance
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/{agents → crew}/__init__.py RENAMED
File without changes
src/crew/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (137 Bytes). View file
 
src/crew/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (190 Bytes). View file
 
src/crew/__pycache__/__init__.cpython-312.pycZone.Identifier ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [ZoneTransfer]
2
+ ZoneId=3
src/crew/__pycache__/agents.cpython-311.pyc ADDED
Binary file (3.57 kB). View file
 
src/crew/__pycache__/agents.cpython-312.pyc ADDED
Binary file (3.43 kB). View file
 
src/crew/__pycache__/agents.cpython-312.pycZone.Identifier ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [ZoneTransfer]
2
+ ZoneId=3
src/crew/__pycache__/analysis_crew.cpython-312.pyc ADDED
Binary file (1.08 kB). View file
 
src/crew/__pycache__/analysis_crew.cpython-312.pycZone.Identifier ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [ZoneTransfer]
2
+ ZoneId=3
src/crew/__pycache__/crew_pool.cpython-311.pyc ADDED
Binary file (2.46 kB). View file
 
src/crew/__pycache__/crew_pool.cpython-312.pyc ADDED
Binary file (2.18 kB). View file
 
src/crew/__pycache__/crew_pool.cpython-312.pycZone.Identifier ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [ZoneTransfer]
2
+ ZoneId=3
src/crew/__pycache__/tasks.cpython-312.pyc ADDED
Binary file (8.39 kB). View file
 
src/crew/__pycache__/tasks.cpython-312.pycZone.Identifier ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [ZoneTransfer]
2
+ ZoneId=3
src/crew/agents.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai import Agent
2
+ from crewai import LLM
3
+ from src.config import crew_openai
4
+
5
+ LLM_agent = crew_openai()
6
+
7
+ # Interview Simulation Agents
8
+ report_generator_agent = Agent(
9
+ role='Rédacteur de Rapports Synthétiques',
10
+ goal='Générer un feedback pertinent, a partir du deroulement de lentretient',
11
+ backstory=(
12
+ "Sepcialisé dans le recrutement et les ressources humaines, capable d'evaluer les candidats"
13
+ "sur la communication et la pertinences des reponses en fonction des questions posées, redige"
14
+ "en un rapport clair, un feedback détaillé sur le candidat."
15
+ ),
16
+ allow_delegation=False,
17
+ verbose=False,
18
+ llm=LLM_agent
19
+ )
20
+
21
+ # CV Parsing Agents
22
+ skills_extractor_agent = Agent(
23
+ role="Spécialiste de l'extraction de compétences (hard & soft skills)",
24
+ goal="Identifier et extraire toutes les compétences pertinentes du CV.",
25
+ backstory="Vous êtes un spécialiste des compétences techniques et comportementales. Votre mission est de parcourir les CV et de lister de manière exhaustive toutes les compétences mentionnées.",
26
+ verbose=False,
27
+ llm=LLM_agent
28
+ )
29
+ experience_extractor_agent = Agent(
30
+ role="Expert en extraction d'expérience professionnelle",
31
+ goal="Extraire en détail l'expérience professionnelle du candidat.",
32
+ backstory="Vous êtes un expert en recrutement spécialisé dans l'analyse des parcours professionnels. Vous devez extraire chaque expérience de manière précise, en notant les rôles, les entreprises, les dates et les responsabilités.",
33
+ verbose=False,
34
+ llm=LLM_agent
35
+ )
36
+ project_extractor_agent = Agent(
37
+ role="Spécialiste de l'identification de projets (pro & perso)",
38
+ goal="Identifier et décrire les projets significatifs mentionnés.",
39
+ backstory="Vous êtes passionné par l'innovation et les réalisations. Votre rôle est de repérer et de décrire les projets professionnels et personnels qui mettent en lumière les compétences et l'initiative des candidats.",
40
+ verbose=False,
41
+ llm=LLM_agent
42
+ )
43
+ education_extractor_agent = Agent(
44
+ role="Expert en extraction d'informations sur la formation",
45
+ goal="Extraire les détails des études et des diplômes obtenus.",
46
+ backstory="Vous êtes un spécialiste des parcours académiques. Votre tâche est d'extraire avec précision les informations relatives aux études, aux diplômes et aux établissements fréquentés par les candidats.",
47
+ verbose=False,
48
+ llm=LLM_agent
49
+ )
50
+ informations_personnelle_agent = Agent(
51
+ role="Spécialiste de l'extraction des coordonnées",
52
+ goal="Identifier et extraire précisément les coordonnées du candidat.",
53
+ backstory="Vous êtes un expert en analyse de CV, particulièrement doué pour localiser et extraire les informations de contact. Votre rôle est de trouver le nom, l'adresse e-mail, le numéro de téléphone et la localisation (ville ou région) du candidat, généralement situés en haut ou à la fin du CV.",
54
+ verbose=False,
55
+ llm=LLM_agent
56
+ )
57
+ ProfileBuilderAgent = Agent(
58
+ role='Constructeur de Profil CV',
59
+ goal='Créer un profil JSON structuré et valide avec la clé candidat',
60
+ backstory=(
61
+ "Tu es un expert en structuration de données JSON. "
62
+ "Ta mission est de créer un profil candidat parfaitement formaté "
63
+ "en respectant scrupuleusement la structure JSON demandée."
64
+ ),
65
+ verbose=True,
66
+ llm=LLM_agent
67
+ )
68
+
69
+ reconversion_detector_agent = Agent(
70
+ role="Détecteur de Reconversion Professionnelle",
71
+ goal="Analyser la chronologie des expériences pour identifier les changements de carrière significatifs.",
72
+ backstory="Vous êtes un conseiller d'orientation expert, capable de repérer les transitions de carrière, d'identifier les compétences transférables et de valoriser les parcours non linéaires. Votre analyse doit mettre en lumière les changements de secteur, de type de poste ou de niveau de responsabilité.",
73
+ verbose=False,
74
+ llm=LLM_agent
75
+ )
src/crew/crew_pool.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai import Crew, Process
2
+ from langchain_core.tools import tool
3
+ import json
4
+ from pydantic import BaseModel, Field
5
+ from typing import Dict, List, Any, Type
6
+ from .agents import report_generator_agent, skills_extractor_agent, experience_extractor_agent, project_extractor_agent, education_extractor_agent, ProfileBuilderAgent, informations_personnelle_agent, reconversion_detector_agent
7
+ from .tasks import generate_report_task, task_extract_skills, task_extract_experience, task_extract_projects, task_extract_education, task_build_profile, task_extract_informations, task_detect_reconversion
8
+ from src.deep_learning_analyzer import MultiModelInterviewAnalyzer
9
+ from src.rag_handler import RAGHandler
10
+ from langchain_core.tools import BaseTool
11
+
12
+ @tool
13
+ def interview_analyser(conversation_history: list, job_description_text: list) -> str:
14
+ """
15
+ Appelle cet outil à la toute fin d'un entretien d'embauche pour analyser
16
+ l'intégralité de la conversation et générer un rapport de feedback.
17
+ Ne l'utilise PAS pour répondre à une question normale, mais seulement pour conclure et analyser l'entretien.
18
+ """
19
+ # 1. Analyse DL de la conversation
20
+ analyzer = MultiModelInterviewAnalyzer()
21
+ structured_analysis = analyzer.run_full_analysis(conversation_history, job_description_text)
22
+
23
+ # 2. Enrichissement avec RAG
24
+ rag_handler = RAGHandler()
25
+ rag_feedback = []
26
+ # Extraire les intentions et sentiments pour trouver des conseils pertinents
27
+ if structured_analysis.get("intent_analysis"):
28
+ for intent in structured_analysis["intent_analysis"]:
29
+ # Exemple de requête basée sur l'intention
30
+ query = f"Conseils pour un candidat qui cherche à {intent['labels'][0]}"
31
+ rag_feedback.extend(rag_handler.get_relevant_feedback(query))
32
+
33
+ if structured_analysis.get("sentiment_analysis"):
34
+ for sentiment_group in structured_analysis["sentiment_analysis"]:
35
+ for sentiment in sentiment_group:
36
+ if sentiment['label'] == 'stress' and sentiment['score'].item() > 0.6:
37
+ rag_feedback.extend(rag_handler.get_relevant_feedback("gestion du stress en entretien"))
38
+ unique_feedback = list(set(rag_feedback))
39
+ interview_crew = Crew(
40
+ agents=[report_generator_agent],
41
+ tasks=[generate_report_task],
42
+ process=Process.sequential,
43
+ verbose=False,
44
+ telemetry=False
45
+ )
46
+
47
+ final_report = interview_crew.kickoff(inputs={
48
+ 'structured_analysis_data': json.dumps(structured_analysis, indent=2),
49
+ 'rag_contextual_feedback': "\n".join(unique_feedback)
50
+ })
51
+ return final_report
52
+
53
+ def analyse_cv(cv_content: str) -> json:
54
+ crew = Crew(
55
+ agents=[
56
+ informations_personnelle_agent,
57
+ skills_extractor_agent,
58
+ experience_extractor_agent,
59
+ project_extractor_agent,
60
+ education_extractor_agent,
61
+ reconversion_detector_agent,
62
+
63
+ ProfileBuilderAgent
64
+ ],
65
+ tasks=[
66
+ task_extract_informations,
67
+ task_extract_skills,
68
+ task_extract_experience,
69
+ task_extract_projects,
70
+ task_extract_education,
71
+ task_detect_reconversion,
72
+ task_build_profile
73
+ ],
74
+ process=Process.sequential,
75
+ verbose=False,
76
+ telemetry=False
77
+ )
78
+ result = crew.kickoff(inputs={"cv_content": cv_content})
79
+ return result
80
+
81
+
src/crew/tasks.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai import Task
2
+ from .agents import report_generator_agent, skills_extractor_agent, experience_extractor_agent, project_extractor_agent, education_extractor_agent, ProfileBuilderAgent, informations_personnelle_agent, reconversion_detector_agent
3
+
4
+ generate_report_task = Task(
5
+ description=(
6
+ """Tu es un rédacteur expert en RH. Ta mission est de rédiger un rapport d'évaluation final.
7
+ Tu dois utiliser deux sources d'information principales :
8
+ 1. Les données d'analyse structurées de l'entretien : '{structured_analysis_data}'.
9
+ 2. Une liste de conseils et de feedback pertinents issus de notre base de connaissances : '{rag_contextual_feedback}'.
10
+
11
+ Ta tâche est de synthétiser ces informations en un rapport cohérent et actionnable."""
12
+ ),
13
+ expected_output=(
14
+ """Un rapport final exceptionnel basé sur l'analyse fournie. Le rapport doit être structuré comme suit:
15
+ 1. **Résumé et Score d'Adéquation** : Synthétise le score de similarité sémantique et donne un aperçu global.
16
+ 2. **Analyse Comportementale** : Interprète les résultats de l'analyse de sentiment et d'intention pour décrire le comportement du candidat.
17
+ 3. **Adéquation Sémantique avec le Poste** : Explique ce que signifie le score de similarité.
18
+ 4. **Points Forts & Axes d'Amélioration Personnalisés** : Utilise les données d'analyse pour identifier les points à améliorer. Ensuite, intègre de manière fluide et naturelle les conseils pertinents de '{rag_contextual_feedback}' pour proposer des pistes d'amélioration concrètes et personnalisées. Ne te contente pas de copier-coller le feedback, mais reformule-le pour qu'il s'intègre parfaitement au rapport.
19
+ 5. **Recommandation Finale**."""
20
+ ),
21
+ agent=report_generator_agent,
22
+ )
23
+
24
+ task_extract_skills = Task(
25
+ description=(
26
+ "Voici le contenu du CV :\n\n{cv_content}\n\n"
27
+ "Extraire uniquement les compétences mentionnées explicitement dans le texte du CV. "
28
+ "Séparer les hard skills (techniques) et les soft skills (comportementales) en analysant les listes ou phrases les contenant. "
29
+ "Les hards skills doivent comprendre des compétences techniques, outils, langages de programmation, etc. "
30
+ "Ne rien inventer. Ne pas déduire de compétences à partir d'un poste ou d'une expérience implicite. "
31
+ "Identifie clairement les compétences, et n'en exclue aucune. "
32
+ "\n\n**CONTRAINTES JSON STRICTES:**\n"
33
+ "- Utiliser UNIQUEMENT des guillemets doubles (\") pour les chaînes\n"
34
+ "- Aucune virgule finale dans les listes ou objets\n"
35
+ "- Vérifier la syntaxe JSON avant de retourner le résultat\n"
36
+ "- Échapper correctement les caractères spéciaux (\\, \", \\n, etc.)"
37
+ ),
38
+ agent=skills_extractor_agent,
39
+ input_keys=["cv_content"],
40
+ expected_output=(
41
+ "Un dictionnaire JSON VALIDE 'Compétences' avec deux clés : 'hard_skills' et 'soft_skills', "
42
+ "contenant uniquement des listes de compétences présentes dans le texte. "
43
+ "FORMAT EXACT: {\"hard_skills\": [\"compétence1\", \"compétence2\"], \"soft_skills\": [\"compétence1\", \"compétence2\"]}"
44
+ )
45
+ )
46
+
47
+ task_extract_experience = Task(
48
+ description=(
49
+ "Voici le contenu du CV :\n\n{cv_content}\n\n"
50
+ """
51
+ Extrais toutes les expériences professionnelles du CV. Pour chaque expérience, tu DOIS fournir les informations suivantes :
52
+ - Poste: Le titre du poste.
53
+ - Entreprise: Le nom de l'entreprise.
54
+ - start_date: La date de début. Si non trouvée, retourne "Non spécifié".
55
+ - end_date: La date de fin. Si le poste est actuel, utilise "Aujourd'hui". Si non trouvée, retourne "Non spécifié".
56
+ - responsabilités: Une liste des tâches et missions.
57
+
58
+ RÈGLES STRICTES :
59
+ 1. NE JAMAIS laisser un champ vide (""). Si une information est introuvable, utilise la valeur "Non spécifié".
60
+ 2. Analyse attentivement les dates. "Depuis 2023" signifie que la date de fin est "Aujourd'hui".
61
+ """
62
+ ),
63
+ agent=experience_extractor_agent,
64
+ input_keys=["cv_content"],
65
+ expected_output=(
66
+ "Un tableau JSON VALIDE d'objets 'Expérience Professionnelle' avec 5 clés par expérience : "
67
+ "'Poste', 'Entreprise', 'start_date', 'end_date', 'responsabilités'. "
68
+ "FORMAT EXACT: [{\"Poste\": \"titre\", \"Entreprise\": \"nom\", \"start_date\": \"année\", \"end_date\": \"année\", \"responsabilités\": [\"resp1\", \"resp2\"]}]"
69
+ )
70
+ )
71
+
72
+ task_extract_projects = Task(
73
+ description=(
74
+ "Voici le contenu du CV :\n\n{cv_content}\n\n"
75
+ """
76
+ Identifie et extrais les PROJETS SPÉCIFIQUES mentionnés dans le CV.
77
+ Un projet est distinct d'une expérience professionnelle générale. Il a un nom ou un objectif clair.
78
+
79
+ RÈGLES STRICTES :
80
+ 1. NE PAS extraire les responsabilités générales d'un poste en tant que projet. Par exemple, si le CV dit "Alternant chez Enedis où j'ai mené le projet 'Simulateur IA'", alors extrais 'Simulateur IA' comme projet. Ne copie pas toutes les tâches de l'alternance.
81
+ 2. Si un projet est clairement lié à une expérience professionnelle, essaie de le noter, mais le plus important est de décrire le projet lui-même.
82
+ """
83
+ ),
84
+ agent=project_extractor_agent,
85
+ input_keys=["cv_content"],
86
+ expected_output=(
87
+ "Un dictionnaire JSON VALIDE 'Projets' avec deux clés : 'professional' et 'personal'. "
88
+ "Chaque clé contient une liste de dictionnaires, chaque dictionnaire représentant un projet avec les clés 'title', 'role', 'technologies', et 'outcomes'. "
89
+ "FORMAT EXACT: {\"professional\": [{\"title\": \"titre\", \"role\": \"rôle\", \"technologies\": [\"tech1\"], \"outcomes\": [\"résultat1\"]}], \"personal\": []}"
90
+ )
91
+ )
92
+
93
+ task_extract_education = Task(
94
+ description=(
95
+ "Voici le contenu du CV :\n\n{cv_content}\n\n"
96
+ """
97
+ Extrais le parcours de formation et les certifications. Fais une distinction claire entre les types de formation.
98
+ Pour chaque élément, fournis :
99
+ - degree: Le nom du diplôme, du titre (ex: 'Titre RNCP niveau 6') ou de la certification (ex: 'Core Designer Certification').
100
+ - institution: L'école, l'université ou la plateforme (ex: 'WILD CODE SCHOOL', 'DataIku', 'DataCamp').
101
+ - start_date: La date de début. Si non trouvée, retourne "Non spécifié".
102
+ - end_date: La date de fin. Si non trouvée, retourne "Non spécifié".
103
+
104
+ RÈGLES STRICTES :
105
+ 1. Si tu vois une certification comme "DataIku (core designer)", le diplôme est "Core Designer" et l'institution est "DataIku". NE PAS les mélanger.
106
+ 2. NE PAS extraire une simple compétence (ex: 'Python') comme une formation.
107
+ """
108
+ ),
109
+ agent=education_extractor_agent,
110
+ input_keys=["cv_content"],
111
+ expected_output=(
112
+ "Un tableau JSON VALIDE d'objets 'Formation' avec les clés : 'degree', 'institution', 'start_date', 'end_date'. "
113
+ "FORMAT EXACT: [{\"degree\": \"diplôme\", \"institution\": \"établissement\", \"start_date\": \"année\", \"end_date\": \"année\"]}"
114
+ )
115
+ )
116
+
117
+ task_extract_informations = Task(
118
+ description=(
119
+ "Voici le contenu du CV :\n\n{cv_content}\n\n"
120
+ "Votre tâche est d'extraire les informations de contact du candidat. Ces informations se trouvent généralement au début ou à la fin du CV, souvent sous une section intitulée 'CONTACT'.\n"
121
+ "Extrayez précisément :\n"
122
+ "- Le **Nom complet**.\n"
123
+ "- L'**Adresse e-mail**.\n"
124
+ "- Le **Numéro de téléphone**.\n"
125
+ "- La **Localisation** (ville ou région).\n"
126
+ "toutes les informations devront être normalisées, principalement le nom si il est en majuscule en titre. "
127
+ ),
128
+ agent=informations_personnelle_agent,
129
+ input_keys=["cv_content"],
130
+ expected_output=(
131
+ "Un dictionnaire JSON VALIDE 'informations_personnelles' contenant le nom, l'email, le numéro de téléphone et la localisation du candidat. "
132
+ "FORMAT EXACT: {\"nom\": \"nom\", \"email\": \"email\", \"numero_de_telephone\": \"tel\", \"localisation\": \"lieu\"}"
133
+ )
134
+ )
135
+
136
+ task_detect_reconversion = Task(
137
+ description=(
138
+ "En te basant sur les données extraites de la tâche `task_extract_experience`, analyse la chronologie des expériences professionnelles. "
139
+ "Ton objectif est de déterminer si le candidat est en reconversion professionnelle. "
140
+ "Cherche des changements de secteur d'activité (ex: de la restauration à la tech), des changements de type de poste (ex: de commercial à développeur), ou des sauts de carrière importants. "
141
+ "Si une reconversion est détectée, identifie les compétences qui semblent avoir été transférées."
142
+ ),
143
+ agent=reconversion_detector_agent,
144
+ context=[task_extract_experience],
145
+ expected_output=(
146
+ "Un dictionnaire JSON VALIDE avec une clé 'reconversion_analysis'. "
147
+ "Ce dictionnaire doit contenir deux clés : 'is_reconversion' (un booléen) et 'analysis' (une chaîne de caractères expliquant pourquoi, ou pourquoi pas, et listant les compétences transférables si applicable). "
148
+ "FORMAT EXACT: {\"reconversion_analysis\": {\"is_reconversion\": true, \"analysis\": \"Le candidat a changé de secteur...\"}}"
149
+ )
150
+ )
151
+
152
+ task_build_profile = Task(
153
+ description=(
154
+ "Ta mission est d'agir comme un architecte de données. En utilisant les extractions des tâches précédentes, "
155
+ "assemble un profil de candidat complet. "
156
+ "Le résultat final doit être un unique objet JSON, parfaitement valide."
157
+ ),
158
+ agent=ProfileBuilderAgent,
159
+ context=[
160
+ task_extract_informations,
161
+ task_extract_skills,
162
+ task_extract_experience,
163
+ task_extract_projects,
164
+ task_extract_education,
165
+ task_detect_reconversion
166
+ ],
167
+ expected_output=(
168
+ "Retourner un unique objet JSON valide. Cet objet doit avoir une seule clé à la racine : 'candidat'. "
169
+ "La valeur de cette clé sera un autre objet contenant toutes les informations assemblées. "
170
+ "Assure-toi que la syntaxe est parfaite, que tous les guillemets sont des guillemets doubles et qu'il n'y a aucune virgule finale. "
171
+ "Le JSON doit être immédiatement parsable par un programme.\n\n"
172
+ "FORMAT EXACT:\n"
173
+ "{\n"
174
+ " \"candidat\": {\n"
175
+ " \"informations_personnelles\": {\"nom\": \"...\", \"email\": \"...\", ...},\n"
176
+ " \"compétences\": {\"hard_skills\": [...], \"soft_skills\": [...]},\n"
177
+ " \"expériences\": [{\"Poste\": \"...\", ...}],\n"
178
+ " \"projets\": {\"professional\": [...], \"personal\": [...]},\n"
179
+ " \"formations\": [{\"degree\": \"...\", ...}],\n"
180
+ " \"reconversion\": {\"is_reconversion\": true, \"analysis\": \"...\"}\n"
181
+ " }\n"
182
+ "}"
183
+ )
184
+ )
src/cv_parsing_agents.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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):
42
+ return [clean_dict_keys(element) for element in data]
43
+ else:
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": "test@example.com",
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": "test@example.com",
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")
src/{core/deep_learning_analyzer.py → deep_learning_analyzer.py} RENAMED
@@ -1,56 +1,57 @@
1
- import torch
2
- from transformers import pipeline
3
- from sentence_transformers import SentenceTransformer, util
4
- from typing import List, Dict, Any
5
-
6
- class MultiModelInterviewAnalyzer:
7
- def __init__(self):
8
- self.sentiment_analyzer = pipeline(
9
- "text-classification",
10
- model="astrosbd/french_emotion_camembert",
11
- return_all_scores=True,
12
- device=0 if torch.cuda.is_available() else -1,
13
- )
14
- self.similarity_model = SentenceTransformer('all-MiniLM-L6-v2')
15
- self.intent_classifier = pipeline(
16
- "zero-shot-classification",
17
- model="joeddav/xlm-roberta-large-xnli"
18
- )
19
-
20
- def analyze_sentiment(self, messages: List[Dict[str, str]]) -> List[List[Dict[str, Any]]]:
21
- user_messages = [msg['content'] for msg in messages if msg['role'] == 'user']
22
- if not user_messages:
23
- return []
24
- return self.sentiment_analyzer(user_messages)
25
-
26
- def compute_semantic_similarity(self, messages: List[Dict[str, str]], job_requirements: str) -> float:
27
- user_answers = " ".join([msg['content'] for msg in messages if msg['role'] == 'user'])
28
- embedding_answers = self.similarity_model.encode(user_answers, convert_to_tensor=True)
29
- embedding_requirements = self.similarity_model.encode(job_requirements, convert_to_tensor=True)
30
- cosine_score = util.cos_sim(embedding_answers, embedding_requirements)
31
- return cosine_score.item()
32
-
33
- def classify_candidate_intent(self, messages: List[Dict[str, str]]) -> List[Dict[str, Any]]:
34
- user_answers = [msg['content'] for msg in messages if msg['role'] == 'user']
35
- if not user_answers:
36
- return []
37
-
38
- candidate_labels = [
39
- "parle de son expérience technique",
40
- "exprime sa motivation",
41
- "pose une question",
42
- "exprime de l'incertitude ou du stress"
43
- ]
44
- return self.intent_classifier(user_answers, candidate_labels, multi_label=False)
45
-
46
- def run_full_analysis(self, conversation_history: List[Dict[str, str]], job_requirements: str) -> Dict[str, Any]:
47
- sentiment_results = self.analyze_sentiment(conversation_history)
48
- similarity_score = self.compute_semantic_similarity(conversation_history, job_requirements)
49
- intent_results = self.classify_candidate_intent(conversation_history)
50
-
51
- return {
52
- "overall_similarity_score": round(similarity_score, 2),
53
- "sentiment_analysis": sentiment_results,
54
- "intent_analysis": intent_results,
55
- "raw_transcript": conversation_history
56
- }
 
 
1
+ import torch
2
+ from transformers import pipeline
3
+ from sentence_transformers import SentenceTransformer, util
4
+
5
+ class MultiModelInterviewAnalyzer:
6
+ def __init__(self):
7
+ self.sentiment_analyzer = pipeline(
8
+ "text-classification",
9
+ model="astrosbd/french_emotion_camembert",
10
+ return_all_scores=True,
11
+ device=0 if torch.cuda.is_available() else -1,
12
+ )
13
+ self.similarity_model = SentenceTransformer('all-MiniLM-L6-v2')
14
+ self.intent_classifier = pipeline(
15
+ "zero-shot-classification",
16
+ model="joeddav/xlm-roberta-large-xnli"
17
+ #device=0 if torch.cuda.is_available() else -1,
18
+ )
19
+
20
+ def analyze_sentiment(self, messages):
21
+ user_messages = [msg['content'] for msg in messages if msg['role'] == 'user']
22
+ if not user_messages:
23
+ return []
24
+ sentiments = self.sentiment_analyzer(user_messages)
25
+ return sentiments
26
+
27
+ def compute_semantic_similarity(self, messages, job_requirements):
28
+ user_answers = " ".join([msg['content'] for msg in messages if msg['role'] == 'user'])
29
+ embedding_answers = self.similarity_model.encode(user_answers, convert_to_tensor=True)
30
+ embedding_requirements = self.similarity_model.encode(job_requirements, convert_to_tensor=True)
31
+ cosine_score = util.cos_sim(embedding_answers, embedding_requirements)
32
+ return cosine_score.item()
33
+
34
+ def classify_candidate_intent(self, messages):
35
+ user_answers = [msg['content'] for msg in messages if msg['role'] == 'user']
36
+ if not user_answers:
37
+ return []
38
+ candidate_labels = [
39
+ "parle de son expérience technique",
40
+ "exprime sa motivation",
41
+ "pose une question",
42
+ "exprime de lincertitude ou du stress"
43
+ ]
44
+ classifications = self.intent_classifier(user_answers, candidate_labels, multi_label=False)
45
+ return classifications
46
+
47
+ def run_full_analysis(self, conversation_history, job_requirements):
48
+ sentiment_results = self.analyze_sentiment(conversation_history)
49
+ similarity_score = self.compute_semantic_similarity(conversation_history, job_requirements)
50
+ intent_results = self.classify_candidate_intent(conversation_history)
51
+ analysis_output = {
52
+ "overall_similarity_score": round(similarity_score, 2),
53
+ "sentiment_analysis": sentiment_results,
54
+ "intent_analysis": intent_results,
55
+ "raw_transcript": conversation_history
56
+ }
57
+ return analysis_output
src/{core → interview_simulator}/__init__.py RENAMED
File without changes
src/interview_simulator/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (191 Bytes). View file
 
src/interview_simulator/__pycache__/entretient_version_prod.cpython-312.pyc ADDED
Binary file (5.44 kB). View file
 
src/{services/interview_service.py → interview_simulator/entretient_version_prod.py} RENAMED
@@ -4,12 +4,16 @@ import json
4
  from typing import Dict, List, Any, Annotated
5
  from typing_extensions import TypedDict
6
 
7
- from langchain_core.messages import AIMessage, SystemMessage, HumanMessage
 
8
  from langgraph.graph import StateGraph, START, END
9
  from langgraph.graph.message import add_messages
 
10
  from langchain_openai import ChatOpenAI
11
 
12
  from src.config import read_system_prompt, format_cv
 
 
13
 
14
  class State(TypedDict):
15
  messages: Annotated[list, add_messages]
@@ -24,7 +28,9 @@ class InterviewProcessor:
24
  self.job_offer = job_offer
25
  self.cv_data = cv_document['candidat']
26
  self.conversation_history = conversation_history
 
27
  self.llm = self._get_llm()
 
28
 
29
  self.system_prompt_template = self._load_prompt_template()
30
  self.graph = self._build_graph()
@@ -32,73 +38,59 @@ class InterviewProcessor:
32
  def _get_llm(self) -> ChatOpenAI:
33
  openai_api_key = os.getenv("OPENAI_API_KEY")
34
  return ChatOpenAI(
35
- temperature=0.6,
36
- model_name="gpt-4o-mini",
37
- api_key=openai_api_key
38
- )
39
 
40
  def _load_prompt_template(self) -> str:
41
- return read_system_prompt('prompts/rag_prompt_old.txt')
42
-
43
- def _extract_skills_summary(self) -> str:
44
- """Extrait un résumé simple des compétences avec niveaux"""
45
- competences = self.cv_data.get('analyse_competences', [])
46
- if not competences:
47
- return "Aucune analyse de compétences disponible."
48
-
49
- summary = []
50
- for comp in competences:
51
- skill = comp.get('skill', '')
52
- level = comp.get('level', 'débutant')
53
- summary.append(f"{skill}: {level}")
54
-
55
- return "Niveaux de compétences du candidat: " + " | ".join(summary)
56
-
57
- def _extract_reconversion_info(self) -> str:
58
- """Extrait les infos de reconversion"""
59
- reconversion = self.cv_data.get('reconversion', {})
60
- if not reconversion:
61
- return ""
62
-
63
- is_reconversion = reconversion.get('is_reconversion', False)
64
- if not is_reconversion:
65
- return ""
66
-
67
- analysis = reconversion.get('analysis', '')
68
- return f"CANDIDAT EN RECONVERSION: {analysis}"
69
 
70
  def _chatbot_node(self, state: State) -> dict:
 
 
 
71
  messages = state["messages"]
72
  formatted_cv_str = format_cv(self.cv_data)
73
-
74
- # Extractions simples
75
- skills_summary = self._extract_skills_summary()
76
- reconversion_info = self._extract_reconversion_info()
77
 
78
- # Formatage du prompt système avec les nouvelles données
 
 
 
79
  system_prompt = self.system_prompt_template.format(
80
  entreprise=self.job_offer.get('entreprise', 'notre entreprise'),
81
  poste=self.job_offer.get('poste', 'ce poste'),
82
- mission=self.job_offer.get('mission', 'Non spécifiée'),
83
- profil_recherche=self.job_offer.get('profil_recherche', 'Non spécifié'),
84
- competences=self.job_offer.get('competences', 'Non spécifiées'),
85
- pole=self.job_offer.get('pole', 'Non spécifié'),
86
- cv=formatted_cv_str,
87
- skills_analysis=skills_summary,
88
- reconversion_analysis=reconversion_info
89
  )
90
-
91
  llm_messages = [SystemMessage(content=system_prompt)] + messages
92
- response = self.llm.invoke(llm_messages)
93
  return {"messages": [response]}
94
 
 
 
 
 
 
 
95
  def _build_graph(self) -> any:
96
  graph_builder = StateGraph(State)
97
 
98
  graph_builder.add_node("chatbot", self._chatbot_node)
99
- graph_builder.add_edge(START, "chatbot")
100
- graph_builder.add_edge("chatbot", END)
101
-
 
 
 
 
 
 
 
 
102
  return graph_builder.compile()
103
 
104
  def run(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
 
4
  from typing import Dict, List, Any, Annotated
5
  from typing_extensions import TypedDict
6
 
7
+ from langchain_core.messages import AIMessage, SystemMessage, HumanMessage, ToolMessage
8
+ from langchain_groq import ChatGroq
9
  from langgraph.graph import StateGraph, START, END
10
  from langgraph.graph.message import add_messages
11
+ from langgraph.prebuilt import ToolNode
12
  from langchain_openai import ChatOpenAI
13
 
14
  from src.config import read_system_prompt, format_cv
15
+ from src.crew.crew_pool import interview_analyser
16
+
17
 
18
  class State(TypedDict):
19
  messages: Annotated[list, add_messages]
 
28
  self.job_offer = job_offer
29
  self.cv_data = cv_document['candidat']
30
  self.conversation_history = conversation_history
31
+ self.tools = [interview_analyser]
32
  self.llm = self._get_llm()
33
+ self.llm_with_tools = self.llm.bind_tools(self.tools)
34
 
35
  self.system_prompt_template = self._load_prompt_template()
36
  self.graph = self._build_graph()
 
38
  def _get_llm(self) -> ChatOpenAI:
39
  openai_api_key = os.getenv("OPENAI_API_KEY")
40
  return ChatOpenAI(
41
+ temperature=0.6,
42
+ model_name="gpt-4o-mini",
43
+ api_key=openai_api_key
44
+ )
45
 
46
  def _load_prompt_template(self) -> str:
47
+ return read_system_prompt('prompts/rag_prompt_old.txt')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  def _chatbot_node(self, state: State) -> dict:
50
+ if state["messages"] and isinstance(state["messages"][-1], ToolMessage):
51
+ tool_message = state["messages"][-1]
52
+ return {"messages": [AIMessage(content=tool_message.content)]}
53
  messages = state["messages"]
54
  formatted_cv_str = format_cv(self.cv_data)
 
 
 
 
55
 
56
+ mission = self.job_offer.get('mission', 'Non spécifiée')
57
+ profil_recherche = self.job_offer.get('profil_recherche', 'Non spécifié')
58
+ competences = self.job_offer.get('competences', 'Non spécifiées')
59
+ pole = self.job_offer.get('pole', 'Non spécifié')
60
  system_prompt = self.system_prompt_template.format(
61
  entreprise=self.job_offer.get('entreprise', 'notre entreprise'),
62
  poste=self.job_offer.get('poste', 'ce poste'),
63
+ mission=mission,
64
+ profil_recherche=profil_recherche,
65
+ competences=competences,
66
+ pole=pole,
67
+ cv=formatted_cv_str
 
 
68
  )
 
69
  llm_messages = [SystemMessage(content=system_prompt)] + messages
70
+ response = self.llm_with_tools.invoke(llm_messages)
71
  return {"messages": [response]}
72
 
73
+ def _route_after_chatbot(self, state: State) -> str:
74
+ last_message = state["messages"][-1]
75
+ if last_message.tool_calls:
76
+ return "call_tool"
77
+ return END
78
+
79
  def _build_graph(self) -> any:
80
  graph_builder = StateGraph(State)
81
 
82
  graph_builder.add_node("chatbot", self._chatbot_node)
83
+ graph_builder.add_node("call_tool", ToolNode(self.tools))
84
+ graph_builder.add_edge(START, "chatbot")
85
+ graph_builder.add_conditional_edges(
86
+ "chatbot",
87
+ self._route_after_chatbot,
88
+ {
89
+ "call_tool": "call_tool",
90
+ END: END
91
+ }
92
+ )
93
+ graph_builder.add_edge("call_tool", "chatbot")
94
  return graph_builder.compile()
95
 
96
  def run(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
src/models.py DELETED
@@ -1,37 +0,0 @@
1
- import logging
2
- from typing import Dict, Any, Optional
3
-
4
- logger = logging.getLogger(__name__)
5
-
6
- def load_all_models() -> Dict[str, Any]:
7
- models = {
8
- "status": False,
9
- "deep_learning_analyzer": None,
10
- "rag_handler": None,
11
- "llm": None
12
- }
13
-
14
- try:
15
- from src.core.deep_learning_analyzer import MultiModelInterviewAnalyzer
16
- models["deep_learning_analyzer"] = MultiModelInterviewAnalyzer()
17
- logger.info("✅ Deep Learning Analyzer chargé")
18
- except Exception as e:
19
- logger.error(f"❌ Erreur chargement Deep Learning Analyzer: {e}")
20
-
21
- try:
22
- from src.core.rag_handler import get_rag_handler
23
- models["rag_handler"] = get_rag_handler()
24
- logger.info("✅ RAG Handler chargé")
25
- except Exception as e:
26
- logger.error(f"❌ Erreur chargement RAG Handler: {e}")
27
-
28
- try:
29
- from src.config import crew_openai
30
- models["llm"] = crew_openai()
31
- logger.info("✅ LLM chargé")
32
- except Exception as e:
33
- logger.error(f"❌ Erreur chargement LLM: {e}")
34
-
35
- models["status"] = all(v is not None for k, v in models.items() if k != "status")
36
-
37
- return models
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/rag_handler.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from typing import Optional
4
+ from langchain_community.document_loaders import DirectoryLoader, TextLoader
5
+ from langchain_community.vectorstores import FAISS
6
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ # Variables globales pour l'initialisation différée
11
+ _embeddings_model = None
12
+ _rag_handler_instance = None
13
+
14
+ # Utiliser /tmp qui est toujours writable dans les conteneurs
15
+ VECTOR_STORE_PATH = "/tmp/vector_store"
16
+
17
+ def get_embeddings_model():
18
+ """Obtient le modèle d'embeddings avec initialisation différée."""
19
+ global _embeddings_model
20
+ if _embeddings_model is None:
21
+ try:
22
+ from langchain_huggingface import HuggingFaceEmbeddings
23
+ logger.info("Initialisation du modèle d'embeddings...")
24
+ _embeddings_model = HuggingFaceEmbeddings(
25
+ model_name='sentence-transformers/all-MiniLM-L6-v2',
26
+ model_kwargs={'device': 'cpu'},
27
+ encode_kwargs={'normalize_embeddings': True}
28
+ )
29
+ logger.info("✅ Modèle d'embeddings initialisé avec succès")
30
+ except Exception as e:
31
+ logger.error(f"❌ Erreur lors de l'initialisation du modèle d'embeddings: {e}")
32
+ _embeddings_model = None
33
+ return _embeddings_model
34
+
35
+ class RAGHandler:
36
+ def __init__(self, knowledge_base_path: str = "/app/knowledge_base", lazy_init: bool = True):
37
+ """
38
+ Initialise le RAG Handler.
39
+
40
+ Args:
41
+ knowledge_base_path (str): Le chemin vers le dossier contenant les documents de connaissances (.md).
42
+ lazy_init (bool): Si True, initialise le vector store seulement lors de la première utilisation.
43
+ """
44
+ self.knowledge_base_path = knowledge_base_path
45
+ self.embeddings = None
46
+ self.vector_store = None
47
+ self._initialized = False
48
+
49
+ # S'assurer que le répertoire /tmp/vector_store existe
50
+ os.makedirs(VECTOR_STORE_PATH, exist_ok=True)
51
+
52
+ if not lazy_init:
53
+ self._initialize()
54
+
55
+ def _initialize(self):
56
+ """Initialise le RAG Handler de manière différée."""
57
+ if self._initialized:
58
+ return
59
+
60
+ try:
61
+ logger.info("Initialisation du RAG Handler...")
62
+ self.embeddings = get_embeddings_model()
63
+
64
+ if self.embeddings is None:
65
+ logger.error("Impossible d'initialiser les embeddings")
66
+ return
67
+
68
+ self.vector_store = self._load_or_create_vector_store(self.knowledge_base_path)
69
+ self._initialized = True
70
+ logger.info("✅ RAG Handler initialisé avec succès")
71
+
72
+ except Exception as e:
73
+ logger.error(f"❌ Erreur lors de l'initialisation du RAG Handler: {e}")
74
+ self._initialized = False
75
+
76
+ def _load_documents(self, path: str) -> list:
77
+ """Charge les documents depuis un chemin de répertoire spécifié."""
78
+ try:
79
+ if not os.path.exists(path):
80
+ logger.warning(f"Répertoire {path} non trouvé")
81
+ return []
82
+
83
+ loader = DirectoryLoader(
84
+ path,
85
+ glob="**/*.md",
86
+ loader_cls=TextLoader,
87
+ loader_kwargs={"encoding": "utf-8"}
88
+ )
89
+ logger.info(f"Chargement des documents depuis : {path}")
90
+ documents = loader.load()
91
+ logger.info(f"✅ {len(documents)} documents chargés")
92
+ return documents
93
+ except Exception as e:
94
+ logger.error(f"❌ Erreur lors du chargement des documents: {e}")
95
+ return []
96
+
97
+ def _create_vector_store(self, knowledge_base_path: str) -> Optional[FAISS]:
98
+ """Crée et sauvegarde la base de données vectorielle à partir des documents."""
99
+ try:
100
+ documents = self._load_documents(knowledge_base_path)
101
+ if not documents:
102
+ logger.warning("Aucun document trouvé - création d'un vector store vide")
103
+ # Créer un document fictif pour initialiser le vector store
104
+ from langchain.schema import Document
105
+ dummy_doc = Document(
106
+ page_content="Document de test pour initialiser le vector store",
107
+ metadata={"source": "dummy"}
108
+ )
109
+ documents = [dummy_doc]
110
+
111
+ logger.info(f"{len(documents)} documents chargés. Création des vecteurs...")
112
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
113
+ texts = text_splitter.split_documents(documents)
114
+
115
+ vector_store = FAISS.from_documents(texts, self.embeddings)
116
+
117
+ # Sauvegarder dans /tmp
118
+ try:
119
+ vector_store.save_local(VECTOR_STORE_PATH)
120
+ logger.info(f"✅ Vector store cr��é et sauvegardé dans : {VECTOR_STORE_PATH}")
121
+ except Exception as save_error:
122
+ logger.warning(f"⚠️ Impossible de sauvegarder le vector store: {save_error}")
123
+ # Continuer sans sauvegarde - le vector store reste en mémoire
124
+
125
+ return vector_store
126
+
127
+ except Exception as e:
128
+ logger.error(f"❌ Erreur lors de la création du vector store: {e}")
129
+ return None
130
+
131
+ def _load_or_create_vector_store(self, knowledge_base_path: str) -> Optional[FAISS]:
132
+ """Charge le vector store s'il existe, sinon le crée."""
133
+ try:
134
+ index_path = os.path.join(VECTOR_STORE_PATH, "index.faiss")
135
+ if os.path.exists(index_path):
136
+ logger.info(f"Chargement du vector store existant depuis : {VECTOR_STORE_PATH}")
137
+ return FAISS.load_local(
138
+ VECTOR_STORE_PATH,
139
+ embeddings=self.embeddings,
140
+ allow_dangerous_deserialization=True
141
+ )
142
+ else:
143
+ logger.info("Aucun vector store trouvé. Création d'un nouveau...")
144
+ return self._create_vector_store(knowledge_base_path)
145
+ except Exception as e:
146
+ logger.error(f"❌ Erreur lors du chargement/création du vector store: {e}")
147
+ # En cas d'échec total, retourner None plutôt que planter
148
+ return None
149
+
150
+ def get_relevant_feedback(self, query: str, k: int = 1) -> list[str]:
151
+ """Recherche les k conseils les plus pertinents pour une requête."""
152
+ # Initialisation différée si nécessaire
153
+ if not self._initialized:
154
+ self._initialize()
155
+
156
+ if not self.vector_store:
157
+ logger.warning("Vector store non disponible - retour de conseils génériques")
158
+ return [
159
+ "Préparez vos réponses aux questions comportementales",
160
+ "Montrez votre motivation pour le poste",
161
+ "Donnez des exemples concrets de vos réalisations"
162
+ ]
163
+
164
+ try:
165
+ results = self.vector_store.similarity_search(query, k=k)
166
+ feedback = [doc.page_content for doc in results if doc.page_content.strip()]
167
+
168
+ # Fallback si pas de résultats pertinents
169
+ if not feedback:
170
+ return ["Conseil général: Préparez-vous bien pour les entretiens futurs."]
171
+
172
+ return feedback
173
+ except Exception as e:
174
+ logger.error(f"❌ Erreur lors de la recherche: {e}")
175
+ return ["Conseil général: Travaillez sur vos compétences de communication."]
176
+
177
+ # Fonction pour obtenir une instance partagée
178
+ def get_rag_handler() -> Optional[RAGHandler]:
179
+ """Obtient une instance partagée du RAG Handler."""
180
+ global _rag_handler_instance
181
+ if _rag_handler_instance is None:
182
+ try:
183
+ _rag_handler_instance = RAGHandler(lazy_init=True)
184
+ except Exception as e:
185
+ logger.error(f"❌ Erreur lors de la création du RAG Handler: {e}")
186
+ _rag_handler_instance = None
187
+ return _rag_handler_instance
188
+
189
+ if __name__ == '__main__':
190
+ print("Test du RAG Handler avec /tmp vector store...")
191
+ handler = RAGHandler(knowledge_base_path="/app/knowledge_base", lazy_init=False)
192
+
193
+ test_query = "gestion du stress"
194
+ feedback = handler.get_relevant_feedback(test_query, k=2)
195
+
196
+ print(f"\nTest de recherche pour : '{test_query}'")
197
+ if feedback:
198
+ print("Feedback trouvé :")
199
+ for f in feedback:
200
+ print(f"- {f[:150]}...")
201
+ else:
202
+ print("Aucun feedback trouvé.")
src/scoring_engine.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from datetime import datetime
3
+
4
+ # Pondérations basées sur la fiche projet
5
+ CONTEXT_WEIGHTS = {
6
+ "formations": 0.3,
7
+ "projets": 0.6,
8
+ "expériences": 0.8,
9
+ "multiple": 1.0
10
+ }
11
+
12
+ # Facteurs pour la formule de scoring
13
+ ALPHA = 0.5 # Poids du contexte
14
+ BETA = 0.3 # Poids de la fréquence
15
+ GAMMA = 0.2 # Poids de la profondeur (durée)
16
+
17
+ class ContextualScoringEngine:
18
+ def __init__(self, cv_data: dict):
19
+ self.cv_data = cv_data.get("candidat", {})
20
+ self.full_text = self._get_full_text_from_cv()
21
+
22
+ def _get_full_text_from_cv(self) -> str:
23
+ """Concatène tout le contenu textuel du CV pour le comptage de fréquence."""
24
+ return json.dumps(self.cv_data, ensure_ascii=False).lower()
25
+
26
+ def _parse_date(self, date_str: str) -> datetime:
27
+ """Parse une date, en gérant les cas spéciaux comme 'Aujourd'hui'."""
28
+ if not date_str or date_str.lower() == "non spécifié":
29
+ return None
30
+ if date_str.lower() == "aujourd'hui":
31
+ return datetime.now()
32
+ try:
33
+ return datetime.strptime(date_str, "%Y")
34
+ except ValueError:
35
+ return None
36
+
37
+ def _calculate_duration_in_years(self, start_date_str: str, end_date_str: str) -> float:
38
+ """Calcule la durée d'une expérience en années."""
39
+ start_date = self._parse_date(start_date_str)
40
+ end_date = self._parse_date(end_date_str)
41
+ if start_date and end_date:
42
+ return abs((end_date - start_date).days / 365.25)
43
+ return 0.5
44
+
45
+ def calculate_scores(self) -> dict:
46
+ """Calcule les scores pondérés pour toutes les hard skills."""
47
+ skills = self.cv_data.get("compétences", {}).get("hard_skills", [])
48
+ if not skills:
49
+ return {}
50
+
51
+ scored_skills = []
52
+ for skill in skills:
53
+ skill_lower = skill.lower()
54
+ contexts = []
55
+ if skill_lower in json.dumps(self.cv_data.get("formations", []), ensure_ascii=False).lower():
56
+ contexts.append(CONTEXT_WEIGHTS["formations"])
57
+ if skill_lower in json.dumps(self.cv_data.get("projets", []), ensure_ascii=False).lower():
58
+ contexts.append(CONTEXT_WEIGHTS["projets"])
59
+ if skill_lower in json.dumps(self.cv_data.get("expériences", []), ensure_ascii=False).lower():
60
+ contexts.append(CONTEXT_WEIGHTS["expériences"])
61
+
62
+ if len(contexts) > 1:
63
+ context_score = CONTEXT_WEIGHTS["multiple"]
64
+ elif contexts:
65
+ context_score = contexts[0]
66
+ else:
67
+ context_score = 0.1
68
+
69
+ # 2. Fréquence de mention
70
+ frequency_score = self.full_text.count(skill_lower)
71
+
72
+ # 3. Profondeur d'utilisation (durée max en années)
73
+ max_duration = 0
74
+ for exp in self.cv_data.get("expériences", []):
75
+ if skill_lower in json.dumps(exp, ensure_ascii=False).lower():
76
+ duration = self._calculate_duration_in_years(exp.get("start_date"), exp.get("end_date"))
77
+ if duration > max_duration:
78
+ max_duration = duration
79
+ depth_score = max_duration
80
+
81
+ # Normalisation simple (peut être affinée)
82
+ normalized_frequency = 1 - (1 / (1 + frequency_score))
83
+ normalized_depth = 1 - (1 / (1 + depth_score))
84
+
85
+ # Calcul du score final
86
+ final_score = (ALPHA * context_score) + \
87
+ (BETA * normalized_frequency) + \
88
+ (GAMMA * normalized_depth)
89
+
90
+ scored_skills.append({
91
+ "skill": skill,
92
+ "score": round(final_score, 2),
93
+ "details": {
94
+ "context_score": context_score,
95
+ "frequency": frequency_score,
96
+ "max_duration_years": round(depth_score, 1)
97
+ }
98
+ })
99
+
100
+ # Trier par score décroissant
101
+ scored_skills.sort(key=lambda x: x["score"], reverse=True)
102
+ return {"analyse_competences": scored_skills}
src/services/__init__.py DELETED
File without changes
src/services/analysis_service.py DELETED
@@ -1,97 +0,0 @@
1
- import json
2
- import logging
3
- from typing import Dict, List, Any
4
- from crewai import Agent, Task, Crew, Process
5
-
6
- logger = logging.getLogger(__name__)
7
-
8
- class AnalysisService:
9
- def __init__(self, models: Dict[str, Any]):
10
- self.models = models
11
- self.analyzer = models.get("deep_learning_analyzer")
12
- self.rag_handler = models.get("rag_handler")
13
- self.llm = models.get("llm")
14
- self._create_report_agent()
15
-
16
- def _create_report_agent(self):
17
- self.report_agent = Agent(
18
- role='Rédacteur de Rapports Synthétiques',
19
- goal='Générer un feedback pertinent à partir du déroulement de l\'entretien',
20
- backstory=(
21
- "Spécialisé dans le recrutement et les ressources humaines, capable d'évaluer les candidats "
22
- "sur la communication et la pertinence des réponses en fonction des questions posées, rédige "
23
- "en un rapport clair, un feedback détaillé sur le candidat."
24
- ),
25
- allow_delegation=False,
26
- verbose=False,
27
- llm=self.llm
28
- )
29
-
30
- def run_analysis(self, conversation_history: List[Dict[str, Any]], job_description: str) -> Dict[str, Any]:
31
- if not self.analyzer:
32
- return {"error": "Analyzer non disponible"}
33
-
34
- structured_analysis = self.analyzer.run_full_analysis(conversation_history, job_description)
35
-
36
- rag_feedback = []
37
- if self.rag_handler:
38
- rag_feedback = self._get_contextual_feedback(structured_analysis)
39
-
40
- report = self._generate_final_report(structured_analysis, rag_feedback)
41
-
42
- return report
43
-
44
- def _get_contextual_feedback(self, structured_analysis: Dict[str, Any]) -> List[str]:
45
- rag_feedback = []
46
-
47
- if structured_analysis.get("intent_analysis"):
48
- for intent in structured_analysis["intent_analysis"]:
49
- query = f"Conseils pour un candidat qui cherche à {intent['labels'][0]}"
50
- rag_feedback.extend(self.rag_handler.get_relevant_feedback(query))
51
-
52
- if structured_analysis.get("sentiment_analysis"):
53
- for sentiment_group in structured_analysis["sentiment_analysis"]:
54
- for sentiment in sentiment_group:
55
- if sentiment['label'] == 'stress' and sentiment['score'].item() > 0.6:
56
- rag_feedback.extend(
57
- self.rag_handler.get_relevant_feedback("gestion du stress en entretien")
58
- )
59
-
60
- return list(set(rag_feedback))
61
-
62
- def _generate_final_report(
63
- self,
64
- structured_analysis: Dict[str, Any],
65
- rag_feedback: List[str]
66
- ) -> Dict[str, Any]:
67
-
68
- task = Task(
69
- description=(
70
- f"Tu es un rédacteur expert en RH. Ta mission est de rédiger un rapport d'évaluation final. "
71
- f"Tu dois utiliser deux sources d'information principales : "
72
- f"1. Les données d'analyse structurées de l'entretien : '{json.dumps(structured_analysis, indent=2)}'. "
73
- f"2. Une liste de conseils et de feedback pertinents issus de notre base de connaissances : '{chr(10).join(rag_feedback)}'. "
74
- f"Ta tâche est de synthétiser ces informations en un rapport cohérent et actionnable."
75
- ),
76
- expected_output=(
77
- "Un rapport final exceptionnel basé sur l'analyse fournie. Le rapport doit être structuré comme suit: "
78
- "1. **Résumé et Score d'Adéquation** : Synthétise le score de similarité sémantique et donne un aperçu global. "
79
- "2. **Analyse Comportementale** : Interprète les résultats de l'analyse de sentiment et d'intention pour décrire le comportement du candidat. "
80
- "3. **Adéquation Sémantique avec le Poste** : Explique ce que signifie le score de similarité. "
81
- "4. **Points Forts & Axes d'Amélioration Personnalisés** : Utilise les données d'analyse pour identifier les points à améliorer. "
82
- "Ensuite, intègre de manière fluide et naturelle les conseils pertinents pour proposer des pistes d'amélioration concrètes et personnalisées. "
83
- "5. **Recommandation Finale**."
84
- ),
85
- agent=self.report_agent
86
- )
87
-
88
- crew = Crew(
89
- agents=[self.report_agent],
90
- tasks=[task],
91
- process=Process.sequential,
92
- verbose=False,
93
- telemetry=False
94
- )
95
-
96
- result = crew.kickoff()
97
- return {"analysis_report": result.raw if hasattr(result, 'raw') else str(result)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/services/graph_service.py DELETED
@@ -1,193 +0,0 @@
1
- import os
2
- import logging
3
- import json
4
- from typing import TypedDict, Annotated, Sequence, Dict, Any, List
5
-
6
- from langchain_openai import ChatOpenAI
7
- from langchain_core.runnables import Runnable
8
- from langchain_core.messages import BaseMessage, AIMessage, HumanMessage
9
- from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
10
- from langgraph.graph import StateGraph, END
11
- from langgraph.prebuilt import ToolNode
12
-
13
- from tools.analysis_tools import trigger_interview_analysis
14
-
15
- class AgentState(TypedDict):
16
- messages: Annotated[Sequence[BaseMessage], lambda x, y: x + y]
17
- user_id: str
18
- job_offer_id: str
19
- job_description: str
20
-
21
- class GraphInterviewProcessor:
22
- """
23
- Cette classe encapsule la logique d'un entretien en utilisant LangGraph.
24
- Elle prépare toutes les données nécessaires à l'initialisation.
25
- """
26
- def __init__(self, payload: Dict[str, Any]):
27
- logging.info("Initialisation de GraphInterviewProcessor...")
28
-
29
- self.user_id = payload["user_id"]
30
- self.job_offer_id = payload["job_offer_id"]
31
- self.job_offer = payload["job_offer"]
32
- self.cv_data = payload.get("cv_document", {}).get('candidat', {})
33
-
34
- if not self.cv_data:
35
- raise ValueError("Données du candidat non trouvées dans le payload.")
36
-
37
- self.system_prompt_template = self._load_prompt_template('prompts/rag_prompt_old.txt')
38
- self.formatted_cv_str = self._format_cv_for_prompt()
39
- self.skills_summary = self._extract_skills_summary()
40
- self.reconversion_info = self._extract_reconversion_info()
41
-
42
- self.agent_runnable = self._create_agent_runnable()
43
- self.graph = self._build_graph()
44
- logging.info("GraphInterviewProcessor initialisé avec succès.")
45
-
46
- def _load_prompt_template(self, file_path: str) -> str:
47
- try:
48
- with open(file_path, 'r', encoding='utf-8') as f:
49
- return f.read()
50
- except FileNotFoundError:
51
- logging.error(f"Fichier prompt introuvable: {file_path}")
52
- return "Vous êtes un assistant RH."
53
-
54
- def _format_cv_for_prompt(self) -> str:
55
- return json.dumps(self.cv_data, indent=2, ensure_ascii=False)
56
-
57
- def _extract_skills_summary(self) -> str:
58
- competences = self.cv_data.get('analyse_competences', [])
59
- if not competences: return "Aucune analyse de compétences disponible."
60
- summary = [f"{comp.get('skill', '')}: {comp.get('level', 'débutant')}" for comp in competences]
61
- return "Niveaux de compétences du candidat: " + " | ".join(summary)
62
-
63
- def _extract_reconversion_info(self) -> str:
64
- reconversion = self.cv_data.get('reconversion', {})
65
- if reconversion.get('is_reconversion'):
66
- return f"CANDIDAT EN RECONVERSION: {reconversion.get('analysis', '')}"
67
- return "Le candidat n'est pas identifié comme étant en reconversion."
68
-
69
- def _create_agent_runnable(self) -> Runnable:
70
- """Crée une chaîne (runnable) qui agit comme notre agent."""
71
- prompt = ChatPromptTemplate.from_messages([
72
- ("system", "{system_prompt_content}"),
73
- MessagesPlaceholder(variable_name="messages"),
74
- ])
75
- llm = ChatOpenAI(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o-mini", temperature=0.7)
76
- tools = [trigger_interview_analysis]
77
- llm_with_tools = llm.bind_tools(tools)
78
- return prompt | llm_with_tools
79
-
80
- def _agent_node(self, state: AgentState):
81
- """Prépare le prompt et appelle le runnable de l'agent."""
82
- job_description_str = json.dumps(self.job_offer, ensure_ascii=False)
83
-
84
- system_prompt_content = self.system_prompt_template.format(
85
- user_id=state['user_id'],
86
- job_offer_id=state['job_offer_id'],
87
- entreprise=self.job_offer.get('entreprise', 'notre entreprise'),
88
- poste=self.job_offer.get('poste', 'ce poste'),
89
- mission=self.job_offer.get('mission', 'Non spécifiée'),
90
- profil_recherche=self.job_offer.get('profil_recherche', 'Non spécifié'),
91
- competences=self.job_offer.get('competences', 'Non spécifiées'),
92
- pole=self.job_offer.get('pole', 'Non spécifié'),
93
- cv=self.formatted_cv_str,
94
- skills_analysis=self.skills_summary,
95
- reconversion_analysis=self.reconversion_info,
96
- job_description=job_description_str
97
- )
98
-
99
- response = self.agent_runnable.invoke({
100
- "system_prompt_content": system_prompt_content,
101
- "messages": state["messages"]
102
- })
103
-
104
- return {"messages": [response]}
105
-
106
- def _router(self, state: AgentState) -> str:
107
- """Route le flux du graphe en fonction de la dernière réponse de l'agent."""
108
- last_message = state["messages"][-1]
109
- if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
110
- if any(tool_call.get('name') == 'trigger_interview_analysis' for tool_call in last_message.tool_calls):
111
- return "call_final_tool"
112
- return "call_tool"
113
- return "end_turn"
114
-
115
- def _final_analysis_node(self, state: AgentState):
116
- """
117
- Appelle l'outil d'analyse finale. Construit les arguments manuellement
118
- à partir de l'état du graphe pour garantir la fiabilité.
119
- """
120
- conversation_history = []
121
- for msg in state["messages"]:
122
- if isinstance(msg, HumanMessage):
123
- role = "user"
124
- elif isinstance(msg, AIMessage):
125
- role = "assistant"
126
- else:
127
- continue
128
- conversation_history.append({"role": role, "content": msg.content})
129
-
130
- tool_input = {
131
- "user_id": state['user_id'],
132
- "job_offer_id": state['job_offer_id'],
133
- "job_description": state['job_description'],
134
- "conversation_history": conversation_history
135
- }
136
-
137
- trigger_interview_analysis.invoke(tool_input)
138
- return {}
139
-
140
- def _build_graph(self) -> any:
141
- """Construit et compile le graphe d'états."""
142
- tool_node = ToolNode([trigger_interview_analysis])
143
-
144
- graph = StateGraph(AgentState)
145
- graph.add_node("agent", self._agent_node)
146
- graph.add_node("tools", tool_node)
147
- graph.add_node("final_tool_node", self._final_analysis_node)
148
-
149
- graph.set_entry_point("agent")
150
-
151
- graph.add_conditional_edges(
152
- "agent",
153
- self._router,
154
- {
155
- "call_tool": "tools",
156
- "call_final_tool": "final_tool_node",
157
- "end_turn": END
158
- }
159
- )
160
-
161
- graph.add_edge("tools", "agent")
162
- graph.add_edge("final_tool_node", END)
163
-
164
- return graph.compile()
165
-
166
- def invoke(self, messages: List[Dict[str, Any]]):
167
- """Point d'entrée pour lancer une conversation dans le graphe."""
168
- langchain_messages = [HumanMessage(content=m["content"]) if m["role"] == "user" else AIMessage(content=m["content"]) for m in messages]
169
-
170
- if not langchain_messages:
171
- logging.info("Historique de conversation vide. Ajout d'un message de démarrage interne.")
172
- langchain_messages.append(HumanMessage(content="Bonjour, je suis prêt à commencer l'entretien."))
173
-
174
- initial_state = {
175
- "user_id": self.user_id,
176
- "job_offer_id": self.job_offer_id,
177
- "messages": langchain_messages,
178
- "job_description": json.dumps(self.job_offer, ensure_ascii=False),
179
- }
180
-
181
- final_state = self.graph.invoke(initial_state)
182
-
183
- if not final_state or not final_state.get('messages'):
184
- logging.error("L'état final est vide ou ne contient pas de messages.")
185
- return {"response": "Erreur: Impossible de générer une réponse.", "status": "finished"}
186
- last_message = final_state['messages'][-1]
187
- status = "finished" if hasattr(last_message, 'tool_calls') and last_message.tool_calls else "interviewing"
188
- response_content = last_message.content
189
-
190
- return {
191
- "response": response_content,
192
- "status": status
193
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tools/analysis_tools.py DELETED
@@ -1,56 +0,0 @@
1
- import logging
2
- from langchain_core.tools import tool
3
- from src.services.analysis_service import AnalysisService
4
- import json
5
- import os
6
- from datetime import datetime
7
- from pydantic.v1 import BaseModel, Field
8
- from typing import List, Dict, Any
9
- from src.models import load_all_models
10
- from pymongo import MongoClient
11
-
12
- logging.basicConfig(level=logging.INFO)
13
- logger = logging.getLogger(__name__)
14
-
15
- class InterviewAnalysisArgs(BaseModel):
16
- """Arguments for the trigger_interview_analysis tool."""
17
- user_id: str = Field(..., description="The unique identifier for the user, provided in the system prompt.")
18
- job_offer_id: str = Field(..., description="The unique identifier for the job offer, provided in the system prompt.")
19
- job_description: str = Field(..., description="The full JSON string of the job offer description.")
20
- conversation_history: List[Dict[str, Any]] = Field(..., description="The complete conversation history between the user and the agent.")
21
-
22
- @tool("trigger_interview_analysis", args_schema=InterviewAnalysisArgs)
23
- def trigger_interview_analysis(user_id: str, job_offer_id: str, job_description: str, conversation_history: List[Dict[str, Any]]):
24
- """
25
- Call this tool to end the interview and launch the final analysis.
26
- You MUST provide all arguments: user_id, job_offer_id, job_description, and the complete conversation_history.
27
- """
28
- try:
29
- logger.info(f"Outil 'trigger_interview_analysis' appelé pour user_id: {user_id} et job_offer_id: {job_offer_id}")
30
- if '@' in user_id or ' ' in job_offer_id:
31
- logger.error(f"Appel de l'outil avec des données invalides. User ID: {user_id}, Job Offer ID: {job_offer_id}")
32
- return "Erreur: Appel de l'outil avec des paramètres invalides. L'analyse n'a pas pu être lancée."
33
- mongo_client = MongoClient(os.getenv("MONGO_URI"))
34
- db = mongo_client[os.getenv("MONGO_DB_NAME")]
35
- collection = db[os.getenv("MONGO_FEEDBACK")]
36
-
37
- models = load_all_models()
38
- analysis_service = AnalysisService(models=models)
39
- feedback_data = analysis_service.run_analysis(
40
- conversation_history=conversation_history,
41
- job_description=job_description
42
- )
43
- mongo_document = {
44
- "user_id": user_id,
45
- "job_offer_id": job_offer_id,
46
- "feedback_data": feedback_data,
47
- "updated_at": datetime.utcnow()
48
- }
49
- result = collection.insert_one(mongo_document)
50
- logger.info(f"Analyse pour l'utilisateur {user_id} terminée et sauvegardée dans MongoDB avec l'ID: {result.inserted_id}")
51
-
52
- return "L'analyse a été déclenchée et terminée avec succès."
53
-
54
- except Exception as e:
55
- logger.error(f"Erreur dans l'outil d'analyse : {e}", exc_info=True)
56
- return "Une erreur est survenue lors du lancement de l'analyse."