Upload 39 files
Browse files- docker-compose.yml +47 -0
- knowledge_base/soft_skills_feedback.md +87 -0
- main.py +161 -0
- prompts/rag_prompt.txt +39 -0
- prompts/rag_prompt_old.txt +35 -0
- requirements.txt +25 -0
- src/__init__.py +0 -0
- src/__pycache__/__init__.cpython-312.pyc +0 -0
- src/__pycache__/config.cpython-312.pyc +0 -0
- src/__pycache__/cv_parsing_agents.cpython-312.pyc +0 -0
- src/__pycache__/deep_learning_analyzer.cpython-312.pyc +0 -0
- src/config.py +74 -0
- src/crew/__init__.py +0 -0
- src/crew/__pycache__/__init__.cpython-311.pyc +0 -0
- src/crew/__pycache__/__init__.cpython-312.pyc +0 -0
- src/crew/__pycache__/__init__.cpython-312.pycZone.Identifier +2 -0
- src/crew/__pycache__/agents.cpython-311.pyc +0 -0
- src/crew/__pycache__/agents.cpython-312.pyc +0 -0
- src/crew/__pycache__/agents.cpython-312.pycZone.Identifier +2 -0
- src/crew/__pycache__/analysis_crew.cpython-312.pyc +0 -0
- src/crew/__pycache__/analysis_crew.cpython-312.pycZone.Identifier +2 -0
- src/crew/__pycache__/crew_pool.cpython-311.pyc +0 -0
- src/crew/__pycache__/crew_pool.cpython-312.pyc +0 -0
- src/crew/__pycache__/crew_pool.cpython-312.pycZone.Identifier +2 -0
- src/crew/__pycache__/tasks.cpython-312.pyc +0 -0
- src/crew/__pycache__/tasks.cpython-312.pycZone.Identifier +2 -0
- src/crew/agents.py +75 -0
- src/crew/crew_pool.py +119 -0
- src/crew/tasks.py +184 -0
- src/cv_parsing_agents.py +50 -0
- src/deep_learning_analyzer.py +57 -0
- src/interview_simulator/__init__.py +0 -0
- src/interview_simulator/__pycache__/__init__.cpython-312.pyc +0 -0
- src/interview_simulator/__pycache__/entretient_version_prod.cpython-312.pyc +0 -0
- src/interview_simulator/entretient_version_prod.py +98 -0
- src/rag_handler.py +85 -0
- src/scoring_engine.py +102 -0
- tasks/__init__.py +0 -0
- tasks/worker_celery.py +72 -0
docker-compose.yml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
|
| 5 |
+
model-api:
|
| 6 |
+
build:
|
| 7 |
+
context: .
|
| 8 |
+
dockerfile: Dockerfile
|
| 9 |
+
container_name: projet_fil_rouge_api-model-api-1
|
| 10 |
+
ports:
|
| 11 |
+
- "9500:8000"
|
| 12 |
+
env_file:
|
| 13 |
+
- .env
|
| 14 |
+
restart: unless-stopped
|
| 15 |
+
environment:
|
| 16 |
+
- HF_HOME=/app/cache
|
| 17 |
+
- CELERY_BROKER_URL=redis://redis:6379/0
|
| 18 |
+
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
| 19 |
+
volumes:
|
| 20 |
+
- ./.cache/huggingface:/app/cache
|
| 21 |
+
|
| 22 |
+
worker:
|
| 23 |
+
build:
|
| 24 |
+
context: .
|
| 25 |
+
dockerfile: Dockerfile
|
| 26 |
+
command: celery -A tasks.worker_celery:celery_app worker --loglevel=info
|
| 27 |
+
restart: unless-stopped
|
| 28 |
+
depends_on:
|
| 29 |
+
- model-api
|
| 30 |
+
- redis
|
| 31 |
+
env_file:
|
| 32 |
+
- .env
|
| 33 |
+
environment:
|
| 34 |
+
- HF_HOME=/app/cache
|
| 35 |
+
- CELERY_BROKER_URL=redis://redis:6379/0
|
| 36 |
+
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
| 37 |
+
volumes:
|
| 38 |
+
- ./.cache/huggingface:/app/cache
|
| 39 |
+
|
| 40 |
+
redis:
|
| 41 |
+
image: "redis:alpine"
|
| 42 |
+
ports:
|
| 43 |
+
- "6379:6379"
|
| 44 |
+
restart: unless-stopped
|
| 45 |
+
|
| 46 |
+
volumes:
|
| 47 |
+
huggingface_cache:
|
knowledge_base/soft_skills_feedback.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Guide pour le Feedback sur les Soft Skills
|
| 2 |
+
|
| 3 |
+
Ce guide fournit des observations courantes et des conseils constructifs pour aider les candidats à améliorer leurs soft skills lors des entretiens d'embauche.
|
| 4 |
+
|
| 5 |
+
## Communication
|
| 6 |
+
|
| 7 |
+
**Observation** : Le candidat a du mal à structurer ses réponses ou semble décousu.
|
| 8 |
+
|
| 9 |
+
**Conseil à fournir** : "Pour mieux mettre en valeur votre expérience, essayez de structurer vos réponses avec la méthode STAR (Situation, Tâche, Action, Résultat). Par exemple, lorsque vous parlez d'un projet, commencez par décrire le contexte, puis votre rôle, les actions que vous avez menées, et enfin les résultats que vous avez obtenus. Cela rendra votre discours plus clair et percutant."
|
| 10 |
+
|
| 11 |
+
**Observation** : Le candidat utilise un langage trop technique ou du jargon.
|
| 12 |
+
|
| 13 |
+
**Conseil à fournir** : "Votre expertise technique est évidente. Pour vous assurer que tous vos interlocuteurs comprennent bien l'impact de votre travail, pensez à vulgariser certains concepts. Expliquer un projet complexe en des termes simples est une compétence très appréciée. Entraînez-vous à expliquer votre travail à quelqu'un qui n'est pas du même domaine."
|
| 14 |
+
|
| 15 |
+
**Observation** : Le candidat ne pose pas de questions ou ne montre pas de curiosité.
|
| 16 |
+
|
| 17 |
+
**Conseil à fournir** : "Un entretien est aussi une opportunité pour vous de poser des questions sur le poste, l'équipe ou la culture d'entreprise. Cela démontre votre intérêt et votre proactivité. Préparez quelques questions en amont pour la fin de l'entretien."
|
| 18 |
+
|
| 19 |
+
## Esprit d'équipe
|
| 20 |
+
|
| 21 |
+
**Observation** : Le candidat utilise beaucoup le "je" et mentionne peu ses collaborateurs.
|
| 22 |
+
|
| 23 |
+
**Conseil à fournir** : "N'hésitez pas à mentionner comment vous avez collaboré avec les autres membres de votre équipe. Parler des succès collectifs et de votre rôle au sein du groupe montre que vous savez travailler en équipe, une qualité essentielle dans la plupart des entreprises. Mettez en avant les synergies et les contributions mutuelles."
|
| 24 |
+
|
| 25 |
+
**Observation** : Le candidat ne parle pas de situations de conflit ou de désaccord en équipe.
|
| 26 |
+
|
| 27 |
+
**Conseil à fournir** : "Les désaccords font partie de la vie d'équipe. Expliquer comment vous avez géré une situation de conflit ou de désaccord avec un collègue, et comment vous avez contribué à trouver une solution, peut démontrer votre maturité et vos compétences en résolution de problèmes interpersonnels."
|
| 28 |
+
|
| 29 |
+
## Proactivité et Prise d'initiative
|
| 30 |
+
|
| 31 |
+
**Observation** : Le candidat décrit ses tâches de manière passive, sans mentionner de contributions personnelles.
|
| 32 |
+
|
| 33 |
+
**Conseil à fournir** : "Pensez à des moments où vous avez pris une initiative, même petite. Avez-vous suggéré une amélioration ? Avez-vous identifié un problème avant qu'il ne devienne critique ? Mettre en avant ces situations démontre votre proactivité et votre engagement. Montrez que vous êtes force de proposition."
|
| 34 |
+
|
| 35 |
+
**Observation** : Le candidat attend d'être sollicité pour agir.
|
| 36 |
+
|
| 37 |
+
**Conseil à fournir** : "Les recruteurs apprécient les profils qui ne se contentent pas d'exécuter. Décrivez des situations où vous avez anticipé un besoin, proposé une solution avant qu'on ne vous le demande, ou pris des responsabilités supplémentaires. Cela illustre votre autonomie et votre sens des responsabilités."
|
| 38 |
+
|
| 39 |
+
## Gestion du Stress
|
| 40 |
+
|
| 41 |
+
**Observation** : Le candidat semble visiblement stressé, ce qui affecte ses réponses.
|
| 42 |
+
|
| 43 |
+
**Conseil à fournir** : "Il est tout à fait normal de ressentir du stress en entretien. Pour vous aider, n'hésitez pas à prendre une seconde pour respirer avant de répondre. Si une question vous surprend, vous pouvez dire 'C'est une excellente question, laissez-moi un instant pour y réfléchir'. Cela montre que vous prenez le temps de construire une réponse pertinente et que vous gérez la pression."
|
| 44 |
+
|
| 45 |
+
**Observation** : Le candidat perd ses moyens face à une question inattendue ou difficile.
|
| 46 |
+
|
| 47 |
+
**Conseil à fournir** : "Face à une question déstabilisante, il est préférable de demander des précisions ou de reformuler la question pour s'assurer de bien la comprendre. Vous pouvez aussi demander un court instant pour organiser vos idées. Cela est perçu positivement et montre votre capacité à gérer l'incertitude."
|
| 48 |
+
|
| 49 |
+
## Adaptabilité
|
| 50 |
+
|
| 51 |
+
**Observation** : Le candidat a du mal à parler de changements ou de situations imprévues.
|
| 52 |
+
|
| 53 |
+
**Conseil à fournir** : "Le monde du travail évolue rapidement. Mettre en avant des situations où vous avez dû vous adapter à de nouvelles technologies, de nouvelles méthodes de travail, ou des changements d'équipe, démontre votre flexibilité. Expliquez comment vous avez géré ces transitions et ce que vous en avez appris."
|
| 54 |
+
|
| 55 |
+
**Observation** : Le candidat semble rigide dans ses approches ou ses idées.
|
| 56 |
+
|
| 57 |
+
**Conseil à fournir** : "Montrez que vous êtes ouvert aux nouvelles idées et aux retours. Décrivez une situation où vous avez dû changer d'avis ou modifier votre approche suite à de nouvelles informations ou un feedback. Cela prouve votre capacité à évoluer et à apprendre."
|
| 58 |
+
|
| 59 |
+
## Pensée Critique / Résolution de Problèmes
|
| 60 |
+
|
| 61 |
+
**Observation** : Le candidat décrit des problèmes sans détailler sa démarche de résolution.
|
| 62 |
+
|
| 63 |
+
**Conseil à fournir** : "Lorsque vous parlez d'un problème rencontré, ne vous contentez pas de décrire le problème et la solution. Expliquez votre processus de réflexion : comment avez-vous analysé la situation ? Quelles options avez-vous envisagées ? Pourquoi avez-vous choisi cette solution spécifique ? Quels ont été les résultats ? Cela met en lumière votre pensée critique."
|
| 64 |
+
|
| 65 |
+
**Observation** : Le candidat ne semble pas analyser les causes profondes des problèmes.
|
| 66 |
+
|
| 67 |
+
**Conseil à fournir** : "Les recruteurs recherchent des personnes capables d'aller au-delà des symptômes. Lorsque vous décrivez un défi, essayez d'identifier la cause racine du problème et comment votre solution y a remédié durablement. Cela montre une approche plus stratégique."
|
| 68 |
+
|
| 69 |
+
## Leadership
|
| 70 |
+
|
| 71 |
+
**Observation** : Le candidat parle de son rôle dans un projet sans mentionner comment il a influencé ou guidé les autres.
|
| 72 |
+
|
| 73 |
+
**Conseil à fournir** : "Le leadership ne se limite pas à un titre. Pensez à des moments où vous avez inspiré vos collègues, résolu des blocages pour l'équipe, ou pris la responsabilité d'un livrable important. Même sans être manager, vous pouvez démontrer des qualités de leader en montrant comment vous avez contribué à faire avancer le groupe."
|
| 74 |
+
|
| 75 |
+
**Observation** : Le candidat ne mentionne pas de situations où il a dû prendre des décisions difficiles.
|
| 76 |
+
|
| 77 |
+
**Conseil à fournir** : "Les leaders sont souvent confrontés à des choix complexes. Décrivez une situation où vous avez dû prendre une décision difficile, expliquez le contexte, les options, votre raisonnement et l'impact de cette décision. Cela met en évidence votre capacité à assumer des responsabilités."
|
| 78 |
+
|
| 79 |
+
## Gestion du Temps / Organisation
|
| 80 |
+
|
| 81 |
+
**Observation** : Le candidat semble désorganisé dans ses réponses ou ne mentionne pas de méthodes de travail.
|
| 82 |
+
|
| 83 |
+
**Conseil à fournir** : "Parlez de la manière dont vous organisez votre travail, gérez vos priorités et respectez les délais. Mentionnez des outils ou des méthodes (ex: to-do lists, gestion de projet agile) que vous utilisez. Cela rassure sur votre capacité à être efficace et autonome."
|
| 84 |
+
|
| 85 |
+
**Observation** : Le candidat a du mal à gérer plusieurs tâches ou projets simultanément.
|
| 86 |
+
|
| 87 |
+
**Conseil à fournir** : "Décrivez une situation où vous avez dû jongler avec plusieurs responsabilités. Expliquez comment vous avez priorisé, délégué si possible, et maintenu la qualité de votre travail. Cela démontre votre capacité à gérer la charge de travail et à rester performant sous pression."
|
main.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tempfile
|
| 2 |
+
from fastapi import FastAPI, UploadFile, File, HTTPException, Body
|
| 3 |
+
from fastapi.concurrency import run_in_threadpool
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from typing import List, Dict, Any
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from pymongo import MongoClient
|
| 8 |
+
from bson.objectid import ObjectId
|
| 9 |
+
import uvicorn
|
| 10 |
+
import os
|
| 11 |
+
import logging
|
| 12 |
+
from celery.result import AsyncResult
|
| 13 |
+
from tasks.worker_celery import run_interview_analysis_task
|
| 14 |
+
logging.basicConfig(level=logging.INFO)
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
from src.cv_parsing_agents import CvParserAgent
|
| 17 |
+
from src.interview_simulator.entretient_version_prod import InterviewProcessor
|
| 18 |
+
from src.scoring_engine import ContextualScoringEngine
|
| 19 |
+
from src.rag_handler import RAGHandler
|
| 20 |
+
|
| 21 |
+
app = FastAPI(
|
| 22 |
+
title="API d'IA pour la RH",
|
| 23 |
+
description="Une API pour le parsing de CV et la simulation d'entretiens.",
|
| 24 |
+
version="1.2.0"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Initialisation des services au démarrage
|
| 28 |
+
try:
|
| 29 |
+
logger.info("Initialisation du RAG Handler...")
|
| 30 |
+
rag_handler = RAGHandler()
|
| 31 |
+
if rag_handler.vector_store:
|
| 32 |
+
logger.info(f"Vector store chargé avec {rag_handler.vector_store.index.ntotal} vecteurs.")
|
| 33 |
+
else:
|
| 34 |
+
logger.warning("Le RAG Handler n'a pas pu être initialisé (pas de documents ?). Le feedback contextuel sera désactivé.")
|
| 35 |
+
except Exception as e:
|
| 36 |
+
logger.error(f"Erreur critique lors de l'initialisation du RAG Handler: {e}", exc_info=True)
|
| 37 |
+
rag_handler = None
|
| 38 |
+
|
| 39 |
+
# Configuration MongoDB
|
| 40 |
+
MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/")
|
| 41 |
+
client = MongoClient(MONGO_URI)
|
| 42 |
+
db = client.hr_ai_system
|
| 43 |
+
feedback_collection = db.interview_feedbacks
|
| 44 |
+
|
| 45 |
+
class InterviewRequest(BaseModel):
|
| 46 |
+
user_id: str = Field(..., example="google_user_12345")
|
| 47 |
+
job_offer_id: str = Field(..., example="job_offer_abcde")
|
| 48 |
+
cv_document: Dict[str, Any] = Field(..., example={"candidat": {"nom": "John Doe", "compétences": {"hard_skills": ["Python", "FastAPI"]}}})
|
| 49 |
+
job_offer: Dict[str, Any] = Field(..., example={"poste": "Développeur Python", "description": "Recherche développeur expérimenté..."})
|
| 50 |
+
messages: List[Dict[str, Any]]
|
| 51 |
+
conversation_history: List[Dict[str, Any]]
|
| 52 |
+
|
| 53 |
+
class HealthCheck(BaseModel):
|
| 54 |
+
status: str = Field(default="ok", example="ok")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@app.get("/", tags=["Status"], summary="Vérification de l'état de l'API")
|
| 58 |
+
def read_root() -> HealthCheck:
|
| 59 |
+
"""Vérifie que l'API est en cours d'exécution."""
|
| 60 |
+
return HealthCheck(status="ok")
|
| 61 |
+
|
| 62 |
+
# --- Endpoint du parser de CV ---
|
| 63 |
+
@app.post("/parse-cv/", tags=["CV Parsing"], summary="Analyser un CV au format PDF avec scoring contextuel")
|
| 64 |
+
async def parse_cv_endpoint(file: UploadFile = File(...)):
|
| 65 |
+
if file.content_type != "application/pdf":
|
| 66 |
+
raise HTTPException(status_code=400, detail="Le fichier doit être au format PDF.")
|
| 67 |
+
tmp_path = None
|
| 68 |
+
try:
|
| 69 |
+
contents = await file.read()
|
| 70 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
|
| 71 |
+
tmp.write(contents)
|
| 72 |
+
tmp.flush()
|
| 73 |
+
tmp_path = tmp.name
|
| 74 |
+
|
| 75 |
+
logger.info(f"Début du parsing du CV temporaire : {tmp_path}")
|
| 76 |
+
cv_agent = CvParserAgent(pdf_path=tmp_path)
|
| 77 |
+
parsed_data = await run_in_threadpool(cv_agent.process)
|
| 78 |
+
if not parsed_data:
|
| 79 |
+
raise HTTPException(status_code=500, detail="Échec du parsing du CV.")
|
| 80 |
+
logger.info("Parsing du CV réussi. Lancement du scoring contextuel.")
|
| 81 |
+
scoring_engine = ContextualScoringEngine(parsed_data)
|
| 82 |
+
scored_skills_data = await run_in_threadpool(scoring_engine.calculate_scores)
|
| 83 |
+
if parsed_data.get("candidat"):
|
| 84 |
+
parsed_data["candidat"].update(scored_skills_data)
|
| 85 |
+
else:
|
| 86 |
+
parsed_data.update(scored_skills_data)
|
| 87 |
+
|
| 88 |
+
logger.info("Scoring terminé. Retour de la réponse complète.")
|
| 89 |
+
return parsed_data
|
| 90 |
+
|
| 91 |
+
except Exception as e:
|
| 92 |
+
logger.error(f"Erreur lors du parsing ou du scoring du CV : {e}", exc_info=True)
|
| 93 |
+
raise HTTPException(status_code=500, detail=f"Erreur interne du serveur : {e}")
|
| 94 |
+
finally:
|
| 95 |
+
if tmp_path and os.path.exists(tmp_path):
|
| 96 |
+
try:
|
| 97 |
+
os.remove(tmp_path)
|
| 98 |
+
logger.info(f"Fichier temporaire supprimé : {tmp_path}")
|
| 99 |
+
except Exception as cleanup_error:
|
| 100 |
+
logger.warning(f"Erreur lors de la suppression du fichier temporaire : {cleanup_error}")
|
| 101 |
+
|
| 102 |
+
# --- Endpoint de simulation d'entretien ---
|
| 103 |
+
@app.post("/simulate-interview/", tags=["Simulation d'Entretien"], summary="Gérer une conversation d'entretien")
|
| 104 |
+
async def simulate_interview_endpoint(request: InterviewRequest):
|
| 105 |
+
try:
|
| 106 |
+
processor = InterviewProcessor(
|
| 107 |
+
cv_document=request.cv_document,
|
| 108 |
+
job_offer=request.job_offer,
|
| 109 |
+
conversation_history=request.conversation_history
|
| 110 |
+
)
|
| 111 |
+
ai_response_object = await run_in_threadpool(processor.run, messages=request.messages)
|
| 112 |
+
|
| 113 |
+
# On retourne juste la réponse de l'assistant pour le chat
|
| 114 |
+
return {"response": ai_response_object["messages"][-1].content}
|
| 115 |
+
|
| 116 |
+
except Exception as e:
|
| 117 |
+
logger.error(f"Erreur interne dans /simulate-interview/: {e}", exc_info=True)
|
| 118 |
+
raise HTTPException(status_code=500, detail=f"Erreur interne du serveur : {e}")
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# --- Endpoints pour l'analyse asynchrone ---
|
| 122 |
+
class AnalysisRequest(BaseModel):
|
| 123 |
+
conversation_history: List[Dict[str, Any]]
|
| 124 |
+
job_description_text: str
|
| 125 |
+
|
| 126 |
+
@app.post("/trigger-analysis/", tags=["Analyse Asynchrone"], status_code=202)
|
| 127 |
+
def trigger_analysis(request: AnalysisRequest):
|
| 128 |
+
"""
|
| 129 |
+
Déclenche l'analyse de l'entretien en tâche de fond.
|
| 130 |
+
Retourne immédiatement un ID de tâche.
|
| 131 |
+
"""
|
| 132 |
+
task = run_interview_analysis_task.delay(
|
| 133 |
+
request.conversation_history,
|
| 134 |
+
[request.job_description_text]
|
| 135 |
+
)
|
| 136 |
+
return {"task_id": task.id}
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
@app.get("/analysis-status/{task_id}", tags=["Analyse Asynchrone"])
|
| 140 |
+
def get_analysis_status(task_id: str):
|
| 141 |
+
"""
|
| 142 |
+
Vérifie le statut de la tâche d'analyse.
|
| 143 |
+
Si terminée, retourne le résultat.
|
| 144 |
+
"""
|
| 145 |
+
task_result = AsyncResult(task_id)
|
| 146 |
+
if task_result.ready():
|
| 147 |
+
if task_result.successful():
|
| 148 |
+
return {
|
| 149 |
+
"status": "SUCCESS",
|
| 150 |
+
"result": task_result.get()
|
| 151 |
+
}
|
| 152 |
+
else:
|
| 153 |
+
return {"status": "FAILURE", "error": str(task_result.info)}
|
| 154 |
+
else:
|
| 155 |
+
return {"status": "PENDING"}
|
| 156 |
+
|
| 157 |
+
if __name__ == "__main__":
|
| 158 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
## la bonne version de l'API est celle-ci, avec les imports et la structure de base.
|
prompts/rag_prompt.txt
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Tu es un assistant RH expert qui aide à l'analyse d'offres d'emploi et à la préparation d'entretiens.
|
| 2 |
+
Ton rôle est de te comporter comme dans un entretien pour un poste.
|
| 3 |
+
|
| 4 |
+
Tu as accès aux informations suivantes sur le poste actuel :
|
| 5 |
+
entreprise : {entreprise}
|
| 6 |
+
poste : {poste}
|
| 7 |
+
description : {description}
|
| 8 |
+
|
| 9 |
+
Les informations sur le candidat sont :
|
| 10 |
+
cv : {cv}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
Tu as accès au CV d'un candidat appelle-le toujours par son nom et utilise les informations de son CV {cv} pour lui poser des questions
|
| 14 |
+
ou avoir des précisions si nécessaire.
|
| 15 |
+
Identifie clairement experience professionnelle et projet, et ne confond pas les 2.
|
| 16 |
+
Essaye d'evaluer les compétences et skills d'un candidat en fonction de ses projets, si par exemple le candidat a simplement travaillé sur un dashboard
|
| 17 |
+
powerBi ne considére pas cela comme une experience solide.
|
| 18 |
+
À partir des informations de {description}, tu devras élaborer une série de questions pour le candidat.
|
| 19 |
+
Pose exactement les questions une par une.
|
| 20 |
+
Attends la réponse du candidat avant de poser la question suivante.
|
| 21 |
+
|
| 22 |
+
Commence l'entretien par te présenter avec une formule de politesse.
|
| 23 |
+
Tu devras te présenter avec un nom choisi aléatoirement, présenter l'entreprise et introduire la mission.
|
| 24 |
+
Introduis les besoins de l'entreprise en analysant les informations contenues dans {poste}.
|
| 25 |
+
Évite d'introduire les questions en parlant de 'questions' maintient toujours une conversation le plus naturelle possible.
|
| 26 |
+
Après ta présentation demande toujours dans un premier temps au candidat de se présenter et de présenter son parcours.
|
| 27 |
+
|
| 28 |
+
Tu dois toujours te mettre dans la situation d'un recruteur et adapter ton langage selon si c'est une femme ou un homme.
|
| 29 |
+
Introduis toujours les informations de {description} comme si tu représentais l'entreprise et tu étais déjà au courant de ces infos.
|
| 30 |
+
N'oublie pas de varier la structure de tes phrases et utilise des expressions comme 'D'accord', 'Je vois', 'C'est intéressant' pour montrer que tu écoutes activement.
|
| 31 |
+
Adopte un ton décontracté et évite le jargon RH trop formel.
|
| 32 |
+
Au lieu de dire 'Pouvez-vous me parler de...', essaye plutôt 'Racontez-moi un peu...' ou 'J'aimerais en savoir plus sur...
|
| 33 |
+
Tu devras poser les questions et communiquer de la manière la plus humaine possible.
|
| 34 |
+
Tu devras adapter l'entretien au profil du candidat.
|
| 35 |
+
|
| 36 |
+
Quand tu estimes que l'entretien est terminé et que tu as assez d'informations, utilise l'outil `interview_analyser` pour conclure et lancer l'analyse du feedback.
|
| 37 |
+
Termine toujours l'entretien par une phrase de politesse, positive.
|
| 38 |
+
Ne fais pas d'analyse, elle est faite par une équipe d'agents, contente-toi seulement d'occuper ton rôle de recruteur.
|
| 39 |
+
**À la fin de l'entretien, après ta dernière phrase de politesse, conclus toujours par : nous allons maintenant passer a l'analyse **
|
prompts/rag_prompt_old.txt
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Tu es un recruteur expert, menant un premier entretien de qualification. Ton ton est professionnel mais engageant. Ta mission est d'évaluer l'adéquation d'un candidat pour un poste.
|
| 2 |
+
|
| 3 |
+
CONTEXTE DE L'ENTRETIEN
|
| 4 |
+
Tu dois baser ta conversation sur les informations suivantes :
|
| 5 |
+
|
| 6 |
+
1. Informations à utiliser activement dans la conversation :
|
| 7 |
+
Entreprise : {entreprise}
|
| 8 |
+
Intitulé du poste : {poste}
|
| 9 |
+
Équipe / Pôle : {pole}
|
| 10 |
+
Missions principales : {mission}
|
| 11 |
+
|
| 12 |
+
2. Informations pour guider tes questions (à ne PAS mentionner directement) :
|
| 13 |
+
Profil recherché : {profil_recherche}
|
| 14 |
+
Compétences clés attendues : {competences}
|
| 15 |
+
(Utilise ces deux points comme une grille d'analyse interne pour formuler des questions pertinentes. Tes questions doivent permettre de vérifier si le candidat possède ces compétences et correspond au profil.)
|
| 16 |
+
3. Informations sur le candidat :
|
| 17 |
+
Les données de son CV sont : {cv}
|
| 18 |
+
|
| 19 |
+
DIRECTIVES PRÉCISES
|
| 20 |
+
|
| 21 |
+
1. Déroulement de l'entretien :
|
| 22 |
+
Introduction : Commence par te présenter avec un prénom (ex: Camille, Thomas...). Présente l'entreprise ({entreprise}) et le contexte du recrutement en t'appuyant sur l'intitulé du poste ({poste}) et les missions ({mission}).
|
| 23 |
+
Présentation du candidat : Ta toute première question doit inviter le candidat à se présenter. Par exemple : "Pour commencer, parlez-moi un peu de votre parcours."
|
| 24 |
+
Questions ciblées : En te basant sur les compétences et le profil recherché (que tu gardes en tête), pose des questions ouvertes pour évaluer le candidat. Fais des liens entre ses expériences ({cv}) et les missions du poste ({mission}). Par exemple, si une compétence attendue est "l'analyse de données", demande au candidat de décrire un projet où il a dû analyser un ensemble de données complexe.
|
| 25 |
+
Une question à la fois : Pose une seule question à la fois et attends la réponse complète du candidat avant de poursuivre.
|
| 26 |
+
|
| 27 |
+
2. Style et Comportement :
|
| 28 |
+
Personnalisation : Appelle toujours le candidat par son nom (présent dans le CV).
|
| 29 |
+
Langage Naturel : Évite le jargon RH. Utilise des formulations fluides comme "J'ai noté dans votre CV que...", "Racontez-moi l'expérience chez...". Montre que tu écoutes avec des relances comme "D'accord, je vois.", "C'est intéressant.".
|
| 30 |
+
Évaluation subtile : Ne dis jamais "la compétence requise est...". À la place, évalue la compétence à travers des questions situationnelles ou comportementales.
|
| 31 |
+
|
| 32 |
+
3. Conclusion de l'entretien :
|
| 33 |
+
Quand tu estimes avoir assez d'informations, conclus l'échange de manière positive.
|
| 34 |
+
Termine par une phrase de politesse.
|
| 35 |
+
Action finale OBLIGATOIRE : Ta toute dernière phrase, après la politesse, doit être exactement : "nous allons maintenant passer a l'analyse". Juste après, tu dois utiliser l'outil interview_analyser.
|
requirements.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.111.1
|
| 2 |
+
uvicorn[standard]==0.30.1
|
| 3 |
+
pydantic==2.8.2
|
| 4 |
+
|
| 5 |
+
langchain-core==0.2.26
|
| 6 |
+
langchain-community==0.2.5
|
| 7 |
+
langchain-openai==0.1.20
|
| 8 |
+
langchain_groq
|
| 9 |
+
langchain-huggingface
|
| 10 |
+
langgraph==0.1.9
|
| 11 |
+
crewai
|
| 12 |
+
crewai-tools
|
| 13 |
+
sentence_transformers
|
| 14 |
+
torch
|
| 15 |
+
transformers
|
| 16 |
+
sentencepiece
|
| 17 |
+
accelerate
|
| 18 |
+
celery
|
| 19 |
+
redis
|
| 20 |
+
pypdf==4.3.1
|
| 21 |
+
python-dotenv==1.0.1
|
| 22 |
+
pymongo
|
| 23 |
+
|
| 24 |
+
requests==2.32.3
|
| 25 |
+
faiss-cpu==1.8.0
|
src/__init__.py
ADDED
|
File without changes
|
src/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (171 Bytes). View file
|
|
|
src/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (3.5 kB). View file
|
|
|
src/__pycache__/cv_parsing_agents.cpython-312.pyc
ADDED
|
Binary file (3 kB). View file
|
|
|
src/__pycache__/deep_learning_analyzer.cpython-312.pyc
ADDED
|
Binary file (3.64 kB). View file
|
|
|
src/config.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
load_dotenv()
|
| 4 |
+
from langchain_groq import ChatGroq
|
| 5 |
+
from langchain_community.document_loaders import PyPDFLoader
|
| 6 |
+
from langchain_openai import ChatOpenAI
|
| 7 |
+
from typing import Dict, List, Any, Tuple, Optional, Type
|
| 8 |
+
from crewai import LLM
|
| 9 |
+
#########################################################################################################
|
| 10 |
+
# formatage du json
|
| 11 |
+
def format_cv(document):
|
| 12 |
+
def format_section(title, data, indent=0):
|
| 13 |
+
prefix = " " * indent
|
| 14 |
+
lines = [f"{title}:"]
|
| 15 |
+
if isinstance(data, dict):
|
| 16 |
+
for k, v in data.items():
|
| 17 |
+
if isinstance(v, (dict, list)):
|
| 18 |
+
lines.append(f"{prefix}- {k.capitalize()}:")
|
| 19 |
+
lines.extend(format_section("", v, indent + 1))
|
| 20 |
+
else:
|
| 21 |
+
lines.append(f"{prefix}- {k.capitalize()}: {v}")
|
| 22 |
+
elif isinstance(data, list):
|
| 23 |
+
for i, item in enumerate(data):
|
| 24 |
+
lines.append(f"{prefix}- Élément {i + 1}:")
|
| 25 |
+
lines.extend(format_section("", item, indent + 1))
|
| 26 |
+
else:
|
| 27 |
+
lines.append(f"{prefix}- {data}")
|
| 28 |
+
return lines
|
| 29 |
+
sections = []
|
| 30 |
+
for section_name, content in document.items():
|
| 31 |
+
title = section_name.replace("_", " ").capitalize()
|
| 32 |
+
sections.extend(format_section(title, content))
|
| 33 |
+
sections.append("")
|
| 34 |
+
return "\n".join(sections)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def read_system_prompt(file_path):
|
| 38 |
+
with open(file_path, 'r', encoding='utf-8') as file:
|
| 39 |
+
return file.read()
|
| 40 |
+
|
| 41 |
+
def load_pdf(pdf_path):
|
| 42 |
+
loader = PyPDFLoader(pdf_path)
|
| 43 |
+
pages = loader.load_and_split()
|
| 44 |
+
cv_text = ""
|
| 45 |
+
for page in pages:
|
| 46 |
+
cv_text += page.page_content + "\n\n"
|
| 47 |
+
return cv_text
|
| 48 |
+
|
| 49 |
+
#########################################################################################################
|
| 50 |
+
# modéles
|
| 51 |
+
|
| 52 |
+
"""GEMINI_API_KEY = os.getenv("GOOGLE_API_KEY")
|
| 53 |
+
model_google = "gemini/gemma-3-27b-it"
|
| 54 |
+
def chat_gemini():
|
| 55 |
+
llm = ChatGoogleGenerativeAI("gemini/gemma-3-27b-it")"""
|
| 56 |
+
|
| 57 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 58 |
+
model_openai = "gpt-4o"
|
| 59 |
+
|
| 60 |
+
def crew_openai():
|
| 61 |
+
llm = ChatOpenAI(
|
| 62 |
+
model="gpt-4o-mini",
|
| 63 |
+
temperature=0.1,
|
| 64 |
+
api_key=OPENAI_API_KEY
|
| 65 |
+
)
|
| 66 |
+
return llm
|
| 67 |
+
|
| 68 |
+
def chat_openai():
|
| 69 |
+
llm = ChatOpenAI(
|
| 70 |
+
model="gpt-4o",
|
| 71 |
+
temperature=0.6,
|
| 72 |
+
api_key=OPENAI_API_KEY
|
| 73 |
+
)
|
| 74 |
+
return llm
|
src/crew/__init__.py
ADDED
|
File without changes
|
src/crew/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (137 Bytes). View file
|
|
|
src/crew/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (190 Bytes). View file
|
|
|
src/crew/__pycache__/__init__.cpython-312.pycZone.Identifier
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[ZoneTransfer]
|
| 2 |
+
ZoneId=3
|
src/crew/__pycache__/agents.cpython-311.pyc
ADDED
|
Binary file (3.57 kB). View file
|
|
|
src/crew/__pycache__/agents.cpython-312.pyc
ADDED
|
Binary file (3.43 kB). View file
|
|
|
src/crew/__pycache__/agents.cpython-312.pycZone.Identifier
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[ZoneTransfer]
|
| 2 |
+
ZoneId=3
|
src/crew/__pycache__/analysis_crew.cpython-312.pyc
ADDED
|
Binary file (1.08 kB). View file
|
|
|
src/crew/__pycache__/analysis_crew.cpython-312.pycZone.Identifier
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[ZoneTransfer]
|
| 2 |
+
ZoneId=3
|
src/crew/__pycache__/crew_pool.cpython-311.pyc
ADDED
|
Binary file (2.46 kB). View file
|
|
|
src/crew/__pycache__/crew_pool.cpython-312.pyc
ADDED
|
Binary file (2.18 kB). View file
|
|
|
src/crew/__pycache__/crew_pool.cpython-312.pycZone.Identifier
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[ZoneTransfer]
|
| 2 |
+
ZoneId=3
|
src/crew/__pycache__/tasks.cpython-312.pyc
ADDED
|
Binary file (8.39 kB). View file
|
|
|
src/crew/__pycache__/tasks.cpython-312.pycZone.Identifier
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[ZoneTransfer]
|
| 2 |
+
ZoneId=3
|
src/crew/agents.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Agent
|
| 2 |
+
from crewai import LLM
|
| 3 |
+
from src.config import crew_openai
|
| 4 |
+
|
| 5 |
+
LLM_agent = crew_openai()
|
| 6 |
+
|
| 7 |
+
# Interview Simulation Agents
|
| 8 |
+
report_generator_agent = Agent(
|
| 9 |
+
role='Rédacteur de Rapports Synthétiques',
|
| 10 |
+
goal='Générer un feedback pertinent, a partir du deroulement de lentretient',
|
| 11 |
+
backstory=(
|
| 12 |
+
"Sepcialisé dans le recrutement et les ressources humaines, capable d'evaluer les candidats"
|
| 13 |
+
"sur la communication et la pertinences des reponses en fonction des questions posées, redige"
|
| 14 |
+
"en un rapport clair, un feedback détaillé sur le candidat."
|
| 15 |
+
),
|
| 16 |
+
allow_delegation=False,
|
| 17 |
+
verbose=False,
|
| 18 |
+
llm=LLM_agent
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# CV Parsing Agents
|
| 22 |
+
skills_extractor_agent = Agent(
|
| 23 |
+
role="Spécialiste de l'extraction de compétences (hard & soft skills)",
|
| 24 |
+
goal="Identifier et extraire toutes les compétences pertinentes du CV.",
|
| 25 |
+
backstory="Vous êtes un spécialiste des compétences techniques et comportementales. Votre mission est de parcourir les CV et de lister de manière exhaustive toutes les compétences mentionnées.",
|
| 26 |
+
verbose=False,
|
| 27 |
+
llm=LLM_agent
|
| 28 |
+
)
|
| 29 |
+
experience_extractor_agent = Agent(
|
| 30 |
+
role="Expert en extraction d'expérience professionnelle",
|
| 31 |
+
goal="Extraire en détail l'expérience professionnelle du candidat.",
|
| 32 |
+
backstory="Vous êtes un expert en recrutement spécialisé dans l'analyse des parcours professionnels. Vous devez extraire chaque expérience de manière précise, en notant les rôles, les entreprises, les dates et les responsabilités.",
|
| 33 |
+
verbose=False,
|
| 34 |
+
llm=LLM_agent
|
| 35 |
+
)
|
| 36 |
+
project_extractor_agent = Agent(
|
| 37 |
+
role="Spécialiste de l'identification de projets (pro & perso)",
|
| 38 |
+
goal="Identifier et décrire les projets significatifs mentionnés.",
|
| 39 |
+
backstory="Vous êtes passionné par l'innovation et les réalisations. Votre rôle est de repérer et de décrire les projets professionnels et personnels qui mettent en lumière les compétences et l'initiative des candidats.",
|
| 40 |
+
verbose=False,
|
| 41 |
+
llm=LLM_agent
|
| 42 |
+
)
|
| 43 |
+
education_extractor_agent = Agent(
|
| 44 |
+
role="Expert en extraction d'informations sur la formation",
|
| 45 |
+
goal="Extraire les détails des études et des diplômes obtenus.",
|
| 46 |
+
backstory="Vous êtes un spécialiste des parcours académiques. Votre tâche est d'extraire avec précision les informations relatives aux études, aux diplômes et aux établissements fréquentés par les candidats.",
|
| 47 |
+
verbose=False,
|
| 48 |
+
llm=LLM_agent
|
| 49 |
+
)
|
| 50 |
+
informations_personnelle_agent = Agent(
|
| 51 |
+
role="Spécialiste de l'extraction des coordonnées",
|
| 52 |
+
goal="Identifier et extraire précisément les coordonnées du candidat.",
|
| 53 |
+
backstory="Vous êtes un expert en analyse de CV, particulièrement doué pour localiser et extraire les informations de contact. Votre rôle est de trouver le nom, l'adresse e-mail, le numéro de téléphone et la localisation (ville ou région) du candidat, généralement situés en haut ou à la fin du CV.",
|
| 54 |
+
verbose=False,
|
| 55 |
+
llm=LLM_agent
|
| 56 |
+
)
|
| 57 |
+
ProfileBuilderAgent = Agent(
|
| 58 |
+
role='Constructeur de Profil CV',
|
| 59 |
+
goal='Créer un profil JSON structuré et valide avec la clé candidat',
|
| 60 |
+
backstory=(
|
| 61 |
+
"Tu es un expert en structuration de données JSON. "
|
| 62 |
+
"Ta mission est de créer un profil candidat parfaitement formaté "
|
| 63 |
+
"en respectant scrupuleusement la structure JSON demandée."
|
| 64 |
+
),
|
| 65 |
+
verbose=True,
|
| 66 |
+
llm=LLM_agent
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
reconversion_detector_agent = Agent(
|
| 70 |
+
role="Détecteur de Reconversion Professionnelle",
|
| 71 |
+
goal="Analyser la chronologie des expériences pour identifier les changements de carrière significatifs.",
|
| 72 |
+
backstory="Vous êtes un conseiller d'orientation expert, capable de repérer les transitions de carrière, d'identifier les compétences transférables et de valoriser les parcours non linéaires. Votre analyse doit mettre en lumière les changements de secteur, de type de poste ou de niveau de responsabilité.",
|
| 73 |
+
verbose=False,
|
| 74 |
+
llm=LLM_agent
|
| 75 |
+
)
|
src/crew/crew_pool.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Crew, Process
|
| 2 |
+
from langchain_core.tools import tool
|
| 3 |
+
import json
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from typing import Dict, List, Any, Type
|
| 6 |
+
from .agents import report_generator_agent, skills_extractor_agent, experience_extractor_agent, project_extractor_agent, education_extractor_agent, ProfileBuilderAgent, informations_personnelle_agent, reconversion_detector_agent
|
| 7 |
+
from .tasks import generate_report_task, task_extract_skills, task_extract_experience, task_extract_projects, task_extract_education, task_build_profile, task_extract_informations, task_detect_reconversion
|
| 8 |
+
from src.deep_learning_analyzer import MultiModelInterviewAnalyzer
|
| 9 |
+
from src.rag_handler import RAGHandler
|
| 10 |
+
from langchain_core.tools import BaseTool
|
| 11 |
+
|
| 12 |
+
@tool
|
| 13 |
+
def interview_analyser(conversation_history: list, job_description_text: list) -> str:
|
| 14 |
+
"""
|
| 15 |
+
Appelle cet outil à la toute fin d'un entretien d'embauche pour analyser
|
| 16 |
+
l'intégralité de la conversation et générer un rapport de feedback.
|
| 17 |
+
Ne l'utilise PAS pour répondre à une question normale, mais seulement pour conclure et analyser l'entretien.
|
| 18 |
+
"""
|
| 19 |
+
# 1. Analyse DL de la conversation
|
| 20 |
+
analyzer = MultiModelInterviewAnalyzer()
|
| 21 |
+
structured_analysis = analyzer.run_full_analysis(conversation_history, job_description_text)
|
| 22 |
+
|
| 23 |
+
# 2. Enrichissement avec RAG
|
| 24 |
+
rag_handler = RAGHandler()
|
| 25 |
+
rag_feedback = []
|
| 26 |
+
# Extraire les intentions et sentiments pour trouver des conseils pertinents
|
| 27 |
+
if structured_analysis.get("intent_analysis"):
|
| 28 |
+
for intent in structured_analysis["intent_analysis"]:
|
| 29 |
+
# Exemple de requête basée sur l'intention
|
| 30 |
+
query = f"Conseils pour un candidat qui cherche à {intent['labels'][0]}"
|
| 31 |
+
rag_feedback.extend(rag_handler.get_relevant_feedback(query))
|
| 32 |
+
|
| 33 |
+
if structured_analysis.get("sentiment_analysis"):
|
| 34 |
+
for sentiment_group in structured_analysis["sentiment_analysis"]:
|
| 35 |
+
for sentiment in sentiment_group:
|
| 36 |
+
if sentiment['label'] == 'stress' and sentiment['score'].item() > 0.6:
|
| 37 |
+
rag_feedback.extend(rag_handler.get_relevant_feedback("gestion du stress en entretien"))
|
| 38 |
+
unique_feedback = list(set(rag_feedback))
|
| 39 |
+
interview_crew = Crew(
|
| 40 |
+
agents=[report_generator_agent],
|
| 41 |
+
tasks=[generate_report_task],
|
| 42 |
+
process=Process.sequential,
|
| 43 |
+
verbose=False,
|
| 44 |
+
telemetry=False
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
final_report = interview_crew.kickoff(inputs={
|
| 48 |
+
'structured_analysis_data': json.dumps(structured_analysis, indent=2),
|
| 49 |
+
'rag_contextual_feedback': "\n".join(unique_feedback)
|
| 50 |
+
})
|
| 51 |
+
return final_report
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
'''
|
| 55 |
+
class EmptyInput(BaseModel):
|
| 56 |
+
pass
|
| 57 |
+
|
| 58 |
+
class InterviewAnalysisTool(BaseTool):
|
| 59 |
+
"""
|
| 60 |
+
Appelle cet outil à la toute fin d'un entretien d'embauche pour analyser
|
| 61 |
+
l'intégralité de la conversation et générer un rapport de feedback.
|
| 62 |
+
Ne l'utilise PAS pour répondre à une question normale, mais seulement pour conclure et analyser l'entretien.
|
| 63 |
+
"""
|
| 64 |
+
name: str = "interview_analyser"
|
| 65 |
+
description: str = (
|
| 66 |
+
"Appelle cet outil à la toute fin d'un entretien d'embauche pour analyser "
|
| 67 |
+
"l'intégralité de la conversation et générer un rapport de feedback. "
|
| 68 |
+
"Ne l'utilise PAS pour répondre à une question normale, mais seulement pour conclure et analyser l'entretien."
|
| 69 |
+
)
|
| 70 |
+
args_schema: type[BaseModel] = EmptyInput
|
| 71 |
+
job_offer: Dict[str, Any]
|
| 72 |
+
conversation_history: List[Dict[str, Any]]
|
| 73 |
+
|
| 74 |
+
def _run(self) -> str:
|
| 75 |
+
"""Exécute l'analyse de l'entretien."""
|
| 76 |
+
interview_crew = Crew(
|
| 77 |
+
agents=[report_generator_agent],
|
| 78 |
+
tasks=[generate_report_task],
|
| 79 |
+
process=Process.sequential,
|
| 80 |
+
verbose=False,
|
| 81 |
+
telemetry=False
|
| 82 |
+
)
|
| 83 |
+
analyzer = MultiModelInterviewAnalyzer()
|
| 84 |
+
structured_analysis = analyzer.run_full_analysis(self.conversation_history, self.job_offer)
|
| 85 |
+
|
| 86 |
+
final_report = interview_crew.kickoff(inputs={
|
| 87 |
+
'structured_analysis_data': json.dumps(structured_analysis, indent=2)
|
| 88 |
+
})
|
| 89 |
+
return final_report
|
| 90 |
+
'''
|
| 91 |
+
def analyse_cv(cv_content: str) -> json:
|
| 92 |
+
crew = Crew(
|
| 93 |
+
agents=[
|
| 94 |
+
informations_personnelle_agent,
|
| 95 |
+
skills_extractor_agent,
|
| 96 |
+
experience_extractor_agent,
|
| 97 |
+
project_extractor_agent,
|
| 98 |
+
education_extractor_agent,
|
| 99 |
+
reconversion_detector_agent,
|
| 100 |
+
|
| 101 |
+
ProfileBuilderAgent
|
| 102 |
+
],
|
| 103 |
+
tasks=[
|
| 104 |
+
task_extract_informations,
|
| 105 |
+
task_extract_skills,
|
| 106 |
+
task_extract_experience,
|
| 107 |
+
task_extract_projects,
|
| 108 |
+
task_extract_education,
|
| 109 |
+
task_detect_reconversion,
|
| 110 |
+
task_build_profile
|
| 111 |
+
],
|
| 112 |
+
process=Process.sequential,
|
| 113 |
+
verbose=False,
|
| 114 |
+
telemetry=False
|
| 115 |
+
)
|
| 116 |
+
result = crew.kickoff(inputs={"cv_content": cv_content})
|
| 117 |
+
return result
|
| 118 |
+
|
| 119 |
+
|
src/crew/tasks.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Task
|
| 2 |
+
from .agents import report_generator_agent, skills_extractor_agent, experience_extractor_agent, project_extractor_agent, education_extractor_agent, ProfileBuilderAgent, informations_personnelle_agent, reconversion_detector_agent
|
| 3 |
+
|
| 4 |
+
generate_report_task = Task(
|
| 5 |
+
description=(
|
| 6 |
+
"""Tu es un rédacteur expert en RH. Ta mission est de rédiger un rapport d'évaluation final.
|
| 7 |
+
Tu dois utiliser deux sources d'information principales :
|
| 8 |
+
1. Les données d'analyse structurées de l'entretien : '{structured_analysis_data}'.
|
| 9 |
+
2. Une liste de conseils et de feedback pertinents issus de notre base de connaissances : '{rag_contextual_feedback}'.
|
| 10 |
+
|
| 11 |
+
Ta tâche est de synthétiser ces informations en un rapport cohérent et actionnable."""
|
| 12 |
+
),
|
| 13 |
+
expected_output=(
|
| 14 |
+
"""Un rapport final exceptionnel basé sur l'analyse fournie. Le rapport doit être structuré comme suit:
|
| 15 |
+
1. **Résumé et Score d'Adéquation** : Synthétise le score de similarité sémantique et donne un aperçu global.
|
| 16 |
+
2. **Analyse Comportementale** : Interprète les résultats de l'analyse de sentiment et d'intention pour décrire le comportement du candidat.
|
| 17 |
+
3. **Adéquation Sémantique avec le Poste** : Explique ce que signifie le score de similarité.
|
| 18 |
+
4. **Points Forts & Axes d'Amélioration Personnalisés** : Utilise les données d'analyse pour identifier les points à améliorer. Ensuite, intègre de manière fluide et naturelle les conseils pertinents de '{rag_contextual_feedback}' pour proposer des pistes d'amélioration concrètes et personnalisées. Ne te contente pas de copier-coller le feedback, mais reformule-le pour qu'il s'intègre parfaitement au rapport.
|
| 19 |
+
5. **Recommandation Finale**."""
|
| 20 |
+
),
|
| 21 |
+
agent=report_generator_agent,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
task_extract_skills = Task(
|
| 25 |
+
description=(
|
| 26 |
+
"Voici le contenu du CV :\n\n{cv_content}\n\n"
|
| 27 |
+
"Extraire uniquement les compétences mentionnées explicitement dans le texte du CV. "
|
| 28 |
+
"Séparer les hard skills (techniques) et les soft skills (comportementales) en analysant les listes ou phrases les contenant. "
|
| 29 |
+
"Les hards skills doivent comprendre des compétences techniques, outils, langages de programmation, etc. "
|
| 30 |
+
"Ne rien inventer. Ne pas déduire de compétences à partir d'un poste ou d'une expérience implicite. "
|
| 31 |
+
"Identifie clairement les compétences, et n'en exclue aucune. "
|
| 32 |
+
"\n\n**CONTRAINTES JSON STRICTES:**\n"
|
| 33 |
+
"- Utiliser UNIQUEMENT des guillemets doubles (\") pour les chaînes\n"
|
| 34 |
+
"- Aucune virgule finale dans les listes ou objets\n"
|
| 35 |
+
"- Vérifier la syntaxe JSON avant de retourner le résultat\n"
|
| 36 |
+
"- Échapper correctement les caractères spéciaux (\\, \", \\n, etc.)"
|
| 37 |
+
),
|
| 38 |
+
agent=skills_extractor_agent,
|
| 39 |
+
input_keys=["cv_content"],
|
| 40 |
+
expected_output=(
|
| 41 |
+
"Un dictionnaire JSON VALIDE 'Compétences' avec deux clés : 'hard_skills' et 'soft_skills', "
|
| 42 |
+
"contenant uniquement des listes de compétences présentes dans le texte. "
|
| 43 |
+
"FORMAT EXACT: {\"hard_skills\": [\"compétence1\", \"compétence2\"], \"soft_skills\": [\"compétence1\", \"compétence2\"]}"
|
| 44 |
+
)
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
task_extract_experience = Task(
|
| 48 |
+
description=(
|
| 49 |
+
"Voici le contenu du CV :\n\n{cv_content}\n\n"
|
| 50 |
+
"""
|
| 51 |
+
Extrais toutes les expériences professionnelles du CV. Pour chaque expérience, tu DOIS fournir les informations suivantes :
|
| 52 |
+
- Poste: Le titre du poste.
|
| 53 |
+
- Entreprise: Le nom de l'entreprise.
|
| 54 |
+
- start_date: La date de début. Si non trouvée, retourne "Non spécifié".
|
| 55 |
+
- end_date: La date de fin. Si le poste est actuel, utilise "Aujourd'hui". Si non trouvée, retourne "Non spécifié".
|
| 56 |
+
- responsabilités: Une liste des tâches et missions.
|
| 57 |
+
|
| 58 |
+
RÈGLES STRICTES :
|
| 59 |
+
1. NE JAMAIS laisser un champ vide (""). Si une information est introuvable, utilise la valeur "Non spécifié".
|
| 60 |
+
2. Analyse attentivement les dates. "Depuis 2023" signifie que la date de fin est "Aujourd'hui".
|
| 61 |
+
"""
|
| 62 |
+
),
|
| 63 |
+
agent=experience_extractor_agent,
|
| 64 |
+
input_keys=["cv_content"],
|
| 65 |
+
expected_output=(
|
| 66 |
+
"Un tableau JSON VALIDE d'objets 'Expérience Professionnelle' avec 5 clés par expérience : "
|
| 67 |
+
"'Poste', 'Entreprise', 'start_date', 'end_date', 'responsabilités'. "
|
| 68 |
+
"FORMAT EXACT: [{\"Poste\": \"titre\", \"Entreprise\": \"nom\", \"start_date\": \"année\", \"end_date\": \"année\", \"responsabilités\": [\"resp1\", \"resp2\"]}]"
|
| 69 |
+
)
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
task_extract_projects = Task(
|
| 73 |
+
description=(
|
| 74 |
+
"Voici le contenu du CV :\n\n{cv_content}\n\n"
|
| 75 |
+
"""
|
| 76 |
+
Identifie et extrais les PROJETS SPÉCIFIQUES mentionnés dans le CV.
|
| 77 |
+
Un projet est distinct d'une expérience professionnelle générale. Il a un nom ou un objectif clair.
|
| 78 |
+
|
| 79 |
+
RÈGLES STRICTES :
|
| 80 |
+
1. NE PAS extraire les responsabilités générales d'un poste en tant que projet. Par exemple, si le CV dit "Alternant chez Enedis où j'ai mené le projet 'Simulateur IA'", alors extrais 'Simulateur IA' comme projet. Ne copie pas toutes les tâches de l'alternance.
|
| 81 |
+
2. Si un projet est clairement lié à une expérience professionnelle, essaie de le noter, mais le plus important est de décrire le projet lui-même.
|
| 82 |
+
"""
|
| 83 |
+
),
|
| 84 |
+
agent=project_extractor_agent,
|
| 85 |
+
input_keys=["cv_content"],
|
| 86 |
+
expected_output=(
|
| 87 |
+
"Un dictionnaire JSON VALIDE 'Projets' avec deux clés : 'professional' et 'personal'. "
|
| 88 |
+
"Chaque clé contient une liste de dictionnaires, chaque dictionnaire représentant un projet avec les clés 'title', 'role', 'technologies', et 'outcomes'. "
|
| 89 |
+
"FORMAT EXACT: {\"professional\": [{\"title\": \"titre\", \"role\": \"rôle\", \"technologies\": [\"tech1\"], \"outcomes\": [\"résultat1\"]}], \"personal\": []}"
|
| 90 |
+
)
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
task_extract_education = Task(
|
| 94 |
+
description=(
|
| 95 |
+
"Voici le contenu du CV :\n\n{cv_content}\n\n"
|
| 96 |
+
"""
|
| 97 |
+
Extrais le parcours de formation et les certifications. Fais une distinction claire entre les types de formation.
|
| 98 |
+
Pour chaque élément, fournis :
|
| 99 |
+
- degree: Le nom du diplôme, du titre (ex: 'Titre RNCP niveau 6') ou de la certification (ex: 'Core Designer Certification').
|
| 100 |
+
- institution: L'école, l'université ou la plateforme (ex: 'WILD CODE SCHOOL', 'DataIku', 'DataCamp').
|
| 101 |
+
- start_date: La date de début. Si non trouvée, retourne "Non spécifié".
|
| 102 |
+
- end_date: La date de fin. Si non trouvée, retourne "Non spécifié".
|
| 103 |
+
|
| 104 |
+
RÈGLES STRICTES :
|
| 105 |
+
1. Si tu vois une certification comme "DataIku (core designer)", le diplôme est "Core Designer" et l'institution est "DataIku". NE PAS les mélanger.
|
| 106 |
+
2. NE PAS extraire une simple compétence (ex: 'Python') comme une formation.
|
| 107 |
+
"""
|
| 108 |
+
),
|
| 109 |
+
agent=education_extractor_agent,
|
| 110 |
+
input_keys=["cv_content"],
|
| 111 |
+
expected_output=(
|
| 112 |
+
"Un tableau JSON VALIDE d'objets 'Formation' avec les clés : 'degree', 'institution', 'start_date', 'end_date'. "
|
| 113 |
+
"FORMAT EXACT: [{\"degree\": \"diplôme\", \"institution\": \"établissement\", \"start_date\": \"année\", \"end_date\": \"année\"]}"
|
| 114 |
+
)
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
task_extract_informations = Task(
|
| 118 |
+
description=(
|
| 119 |
+
"Voici le contenu du CV :\n\n{cv_content}\n\n"
|
| 120 |
+
"Votre tâche est d'extraire les informations de contact du candidat. Ces informations se trouvent généralement au début ou à la fin du CV, souvent sous une section intitulée 'CONTACT'.\n"
|
| 121 |
+
"Extrayez précisément :\n"
|
| 122 |
+
"- Le **Nom complet**.\n"
|
| 123 |
+
"- L'**Adresse e-mail**.\n"
|
| 124 |
+
"- Le **Numéro de téléphone**.\n"
|
| 125 |
+
"- La **Localisation** (ville ou région).\n"
|
| 126 |
+
"toutes les informations devront être normalisées, principalement le nom si il est en majuscule en titre. "
|
| 127 |
+
),
|
| 128 |
+
agent=informations_personnelle_agent,
|
| 129 |
+
input_keys=["cv_content"],
|
| 130 |
+
expected_output=(
|
| 131 |
+
"Un dictionnaire JSON VALIDE 'informations_personnelles' contenant le nom, l'email, le numéro de téléphone et la localisation du candidat. "
|
| 132 |
+
"FORMAT EXACT: {\"nom\": \"nom\", \"email\": \"email\", \"numero_de_telephone\": \"tel\", \"localisation\": \"lieu\"}"
|
| 133 |
+
)
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
task_detect_reconversion = Task(
|
| 137 |
+
description=(
|
| 138 |
+
"En te basant sur les données extraites de la tâche `task_extract_experience`, analyse la chronologie des expériences professionnelles. "
|
| 139 |
+
"Ton objectif est de déterminer si le candidat est en reconversion professionnelle. "
|
| 140 |
+
"Cherche des changements de secteur d'activité (ex: de la restauration à la tech), des changements de type de poste (ex: de commercial à développeur), ou des sauts de carrière importants. "
|
| 141 |
+
"Si une reconversion est détectée, identifie les compétences qui semblent avoir été transférées."
|
| 142 |
+
),
|
| 143 |
+
agent=reconversion_detector_agent,
|
| 144 |
+
context=[task_extract_experience],
|
| 145 |
+
expected_output=(
|
| 146 |
+
"Un dictionnaire JSON VALIDE avec une clé 'reconversion_analysis'. "
|
| 147 |
+
"Ce dictionnaire doit contenir deux clés : 'is_reconversion' (un booléen) et 'analysis' (une chaîne de caractères expliquant pourquoi, ou pourquoi pas, et listant les compétences transférables si applicable). "
|
| 148 |
+
"FORMAT EXACT: {\"reconversion_analysis\": {\"is_reconversion\": true, \"analysis\": \"Le candidat a changé de secteur...\"}}"
|
| 149 |
+
)
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
task_build_profile = Task(
|
| 153 |
+
description=(
|
| 154 |
+
"Ta mission est d'agir comme un architecte de données. En utilisant les extractions des tâches précédentes, "
|
| 155 |
+
"assemble un profil de candidat complet. "
|
| 156 |
+
"Le résultat final doit être un unique objet JSON, parfaitement valide."
|
| 157 |
+
),
|
| 158 |
+
agent=ProfileBuilderAgent,
|
| 159 |
+
context=[
|
| 160 |
+
task_extract_informations,
|
| 161 |
+
task_extract_skills,
|
| 162 |
+
task_extract_experience,
|
| 163 |
+
task_extract_projects,
|
| 164 |
+
task_extract_education,
|
| 165 |
+
task_detect_reconversion
|
| 166 |
+
],
|
| 167 |
+
expected_output=(
|
| 168 |
+
"Retourner un unique objet JSON valide. Cet objet doit avoir une seule clé à la racine : 'candidat'. "
|
| 169 |
+
"La valeur de cette clé sera un autre objet contenant toutes les informations assemblées. "
|
| 170 |
+
"Assure-toi que la syntaxe est parfaite, que tous les guillemets sont des guillemets doubles et qu'il n'y a aucune virgule finale. "
|
| 171 |
+
"Le JSON doit être immédiatement parsable par un programme.\n\n"
|
| 172 |
+
"FORMAT EXACT:\n"
|
| 173 |
+
"{\n"
|
| 174 |
+
" \"candidat\": {\n"
|
| 175 |
+
" \"informations_personnelles\": {\"nom\": \"...\", \"email\": \"...\", ...},\n"
|
| 176 |
+
" \"compétences\": {\"hard_skills\": [...], \"soft_skills\": [...]},\n"
|
| 177 |
+
" \"expériences\": [{\"Poste\": \"...\", ...}],\n"
|
| 178 |
+
" \"projets\": {\"professional\": [...], \"personal\": [...]},\n"
|
| 179 |
+
" \"formations\": [{\"degree\": \"...\", ...}],\n"
|
| 180 |
+
" \"reconversion\": {\"is_reconversion\": true, \"analysis\": \"...\"}\n"
|
| 181 |
+
" }\n"
|
| 182 |
+
"}"
|
| 183 |
+
)
|
| 184 |
+
)
|
src/cv_parsing_agents.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
|
| 4 |
+
from src.crew.crew_pool import analyse_cv
|
| 5 |
+
from src.config import load_pdf
|
| 6 |
+
|
| 7 |
+
def clean_dict_keys(data):
|
| 8 |
+
if isinstance(data, dict):
|
| 9 |
+
return {str(key): clean_dict_keys(value) for key, value in data.items()}
|
| 10 |
+
elif isinstance(data, list):
|
| 11 |
+
return [clean_dict_keys(element) for element in data]
|
| 12 |
+
else:
|
| 13 |
+
return data
|
| 14 |
+
|
| 15 |
+
class CvParserAgent:
|
| 16 |
+
def __init__(self, pdf_path: str):
|
| 17 |
+
self.pdf_path = pdf_path
|
| 18 |
+
|
| 19 |
+
def process(self) -> dict:
|
| 20 |
+
"""
|
| 21 |
+
Traite le fichier PDF pour en extraire le contenu sous forme de JSON.
|
| 22 |
+
Ne se connecte à aucune base de données.
|
| 23 |
+
|
| 24 |
+
Retourne :
|
| 25 |
+
Un dictionnaire contenant les données extraites du CV, ou None en cas d'erreur.
|
| 26 |
+
"""
|
| 27 |
+
print(f"Début du traitement du CV : {self.pdf_path}")
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
cv_text_content = load_pdf(self.pdf_path)
|
| 31 |
+
crew_output = analyse_cv(cv_text_content)
|
| 32 |
+
|
| 33 |
+
if not crew_output or not hasattr(crew_output, 'raw') or not crew_output.raw.strip():
|
| 34 |
+
print("Erreur : L'analyse par le crew n'a pas retourné de résultat.")
|
| 35 |
+
return None
|
| 36 |
+
raw_string = crew_output.raw
|
| 37 |
+
json_string_cleaned = raw_string
|
| 38 |
+
if '```' in raw_string:
|
| 39 |
+
json_part = raw_string.split('```json')[1].split('```')[0]
|
| 40 |
+
json_string_cleaned = json_part.strip()
|
| 41 |
+
profile_data = json.loads(json_string_cleaned)
|
| 42 |
+
return clean_dict_keys(profile_data)
|
| 43 |
+
|
| 44 |
+
except json.JSONDecodeError as e:
|
| 45 |
+
print(f"Erreur de décodage JSON : {e}")
|
| 46 |
+
print(f"Données brutes reçues : {crew_output.raw}")
|
| 47 |
+
return None
|
| 48 |
+
except Exception as e:
|
| 49 |
+
print(f"Une erreur inattendue est survenue dans CvParserAgent : {e}")
|
| 50 |
+
return None
|
src/deep_learning_analyzer.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from transformers import pipeline
|
| 3 |
+
from sentence_transformers import SentenceTransformer, util
|
| 4 |
+
|
| 5 |
+
class MultiModelInterviewAnalyzer:
|
| 6 |
+
def __init__(self):
|
| 7 |
+
self.sentiment_analyzer = pipeline(
|
| 8 |
+
"text-classification",
|
| 9 |
+
model="astrosbd/french_emotion_camembert",
|
| 10 |
+
return_all_scores=True,
|
| 11 |
+
device=0 if torch.cuda.is_available() else -1,
|
| 12 |
+
)
|
| 13 |
+
self.similarity_model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 14 |
+
self.intent_classifier = pipeline(
|
| 15 |
+
"zero-shot-classification",
|
| 16 |
+
model="joeddav/xlm-roberta-large-xnli"
|
| 17 |
+
#device=0 if torch.cuda.is_available() else -1,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
def analyze_sentiment(self, messages):
|
| 21 |
+
user_messages = [msg['content'] for msg in messages if msg['role'] == 'user']
|
| 22 |
+
if not user_messages:
|
| 23 |
+
return []
|
| 24 |
+
sentiments = self.sentiment_analyzer(user_messages)
|
| 25 |
+
return sentiments
|
| 26 |
+
|
| 27 |
+
def compute_semantic_similarity(self, messages, job_requirements):
|
| 28 |
+
user_answers = " ".join([msg['content'] for msg in messages if msg['role'] == 'user'])
|
| 29 |
+
embedding_answers = self.similarity_model.encode(user_answers, convert_to_tensor=True)
|
| 30 |
+
embedding_requirements = self.similarity_model.encode(job_requirements, convert_to_tensor=True)
|
| 31 |
+
cosine_score = util.cos_sim(embedding_answers, embedding_requirements)
|
| 32 |
+
return cosine_score.item()
|
| 33 |
+
|
| 34 |
+
def classify_candidate_intent(self, messages):
|
| 35 |
+
user_answers = [msg['content'] for msg in messages if msg['role'] == 'user']
|
| 36 |
+
if not user_answers:
|
| 37 |
+
return []
|
| 38 |
+
candidate_labels = [
|
| 39 |
+
"parle de son expérience technique",
|
| 40 |
+
"exprime sa motivation",
|
| 41 |
+
"pose une question",
|
| 42 |
+
"exprime de l’incertitude ou du stress"
|
| 43 |
+
]
|
| 44 |
+
classifications = self.intent_classifier(user_answers, candidate_labels, multi_label=False)
|
| 45 |
+
return classifications
|
| 46 |
+
|
| 47 |
+
def run_full_analysis(self, conversation_history, job_requirements):
|
| 48 |
+
sentiment_results = self.analyze_sentiment(conversation_history)
|
| 49 |
+
similarity_score = self.compute_semantic_similarity(conversation_history, job_requirements)
|
| 50 |
+
intent_results = self.classify_candidate_intent(conversation_history)
|
| 51 |
+
analysis_output = {
|
| 52 |
+
"overall_similarity_score": round(similarity_score, 2),
|
| 53 |
+
"sentiment_analysis": sentiment_results,
|
| 54 |
+
"intent_analysis": intent_results,
|
| 55 |
+
"raw_transcript": conversation_history
|
| 56 |
+
}
|
| 57 |
+
return analysis_output
|
src/interview_simulator/__init__.py
ADDED
|
File without changes
|
src/interview_simulator/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (191 Bytes). View file
|
|
|
src/interview_simulator/__pycache__/entretient_version_prod.cpython-312.pyc
ADDED
|
Binary file (5.44 kB). View file
|
|
|
src/interview_simulator/entretient_version_prod.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
from typing import Dict, List, Any, Annotated
|
| 5 |
+
from typing_extensions import TypedDict
|
| 6 |
+
|
| 7 |
+
from langchain_core.messages import AIMessage, SystemMessage, HumanMessage, ToolMessage
|
| 8 |
+
from langchain_groq import ChatGroq
|
| 9 |
+
from langgraph.graph import StateGraph, START, END
|
| 10 |
+
from langgraph.graph.message import add_messages
|
| 11 |
+
from langgraph.prebuilt import ToolNode
|
| 12 |
+
from langchain_openai import ChatOpenAI
|
| 13 |
+
|
| 14 |
+
from src.config import read_system_prompt, format_cv
|
| 15 |
+
from src.crew.crew_pool import interview_analyser
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class State(TypedDict):
|
| 19 |
+
messages: Annotated[list, add_messages]
|
| 20 |
+
|
| 21 |
+
class InterviewProcessor:
|
| 22 |
+
def __init__(self, cv_document: Dict[str, Any], job_offer: Dict[str, Any], conversation_history: List[Dict[str, Any]]):
|
| 23 |
+
if not cv_document or 'candidat' not in cv_document:
|
| 24 |
+
raise ValueError("Document CV invalide fourni.")
|
| 25 |
+
if not job_offer:
|
| 26 |
+
raise ValueError("Données de l'offre d'emploi non fournies.")
|
| 27 |
+
|
| 28 |
+
self.job_offer = job_offer
|
| 29 |
+
self.cv_data = cv_document['candidat']
|
| 30 |
+
self.conversation_history = conversation_history
|
| 31 |
+
self.tools = [interview_analyser]
|
| 32 |
+
self.llm = self._get_llm()
|
| 33 |
+
self.llm_with_tools = self.llm.bind_tools(self.tools)
|
| 34 |
+
|
| 35 |
+
self.system_prompt_template = self._load_prompt_template()
|
| 36 |
+
self.graph = self._build_graph()
|
| 37 |
+
|
| 38 |
+
def _get_llm(self) -> ChatOpenAI:
|
| 39 |
+
openai_api_key = os.getenv("OPENAI_API_KEY")
|
| 40 |
+
return ChatOpenAI(
|
| 41 |
+
temperature=0.6,
|
| 42 |
+
model_name="gpt-4o-mini",
|
| 43 |
+
api_key=openai_api_key
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
def _load_prompt_template(self) -> str:
|
| 47 |
+
return read_system_prompt('prompts/rag_prompt_old.txt')
|
| 48 |
+
|
| 49 |
+
def _chatbot_node(self, state: State) -> dict:
|
| 50 |
+
if state["messages"] and isinstance(state["messages"][-1], ToolMessage):
|
| 51 |
+
tool_message = state["messages"][-1]
|
| 52 |
+
return {"messages": [AIMessage(content=tool_message.content)]}
|
| 53 |
+
messages = state["messages"]
|
| 54 |
+
formatted_cv_str = format_cv(self.cv_data)
|
| 55 |
+
|
| 56 |
+
mission = self.job_offer.get('mission', 'Non spécifiée')
|
| 57 |
+
profil_recherche = self.job_offer.get('profil_recherche', 'Non spécifié')
|
| 58 |
+
competences = self.job_offer.get('competences', 'Non spécifiées')
|
| 59 |
+
pole = self.job_offer.get('pole', 'Non spécifié')
|
| 60 |
+
system_prompt = self.system_prompt_template.format(
|
| 61 |
+
entreprise=self.job_offer.get('entreprise', 'notre entreprise'),
|
| 62 |
+
poste=self.job_offer.get('poste', 'ce poste'),
|
| 63 |
+
mission=mission,
|
| 64 |
+
profil_recherche=profil_recherche,
|
| 65 |
+
competences=competences,
|
| 66 |
+
pole=pole,
|
| 67 |
+
cv=formatted_cv_str
|
| 68 |
+
)
|
| 69 |
+
llm_messages = [SystemMessage(content=system_prompt)] + messages
|
| 70 |
+
response = self.llm_with_tools.invoke(llm_messages)
|
| 71 |
+
return {"messages": [response]}
|
| 72 |
+
|
| 73 |
+
def _route_after_chatbot(self, state: State) -> str:
|
| 74 |
+
last_message = state["messages"][-1]
|
| 75 |
+
if last_message.tool_calls:
|
| 76 |
+
return "call_tool"
|
| 77 |
+
return END
|
| 78 |
+
|
| 79 |
+
def _build_graph(self) -> any:
|
| 80 |
+
graph_builder = StateGraph(State)
|
| 81 |
+
|
| 82 |
+
graph_builder.add_node("chatbot", self._chatbot_node)
|
| 83 |
+
graph_builder.add_node("call_tool", ToolNode(self.tools))
|
| 84 |
+
graph_builder.add_edge(START, "chatbot")
|
| 85 |
+
graph_builder.add_conditional_edges(
|
| 86 |
+
"chatbot",
|
| 87 |
+
self._route_after_chatbot,
|
| 88 |
+
{
|
| 89 |
+
"call_tool": "call_tool",
|
| 90 |
+
END: END
|
| 91 |
+
}
|
| 92 |
+
)
|
| 93 |
+
graph_builder.add_edge("call_tool", "chatbot")
|
| 94 |
+
return graph_builder.compile()
|
| 95 |
+
|
| 96 |
+
def run(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
|
| 97 |
+
initial_state = self.conversation_history + messages
|
| 98 |
+
return self.graph.invoke({"messages": initial_state})
|
src/rag_handler.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from langchain_community.document_loaders import DirectoryLoader, TextLoader
|
| 3 |
+
from langchain_community.vectorstores import FAISS
|
| 4 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 5 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 6 |
+
|
| 7 |
+
embeddings_model = HuggingFaceEmbeddings(model_name='sentence-transformers/all-MiniLM-L6-v2')
|
| 8 |
+
VECTOR_STORE_PATH = "/app/vector_store"
|
| 9 |
+
|
| 10 |
+
class RAGHandler:
|
| 11 |
+
def __init__(self, knowledge_base_path: str = "/app/knowledge_base"):
|
| 12 |
+
"""
|
| 13 |
+
Initialise le RAG Handler.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
knowledge_base_path (str): Le chemin vers le dossier contenant les documents de connaissances (.md).
|
| 17 |
+
"""
|
| 18 |
+
self.embeddings = embeddings_model
|
| 19 |
+
self.vector_store = self._load_or_create_vector_store(knowledge_base_path)
|
| 20 |
+
|
| 21 |
+
def _load_documents(self, path: str) -> list:
|
| 22 |
+
"""Charge les documents depuis un chemin de répertoire spécifié."""
|
| 23 |
+
loader = DirectoryLoader(
|
| 24 |
+
path,
|
| 25 |
+
glob="**/*.md",
|
| 26 |
+
loader_cls=TextLoader,
|
| 27 |
+
loader_kwargs={"encoding": "utf-8"}
|
| 28 |
+
)
|
| 29 |
+
print(f"Chargement des documents depuis : {path}")
|
| 30 |
+
return loader.load()
|
| 31 |
+
|
| 32 |
+
def _create_vector_store(self, knowledge_base_path: str) -> FAISS | None:
|
| 33 |
+
"""Crée et sauvegarde la base de données vectorielle à partir des documents."""
|
| 34 |
+
documents = self._load_documents(knowledge_base_path)
|
| 35 |
+
if not documents:
|
| 36 |
+
print("Aucun document trouvé pour créer le vector store.")
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
print(f"{len(documents)} documents chargés. Création des vecteurs...")
|
| 40 |
+
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
|
| 41 |
+
texts = text_splitter.split_documents(documents)
|
| 42 |
+
vector_store = FAISS.from_documents(texts, self.embeddings)
|
| 43 |
+
os.makedirs(VECTOR_STORE_PATH, exist_ok=True)
|
| 44 |
+
vector_store.save_local(VECTOR_STORE_PATH)
|
| 45 |
+
print(f"Vector store créé et sauvegardé dans : {VECTOR_STORE_PATH}")
|
| 46 |
+
return vector_store
|
| 47 |
+
|
| 48 |
+
def _load_or_create_vector_store(self, knowledge_base_path: str) -> FAISS | None:
|
| 49 |
+
"""Charge le vector store s'il existe, sinon le crée."""
|
| 50 |
+
if os.path.exists(os.path.join(VECTOR_STORE_PATH, "index.faiss")):
|
| 51 |
+
print(f"Chargement du vector store existant depuis : {VECTOR_STORE_PATH}")
|
| 52 |
+
return FAISS.load_local(
|
| 53 |
+
VECTOR_STORE_PATH,
|
| 54 |
+
embeddings=self.embeddings,
|
| 55 |
+
allow_dangerous_deserialization=True
|
| 56 |
+
)
|
| 57 |
+
else:
|
| 58 |
+
print("Aucun vector store trouvé. Création d'un nouveau...")
|
| 59 |
+
return self._create_vector_store(knowledge_base_path)
|
| 60 |
+
|
| 61 |
+
def get_relevant_feedback(self, query: str, k: int = 1) -> list[str]:
|
| 62 |
+
"""Recherche les k conseils les plus pertinents pour une requête."""
|
| 63 |
+
if not self.vector_store:
|
| 64 |
+
return []
|
| 65 |
+
results = self.vector_store.similarity_search(query, k=k)
|
| 66 |
+
return [doc.page_content for doc in results]
|
| 67 |
+
|
| 68 |
+
if __name__ == '__main__':
|
| 69 |
+
print("Initialisation du RAG Handler en mode test...")
|
| 70 |
+
handler = RAGHandler(knowledge_base_path="/app/knowledge_base")
|
| 71 |
+
if handler.vector_store and hasattr(handler.vector_store, 'index'):
|
| 72 |
+
print(f"Vector store chargé avec {handler.vector_store.index.ntotal} vecteurs.")
|
| 73 |
+
|
| 74 |
+
test_query = "gestion du stress"
|
| 75 |
+
feedback = handler.get_relevant_feedback(test_query, k=2)
|
| 76 |
+
|
| 77 |
+
print(f"\nTest de recherche pour : '{test_query}'")
|
| 78 |
+
if feedback:
|
| 79 |
+
print("Feedback pertinent trouvé :")
|
| 80 |
+
for f in feedback:
|
| 81 |
+
print(f"- {f[:150]}...") # Affiche un aperçu
|
| 82 |
+
else:
|
| 83 |
+
print("Aucun feedback pertinent trouvé pour cette requête.")
|
| 84 |
+
else:
|
| 85 |
+
print("Le RAG Handler n'a pas pu être initialisé ou le vector store est vide.")
|
src/scoring_engine.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
|
| 4 |
+
# Pondérations basées sur la fiche projet
|
| 5 |
+
CONTEXT_WEIGHTS = {
|
| 6 |
+
"formations": 0.3,
|
| 7 |
+
"projets": 0.6,
|
| 8 |
+
"expériences": 0.8,
|
| 9 |
+
"multiple": 1.0
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
# Facteurs pour la formule de scoring
|
| 13 |
+
ALPHA = 0.5 # Poids du contexte
|
| 14 |
+
BETA = 0.3 # Poids de la fréquence
|
| 15 |
+
GAMMA = 0.2 # Poids de la profondeur (durée)
|
| 16 |
+
|
| 17 |
+
class ContextualScoringEngine:
|
| 18 |
+
def __init__(self, cv_data: dict):
|
| 19 |
+
self.cv_data = cv_data.get("candidat", {})
|
| 20 |
+
self.full_text = self._get_full_text_from_cv()
|
| 21 |
+
|
| 22 |
+
def _get_full_text_from_cv(self) -> str:
|
| 23 |
+
"""Concatène tout le contenu textuel du CV pour le comptage de fréquence."""
|
| 24 |
+
return json.dumps(self.cv_data, ensure_ascii=False).lower()
|
| 25 |
+
|
| 26 |
+
def _parse_date(self, date_str: str) -> datetime:
|
| 27 |
+
"""Parse une date, en gérant les cas spéciaux comme 'Aujourd'hui'."""
|
| 28 |
+
if not date_str or date_str.lower() == "non spécifié":
|
| 29 |
+
return None
|
| 30 |
+
if date_str.lower() == "aujourd'hui":
|
| 31 |
+
return datetime.now()
|
| 32 |
+
try:
|
| 33 |
+
return datetime.strptime(date_str, "%Y")
|
| 34 |
+
except ValueError:
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
def _calculate_duration_in_years(self, start_date_str: str, end_date_str: str) -> float:
|
| 38 |
+
"""Calcule la durée d'une expérience en années."""
|
| 39 |
+
start_date = self._parse_date(start_date_str)
|
| 40 |
+
end_date = self._parse_date(end_date_str)
|
| 41 |
+
if start_date and end_date:
|
| 42 |
+
return abs((end_date - start_date).days / 365.25)
|
| 43 |
+
return 0.5
|
| 44 |
+
|
| 45 |
+
def calculate_scores(self) -> dict:
|
| 46 |
+
"""Calcule les scores pondérés pour toutes les hard skills."""
|
| 47 |
+
skills = self.cv_data.get("compétences", {}).get("hard_skills", [])
|
| 48 |
+
if not skills:
|
| 49 |
+
return {}
|
| 50 |
+
|
| 51 |
+
scored_skills = []
|
| 52 |
+
for skill in skills:
|
| 53 |
+
skill_lower = skill.lower()
|
| 54 |
+
contexts = []
|
| 55 |
+
if skill_lower in json.dumps(self.cv_data.get("formations", []), ensure_ascii=False).lower():
|
| 56 |
+
contexts.append(CONTEXT_WEIGHTS["formations"])
|
| 57 |
+
if skill_lower in json.dumps(self.cv_data.get("projets", []), ensure_ascii=False).lower():
|
| 58 |
+
contexts.append(CONTEXT_WEIGHTS["projets"])
|
| 59 |
+
if skill_lower in json.dumps(self.cv_data.get("expériences", []), ensure_ascii=False).lower():
|
| 60 |
+
contexts.append(CONTEXT_WEIGHTS["expériences"])
|
| 61 |
+
|
| 62 |
+
if len(contexts) > 1:
|
| 63 |
+
context_score = CONTEXT_WEIGHTS["multiple"]
|
| 64 |
+
elif contexts:
|
| 65 |
+
context_score = contexts[0]
|
| 66 |
+
else:
|
| 67 |
+
context_score = 0.1
|
| 68 |
+
|
| 69 |
+
# 2. Fréquence de mention
|
| 70 |
+
frequency_score = self.full_text.count(skill_lower)
|
| 71 |
+
|
| 72 |
+
# 3. Profondeur d'utilisation (durée max en années)
|
| 73 |
+
max_duration = 0
|
| 74 |
+
for exp in self.cv_data.get("expériences", []):
|
| 75 |
+
if skill_lower in json.dumps(exp, ensure_ascii=False).lower():
|
| 76 |
+
duration = self._calculate_duration_in_years(exp.get("start_date"), exp.get("end_date"))
|
| 77 |
+
if duration > max_duration:
|
| 78 |
+
max_duration = duration
|
| 79 |
+
depth_score = max_duration
|
| 80 |
+
|
| 81 |
+
# Normalisation simple (peut être affinée)
|
| 82 |
+
normalized_frequency = 1 - (1 / (1 + frequency_score))
|
| 83 |
+
normalized_depth = 1 - (1 / (1 + depth_score))
|
| 84 |
+
|
| 85 |
+
# Calcul du score final
|
| 86 |
+
final_score = (ALPHA * context_score) + \
|
| 87 |
+
(BETA * normalized_frequency) + \
|
| 88 |
+
(GAMMA * normalized_depth)
|
| 89 |
+
|
| 90 |
+
scored_skills.append({
|
| 91 |
+
"skill": skill,
|
| 92 |
+
"score": round(final_score, 2),
|
| 93 |
+
"details": {
|
| 94 |
+
"context_score": context_score,
|
| 95 |
+
"frequency": frequency_score,
|
| 96 |
+
"max_duration_years": round(depth_score, 1)
|
| 97 |
+
}
|
| 98 |
+
})
|
| 99 |
+
|
| 100 |
+
# Trier par score décroissant
|
| 101 |
+
scored_skills.sort(key=lambda x: x["score"], reverse=True)
|
| 102 |
+
return {"analyse_competences": scored_skills}
|
tasks/__init__.py
ADDED
|
File without changes
|
tasks/worker_celery.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from celery import Celery
|
| 4 |
+
from crewai import Crew, Process
|
| 5 |
+
from src.deep_learning_analyzer import MultiModelInterviewAnalyzer
|
| 6 |
+
from src.rag_handler import RAGHandler
|
| 7 |
+
from src.crew.agents import report_generator_agent
|
| 8 |
+
from src.crew.tasks import generate_report_task
|
| 9 |
+
|
| 10 |
+
celery_app = Celery(
|
| 11 |
+
'worker_celery', # Nom de l'application Celery
|
| 12 |
+
broker=os.environ.get("CELERY_BROKER_URL", "redis://redis:6379/0"),
|
| 13 |
+
backend=os.environ.get("CELERY_RESULT_BACKEND", "redis://redis:6379/0"),
|
| 14 |
+
include=['tasks.worker_celery'] # Indique à Celery où trouver les tâches
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
celery_app.conf.update(
|
| 18 |
+
task_serializer='json',
|
| 19 |
+
result_serializer='json',
|
| 20 |
+
accept_content=['json'],
|
| 21 |
+
timezone='Europe/Paris',
|
| 22 |
+
enable_utc=True,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
@celery_app.task(name="tasks.run_interview_analysis")
|
| 26 |
+
def run_interview_analysis_task(conversation_history: list, job_description_text: list):
|
| 27 |
+
"""
|
| 28 |
+
Tâche Celery qui exécute l'analyse complète de l'entretien en arrière-plan.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
conversation_history (list): L'historique complet de la conversation de l'entretien.
|
| 32 |
+
job_description_text (list): La description du poste sous forme de liste de textes.
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
str: Le rapport final généré par le crew d'agents, au format string (potentiellement JSON).
|
| 36 |
+
"""
|
| 37 |
+
print(f"Début de l'analyse pour un entretien de {len(conversation_history)} messages.")
|
| 38 |
+
print("Étape 1/3: Exécution de l'analyse par Deep Learning...")
|
| 39 |
+
analyzer = MultiModelInterviewAnalyzer()
|
| 40 |
+
structured_analysis = analyzer.run_full_analysis(conversation_history, job_description_text)
|
| 41 |
+
print("Analyse DL terminée.")
|
| 42 |
+
print("Étape 2/3: Enrichissement avec le RAG...")
|
| 43 |
+
rag_handler = RAGHandler()
|
| 44 |
+
rag_feedback = []
|
| 45 |
+
|
| 46 |
+
if structured_analysis.get("intent_analysis"):
|
| 47 |
+
for intent in structured_analysis["intent_analysis"]:
|
| 48 |
+
query = f"Conseils pour un candidat qui cherche à {intent['labels'][0]}"
|
| 49 |
+
rag_feedback.extend(rag_handler.get_relevant_feedback(query))
|
| 50 |
+
|
| 51 |
+
if structured_analysis.get("sentiment_analysis"):
|
| 52 |
+
for sentiment_group in structured_analysis["sentiment_analysis"]:
|
| 53 |
+
for sentiment in sentiment_group:
|
| 54 |
+
if sentiment['label'] == 'stress' and sentiment['score'] > 0.6:
|
| 55 |
+
rag_feedback.extend(rag_handler.get_relevant_feedback("gestion du stress en entretien"))
|
| 56 |
+
unique_feedback = list(set(rag_feedback))
|
| 57 |
+
print("Enrichissement RAG terminé.")
|
| 58 |
+
print("Étape 3/3: Lancement du CrewAI pour la génération du rapport...")
|
| 59 |
+
interview_crew = Crew(
|
| 60 |
+
agents=[report_generator_agent],
|
| 61 |
+
tasks=[generate_report_task],
|
| 62 |
+
process=Process.sequential,
|
| 63 |
+
verbose=False, # Mettre à True pour un débuggage détaillé du crew
|
| 64 |
+
telemetry=False
|
| 65 |
+
)
|
| 66 |
+
final_report = interview_crew.kickoff(inputs={
|
| 67 |
+
'structured_analysis_data': json.dumps(structured_analysis, indent=2, ensure_ascii=False),
|
| 68 |
+
'rag_contextual_feedback': "\n- ".join(unique_feedback)
|
| 69 |
+
})
|
| 70 |
+
|
| 71 |
+
print("Rapport final généré. Tâche terminée.")
|
| 72 |
+
return final_report
|