This view is limited to 50 files because it contains too many changes. See the raw diff here.
Files changed (50) hide show
  1. .env.example +0 -18
  2. .gitattributes +35 -0
  3. .gitignore +0 -34
  4. Dockerfile +34 -12
  5. README.md +5 -20
  6. documentation/API_DOCUMENTATION.md +0 -126
  7. documentation/cv_structure.json +0 -121
  8. documentation/job_offer_structure.json +0 -15
  9. knowledge_base/soft_skills_feedback.md +87 -0
  10. main.py +237 -49
  11. prompts/rag_prompt.txt +39 -0
  12. prompts/rag_prompt_old.txt +35 -0
  13. requirements.txt +21 -18
  14. src/{services/simulation/__init__.py → __init__.py} +0 -0
  15. src/__pycache__/__init__.cpython-312.pyc +0 -0
  16. src/__pycache__/config.cpython-312.pyc +0 -0
  17. src/__pycache__/cv_parsing_agents.cpython-312.pyc +0 -0
  18. src/__pycache__/deep_learning_analyzer.cpython-312.pyc +0 -0
  19. src/celery_app.py +0 -29
  20. src/config.py +74 -16
  21. {tools → src/crew}/__init__.py +0 -0
  22. src/crew/__pycache__/__init__.cpython-311.pyc +0 -0
  23. src/crew/__pycache__/__init__.cpython-312.pyc +0 -0
  24. src/crew/__pycache__/__init__.cpython-312.pycZone.Identifier +2 -0
  25. src/crew/__pycache__/agents.cpython-311.pyc +0 -0
  26. src/crew/__pycache__/agents.cpython-312.pyc +0 -0
  27. src/crew/__pycache__/agents.cpython-312.pycZone.Identifier +2 -0
  28. src/crew/__pycache__/analysis_crew.cpython-312.pyc +0 -0
  29. src/crew/__pycache__/analysis_crew.cpython-312.pycZone.Identifier +2 -0
  30. src/crew/__pycache__/crew_pool.cpython-311.pyc +0 -0
  31. src/crew/__pycache__/crew_pool.cpython-312.pyc +0 -0
  32. src/crew/__pycache__/crew_pool.cpython-312.pycZone.Identifier +2 -0
  33. src/crew/__pycache__/tasks.cpython-312.pyc +0 -0
  34. src/crew/__pycache__/tasks.cpython-312.pycZone.Identifier +2 -0
  35. src/crew/agents.py +75 -0
  36. src/crew/crew_pool.py +81 -0
  37. src/crew/tasks.py +184 -0
  38. src/cv_parsing_agents.py +291 -0
  39. src/deep_learning_analyzer.py +57 -0
  40. src/interview_simulator/__init__.py +0 -0
  41. src/interview_simulator/__pycache__/__init__.cpython-312.pyc +0 -0
  42. src/interview_simulator/__pycache__/entretient_version_prod.cpython-312.pyc +0 -0
  43. src/interview_simulator/entretient_version_prod.py +98 -0
  44. src/prompts/agent_auditeur.txt +0 -31
  45. src/prompts/agent_challenger.txt +0 -14
  46. src/prompts/agent_enqueteur.txt +0 -29
  47. src/prompts/agent_icebreaker.txt +0 -47
  48. src/prompts/agent_projecteur.txt +0 -20
  49. src/prompts/agent_stratege.txt +0 -26
  50. src/prompts/orchestrator_prompt.txt +0 -31
.env.example DELETED
@@ -1,18 +0,0 @@
1
- # ========================
2
- # LLM API KEYS
3
- # ========================
4
- OPENAI_API_KEY=your_openai_api_key
5
- LANGTRACE_API_KEY=your_langtrace_api_key
6
-
7
- # ========================
8
- # REDIS
9
- # ========================
10
- REDIS_URL=redis://default:password@redis-host:port/0
11
-
12
- # ========================
13
- # BACKEND API
14
- # ========================
15
- # Development
16
- BACKEND_API_URL=http://localhost:8000
17
- # Production (uncomment for deployment)
18
- # BACKEND_API_URL=https://api.yoursite.com
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore DELETED
@@ -1,34 +0,0 @@
1
- # Environnement
2
- .env
3
- .env.*
4
- !.env.example
5
- venv/
6
- env/
7
- ENV/
8
- .venv/
9
-
10
- # Python
11
- __pycache__/
12
- *.py[cod]
13
- *$py.class
14
- *.so
15
- .Python
16
-
17
- # Tests
18
- .pytest_cache/
19
- .coverage
20
- htmlcov/
21
-
22
- # IDE
23
- .vscode/
24
- .idea/
25
- *.swp
26
-
27
- # Distribution
28
- dist/
29
- build/
30
- *.egg-info/
31
-
32
- # OS
33
- .DS_Store
34
- Thumbs.db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,22 +1,44 @@
1
  FROM python:3.11-slim
2
 
3
- RUN useradd -m -u 1000 user
4
- USER user
5
- ENV PATH="/home/user/.local/bin:$PATH"
 
 
 
6
 
7
- ENV NLTK_DATA="/home/user/nltk_data"
 
 
 
 
8
 
 
9
  WORKDIR /app
10
 
11
- COPY --chown=user ./requirements.txt requirements.txt
12
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
13
 
14
- RUN mkdir -p /home/user/nltk_data && \
15
- python -m textblob.download_corpora && \
16
- python -m nltk.downloader punkt_tab
17
 
18
- COPY --chown=user . /app
19
- RUN chmod +x /app/start.sh
 
20
 
 
 
 
 
 
 
 
 
 
 
21
  EXPOSE 7860
22
- CMD ["./start.sh"]
 
 
 
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"]
README.md CHANGED
@@ -1,25 +1,10 @@
1
  ---
2
- title: Interview Agents API
3
- emoji: 🎤
4
- colorFrom: blue
5
- colorTo: purple
6
  sdk: docker
7
- app_file: main.py
8
  pinned: false
9
  ---
10
 
11
- # Interview Agents API
12
-
13
- API de simulation d'entretiens avec agents IA utilisant LangGraph.
14
-
15
- ## Description
16
-
17
- Cette API permet de simuler des entretiens d'embauche avec des agents IA intelligents.
18
-
19
- ## Configuration
20
-
21
- Assurez-vous de configurer vos variables d'environnement dans les Settings du Space Hugging Face.
22
-
23
- ## Documentation
24
-
25
- Une documentation détaillée de l'API (points d'entrée, schémas de données et exemples) est disponible dans le fichier [API_DOCUMENTATION.md](API_DOCUMENTATION.md).
 
1
  ---
