quentinL52 commited on
Commit ·
90d6a84
1
Parent(s): d379dd9
update
Browse files- README.md +5 -1
- documentation/API_DOCUMENTATION.md +126 -0
- documentation/cv_structure.json +121 -0
- documentation/job_offer_structure.json +15 -0
- src/prompts/agent_auditeur.txt +1 -1
- src/prompts/agent_enqueteur.txt +1 -1
- src/prompts/agent_icebreaker.txt +37 -18
- src/prompts/agent_projecteur.txt +1 -1
- src/prompts/agent_stratege.txt +1 -1
- src/services/graph_service.py +98 -49
README.md
CHANGED
|
@@ -18,4 +18,8 @@ Cette API permet de simuler des entretiens d'embauche avec des agents IA intelli
|
|
| 18 |
|
| 19 |
## Configuration
|
| 20 |
|
| 21 |
-
Assurez-vous de configurer vos variables d'environnement dans les Settings du Space Hugging Face.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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).
|
documentation/API_DOCUMENTATION.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|
src/prompts/agent_auditeur.txt
CHANGED
|
@@ -28,4 +28,4 @@ RÈGLES DE STYLE :
|
|
| 28 |
|
| 29 |
CONTEXTE :
|
| 30 |
{user_id}
|
| 31 |
-
|
|
|
|
| 28 |
|
| 29 |
CONTEXTE :
|
| 30 |
{user_id}
|
| 31 |
+
|
src/prompts/agent_enqueteur.txt
CHANGED
|
@@ -26,4 +26,4 @@ RÈGLES DE STYLE :
|
|
| 26 |
|
| 27 |
CONTEXTE :
|
| 28 |
{user_id}
|
| 29 |
-
|
|
|
|
| 26 |
|
| 27 |
CONTEXTE :
|
| 28 |
{user_id}
|
| 29 |
+
|
src/prompts/agent_icebreaker.txt
CHANGED
|
@@ -8,21 +8,40 @@ Tu as accès à son contexte :
|
|
| 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 |
-
**
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
**RÈGLES D'
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
CHANGED
|
@@ -17,4 +17,4 @@ RÈGLES DE STYLE :
|
|
| 17 |
|
| 18 |
CONTEXTE :
|
| 19 |
{user_id}
|
| 20 |
-
|
|
|
|
| 17 |
|
| 18 |
CONTEXTE :
|
| 19 |
{user_id}
|
| 20 |
+
|
src/prompts/agent_stratege.txt
CHANGED
|
@@ -23,4 +23,4 @@ RÈGLES DE STYLE :
|
|
| 23 |
|
| 24 |
CONTEXTE :
|
| 25 |
{user_id}
|
| 26 |
-
|
|
|
|
| 23 |
|
| 24 |
CONTEXTE :
|
| 25 |
{user_id}
|
| 26 |
+
|
src/services/graph_service.py
CHANGED
|
@@ -22,7 +22,7 @@ PROMPTS_DIR = Path(__file__).parent.parent / "prompts"
|
|
| 22 |
|
| 23 |
# Number of questions per agent
|
| 24 |
QUESTIONS_PER_AGENT = {
|
| 25 |
-
"icebreaker":
|
| 26 |
"auditeur": 3,
|
| 27 |
"enqueteur": 2,
|
| 28 |
"stratege": 2,
|
|
@@ -32,9 +32,6 @@ QUESTIONS_PER_AGENT = {
|
|
| 32 |
AGENT_ORDER = ["icebreaker", "auditeur", "enqueteur", "stratege", "projecteur"]
|
| 33 |
|
| 34 |
|
| 35 |
-
EXIT_MESSAGE = """L'entretien est maintenant terminé.
|
| 36 |
-
|
| 37 |
-
Merci pour cet échange. Je finalise l'analyse de votre candidature, votre rapport sera disponible dans quelques instants."""
|
| 38 |
|
| 39 |
|
| 40 |
class AgentState(TypedDict):
|
|
@@ -110,48 +107,85 @@ class GraphInterviewProcessor:
|
|
| 110 |
|
| 111 |
for agent in AGENT_ORDER:
|
| 112 |
agent_questions = QUESTIONS_PER_AGENT[agent]
|
| 113 |
-
|
| 114 |
-
# Check if user is still within this agent's range
|
| 115 |
if user_msg_count < cumulative + agent_questions:
|
| 116 |
return agent, False
|
| 117 |
|
| 118 |
cumulative += agent_questions
|
| 119 |
-
|
| 120 |
-
# Past all agents -> END (projecteur completed)
|
| 121 |
return "end", True
|
| 122 |
|
| 123 |
# --- Context builders ---
|
| 124 |
|
|
|
|
|
|
|
| 125 |
def _get_icebreaker_context(self, state: AgentState) -> str:
|
| 126 |
cv = state["cv_data"]
|
| 127 |
prenom = cv.get("first_name", "Candidat")
|
| 128 |
job = state["job_data"]
|
| 129 |
|
| 130 |
# Extract Hobbies/Interests if available
|
| 131 |
-
hobbies = ", ".join(
|
| 132 |
if not hobbies:
|
| 133 |
hobbies = "Non spécifiés"
|
| 134 |
|
| 135 |
-
# Extract Reconversion Context
|
| 136 |
-
reconversion_data =
|
| 137 |
is_reco = reconversion_data.get("is_reconversion", False)
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
-
# Extract Student Context
|
| 141 |
-
etudiant_data =
|
| 142 |
is_etudiant = etudiant_data.get("is_etudiant", False)
|
| 143 |
-
etudiant_context = f"OUI ({etudiant_data.get('niveau_etudes', '')})" if is_etudiant else "NON"
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
return f"""
|
| 146 |
=== CONTEXTE CANDIDAT ===
|
| 147 |
PRENOM: {prenom}
|
| 148 |
POSTE VISÉ: {job.get('poste', 'Non spécifié')}
|
| 149 |
ENTREPRISE: {job.get('entreprise', 'Non spécifié')}
|
|
|
|
| 150 |
|
| 151 |
=== DOSSIER PERSONNEL ===
|
| 152 |
CENTRES D'INTÉRÊT: {hobbies}
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
"""
|
| 156 |
|
| 157 |
def _get_auditeur_context(self, state: AgentState) -> str:
|
|
@@ -190,9 +224,14 @@ class GraphInterviewProcessor:
|
|
| 190 |
"""
|
| 191 |
|
| 192 |
return f"""
|
| 193 |
-
=== CONTEXTE TECHNIQUE ===
|
| 194 |
-
MISSION
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
PROJETS SIGNIFICATIFS:
|
| 197 |
{projets_str}
|
| 198 |
|
|
@@ -203,6 +242,7 @@ class GraphInterviewProcessor:
|
|
| 203 |
|
| 204 |
def _get_enqueteur_context(self, state: AgentState) -> str:
|
| 205 |
cv = state["cv_data"]
|
|
|
|
| 206 |
soft_skills = ", ".join(cv.get("compétences", {}).get("soft_skills", []))
|
| 207 |
reconversion = cv.get("reconversion", {})
|
| 208 |
is_reco = reconversion.get("is_reconversion", False)
|
|
@@ -221,9 +261,13 @@ class GraphInterviewProcessor:
|
|
| 221 |
"""
|
| 222 |
|
| 223 |
return f"""
|
| 224 |
-
=== CONTEXTE
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
SOFT SKILLS: {soft_skills}
|
| 226 |
-
{reco_txt}
|
| 227 |
{tech_context}
|
| 228 |
"""
|
| 229 |
|
|
@@ -243,7 +287,7 @@ class GraphInterviewProcessor:
|
|
| 243 |
return f"""
|
| 244 |
=== CONTEXTE SJT (MISE EN SITUATION) ===
|
| 245 |
MISSION: {job.get('mission', '')}
|
| 246 |
-
|
| 247 |
{beh_context}
|
| 248 |
"""
|
| 249 |
|
|
@@ -252,7 +296,8 @@ class GraphInterviewProcessor:
|
|
| 252 |
return f"""
|
| 253 |
=== CONTEXTE PROJECTION ===
|
| 254 |
ENTREPRISE: {job.get('entreprise', '')}
|
| 255 |
-
DESCRIPTION POSTE: {job.get('
|
|
|
|
| 256 |
|
| 257 |
NOTE: C'est la dernière question de l'entretien.
|
| 258 |
"""
|
|
@@ -284,8 +329,6 @@ class GraphInterviewProcessor:
|
|
| 284 |
|
| 285 |
# Check for Final Report extraction
|
| 286 |
if should_end:
|
| 287 |
-
# Check if we missed situation data (if flow ended abruptly?) - assuming linear flow for now.
|
| 288 |
-
# Only trigger if we haven't tried yet (flag in context)
|
| 289 |
if not state.get("situation_data") and not context_updates.get("situation_attempted"):
|
| 290 |
extract_target = "situation"
|
| 291 |
elif not state.get("simulation_report") and not context_updates.get("report_attempted"):
|
|
@@ -351,7 +394,6 @@ class GraphInterviewProcessor:
|
|
| 351 |
user_id=state["user_id"],
|
| 352 |
first_name=prenom,
|
| 353 |
nb_questions=nb_questions,
|
| 354 |
-
job_description=json.dumps(state["job_data"], ensure_ascii=False),
|
| 355 |
poste=state["job_data"].get("poste", "Poste non spécifié"),
|
| 356 |
entreprise=state["job_data"].get("entreprise", "Entreprise confidentielle")
|
| 357 |
)
|
|
@@ -400,39 +442,46 @@ class GraphInterviewProcessor:
|
|
| 400 |
except Exception as e:
|
| 401 |
logger.error(f"Failed to enqueue analysis task: {e}")
|
| 402 |
|
| 403 |
-
# Generate contextual exit message
|
| 404 |
last_user_msg = state["messages"][-1].content if state["messages"] and isinstance(state["messages"][-1], HumanMessage) else ""
|
| 405 |
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
return {"messages": [AIMessage(content=final_content)], "context": {"next_dest": "end_interview"}}
|
| 424 |
|
| 425 |
# --- Routing ---
|
| 426 |
|
| 427 |
def _route_next_step(self, state: AgentState) -> str:
|
| 428 |
-
# Check if extraction is needed
|
| 429 |
if state.get("context", {}).get("extract_target"):
|
| 430 |
return "extraction_node"
|
| 431 |
|
| 432 |
dest = state.get("context", {}).get("next_dest", "icebreaker")
|
| 433 |
if dest == "end_interview":
|
| 434 |
-
# Ensure we have the report before finishing. Loop back to orchestrator.
|
| 435 |
-
# Only loop back if we haven't attempted to extract the report yet.
|
| 436 |
if not state.get("simulation_report") and not state.get("context", {}).get("report_attempted"):
|
| 437 |
return "orchestrator"
|
| 438 |
return "final_analysis"
|
|
@@ -444,7 +493,7 @@ class GraphInterviewProcessor:
|
|
| 444 |
graph = StateGraph(AgentState)
|
| 445 |
|
| 446 |
graph.add_node("orchestrator", self._orchestrator_node)
|
| 447 |
-
graph.add_node("extraction_node", self._extraction_node)
|
| 448 |
graph.add_node("icebreaker_agent", self._icebreaker_node)
|
| 449 |
graph.add_node("auditeur_agent", self._auditeur_node)
|
| 450 |
graph.add_node("enqueteur_agent", self._enqueteur_node)
|
|
@@ -462,7 +511,7 @@ class GraphInterviewProcessor:
|
|
| 462 |
"stratege_agent": "stratege_agent",
|
| 463 |
"projecteur_agent": "projecteur_agent",
|
| 464 |
"final_analysis": "final_analysis",
|
| 465 |
-
"orchestrator": "orchestrator"
|
| 466 |
}
|
| 467 |
|
| 468 |
graph.add_conditional_edges("orchestrator", self._route_next_step, routing_map)
|
|
|
|
| 22 |
|
| 23 |
# Number of questions per agent
|
| 24 |
QUESTIONS_PER_AGENT = {
|
| 25 |
+
"icebreaker": 3,
|
| 26 |
"auditeur": 3,
|
| 27 |
"enqueteur": 2,
|
| 28 |
"stratege": 2,
|
|
|
|
| 32 |
AGENT_ORDER = ["icebreaker", "auditeur", "enqueteur", "stratege", "projecteur"]
|
| 33 |
|
| 34 |
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
class AgentState(TypedDict):
|
|
|
|
| 107 |
|
| 108 |
for agent in AGENT_ORDER:
|
| 109 |
agent_questions = QUESTIONS_PER_AGENT[agent]
|
|
|
|
|
|
|
| 110 |
if user_msg_count < cumulative + agent_questions:
|
| 111 |
return agent, False
|
| 112 |
|
| 113 |
cumulative += agent_questions
|
|
|
|
|
|
|
| 114 |
return "end", True
|
| 115 |
|
| 116 |
# --- Context builders ---
|
| 117 |
|
| 118 |
+
# --- Context builders ---
|
| 119 |
+
|
| 120 |
def _get_icebreaker_context(self, state: AgentState) -> str:
|
| 121 |
cv = state["cv_data"]
|
| 122 |
prenom = cv.get("first_name", "Candidat")
|
| 123 |
job = state["job_data"]
|
| 124 |
|
| 125 |
# Extract Hobbies/Interests if available
|
| 126 |
+
hobbies = ", ".join(cv.get("centres_interet", []))
|
| 127 |
if not hobbies:
|
| 128 |
hobbies = "Non spécifiés"
|
| 129 |
|
| 130 |
+
# --- Extract Reconversion Context ---
|
| 131 |
+
reconversion_data = cv.get("reconversion", {})
|
| 132 |
is_reco = reconversion_data.get("is_reconversion", False)
|
| 133 |
+
reco_context_str = "NON"
|
| 134 |
+
if is_reco:
|
| 135 |
+
original_job = reconversion_data.get("context", "Ancien métier non spécifié")
|
| 136 |
+
reco_context_str = f"OUI. Contexte transition: {original_job}"
|
| 137 |
|
| 138 |
+
# --- Extract Student Context ---
|
| 139 |
+
etudiant_data = cv.get("etudiant", {})
|
| 140 |
is_etudiant = etudiant_data.get("is_etudiant", False)
|
|
|
|
| 141 |
|
| 142 |
+
niveau_etudes = etudiant_data.get("niveau_etudes", "Non spécifié")
|
| 143 |
+
specialite = etudiant_data.get("specialite", "Non spécifiée")
|
| 144 |
+
|
| 145 |
+
etudiant_context_str = "NON"
|
| 146 |
+
if is_etudiant:
|
| 147 |
+
etudiant_context_str = f"OUI. Niveau: {niveau_etudes}, Spécialité: {specialite}"
|
| 148 |
+
|
| 149 |
+
# --- Internship / Alternance Detection ---
|
| 150 |
+
job_title = job.get('poste', '').lower()
|
| 151 |
+
contract_type = job.get('contrat', '').lower()
|
| 152 |
+
is_internship_or_alternance = any(k in job_title or k in contract_type for k in ['stage', 'alternance', 'apprentissage', 'contrat pro'])
|
| 153 |
+
|
| 154 |
+
type_contrat_str = "CDI/CDD Standard"
|
| 155 |
+
if is_internship_or_alternance:
|
| 156 |
+
type_contrat_str = "STAGE / ALTERNANCE"
|
| 157 |
+
|
| 158 |
+
# --- Priority Logic (Student vs Reconversion) ---
|
| 159 |
+
focus_point = "STANDARD"
|
| 160 |
+
if is_reco:
|
| 161 |
+
focus_point = "RECONVERSION"
|
| 162 |
+
elif is_etudiant:
|
| 163 |
+
focus_point = "ETUDIANT"
|
| 164 |
+
|
| 165 |
+
# --- Background Mismatch Detection (if not student/reco) ---
|
| 166 |
+
last_exp_title = ""
|
| 167 |
+
experiences = cv.get("expériences", [])
|
| 168 |
+
if experiences:
|
| 169 |
+
last_exp_title = experiences[0].get("Poste", "")
|
| 170 |
+
|
| 171 |
return f"""
|
| 172 |
=== CONTEXTE CANDIDAT ===
|
| 173 |
PRENOM: {prenom}
|
| 174 |
POSTE VISÉ: {job.get('poste', 'Non spécifié')}
|
| 175 |
ENTREPRISE: {job.get('entreprise', 'Non spécifié')}
|
| 176 |
+
TYPE DE CONTRAT DÉTECTÉ: {type_contrat_str}
|
| 177 |
|
| 178 |
=== DOSSIER PERSONNEL ===
|
| 179 |
CENTRES D'INTÉRÊT: {hobbies}
|
| 180 |
+
|
| 181 |
+
=== ANALYSE PROFIL ===
|
| 182 |
+
IS_RECONVERSION: {reco_context_str}
|
| 183 |
+
IS_ETUDIANT: {etudiant_context_str}
|
| 184 |
+
DERNIER POSTE OCCUPÉ: {last_exp_title}
|
| 185 |
+
|
| 186 |
+
=== FOCUS PRIORITAIRE ===
|
| 187 |
+
POINT D'ENTRÉE SUGGÉRÉ: {focus_point}
|
| 188 |
+
(Si RECONVERSION -> Creuser motivation transition. Si ETUDIANT -> Valider niveau/spécialité. Si STANDARD -> Vérifier cohérence parcours/poste)
|
| 189 |
"""
|
| 190 |
|
| 191 |
def _get_auditeur_context(self, state: AgentState) -> str:
|
|
|
|
| 224 |
"""
|
| 225 |
|
| 226 |
return f"""
|
| 227 |
+
=== CONTEXTE POSTE (TECHNIQUE) ===
|
| 228 |
+
MISSION: {job.get('mission', '')}
|
| 229 |
+
DESCRIPTION SYNTHÉTIQUE: {job.get('description_nettoyee', '')}
|
| 230 |
+
COMPÉTENCES REQUISES: {job.get('competences', '')}
|
| 231 |
+
PÔLE: {job.get('pole', '')}
|
| 232 |
+
|
| 233 |
+
=== CONTEXTE TECHNIQUE CANDIDAT ===
|
| 234 |
+
EXPÉRIENCES: {experiences}
|
| 235 |
PROJETS SIGNIFICATIFS:
|
| 236 |
{projets_str}
|
| 237 |
|
|
|
|
| 242 |
|
| 243 |
def _get_enqueteur_context(self, state: AgentState) -> str:
|
| 244 |
cv = state["cv_data"]
|
| 245 |
+
job = state["job_data"]
|
| 246 |
soft_skills = ", ".join(cv.get("compétences", {}).get("soft_skills", []))
|
| 247 |
reconversion = cv.get("reconversion", {})
|
| 248 |
is_reco = reconversion.get("is_reconversion", False)
|
|
|
|
| 261 |
"""
|
| 262 |
|
| 263 |
return f"""
|
| 264 |
+
=== CONTEXTE POSTE (HUMAIN) ===
|
| 265 |
+
PROFIL RECHERCHÉ: {job.get('profil_recherche', '')}
|
| 266 |
+
CULTURE/VALEURS (PÔLE): {job.get('pole', '')}
|
| 267 |
+
|
| 268 |
+
=== CONTEXTE COMPORTEMENTAL CANDIDAT ===
|
| 269 |
SOFT SKILLS: {soft_skills}
|
| 270 |
+
RECONVERSION: {reco_txt}
|
| 271 |
{tech_context}
|
| 272 |
"""
|
| 273 |
|
|
|
|
| 287 |
return f"""
|
| 288 |
=== CONTEXTE SJT (MISE EN SITUATION) ===
|
| 289 |
MISSION: {job.get('mission', '')}
|
| 290 |
+
PROFIL RECHERCHÉ: {job.get('profil_recherche', '')}
|
| 291 |
{beh_context}
|
| 292 |
"""
|
| 293 |
|
|
|
|
| 296 |
return f"""
|
| 297 |
=== CONTEXTE PROJECTION ===
|
| 298 |
ENTREPRISE: {job.get('entreprise', '')}
|
| 299 |
+
DESCRIPTION POSTE (NETTOYÉE): {job.get('description_nettoyee', '')}
|
| 300 |
+
PÔLE: {job.get('pole', '')}
|
| 301 |
|
| 302 |
NOTE: C'est la dernière question de l'entretien.
|
| 303 |
"""
|
|
|
|
| 329 |
|
| 330 |
# Check for Final Report extraction
|
| 331 |
if should_end:
|
|
|
|
|
|
|
| 332 |
if not state.get("situation_data") and not context_updates.get("situation_attempted"):
|
| 333 |
extract_target = "situation"
|
| 334 |
elif not state.get("simulation_report") and not context_updates.get("report_attempted"):
|
|
|
|
| 394 |
user_id=state["user_id"],
|
| 395 |
first_name=prenom,
|
| 396 |
nb_questions=nb_questions,
|
|
|
|
| 397 |
poste=state["job_data"].get("poste", "Poste non spécifié"),
|
| 398 |
entreprise=state["job_data"].get("entreprise", "Entreprise confidentielle")
|
| 399 |
)
|
|
|
|
| 442 |
except Exception as e:
|
| 443 |
logger.error(f"Failed to enqueue analysis task: {e}")
|
| 444 |
|
| 445 |
+
# Generate contextual dynamic exit message
|
| 446 |
last_user_msg = state["messages"][-1].content if state["messages"] and isinstance(state["messages"][-1], HumanMessage) else ""
|
| 447 |
|
| 448 |
+
cv = state.get("cv_data", {})
|
| 449 |
+
prenom = cv.get("info_personnelle", {}).get("first_name", "Candidat")
|
| 450 |
+
job = state.get("job_data", {})
|
| 451 |
+
poste = job.get("poste", "le poste visé")
|
| 452 |
+
entreprise = job.get("entreprise", "notre entreprise")
|
| 453 |
+
|
| 454 |
+
closing_prompt = (
|
| 455 |
+
f"Tu es RONI, un recruteur expert en IA. L'entretien pour le poste de {poste} chez {entreprise} est terminé.\n"
|
| 456 |
+
f"Le candidat s'appelle {prenom}.\n\n"
|
| 457 |
+
f"Dernier message du candidat : \"{last_user_msg}\"\n\n"
|
| 458 |
+
f"Tâche : Rédige le message de clôture de l'entretien.\n"
|
| 459 |
+
f"Consignes obligatoires :\n"
|
| 460 |
+
f"1. Si le candidat a posé une question ou fait une remarque finale, réponds-y brièvement et aimablement.\n"
|
| 461 |
+
f"2. Remercie {prenom} pour cet échange.\n"
|
| 462 |
+
f"3. Informe-le que l'analyse complète de l'entretien est en cours et que son rapport détaillé sera disponible dans quelques instants.\n"
|
| 463 |
+
f"4. Adopte un ton chaleureux, professionnel et encourageant.\n"
|
| 464 |
+
f"5. IMPORTANT : NE POSE AUCUNE QUESTION. Ce message marque la fin définitive de la discussion.\n"
|
| 465 |
+
f"6. Fais varier la formulation pour ne pas être robotique."
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
try:
|
| 469 |
+
ai_response = self.llm.invoke([SystemMessage(content=closing_prompt)])
|
| 470 |
+
final_content = ai_response.content
|
| 471 |
+
except Exception as e:
|
| 472 |
+
logger.error(f"Error generating closing message: {e}")
|
| 473 |
+
final_content = "Merci pour cet échange. Votre rapport d'analyse sera disponible dans quelques instants."
|
| 474 |
|
| 475 |
return {"messages": [AIMessage(content=final_content)], "context": {"next_dest": "end_interview"}}
|
| 476 |
|
| 477 |
# --- Routing ---
|
| 478 |
|
| 479 |
def _route_next_step(self, state: AgentState) -> str:
|
|
|
|
| 480 |
if state.get("context", {}).get("extract_target"):
|
| 481 |
return "extraction_node"
|
| 482 |
|
| 483 |
dest = state.get("context", {}).get("next_dest", "icebreaker")
|
| 484 |
if dest == "end_interview":
|
|
|
|
|
|
|
| 485 |
if not state.get("simulation_report") and not state.get("context", {}).get("report_attempted"):
|
| 486 |
return "orchestrator"
|
| 487 |
return "final_analysis"
|
|
|
|
| 493 |
graph = StateGraph(AgentState)
|
| 494 |
|
| 495 |
graph.add_node("orchestrator", self._orchestrator_node)
|
| 496 |
+
graph.add_node("extraction_node", self._extraction_node)
|
| 497 |
graph.add_node("icebreaker_agent", self._icebreaker_node)
|
| 498 |
graph.add_node("auditeur_agent", self._auditeur_node)
|
| 499 |
graph.add_node("enqueteur_agent", self._enqueteur_node)
|
|
|
|
| 511 |
"stratege_agent": "stratege_agent",
|
| 512 |
"projecteur_agent": "projecteur_agent",
|
| 513 |
"final_analysis": "final_analysis",
|
| 514 |
+
"orchestrator": "orchestrator"
|
| 515 |
}
|
| 516 |
|
| 517 |
graph.add_conditional_edges("orchestrator", self._route_next_step, routing_map)
|