File size: 7,293 Bytes
960c990
 
4f1959b
 
 
 
 
 
 
 
 
 
 
 
960c990
4f1959b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
960c990
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4f1959b
 
 
 
960c990
 
 
 
 
4f1959b
 
 
 
 
 
 
960c990
 
 
4f1959b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# Mise à jour pour src/services/interview_service.py

import os
from typing import Dict, List, Any
from typing_extensions import TypedDict
from langchain_core.messages import AIMessage, SystemMessage, HumanMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from src.config import read_system_prompt, format_cv

class State(TypedDict):
    messages: List[add_messages]

class EnhancedInterviewService:
    def __init__(self, models: Dict[str, Any]):
        self.models = models
        self.llm = self._get_llm()
        self.system_prompt_template = self._load_prompt_template()
        self.graph = self._build_graph()

    def _get_llm(self) -> ChatOpenAI:
        openai_api_key = os.getenv("OPENAI_API_KEY")
        return ChatOpenAI(
            temperature=0.6, 
            model_name="gpt-4o-mini", 
            api_key=openai_api_key
        )

    def _load_prompt_template(self) -> str:
        return read_system_prompt('prompts/enhanced_rag_prompt.txt')

    def _analyze_candidate_profile(self, cv_data: Dict[str, Any]) -> Dict[str, str]:
        """Analyse le profil candidat pour générer des insights pour l'entretien"""
        
        # Analyse des compétences avec niveaux
        skills_analysis = self._generate_skills_analysis(cv_data)
        
        # Analyse de reconversion
        reconversion_analysis = self._generate_reconversion_analysis(cv_data)
        
        return {
            "skills_analysis": skills_analysis,
            "reconversion_analysis": reconversion_analysis
        }

    def _generate_skills_analysis(self, cv_data: Dict[str, Any]) -> str:
        """Génère une analyse textuelle des compétences pour le prompt"""
        
        competences = cv_data.get("analyse_competences", [])
        
        if not competences:
            return "Aucune analyse de compétences disponible."
        
        # Grouper par niveau
        levels_groups = {
            "expert": [],
            "avance": [],
            "intermediaire": [],
            "debutant": []
        }
        
        for comp in competences:
            level = comp.get("level", "debutant")
            skill = comp.get("skill", "")
            if skill and level in levels_groups:
                levels_groups[level].append(skill)
        
        # Construire l'analyse textuelle
        analysis_parts = []
        
        if levels_groups["expert"]:
            analysis_parts.append(f"COMPÉTENCES EXPERTES : {', '.join(levels_groups['expert'])}")
            analysis_parts.append("→ Pose des questions techniques approfondies, demande des exemples d'innovation et de leadership technique")
        
        if levels_groups["avance"]:
            analysis_parts.append(f"COMPÉTENCES AVANCÉES : {', '.join(levels_groups['avance'])}")
            analysis_parts.append("→ Explore les défis complexes, l'autonomie et la résolution de problèmes")
        
        if levels_groups["intermediaire"]:
            analysis_parts.append(f"COMPÉTENCES INTERMÉDIAIRES : {', '.join(levels_groups['intermediaire'])}")
            analysis_parts.append("→ Vérifie la compréhension pratique avec des exemples concrets")
        
        if levels_groups["debutant"]:
            analysis_parts.append(f"COMPÉTENCES DÉBUTANTES : {', '.join(levels_groups['debutant'])}")
            analysis_parts.append("→ Teste les connaissances de base et évalue la motivation à apprendre")
        
        return "\n".join(analysis_parts) if analysis_parts else "Aucune compétence analysée."

    def _generate_reconversion_analysis(self, cv_data: Dict[str, Any]) -> str:
        """Génère une analyse de reconversion pour le prompt"""
        
        reconversion_data = cv_data.get("reconversion", {})
        
        if not reconversion_data:
            return "Aucune analyse de reconversion disponible."
        
        is_reconversion = reconversion_data.get("is_reconversion", False)
        analysis = reconversion_data.get("analysis", "")
        
        if not is_reconversion:
            return "PROFIL CLASSIQUE : Parcours cohérent dans le domaine. Focus sur l'évolution et les projets marquants."
        
        reconversion_guidance = [
            "CANDIDAT EN RECONVERSION DÉTECTÉE :",
            f"Analyse : {analysis}",
            "",
            "POINTS À EXPLORER OBLIGATOIREMENT :",
            "1. Motivations du changement de carrière",
            "2. Compétences transférables de l'expérience passée", 
            "3. Démarches d'apprentissage et de formation",
            "4. Engagement et projets dans la nouvelle voie",
            "5. Vision à long terme dans ce nouveau domaine",
            "",
            "APPROCHE : Valorise l'expérience passée, rassure sur la pertinence de la reconversion"
        ]
        
        return "\n".join(reconversion_guidance)

    def _chatbot_node(self, state: State) -> Dict[str, Any]:
        messages = state["messages"]
        formatted_cv_str = format_cv(self.cv_data)
        
        # Générer les analyses du profil candidat
        profile_analysis = self._analyze_candidate_profile(self.cv_data)
        
        # Formatage du prompt système enrichi
        system_prompt = self.system_prompt_template.format(
            entreprise=self.job_offer.get('entreprise', 'notre entreprise'),
            poste=self.job_offer.get('poste', 'ce poste'),
            mission=self.job_offer.get('mission', 'Non spécifiée'),
            profil_recherche=self.job_offer.get('profil_recherche', 'Non spécifié'),
            competences=self.job_offer.get('competences', 'Non spécifiées'),
            pole=self.job_offer.get('pole', 'Non spécifié'),
            cv=formatted_cv_str,
            skills_analysis=profile_analysis["skills_analysis"],
            reconversion_analysis=profile_analysis["reconversion_analysis"]
        )
        
        llm_messages = [SystemMessage(content=system_prompt)] + messages
        response = self.llm.invoke(llm_messages)
        return {"messages": [response]}

    def _build_graph(self) -> any:
        graph_builder = StateGraph(State)
        
        graph_builder.add_node("chatbot", self._chatbot_node)
        graph_builder.add_edge(START, "chatbot")
        graph_builder.add_edge("chatbot", END)
        
        return graph_builder.compile()

    def process_conversation(
        self, 
        cv_document: Dict[str, Any], 
        job_offer: Dict[str, Any], 
        conversation_history: List[Dict[str, Any]], 
        messages: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        
        if not cv_document or 'candidat' not in cv_document:
            raise ValueError("Document CV invalide fourni.")
        
        if not job_offer:
            raise ValueError("Données de l'offre d'emploi non fournies.")

        self.job_offer = job_offer
        self.cv_data = cv_document['candidat']
        self.conversation_history = conversation_history
        initial_state = conversation_history + messages
        result = self.graph.invoke({"messages": initial_state})
        
        response_content = result["messages"][-1].content
        return {"response": response_content}