Spaces:
Sleeping
Sleeping
File size: 7,822 Bytes
4e9b744 | 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 175 176 177 178 179 180 181 182 183 184 185 186 | import json
import logging
from typing import List, Dict, Any
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage, BaseMessage
from src.services.simulation.schemas import (
IceBreakerOutput, TechnicalOutput, BehavioralOutput, SituationOutput,
TechnicalSkillGap, ProjectTechUnderstanding, BehavioralCompetency,
SimulationReport
)
from src.services.simulation.scoring import (
calculate_technical_gap_score,
calculate_project_tech_understanding_score,
calculate_behavioral_score,
calculate_situation_score
)
logger = logging.getLogger(__name__)
class InterviewAgentExtractor:
def __init__(self, llm: ChatOpenAI):
self.llm = llm
def _get_history_text(self, messages: List[BaseMessage]) -> str:
return "\n".join([f"{m.type.upper()}: {m.content}" for m in messages])
def extract_icebreaker(self, messages: List[BaseMessage], cv_data: Dict[str, Any]) -> IceBreakerOutput:
logger.info("Extracting Ice Breaker data...")
history = self._get_history_text(messages)
prompt = f"""
Tu es un expert en analyse d'entretien. Analyse l'échange suivant (phase d'Ice Breaker) et extrais les informations structurées.
CONTEXTE CANDIDAT:
{json.dumps(cv_data.get('info_personnelle', {}), ensure_ascii=False)}
{json.dumps(cv_data.get('reconversion', {}), ensure_ascii=False)}
HISTORIQUE ECHANGE:
{history}
Tâche: Extraire le type de profil, l'expérience, la cohérence, la motivation, le contexte et les points à explorer.
"""
extractor = self.llm.with_structured_output(IceBreakerOutput)
return extractor.invoke([SystemMessage(content=prompt)])
def extract_technical(self, messages: List[BaseMessage], job_offer: Dict[str, Any]) -> TechnicalOutput:
logger.info("Extracting Technical data...")
history = self._get_history_text(messages)
prompt = f"""
Tu es un expert technique. Analyse l'échange suivant (phase Technique) et extrais les compétences validées, les lacunes et la compréhension des technos.
OFFRE:
{json.dumps(job_offer, ensure_ascii=False)}
HISTORIQUE ECHANGE:
{history}
Tâche: Remplir la grille d'évaluation technique. Pour les indicateurs binaires, sois strict : true seulement si le candidat l'a explicitement démontré.
"""
extractor = self.llm.with_structured_output(TechnicalOutput)
data = extractor.invoke([SystemMessage(content=prompt)])
# Calculate scores - normalize all to 0-5 scale
scores = []
for gap in data.lacunes_explorees:
gap.niveau_detecte = calculate_technical_gap_score(gap.indicateurs)
normalized = (gap.niveau_detecte / 4.0) * 5.0 # 0-4 -> 0-5
scores.append(normalized)
for tech in data.comprehension_technos_projets:
tech.score = calculate_project_tech_understanding_score(tech.indicateurs)
scores.append(float(tech.score)) # already 1-5
for val in data.competences_validees:
scores.append(float(val.score)) # already 1-5
if scores:
data.score_technique_global = round(sum(scores) / len(scores), 1)
else:
data.score_technique_global = 0.0
return data
def extract_behavioral(self, messages: List[BaseMessage]) -> BehavioralOutput:
logger.info("Extracting Behavioral data...")
history = self._get_history_text(messages)
prompt = f"""
Tu es un expert RH. Analyse l'échange suivant (phase Comportementale) et extrais l'évaluation des compétences.
HISTORIQUE ECHANGE:
{history}
Tâche: Evaluer chaque compétence comportementale abordée via la méthode STAR.
"""
extractor = self.llm.with_structured_output(BehavioralOutput)
data = extractor.invoke([SystemMessage(content=prompt)])
# Calculate scores
scores = []
for comp in data.competences_evaluees:
comp.score = calculate_behavioral_score(comp.competence, comp.indicateurs)
scores.append(comp.score)
for sjt in data.sjt_results:
if sjt.score_choix is not None and sjt.justification_score is not None:
sjt.score_sjt = round((sjt.score_choix * 0.6) + (sjt.justification_score * 0.4), 1)
scores.append(sjt.score_sjt)
if scores:
data.score_comportemental_global = round(sum(scores) / len(scores), 1)
else:
data.score_comportemental_global = 0.0
return data
def extract_situation(self, messages: List[BaseMessage]) -> SituationOutput:
logger.info("Extracting Situation data...")
history = self._get_history_text(messages)
prompt = f"""
Tu es un expert technique. Analyse l'échange suivant (phase Mise en Situation) et évalue la performance du candidat.
HISTORIQUE ECHANGE:
{history}
Tâche: Remplir la grille d'évaluation de la mise en situation.
"""
extractor = self.llm.with_structured_output(SituationOutput)
data = extractor.invoke([SystemMessage(content=prompt)])
# Calculate score
data.score_mise_en_situation = calculate_situation_score(data.indicateurs)
return data
def extract_simulation_report(self,
messages: List[BaseMessage],
icebreaker: IceBreakerOutput,
technical: TechnicalOutput,
behavioral: BehavioralOutput,
situation: SituationOutput) -> SimulationReport:
logger.info("Generating Final Simulation Report...")
# We don't necessarily need the whole history if we have structured data,
# but the LLM might need it for "Synthese".
# Let's provide a summary of structured data to save tokens.
context_data = {
"icebreaker": icebreaker.dict() if icebreaker else {},
"technical": technical.dict() if technical else {},
"behavioral": behavioral.dict() if behavioral else {},
"situation": situation.dict() if situation else {}
}
prompt = f"""
Tu es un Expert Recruteur Senior. Rédige le rapport final de l'entretien basé sur les données extraites.
DONNÉES STRUCTURÉES (SCORES & INDICATEURS):
{json.dumps(context_data, ensure_ascii=False)}
Tâche:
1. Calcule le score global (Moyenne pondérée : Technique 40%, Comportemental 30%, Situation 20%, Icebreaker/Soft 10% - ou use ton jugement expert).
2. Rédige une synthèse du candidat (2-3 phrases).
3. Liste les points forts et faibles.
4. Donne une recommandation claire (GO/NO GO).
5. Rédige un feedback pour le candidat (bienveillant et constructif).
"""
extractor = self.llm.with_structured_output(SimulationReport)
report = extractor.invoke([SystemMessage(content=prompt)])
# Inject the source objects back into the report (optional, as they are part of the model but null in extraction input)
# Actually LLM might return them null or empty. We should re-attach the real objects.
report.icebreaker = icebreaker
report.technical = technical
report.behavioral = behavioral
report.situation = situation
return report
|