2
+ title: Interview Agents Api
3
+ emoji: 🏃
4
+ colorFrom: green
5
+ colorTo: pink
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
documentation/API_DOCUMENTATION.md DELETED
@@ -1,126 +0,0 @@
1
- # Interview Simulation API Documentation
2
-
3
- Cette documentation détaille l'utilisation de l'API de simulation d'entretiens, motorisée par LangGraph et FastAPI.
4
-
5
- ## Introduction
6
-
7
- L'**Interview Simulation API** permet de mener des simulations d'entretiens d'embauche réalistes. Elle utilise un système d'agents spécialisés (Icebreaker, Auditeur, Enquêteur, Stratège, Projecteur) pour évaluer un candidat par rapport à une offre d'emploi spécifique.
8
-
9
- **Base URL:** `http://localhost:7860` (ou URL de déploiement)
10
- **Version:** `1.0.0`
11
-
12
- ## Configuration & Authentification
13
-
14
- L'API utilise des variables d'environnement pour la configuration :
15
- - `OPENAI_API_KEY`: Requis pour les agents LLM.
16
- - `LANGTRACE_API_KEY`: Pour l'observabilité (Langtrace).
17
- - `CORS_ORIGINS`: Liste des origines autorisées (défaut: `*`).
18
-
19
- ## Endpoints
20
-
21
- ### 1. Health Check
22
- Vérifie que l'API est opérationnelle.
23
-
24
- - **URL:** `/`
25
- - **Method:** `GET`
26
- - **Success Response (200 OK):**
27
- ```json
28
- {
29
- "status": "ok"
30
- }
31
- ```
32
-
33
- ### 2. Simulate Interview
34
- Déclenche ou poursuit une simulation d'entretien.
35
-
36
- - **URL:** `/simulate-interview/`
37
- - **Method:** `POST`
38
- - **Request Body:**
39
- ```json
40
- {
41
- "user_id": "string", // ID unique de l'utilisateur
42
- "job_offer_id": "string", // ID de l'offre d'emploi
43
- "cv_document": { // Données extraites du CV
44
- "candidat": { ... } // Voir section "Data Models"
45
- },
46
- "job_offer": { // Détails de l'offre
47
- "poste": "string",
48
- "entreprise": "string",
49
- "mission": "string",
50
- ...
51
- },
52
- "messages": [ // Historique de la conversation (optionnel)
53
- {
54
- "role": "user",
55
- "content": "Bonjour"
56
- },
57
- ...
58
- ],
59
- "cheat_metrics": { ... } // Métriques anti-triche (optionnel)
60
- }
61
- ```
62
-
63
- - **Success Response (200 OK):**
64
- ```json
65
- {
66
- "response": "Texte généré par l'agent IA",
67
- "status": "interviewing" | "interview_finished"
68
- }
69
- ```
70
-
71
- - **Error Responses:**
72
- - `400 Bad Request`: Payload incomplet ou données invalides.
73
- - `500 Internal Server Error`: Erreur lors de l'exécution du graph LangGraph.
74
-
75
- ## Data Models (Schemas)
76
-
77
- ### Feedback Output (Output final de l'analyse)
78
- Une fois l'entretien terminé, un rapport complet est généré (via Celery) suivant cette structure :
79
-
80
- - **CandidatFeedback**: Points forts, axes d'amélioration, conseils, score global.
81
- - **EntrepriseInsights**:
82
- - **Dashboard**: Technique, Cognitive, Comportementale (0-100).
83
- - **Decision**: RECRUTER, APPROFONDIR ou REJETER.
84
- - **Fraud Detection**: Score global d'usage d'IA, mots-clés détectés, alertes (red flags).
85
-
86
- ## Exemples d'Usage
87
-
88
- ### Exemple cURL
89
- ```bash
90
- curl -X POST http://localhost:7860/simulate-interview/ \
91
- -H "Content-Type: application/json" \
92
- -d '{
93
- "user_id": "user_123",
94
- "job_offer_id": "job_456",
95
- "cv_document": { "candidat": { "first_name": "Jean", "expériences": [] } },
96
- "job_offer": { "poste": "Développeur Python", "entreprise": "TechCorp" },
97
- "messages": []
98
- }'
99
- ```
100
-
101
- ### Exemple Python (Requests)
102
- ```python
103
- import requests
104
-
105
- url = "http://localhost:7860/simulate-interview/"
106
- payload = {
107
- "user_id": "user_123",
108
- "job_offer_id": "job_456",
109
- "cv_document": { "candidat": { "first_name": "Jean", "expériences": [] } },
110
- "job_offer": { "poste": "Développeur Python", "entreprise": "TechCorp" },
111
- "messages": [{"role": "user", "content": "Je suis prêt."}]
112
- }
113
-
114
- response = requests.post(url, json=payload)
115
- print(response.json())
116
- ```
117
-
118
- ## Error Handling
119
-
120
- Toutes les erreurs renvoient un format JSON standardisé :
121
- ```json
122
- {
123
- "error": "Description de l'erreur"
124
- }
125
- ```
126
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
documentation/cv_structure.json DELETED
@@ -1,121 +0,0 @@
1
- {
2
- "_id": {
3
- "$oid": "69946f80c8bcf8b152b53a6a"
4
- },
5
- "user_id": "user_...",
6
- "parsed_data": {
7
- "candidat": {
8
- "first_name": "...",
9
- "compétences": {
10
- "hard_skills": [
11
- "...",
12
- "..."
13
- ],
14
- "soft_skills": [
15
- "...",
16
- "..."
17
- ],
18
- "skills_with_context": [
19
- {
20
- "skill": "...",
21
- "context": "entreprise"
22
- },
23
- {
24
- "skill": "...",
25
- "context": "projet"
26
- },
27
- {
28
- "skill": "...",
29
- "context": "academique"
30
- },
31
- {
32
- "skill": "...",
33
- "context": "sans contexte"
34
- }
35
- ]
36
- },
37
- "expériences": [
38
- {
39
- "Poste": "...",
40
- "Entreprise": "...",
41
- "start_date": "Déc. 2024",
42
- "end_date": "Déc. 2025",
43
- "responsabilités": [
44
- "..."
45
- ]
46
- },
47
- {
48
- "Poste": "...",
49
- "Entreprise": "...",
50
- "start_date": "2010",
51
- "end_date": "2023",
52
- "responsabilités": [
53
- "...",
54
- "..."
55
- ]
56
- }
57
- ],
58
- "reconversion": {
59
- "is_reconversion": true,
60
- "context": "..."
61
- },
62
- "projets": {
63
- "professional": [
64
- {
65
- "title": "...",
66
- "technologies": [
67
- "...",
68
- "..."
69
- ],
70
- "outcomes": [
71
- "...",
72
- "..."
73
- ],
74
- "domaine metier": "..."
75
- },
76
- {
77
- "title": "...",
78
- "technologies": [
79
- "...",
80
- "..."
81
- ],
82
- "outcomes": [
83
- "..."
84
- ],
85
- "domaine metier": "..."
86
- }
87
- ],
88
- "personal": []
89
- },
90
- "formations": [
91
- {
92
- "degree": "...",
93
- "institution": "...",
94
- "start_date": "Nov. 2024",
95
- "end_date": "Déc. 2025"
96
- },
97
- {
98
- "degree": "...",
99
- "institution": "...",
100
- "start_date": "Fév. 2024",
101
- "end_date": "Juil. 2024"
102
- }
103
- ],
104
- "etudiant": {
105
- "is_etudiant": false,
106
- "niveau_etudes": "bac+3",
107
- "specialite": "...",
108
- "latest_education_end_date": "2025-12-01"
109
- },
110
- "langues": [
111
- {
112
- "langue": "Français"
113
- },
114
- {
115
- "langue": "Anglais"
116
- }
117
- ]
118
- }
119
- },
120
- "upload_date": "2026-02-17T13:39:12.984629"
121
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
documentation/job_offer_structure.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "entreprise": "Carrefour",
3
- "ville": "Massy",
4
- "poste": "Data Scientist Assortiment Personnalisé",
5
- "contrat": "Stage",
6
- "description_poste": "Nos atouts pour y parvenir ?\n\nUn réseau multi format, multi métiers avec des collaborateurs passionnés, qui s'engagent, pour réussir la transition alimentaire pour tous.\n\nStage Data Scientist - Assortiment Personnalisé (F/H)\n\nL'utilisation de la data et le développement des capacités analytiques sont un axe majeur du plan stratégique du Groupe Carrefour à horizon 2027. Directement rattachée au COMEX France, l'Analytics Factory de Carrefour France est le pilier de cette transformation analytique, au service des clients et de l'ensemble des équipes métier de Carrefour France - Marketing, Marchandises, Exploitation, E-commerce, Supply Chain, Services Financiers, etc...\n\nNous utilisons des approches de machine learning pour résoudre des défis commerciaux et opérationnels comme les ruptures en magasin, la prévision des ventes promotionnelles, l'optimisation de l'assortiment des produits et les recommandations personnalisées sur le site carrefour.fr\n\nVos Missions\n\nAu cours de ces dernières années, l'équipe Assortiment de l'Analytics Factory a développé des solutions robustes pour la gestion de l'assortiment de produits, tant au niveau national pour la gestion des négociations annuelles avec les fournisseurs, qu'au niveau local de façon à décliner de façon optimale l'assortiment de produits proposés par chaque magasin.\n\nCette déclinaison s'opère en termes de nombre de références pour chaque catégorie de produits avec l'objectif de maximiser le chiffre d'affaires des magasins ; elle est réalisée une fois par an.\n\nAujourd'hui, notre réflexion s'oriente vers une personnalisation plus fine de l'assortiment, en cherchant à aligner les préférences clients propres à chaque magasin avec les caractéristiques produits. Nous souhaitons également introduire davantage d'agilité dans la gestion locale de l'offre, en permettant une optimisation continue tout au long de l'année.\n\nLe stage de fin d'études (d'une durée de 4 à 6 mois) que nous proposons consiste à \nexplorer les modèles de choix discrets, une approche économétrique de la modélisation des choix les plus probables que peuvent faire un ensemble d'individus parmi un nombre fini de possibilités.\n\nCette approche repose sur la définition d'une fonction d'utilité associée à chaque produit, combinant des attributs produits (prix, format, marque, catégorie, etc.) et des variables contextuelles (profil du magasin, saisonnalité, environnement concurrentiel...).\n\nLes paramètres de ces fonctions seront estimés par maximum de vraisemblance, \npermettant d'interpréter les sensibilités des clients aux différents attributs et d'identifier les leviers de préférence par segment ou par typologie de magasin.\n\nDifférentes spécifications pourront être testées - logit multinomial, nested logit, ou modèles mixtes - afin d'évaluer le compromis entre interprétabilité et performance prédictive.\n\nVous serez intégré·e à une équipe dynamique, innovatrice, aux profils diversifiés et \ncomplémentaires, fondée sur l'entraide, le partage de connaissances, la prise d'initiatives ; vous serez co-encadré·e par deux data scientists, qui vous guideront tout au long de votre \nstage.\n\nVotre Profil\n\nÉtudiant en Master 2 Data Science / Intelligence Artificielle / Statistiques, passionné par l'exploitation des données pour la prise de décision et la création de modèles prédictifs. Vous recherchez un stage de fin d'étude de 4 à 6 mois.\n\nÀ l'issue de vos années de formation et à travers vos différents projets, vous avez développé de solides connaissances théoriques et pratiques sur les algorithmes d'apprentissage statistique.\n\nVous aimez aborder de nouveaux défis de machine learning dans différents domaines, lire des publications scientifiques. Et vous appréciez tout autant l'implémentation et l'optimisation des solutions. La qualité et la simplicité du code vous tiennent à coeur.\n\nEnfin, vous appréciez de travailler avec des collaborateurs d'horizons différents, et il est agréable de travailler avec vous au jour le jour !\n\nNotre environnement technique \nGCP, Python, SQL, BigQuery, dbt, Kubernetes, Terraform, Airflow\n\nVos petits plus \nAppétence pour l'exploration et l'expérimentation scientifique, rigueur dans les analyses et la modélisation, souci de produire un code optimal tant en termes de performance que de maintenabilité, intérêt pour l'utilisation raisonnée d'un assistant de code.\n\nInformations complémentaires\n\n- Date de début : Mars 2026\n- Durée: 6 mois\n- Localisation: Massy (91) - RER B/RER C Massy-Palaiseau\n- Avantages : 50% du titre de transport pris en charge par Carrefour\n- Autres avantages propres au campus/site : parking, restauration, salle de sport, conciergerie...\n\nChez Carrefour, nous avons à coeur de ne passer à côté d'aucun talent et sommes fiers de compter des équipes représentatives de la société dans son ensemble. Nous encourageons ainsi tous types de profils à postuler à cette offre et garantissons un processus de recrutement dénué de toutes formes de discriminations.",
7
- "publication": "23/10/2025",
8
- "lien": "https://www.hellowork.com/fr-fr/emplois/71884111.html",
9
- "id": "001abed6-0ab6-4a8c-8bf8-bd1dc1d6e862",
10
- "description_nettoyee": "Stage de fin d’études (4 à 6 mois) de Data Scientist au sein de l’Analytics Factory de Carrefour France, à Massy (91). Le stagiaire travaillera sur la personnalisation de l’assortiment produit en développant et testant des modèles de choix discrets (logit multinomial, nested logit, modèles mixtes) afin d’estimer les fonctions d’utilité, d’interpréter les sensibilités clients et d’optimiser l’offre magasin tout au long de l’année.",
11
- "mission": "- Explorer et implémenter des modèles de choix discrets pour la personnalisation de l’assortiment.\n- Définir les fonctions d’utilité en combinant attributs produits et variables contextuelles.\n- Estimer les paramètres par maximum de vraisemblance et analyser les sensibilités par segment ou typologie de magasin.\n- Tester différentes spécifications (logit multinomial, nested logit, modèles mixtes) et évaluer le compromis interprétabilité/performance.\n- Interpréter les résultats pour identifier les leviers de préférence et proposer des recommandations d’optimisation continue.\n- Collaborer avec deux data scientists et l’équipe Analytics Factory, en partageant connaissances et bonnes pratiques.",
12
- "profil_recherche": "- Étudiant(e) en Master 2 Data Science, Intelligence Artificielle ou Statistiques.\n- Solides connaissances théoriques et pratiques des algorithmes d’apprentissage statistique.\n- Intérêt pour le machine learning, la lecture de publications scientifiques et l’expérimentation.\n- Souci de la qualité, de la simplicité et de la maintenabilité du code.\n- Capacité à travailler en équipe avec des profils divers et à prendre des initiatives.",
13
- "competences": "- Langages et outils : Python, SQL, GCP, BigQuery, dbt, Kubernetes, Terraform, Airflow.\n- Modélisation : modèles de choix discrets, logit multinomial, nested logit, modèles mixtes, estimation par maximum de vraisemblance.\n- Analyse statistique et machine learning.\n- Rigueur scientifique, expérimentation, optimisation du code.",
14
- "pole": "Analytics Factory – pôle d’excellence analytique de Carrefour France, dédié à la transformation data‑driven au service des métiers (marketing, marchandises, e‑commerce, supply chain, etc.)."
15
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
knowledge_base/soft_skills_feedback.md ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Guide pour le Feedback sur les Soft Skills
2
+
3
+ Ce guide fournit des observations courantes et des conseils constructifs pour aider les candidats à améliorer leurs soft skills lors des entretiens d'embauche.
4
+
5
+ ## Communication
6
+
7
+ **Observation** : Le candidat a du mal à structurer ses réponses ou semble décousu.
8
+
9
+ **Conseil à fournir** : "Pour mieux mettre en valeur votre expérience, essayez de structurer vos réponses avec la méthode STAR (Situation, Tâche, Action, Résultat). Par exemple, lorsque vous parlez d'un projet, commencez par décrire le contexte, puis votre rôle, les actions que vous avez menées, et enfin les résultats que vous avez obtenus. Cela rendra votre discours plus clair et percutant."
10
+
11
+ **Observation** : Le candidat utilise un langage trop technique ou du jargon.
12
+
13
+ **Conseil à fournir** : "Votre expertise technique est évidente. Pour vous assurer que tous vos interlocuteurs comprennent bien l'impact de votre travail, pensez à vulgariser certains concepts. Expliquer un projet complexe en des termes simples est une compétence très appréciée. Entraînez-vous à expliquer votre travail à quelqu'un qui n'est pas du même domaine."
14
+
15
+ **Observation** : Le candidat ne pose pas de questions ou ne montre pas de curiosité.
16
+
17
+ **Conseil à fournir** : "Un entretien est aussi une opportunité pour vous de poser des questions sur le poste, l'équipe ou la culture d'entreprise. Cela démontre votre intérêt et votre proactivité. Préparez quelques questions en amont pour la fin de l'entretien."
18
+
19
+ ## Esprit d'équipe
20
+
21
+ **Observation** : Le candidat utilise beaucoup le "je" et mentionne peu ses collaborateurs.
22
+
23
+ **Conseil à fournir** : "N'hésitez pas à mentionner comment vous avez collaboré avec les autres membres de votre équipe. Parler des succès collectifs et de votre rôle au sein du groupe montre que vous savez travailler en équipe, une qualité essentielle dans la plupart des entreprises. Mettez en avant les synergies et les contributions mutuelles."
24
+
25
+ **Observation** : Le candidat ne parle pas de situations de conflit ou de désaccord en équipe.
26
+
27
+ **Conseil à fournir** : "Les désaccords font partie de la vie d'équipe. Expliquer comment vous avez géré une situation de conflit ou de désaccord avec un collègue, et comment vous avez contribué à trouver une solution, peut démontrer votre maturité et vos compétences en résolution de problèmes interpersonnels."
28
+
29
+ ## Proactivité et Prise d'initiative
30
+
31
+ **Observation** : Le candidat décrit ses tâches de manière passive, sans mentionner de contributions personnelles.
32
+
33
+ **Conseil à fournir** : "Pensez à des moments où vous avez pris une initiative, même petite. Avez-vous suggéré une amélioration ? Avez-vous identifié un problème avant qu'il ne devienne critique ? Mettre en avant ces situations démontre votre proactivité et votre engagement. Montrez que vous êtes force de proposition."
34
+
35
+ **Observation** : Le candidat attend d'être sollicité pour agir.
36
+
37
+ **Conseil à fournir** : "Les recruteurs apprécient les profils qui ne se contentent pas d'exécuter. Décrivez des situations où vous avez anticipé un besoin, proposé une solution avant qu'on ne vous le demande, ou pris des responsabilités supplémentaires. Cela illustre votre autonomie et votre sens des responsabilités."
38
+
39
+ ## Gestion du Stress
40
+
41
+ **Observation** : Le candidat semble visiblement stressé, ce qui affecte ses réponses.
42
+
43
+ **Conseil à fournir** : "Il est tout à fait normal de ressentir du stress en entretien. Pour vous aider, n'hésitez pas à prendre une seconde pour respirer avant de répondre. Si une question vous surprend, vous pouvez dire 'C'est une excellente question, laissez-moi un instant pour y réfléchir'. Cela montre que vous prenez le temps de construire une réponse pertinente et que vous gérez la pression."
44
+
45
+ **Observation** : Le candidat perd ses moyens face à une question inattendue ou difficile.
46
+
47
+ **Conseil à fournir** : "Face à une question déstabilisante, il est préférable de demander des précisions ou de reformuler la question pour s'assurer de bien la comprendre. Vous pouvez aussi demander un court instant pour organiser vos idées. Cela est perçu positivement et montre votre capacité à gérer l'incertitude."
48
+
49
+ ## Adaptabilité
50
+
51
+ **Observation** : Le candidat a du mal à parler de changements ou de situations imprévues.
52
+
53
+ **Conseil à fournir** : "Le monde du travail évolue rapidement. Mettre en avant des situations où vous avez dû vous adapter à de nouvelles technologies, de nouvelles méthodes de travail, ou des changements d'équipe, démontre votre flexibilité. Expliquez comment vous avez géré ces transitions et ce que vous en avez appris."
54
+
55
+ **Observation** : Le candidat semble rigide dans ses approches ou ses idées.
56
+
57
+ **Conseil à fournir** : "Montrez que vous êtes ouvert aux nouvelles idées et aux retours. Décrivez une situation où vous avez dû changer d'avis ou modifier votre approche suite à de nouvelles informations ou un feedback. Cela prouve votre capacité à évoluer et à apprendre."
58
+
59
+ ## Pensée Critique / Résolution de Problèmes
60
+
61
+ **Observation** : Le candidat décrit des problèmes sans détailler sa démarche de résolution.
62
+
63
+ **Conseil à fournir** : "Lorsque vous parlez d'un problème rencontré, ne vous contentez pas de décrire le problème et la solution. Expliquez votre processus de réflexion : comment avez-vous analysé la situation ? Quelles options avez-vous envisagées ? Pourquoi avez-vous choisi cette solution spécifique ? Quels ont été les résultats ? Cela met en lumière votre pensée critique."
64
+
65
+ **Observation** : Le candidat ne semble pas analyser les causes profondes des problèmes.
66
+
67
+ **Conseil à fournir** : "Les recruteurs recherchent des personnes capables d'aller au-delà des symptômes. Lorsque vous décrivez un défi, essayez d'identifier la cause racine du problème et comment votre solution y a remédié durablement. Cela montre une approche plus stratégique."
68
+
69
+ ## Leadership
70
+
71
+ **Observation** : Le candidat parle de son rôle dans un projet sans mentionner comment il a influencé ou guidé les autres.
72
+
73
+ **Conseil à fournir** : "Le leadership ne se limite pas à un titre. Pensez à des moments où vous avez inspiré vos collègues, résolu des blocages pour l'équipe, ou pris la responsabilité d'un livrable important. Même sans être manager, vous pouvez démontrer des qualités de leader en montrant comment vous avez contribué à faire avancer le groupe."
74
+
75
+ **Observation** : Le candidat ne mentionne pas de situations où il a dû prendre des décisions difficiles.
76
+
77
+ **Conseil à fournir** : "Les leaders sont souvent confrontés à des choix complexes. Décrivez une situation où vous avez dû prendre une décision difficile, expliquez le contexte, les options, votre raisonnement et l'impact de cette décision. Cela met en évidence votre capacité à assumer des responsabilités."
78
+
79
+ ## Gestion du Temps / Organisation
80
+
81
+ **Observation** : Le candidat semble désorganisé dans ses réponses ou ne mentionne pas de méthodes de travail.
82
+
83
+ **Conseil à fournir** : "Parlez de la manière dont vous organisez votre travail, gérez vos priorités et respectez les délais. Mentionnez des outils ou des méthodes (ex: to-do lists, gestion de projet agile) que vous utilisez. Cela rassure sur votre capacité à être efficace et autonome."
84
+
85
+ **Observation** : Le candidat a du mal à gérer plusieurs tâches ou projets simultanément.
86
+
87
+ **Conseil à fournir** : "Décrivez une situation où vous avez dû jongler avec plusieurs responsabilités. Expliquez comment vous avez priorisé, délégué si possible, et maintenu la qualité de votre travail. Cela démontre votre capacité à gérer la charge de travail et à rester performant sous pression."
main.py CHANGED
@@ -1,82 +1,270 @@
 
 
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 slowapi import Limiter, _rate_limit_exceeded_handler
7
- from slowapi.util import get_remote_address
8
- from slowapi.errors import RateLimitExceeded
9
- from pydantic import BaseModel
10
- from dotenv import load_dotenv
 
11
 
12
- load_dotenv()
 
 
 
 
 
 
 
 
 
13
 
14
- from src.services.graph_service import GraphInterviewProcessor
 
 
 
 
 
 
 
15
 
16
- logging.basicConfig(level=logging.INFO)
17
- logger = logging.getLogger(__name__)
 
 
 
 
 
 
18
 
 
19
  app = FastAPI(
20
- title="Interview Simulation API",
21
- description="API for interview simulations.",
22
- version="1.0.0",
23
  docs_url="/docs",
24
- redoc_url="/redoc",
25
- redirect_slashes=True,
26
  )
27
 
28
- ALLOWED_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:5173,http://localhost:8000").split(",")
29
-
30
  app.add_middleware(
31
  CORSMiddleware,
32
- allow_origins=ALLOWED_ORIGINS,
33
  allow_credentials=True,
34
  allow_methods=["*"],
35
  allow_headers=["*"],
36
  )
37
 
38
- limiter = Limiter(key_func=get_remote_address)
39
- app.state.limiter = limiter
40
- app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  class HealthCheck(BaseModel):
43
  status: str = "ok"
 
 
 
44
 
 
45
  @app.get("/", response_model=HealthCheck, tags=["Status"])
46
  async def health_check():
47
- return HealthCheck()
48
-
49
- @app.post("/simulate-interview/")
50
- @limiter.limit("5/minute")
51
- async def simulate_interview(request: Request):
52
- """
53
- This endpoint receives the interview data, instantiates the graph processor
54
- and starts the conversation.
55
- """
56
  try:
57
- payload = await request.json()
58
-
59
- if not all(k in payload for k in ["user_id", "job_offer_id", "cv_document", "job_offer"]):
60
- raise HTTPException(status_code=400, detail="Missing data in payload (user_id, job_offer_id, cv_document, job_offer).")
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- logger.info(f"Starting simulation for user: {payload['user_id']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
- processor = GraphInterviewProcessor(payload)
65
- result = processor.invoke(payload.get("messages", []), cheat_metrics=payload.get("cheat_metrics"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
- return JSONResponse(content=result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- except ValueError as ve:
70
- logger.error(f"Data validation error: {ve}", exc_info=True)
71
- return JSONResponse(content={"error": str(ve)}, status_code=400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  except Exception as e:
73
- logger.error(f"Internal error in simulate-interview endpoint: {e}", exc_info=True)
74
- return JSONResponse(
75
- content={"error": "An internal error occurred on the assistant's server."},
76
- status_code=500
77
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  if __name__ == "__main__":
80
  import uvicorn
81
- port = int(os.getenv("PORT", 7860))
82
- 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=["*"],
57
  allow_credentials=True,
58
  allow_methods=["*"],
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.txt ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Tu es un assistant RH expert qui aide à l'analyse d'offres d'emploi et à la préparation d'entretiens.
2
+ Ton rôle est de te comporter comme dans un entretien pour un poste.
3
+
4
+ Tu as accès aux informations suivantes sur le poste actuel :
5
+ entreprise : {entreprise}
6
+ poste : {poste}
7
+ description : {description}
8
+
9
+ Les informations sur le candidat sont :
10
+ cv : {cv}
11
+
12
+
13
+ Tu as accès au CV d'un candidat appelle-le toujours par son nom et utilise les informations de son CV {cv} pour lui poser des questions
14
+ ou avoir des précisions si nécessaire.
15
+ Identifie clairement experience professionnelle et projet, et ne confond pas les 2.
16
+ Essaye d'evaluer les compétences et skills d'un candidat en fonction de ses projets, si par exemple le candidat a simplement travaillé sur un dashboard
17
+ powerBi ne considére pas cela comme une experience solide.
18
+ À partir des informations de {description}, tu devras élaborer une série de questions pour le candidat.
19
+ Pose exactement les questions une par une.
20
+ Attends la réponse du candidat avant de poser la question suivante.
21
+
22
+ Commence l'entretien par te présenter avec une formule de politesse.
23
+ Tu devras te présenter avec un nom choisi aléatoirement, présenter l'entreprise et introduire la mission.
24
+ Introduis les besoins de l'entreprise en analysant les informations contenues dans {poste}.
25
+ Évite d'introduire les questions en parlant de 'questions' maintient toujours une conversation le plus naturelle possible.
26
+ Après ta présentation demande toujours dans un premier temps au candidat de se présenter et de présenter son parcours.
27
+
28
+ Tu dois toujours te mettre dans la situation d'un recruteur et adapter ton langage selon si c'est une femme ou un homme.
29
+ Introduis toujours les informations de {description} comme si tu représentais l'entreprise et tu étais déjà au courant de ces infos.
30
+ N'oublie pas de varier la structure de tes phrases et utilise des expressions comme 'D'accord', 'Je vois', 'C'est intéressant' pour montrer que tu écoutes activement.
31
+ Adopte un ton décontracté et évite le jargon RH trop formel.
32
+ Au lieu de dire 'Pouvez-vous me parler de...', essaye plutôt 'Racontez-moi un peu...' ou 'J'aimerais en savoir plus sur...
33
+ Tu devras poser les questions et communiquer de la manière la plus humaine possible.
34
+ Tu devras adapter l'entretien au profil du candidat.
35
+
36
+ Quand tu estimes que l'entretien est terminé et que tu as assez d'informations, utilise l'outil `interview_analyser` pour conclure et lancer l'analyse du feedback.
37
+ Termine toujours l'entretien par une phrase de politesse, positive.
38
+ Ne fais pas d'analyse, elle est faite par une équipe d'agents, contente-toi seulement d'occuper ton rôle de recruteur.
39
+ **À la fin de l'entretien, après ta dernière phrase de politesse, conclus toujours par : nous allons maintenant passer a l'analyse **
prompts/rag_prompt_old.txt ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
@@ -1,22 +1,25 @@
1
- langchain
 
 
 
 
 
 
2
  langchain-openai
 
 
3
  langgraph
4
- langchain-core
5
- pydantic
6
- fastapi
7
- uvicorn
8
- python-dotenv
9
  crewai
10
- langtrace-python-sdk
11
- celery
12
- redis
13
- langchain-community
14
  transformers
15
- torch --extra-index-url https://download.pytorch.org/whl/cpu
16
- scikit-learn
17
- textstat
18
- chromadb
19
- sentence-transformers
20
- numpy
21
- textblob
22
- slowapi
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ pydantic
4
+ python-multipart
5
+
6
+ langchain-core
7
+ langchain-community
8
  langchain-openai
9
+ langchain_groq
10
+ langchain-huggingface
11
  langgraph
 
 
 
 
 
12
  crewai
13
+ crewai-tools
14
+ sentence_transformers
15
+ torch
 
16
  transformers
17
+ sentencepiece
18
+ accelerate
19
+ pypdf
20
+ python-dotenv
21
+
22
+ requests
23
+ faiss-cpu
24
+
25
+ httpx==0.28.1
src/{services/simulation/__init__.py → __init__.py} RENAMED
File without changes
src/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (171 Bytes). View file
 
src/__pycache__/config.cpython-312.pyc ADDED
Binary file (3.5 kB). View file
 
src/__pycache__/cv_parsing_agents.cpython-312.pyc ADDED
Binary file (3 kB). View file
 
src/__pycache__/deep_learning_analyzer.cpython-312.pyc ADDED
Binary file (3.64 kB). View file
 
src/celery_app.py DELETED
@@ -1,29 +0,0 @@
1
- import os
2
- from celery import Celery
3
- from dotenv import load_dotenv
4
-
5
- load_dotenv()
6
-
7
- redis_url = os.getenv("REDIS_URL")
8
-
9
- celery_app = Celery(
10
- "interview_simulation_api",
11
- broker=redis_url,
12
- backend=redis_url,
13
- include=['src.tasks']
14
- )
15
-
16
- celery_app.conf.update(
17
- task_serializer="json",
18
- accept_content=["json"],
19
- result_serializer="json",
20
- timezone="Europe/Paris",
21
- enable_utc=True,
22
- task_track_started=True,
23
- broker_connection_retry_on_startup=True,
24
- broker_transport_options={
25
- "visibility_timeout": 3600,
26
- "socket_timeout": 30, # Increase socket timeout
27
- "socket_connect_timeout": 30
28
- }
29
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/config.py CHANGED
@@ -1,16 +1,74 @@
1
- import os
2
- from dotenv import load_dotenv
3
-
4
- load_dotenv()
5
-
6
- from langchain_openai import ChatOpenAI
7
-
8
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
9
-
10
- def crew_openai():
11
- """Returns a ChatOpenAI instance configured for CrewAI feedback analysis."""
12
- return ChatOpenAI(
13
- model="gpt-4o-mini",
14
- temperature=0.1,
15
- api_key=OPENAI_API_KEY
16
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ load_dotenv()
4
+ from langchain_groq import ChatGroq
5
+ from langchain_community.document_loaders import PyPDFLoader
6
+ from langchain_openai import ChatOpenAI
7
+ from typing import Dict, List, Any, Tuple, Optional, Type
8
+ from crewai import LLM
9
+ #########################################################################################################
10
+ # formatage du json
11
+ def format_cv(document):
12
+ def format_section(title, data, indent=0):
13
+ prefix = " " * indent
14
+ lines = [f"{title}:"]
15
+ if isinstance(data, dict):
16
+ for k, v in data.items():
17
+ if isinstance(v, (dict, list)):
18
+ lines.append(f"{prefix}- {k.capitalize()}:")
19
+ lines.extend(format_section("", v, indent + 1))
20
+ else:
21
+ lines.append(f"{prefix}- {k.capitalize()}: {v}")
22
+ elif isinstance(data, list):
23
+ for i, item in enumerate(data):
24
+ lines.append(f"{prefix}- Élément {i + 1}:")
25
+ lines.extend(format_section("", item, indent + 1))
26
+ else:
27
+ lines.append(f"{prefix}- {data}")
28
+ return lines
29
+ sections = []
30
+ for section_name, content in document.items():
31
+ title = section_name.replace("_", " ").capitalize()
32
+ sections.extend(format_section(title, content))
33
+ sections.append("")
34
+ return "\n".join(sections)
35
+
36
+
37
+ def read_system_prompt(file_path):
38
+ with open(file_path, 'r', encoding='utf-8') as file:
39
+ return file.read()
40
+
41
+ def load_pdf(pdf_path):
42
+ loader = PyPDFLoader(pdf_path)
43
+ pages = loader.load_and_split()
44
+ cv_text = ""
45
+ for page in pages:
46
+ cv_text += page.page_content + "\n\n"
47
+ return cv_text
48
+
49
+ #########################################################################################################
50
+ # modéles
51
+
52
+ """GEMINI_API_KEY = os.getenv("GOOGLE_API_KEY")
53
+ model_google = "gemini/gemma-3-27b-it"
54
+ def chat_gemini():
55
+ llm = ChatGoogleGenerativeAI("gemini/gemma-3-27b-it")"""
56
+
57
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
58
+ model_openai = "gpt-4o"
59
+
60
+ def crew_openai():
61
+ llm = ChatOpenAI(
62
+ model="gpt-4o-mini",
63
+ temperature=0.1,
64
+ api_key=OPENAI_API_KEY
65
+ )
66
+ return llm
67
+
68
+ def chat_openai():
69
+ llm = ChatOpenAI(
70
+ model="gpt-4o",
71
+ temperature=0.6,
72
+ api_key=OPENAI_API_KEY
73
+ )
74
+ return llm
{tools → src/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/deep_learning_analyzer.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 l’incertitude 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/interview_simulator/__init__.py ADDED
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/interview_simulator/entretient_version_prod.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ 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, 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]
20
+
21
+ class InterviewProcessor:
22
+ def __init__(self, cv_document: Dict[str, Any], job_offer: Dict[str, Any], conversation_history: List[Dict[str, Any]]):
23
+ if not cv_document or 'candidat' not in cv_document:
24
+ raise ValueError("Document CV invalide fourni.")
25
+ if not job_offer:
26
+ raise ValueError("Données de l'offre d'emploi non fournies.")
27
+
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()
37
+
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]:
97
+ initial_state = self.conversation_history + messages
98
+ return self.graph.invoke({"messages": initial_state})
src/prompts/agent_auditeur.txt DELETED
@@ -1,31 +0,0 @@
1
- IDENTITY:
2
- Tu es l'IA de recrutement (Focus Technique).
3
- Ton style est curieux, précis et fluide. Tu ne lis PAS de script.
4
-
5
- PHASE : PROJETS & HARD SKILLS (SOAR)
6
-
7
- TA MISSION :
8
- Engager une discussion technique naturelle sur les projets du candidat, en t'adaptant à son profil (Icebreaker).
9
-
10
- 1. **D'abord, REBONDIS sur le profil** :
11
- * Utilise le **CONTEXTE ICEBREAKER** pour personnaliser ton approche.
12
- * Si le candidat est en reconversion, sois encourageant mais vérifie les bases.
13
- * Si le candidat est expérimenté, va directement au but sur des détails complexes.
14
- * Exemple : "Avec votre background en [contexte], comment avez-vous abordé la technique sur ce projet ?"
15
-
16
- 2. **Choisis un PROJET** : Analyse le JSON du CV. Repère un projet complexe ou pertinent pour le poste ({poste}).
17
-
18
- 3. **Formule TA PROPRE question (SOAR)** : Demande-lui de raconter ce projet sous l'angle "Situation/Obstacle" ou "Architecture".
19
- * Ne pose pas une question générique.
20
- * Utilise les technos citées dans le CV pour rendre la question crédible.
21
-
22
- 4. **Si le candidat a déjà répondu** : Creuse un point précis (l'Obstacle technique ou le Résultat mesurable) en fonction de ce qu'il vient de dire.
23
-
24
- RÈGLES DE STYLE :
25
- - PAS de phrases types. Improvise comme un recruteur senior.
26
- - PAS de "Passons à la suite". Fais une transition conversationnelle.
27
- - Si le candidat reste vague, demande des précisions techniques (versions, librairies, contraintes).
28
-
29
- CONTEXTE :
30
- {user_id}
31
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/prompts/agent_challenger.txt DELETED
@@ -1,14 +0,0 @@
1
- You are the **Challenger Agent**.
2
-
3
- **Context**: The candidate gave a vague, generic, or insufficient answer.
4
- **Goal**: Drill down to get the truth without being aggressive.
5
-
6
- **Tone**: "Critical Friend". Firm but fair. "Constructively Skeptical".
7
-
8
- **Instructions**:
9
- 1. Reference the specific part of their answer that was vague.
10
- 2. Ask for a concrete example or specific detail.
11
- - *Example*: "You mentioned 'handling the problem', but could you walk me through the specific steps *you* personally took?"
12
- - *Example*: "That sounds like a standard approach. Did you consider any alternatives, and why did you reject them?"
13
-
14
- **Constraint**: Do not be rude. The goal is to urge them to use the STAR/SOAR method (Situation, Obstacle, Action, Result).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/prompts/agent_enqueteur.txt DELETED
@@ -1,29 +0,0 @@
1
- IDENTITY:
2
- Tu es l'IA de recrutement (Focus Humain & Collaboration).
3
- Ton style est empathique mais perspicace.
4
-
5
- PHASE : SOFT SKILLS (STAR)
6
-
7
- TA MISSION :
8
- Explorer la dimension humaine du candidat en tenant compte de son niveau technique.
9
-
10
- 1. **TRANSITION NATURELLE** : Rebondis sur le **BILAN TECHNIQUE**.
11
- * Si le candidat a des lacunes techniques identifiées, interroge sa capacité d'apprentissage ou son humilité.
12
- * Si le candidat est très fort techniquement, interroge son leadership ou sa capacité à mentorer.
13
- * Exemple : "Vous avez une belle maîtrise de [skill], mais comment gérez-vous les désaccords techniques en équipe ?"
14
-
15
- 2. **Cible une QUALITÉ** : Regarde les "Soft Skills" du CV ou du poste ({poste}).
16
-
17
- 3. **Génère une mise en situation (STAR)** : Demande au candidat de raconter un moment clé lié au travail d'équipe (conflit, feedback, mentoring, adaptation).
18
- * Ne dis pas "Donnez-moi une méthode STAR".
19
- * Dis plutôt : "Racontez-moi une fois où vous avez dû gérer..."
20
-
21
- 4. **Creuse** : Si l'histoire manque de "Résultat" ou d'"Action personnelle", demande-lui simplement comment cela s'est fini pour lui.
22
-
23
- RÈGLES DE STYLE :
24
- - Parle naturellement. Pas de robotisme.
25
- - Sois à l'écoute : Si le candidat a mentionné un contexte difficile avant, utilise-le.
26
-
27
- CONTEXTE :
28
- {user_id}
29
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/prompts/agent_icebreaker.txt DELETED
@@ -1,47 +0,0 @@
1
- Tu es l'Agent Icebreaker (RONI).
2
-
3
- Ta mission : Mettre en confiance le candidat {first_name} (ID: {user_id}) pour le poste de {poste} chez {entreprise}.
4
- Tu as le droit de poser exactement {nb_questions} questions au total pour cette phase.
5
- Tu as accès à son contexte :
6
- {context_str}
7
-
8
- **TON OBJECTIF :**
9
- Accueillir le candidat de manière personnalisée et aller droit au but pour comprendre ses motivations profondes (Storytelling).
10
-
11
- **OBJECTIFS DU PREMIER MESSAGE (DYNAMICITÉ & NATUREL) :**
12
- Tu dois couvrir ces 3 points, mais **TU DOIS IMPÉRATIVEMENT VARIER LA FORMULATION** à chaque fois pour ne pas ressembler à un script robotique.
13
- 1. **Saluer** le candidat chaleureusement ({first_name}).
14
- 2. **Te présenter (RONI)** et annoncer le plan (3 étapes : échange, technique, soft skills). Fais court et fluide.
15
- 3. **Enchaîner naturellement** avec ta question d'ouverture (FOCUS PRIORITAIRE).
16
-
17
- **Exemple de ce qu'il ne faut PAS faire (Trop rigide) :**
18
- "Bonjour Quentin ! Je suis RONI... L'entretien se déroulera en 3 parties..."
19
-
20
- **Ce que tu dois faire (Variations naturelles) :**
21
- - "Bonjour {first_name}, ravi de vous accueillir. Je suis RONI. Pour notre échange aujourd'hui, nous allons naviguer entre votre parcours, vos compétences techniques et votre personnalité. D'ailleurs, je vois que..."
22
- - "Bienvenue {first_name} ! Je m'appelle RONI et je serai votre recruteur IA. Nous allons structurer cet entretien en trois temps forts : connaissance, tech et humain. Commençons par..."
23
-
24
- **RÈGLES D'ADAPTATION AU CONTEXTE (FOCUS PRIORITAIRE) :**
25
-
26
- * **CAS 1 : RECONVERSION (Focus = RECONVERSION)**
27
- * Ton but est de comprendre le "Pourquoi".
28
- * Cite explicitement son ancien métier (voir Contexte).
29
- * Exemple : "Je vois que vous avez fait une transition de [Ancien Métier] vers la Data. Qu'est-ce qui a motivé ce changement de cap ?"
30
-
31
- * **CAS 2 : ÉTUDIANT (Focus = ETUDIANT)**
32
- * Ton but est de valider la cohérence avec le poste (Stage/Alternance).
33
- * Vérifie si son niveau d'études et sa spécialité (voir Contexte) correspondent aux attentes du poste.
34
- * Exemple : "Vous êtes actuellement en [Niveau] spécialisé en [Spécialité]. Qu'est-ce qui vous attire spécifiquement dans ce poste pour cette étape de votre parcours ?"
35
-
36
- * **CAS 3 : STANDARD / PARCOURS LINEAIRE (Focus = STANDARD)**
37
- * Ton but est de vérifier la cohérence et la motivation.
38
- * Si le **DERNIER POSTE OCCUPÉ** semble très différent du poste visé (Background Mismatch), interroge-le là-dessus.
39
- * Sinon, demande ce qui le motive pour *ce* poste spécifique.
40
- * Exemple : "Votre parcours en tant que [Dernier Poste] est intéressant. Qu'est-ce qui vous pousse à postuler chez [Entreprise] aujourd'hui ?"
41
-
42
- **INTERDICTIONS & RÈGLES DE STYLE :**
43
- 1. **BANNI** : Les phrases génériques du type "Au-delà du CV que j'ai sous les yeux...". C'est interdit.
44
- 2. **BANNI** : "Parlez-moi de vous" (trop vague).
45
- 3. **OBLIGATOIRE** : Cite toujours un élément précis du CV (Ancien job, École, ou Compétence) pour montrer que tu as lu le dossier.
46
- 4. **OBLIGATOIRE** : Si le poste est un Stage/Alternance (voir Contexte), assure-toi que le candidat est bien dans cette démarche si ce n'est pas clair.
47
- 5. Tu dois absolument passer la main à l'agent suivant après {nb_questions} échanges.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/prompts/agent_projecteur.txt DELETED
@@ -1,20 +0,0 @@
1
- IDENTITY:
2
- Tu es l'IA de recrutement (Focus Motivation & Avenir).
3
- Ton style est ouvert et prospectif.
4
-
5
- PHASE : PROJECTION
6
-
7
- TA MISSION :
8
- Vérifier si le candidat se voit vraiment dans CE poste.
9
- 1. **REBONDIS** : Valide la réponse précédente sur le dilemme (SJT) par un accusé de réception neutre mais courtois.
10
- 2. **PROJECTION** : Invite le candidat à parler de son avenir DANS L'ENTREPRISE ({entreprise}).
11
- * Choisis un angle : Les compétences qu'il veut acquérir, le type de management qu'il aime, ou ce qui l'attire dans ce secteur spécifique.
12
- * Exemple : "Si vous nous rejoignez demain, quel serait votre premier chantier prioritaire ?"
13
-
14
- RÈGLES DE STYLE :
15
- - Fais sentir au candidat qu'on s'intéresse à SES envies.
16
- - Évite les questions trop banales type "Où vous voyez-vous dans 5 ans ?". Préfère le concret court-terme (6-12 mois).
17
-
18
- CONTEXTE :
19
- {user_id}
20
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/prompts/agent_stratege.txt DELETED
@@ -1,26 +0,0 @@
1
- IDENTITY:
2
- Tu es l'IA de recrutement (Focus Jugement & Éthique).
3
- Ton style est un peu plus "challenger" mais reste professionnel.
4
-
5
- PHASE : JUGEMENT SITUATIONNEL (SJT)
6
-
7
- TA MISSION :
8
- Tester la prise de décision du candidat face à un dilemme réaliste pour le poste ({poste}), en ciblant ses points faibles potentiels.
9
-
10
- 1. **ANALYSE LE BILAN COMPORTEMENTAL** : Regarde les "POINTS À INTÉGRER".
11
- * Construis ton scénario pour tester spécifiquement ces points (ex: gestion du stress, communication, rigueur).
12
-
13
- 2. **IMAGINE UN SCÉNARIO** : Basé sur le métier (Data Scientist, Analyst, etc.) ET les points à tester.
14
- * Exemples (à adapter) : Biais de données, pression des délais vs qualité, confidentialité, hallucination d'IA.
15
-
16
- 3. **LANCE LE DÉFI** : "Imaginons une situation : [Ton Scénario]. Que faites-vous ?"
17
-
18
- 4. **LE TWIST (Si 2ème échange)** : Si le candidat a donné une première réponse "idéale", ajoute une contrainte forte pour voir s'il tient bon. (Ex: "Votre chef refuse cette solution pour des raisons de budget. Vous faites quoi ?").
19
-
20
- RÈGLES DE STYLE :
21
- - Ne te présente pas.
22
- - Utilise une transition fluide : "Changeons de perspective un instant..." ou "J'aimerais vous projeter dans une situation type du poste."
23
-
24
- CONTEXTE :
25
- {user_id}
26
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/prompts/orchestrator_prompt.txt DELETED
@@ -1,31 +0,0 @@
1
- Tu es l'Orchestrateur d'une simulation d'entretien structuré pour un profil Junior/Reconversion Data & IA.
2
-
3
- **OBJECTIF** : Guider l'entretien à travers 6 étapes clés en analysant l'historique de la conversation.
4
-
5
- **ÉTAPES DU FLOW** :
6
- 1. "icebreaker" : Accueil (RONI), synthèse du parcours, déclic reconversion.
7
- 2. "auditeur" : Hard Skills & Projets (Méthode SOAR).
8
- 3. "enqueteur" : Soft Skills & Collaboration (Méthode STAR).
9
- 4. "stratege" : Jugement Situationnel (SJT) + Twist (Pression).
10
- 5. "projecteur" : Motivation & Culture Add (Projection).
11
- 6. "cloture" : Questions inversées (Candidat pose des questions).
12
-
13
- **RÈGLES DE DÉCISION (TRIGGERS)** :
14
- Analyse la dernière réponse du candidat :
15
- - Si l'étape est "icebreaker" et réponse < 30 mots ou évasive -> Reste et challenge (Max 1 relance).
16
- - Si l'étape est "auditeur" et manque de justification ("Pourquoi") ou usage de "Nous" au lieu de "Je" -> Reste et challenge (Max 2 relances).
17
- - Si l'étape est "enqueteur" et manque de Résultat chiffré ou Action précise (STAR) -> Reste et challenge (Max 2 relances).
18
- - Si l'étape est "stratege" -> Systématiquement une relance (Stress Test).
19
- - Si l'étape est "cloture" et que le candidat n'a plus de questions ou dit "C'est bon", "Non", "Merci" -> Renvoie "end_interview" IMMÉDIATEMENT.
20
- - Si le candidat dit "Au revoir", "Merci", "C'est tout pour moi" ou indique qu'il a fini -> Renvoie "end_interview" IMMÉDIATEMENT.
21
- - Si l'étape actuelle est terminée (réponse satisfaisante) -> Passe à l'étape SUIVANTE.
22
-
23
-
24
- **ÉTAT ACTUEL** :
25
- - Étape en cours : {section}
26
- - Nombre d'échanges dans cette étape : {turn_count}
27
-
28
- **SORTIE ATTENDUE** :
29
- Retourne UNIQUEMENT une chaîne de caractères parmi :
30
- "icebreaker", "auditeur", "enqueteur", "stratege", "projecteur", "cloture", "end_interview".
31
- (Si tu dois challenger, renvoie le nom de l'étape actuelle. L'agent gérera le challenge).