quentinL52 commited on
Commit
90d6a84
·
1 Parent(s): d379dd9
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
- {job_description}
 
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
- {job_description}
 
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
- **STRUCTURE OBLIGATOIRE DU PREMIER MESSAGE :**
12
- 1. **Salutation** : "Bonjour {first_name} !" (ou juste "Bonjour !" si prénom vide).
13
- 2. **Présentation & Déroulé** : "Je suis RONI, l'IA qui va vous accompagner. L'entretien se déroulera en 3 parties : nous ferons d'abord connaissance, puis nous parlerons technique, et enfin soft skills."
14
- 3. **Question d'ouverture** : Enchaîne **immédiatement** (dans le même message) sur la phase de découverte/motivation.
15
-
16
- **PHASE DE DÉCOUVERTE (Storytelling & Motivation)**
17
- - Au lieu d'un "Présentez-vous" classique, invite-le au récit.
18
- - Exemple : "Au-delà du CV que j'ai sous les yeux, racontez-moi votre histoire : qu'est-ce qui vous a amené à postuler aujourd'hui ?"
19
- - **ADAPTATION AU CONTEXTE** :
20
- - Si **RECONVERSION = OUI** : Demande ce qui a déclenché ce changement de cap.
21
- - Si **ÉTUDIANT = OUI** : Demande ce qui l'attire dans ce poste pour démarrer sa carrière.
22
- - Sinon : Demande ce qui motive sa passion pour ce domaine.
23
-
24
- **RÈGLES D'OR :**
25
- 1. PAS de questions "Bateau" sur la météo ou la journée ("Comment allez-vous ?").
26
- 2. PAS de questions sur les hobbies sauf si le candidat en parle spontanément.
27
- 3. Sois chaleureux mais professionnel et direct.
28
- 4. Tu dois absolument passer la main à l'agent suivant après {nb_questions} échanges.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- {job_description}
 
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
- {job_description}
 
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": 2,
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(state["cv_data"].get("centres_interet", []))
132
  if not hobbies:
133
  hobbies = "Non spécifiés"
134
 
135
- # Extract Reconversion Context
136
- reconversion_data = state["cv_data"].get("reconversion", {})
137
  is_reco = reconversion_data.get("is_reconversion", False)
138
- reco_context = "OUI" if is_reco else "NON"
 
 
 
139
 
140
- # Extract Student Context
141
- etudiant_data = state["cv_data"].get("etudiant", {})
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
- EN RECONVERSION ?: {reco_context}
154
- ÉTUDIANT ?: {etudiant_context}
 
 
 
 
 
 
 
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 DU POSTE: {job.get('mission', '')}
195
- EXPÉRIENCES CANDIDAT: {experiences}
 
 
 
 
 
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 COMPORTEMENTAL ===
 
 
 
 
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
- CULTURE/VALEURS: {job.get('profil_recherche', '')}
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('description_poste', '')}
 
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
- if last_user_msg:
407
- closing_prompt = (
408
- f"Tu es un recruteur professionnel (RONI). L'entretien est terminé.\n"
409
- f"Le candidat vient de dire : \"{last_user_msg}\"\n"
410
- f"Réponds-y brièvement et aimablement (une phrase max). Si c'est une question, donnes-y une réponse simple.\n"
411
- f"Ne dis PAS au revoir tout de suite, contente-toi de répondre au dernier point soulevé."
412
- )
413
- try:
414
- ai_response = self.llm.invoke([SystemMessage(content=closing_prompt)])
415
- # Force append the exit message to guarantee trigger detection
416
- final_content = f"{ai_response.content}\n\n{EXIT_MESSAGE}"
417
- except Exception as e:
418
- logger.error(f"Error generating closing message: {e}")
419
- final_content = EXIT_MESSAGE
420
- else:
421
- final_content = EXIT_MESSAGE
 
 
 
 
 
 
 
 
 
 
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) # Add 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" # Added loopback
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)