quentinL52
Initial commit
4e9b744
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