import streamlit as st from PyPDF2 import PdfReader import pandas as pd from groq import Groq from referentials import REFERENTIALS import json import re # Configuration avancée de la page st.set_page_config( layout="wide", page_title="Analyse de CV - GFSI (Version 25.12)", page_icon="📄", initial_sidebar_state="expanded" ) # Fonction pour configurer le client Groq avec validation def get_groq_client(api_key): """ Initialise et valide un client Groq avec la clé API fournie. Args: api_key (str): Clé API Groq Returns: Groq: Instance de client Groq ou None en cas d'erreur """ if not api_key or not api_key.startswith("gsk_"): st.error("La clé API semble invalide. Les clés Groq commencent généralement par 'gsk_'.") return None try: client = Groq(api_key=api_key) # Test de la connexion avec une requête minimale test_response = client.chat.completions.create( messages=[{"role": "user", "content": "Test de connexion"}], model="llama-3.1-8b-instant", max_tokens=10 ) if test_response: return client except Exception as e: st.error(f"Erreur de connexion à l'API Groq : {str(e)}") return None # Extraction améliorée de texte depuis un PDF def extract_text_from_pdf(file): """ Extrait le texte d'un fichier PDF avec gestion optimisée des erreurs. Args: file (UploadedFile): Fichier PDF téléchargé Returns: str: Texte extrait du PDF ou None en cas d'erreur """ try: with st.spinner("Extraction du texte en cours..."): reader = PdfReader(file) # Vérification des pages vides if len(reader.pages) == 0: st.warning("Le PDF ne contient aucune page.") return None # Extraction avec nettoyage des caractères spéciaux text_parts = [] for page in reader.pages: page_text = page.extract_text() if page_text: # Nettoyage des caractères problématiques page_text = re.sub(r'\s+', ' ', page_text) text_parts.append(page_text) if not text_parts: st.warning("Aucun texte n'a pu être extrait du PDF. Vérifiez qu'il ne s'agit pas d'un PDF scanné.") return None return " ".join(text_parts) except Exception as e: st.error(f"Erreur lors de l'extraction du texte : {str(e)}") st.info("Conseils: Vérifiez que le PDF n'est pas protégé ou qu'il ne s'agit pas d'un document scanné sans OCR.") return None # Analyse améliorée du CV avec l'API Groq et référentiel def analyze_cv_with_groq(cv_text, referential, groq_client, model="openai/gpt-oss-120b"): """ Analyse approfondie du CV en fonction des référentiels GFSI avec références systématiques. Args: cv_text (str): Texte du CV à analyser referential (str): Nom du référentiel GFSI sélectionné groq_client (Groq): Instance de client Groq model (str): Modèle LLM à utiliser Returns: dict: Résultats structurés de l'analyse ou None en cas d'erreur """ referential_data = REFERENTIALS.get(referential, {}) # Structuration des exigences pour analyse systématique requirements_categories = { "General_Requirements": referential_data.get("General_Requirements", {}), "Qualifications": referential_data.get("Qualifications", {}), "Audit_Experience": referential_data.get("Audit_Experience", {}), "Advanced_Requirements": referential_data.get("Advanced_Requirements", {}) } # Construction d'un prompt structuré pour obtenir une réponse analysable prompt = f""" Vous êtes un expert en évaluation des compétences selon les référentiels GFSI. TÂCHE: Analysez ce CV pour vérifier sa conformité avec le référentiel {referential} de manière systématique. INSTRUCTIONS: 1. Ne jamais mentionner le nom du candidat - utilisez toujours "le candidat" 2. Pour chaque exigence, fournissez: - La référence exacte de l'exigence (code/numéro) - Votre évaluation avec STATUT: CONFORME, NON CONFORME, ou PARTIELLEMENT CONFORME - Les éléments du CV justifiant votre évaluation - Des recommandations spécifiques si nécessaire 3. Structurez votre réponse exactement selon le format JSON demandé à la fin RÉFÉRENTIEL {referential} - EXIGENCES: SECTION 1: EXIGENCES GÉNÉRALES {json.dumps(requirements_categories["General_Requirements"], ensure_ascii=False, indent=2)} SECTION 2: QUALIFICATIONS {json.dumps(requirements_categories["Qualifications"], ensure_ascii=False, indent=2)} SECTION 3: EXPÉRIENCE EN AUDIT {json.dumps(requirements_categories["Audit_Experience"], ensure_ascii=False, indent=2)} SECTION 4: EXIGENCES AVANCÉES {json.dumps(requirements_categories["Advanced_Requirements"], ensure_ascii=False, indent=2)} CV DU CANDIDAT: {cv_text} INSTRUCTIONS SUPPLÉMENTAIRES: - Vérifiez chaque formation et expérience plusieurs fois, car les terminologies peuvent varier - Évaluez si l'équivalence des formations est acceptable selon le référentiel - Identifiez précisément chaque lacune avec référence à l'exigence spécifique FORMAT DE RÉPONSE: Fournissez votre analyse au format JSON avec la structure suivante: {{ "analysis": {{ "general_requirements": [ {{ "reference": "REF-CODE-1", "requirement": "Description de l'exigence", "status": "CONFORME/NON CONFORME/PARTIELLEMENT CONFORME", "evidence": "Éléments du CV justifiant l'évaluation", "recommendations": "Recommandations si nécessaire" }} ], "qualifications": [...], "audit_experience": [...], "advanced_requirements": [...] }}, "summary": {{ "conformant_count": 12, "non_conformant_count": 3, "partially_conformant_count": 2, "overall_assessment": "CONFORME/NON CONFORME/PARTIELLEMENT CONFORME", "key_strengths": ["Force 1", "Force 2"], "key_gaps": ["Lacune 1", "Lacune 2"], "conclusion": "Conclusion générale sur l'adéquation du candidat" }} }} IMPORTANT: Assurez-vous que votre réponse soit un JSON valide et bien structuré. """ try: with st.spinner("Analyse approfondie du CV en cours..."): messages = [ {"role": "system", "content": "Vous êtes un expert en conformité GFSI qui fournit des analyses structurées avec références systématiques aux exigences."}, {"role": "user", "content": prompt} ] response = groq_client.chat.completions.create( messages=messages, model=model, max_tokens=4000, temperature=0.1, response_format={"type": "json_object"} ) # Parsing de la réponse en JSON try: result = json.loads(response.choices[0].message.content) return result except json.JSONDecodeError: # Si le parsing JSON échoue, tenter de récupérer le contenu brut content = response.choices[0].message.content st.warning("L'analyse n'a pas pu être structurée correctement. Affichage des résultats bruts.") return {"raw_content": content} except Exception as e: st.error(f"Erreur lors de l'analyse du CV : {str(e)}") return None # Affichage amélioré des résultats d'analyse def display_analysis_results(analysis_result, referential): """ Affiche les résultats de l'analyse de manière structurée et visuelle. Args: analysis_result (dict): Résultats de l'analyse referential (str): Référentiel GFSI utilisé pour l'analyse """ if not analysis_result: return # Cas où le résultat est brut (non JSON) if "raw_content" in analysis_result: st.markdown( f"""
{analysis_result["raw_content"]}
""", unsafe_allow_html=True ) return # Affichage du résumé global st.markdown(f"## Résumé de l'analyse selon le référentiel {referential}") summary = analysis_result.get("summary", {}) conformant = summary.get("conformant_count", 0) non_conformant = summary.get("non_conformant_count", 0) partially = summary.get("partially_conformant_count", 0) total = conformant + non_conformant + partially # Affichage des statistiques col1, col2, col3, col4 = st.columns(4) with col1: st.metric("Total des exigences", total) with col2: st.metric("Conformes", conformant, f"{int(conformant/total*100 if total else 0)}%") with col3: st.metric("Non conformes", non_conformant, f"{int(non_conformant/total*100 if total else 0)}%") with col4: st.metric("Partiellement conformes", partially, f"{int(partially/total*100 if total else 0)}%") # Conclusion globale conclusion_color = { "CONFORME": "#28a745", "NON CONFORME": "#dc3545", "PARTIELLEMENT CONFORME": "#ffc107" }.get(summary.get("overall_assessment", ""), "#6c757d") st.markdown( f"""
Évaluation générale : {summary.get("overall_assessment", "Non déterminée")}
""", unsafe_allow_html=True ) # Forces et lacunes col1, col2 = st.columns(2) with col1: st.markdown("### Forces principales") for strength in summary.get("key_strengths", []): st.markdown(f"✅ {strength}") with col2: st.markdown("### Points d'amélioration") for gap in summary.get("key_gaps", []): st.markdown(f"❌ {gap}") # Conclusion détaillée st.markdown("### Conclusion") st.markdown(summary.get("conclusion", "Aucune conclusion disponible.")) # Affichage détaillé des sections d'analyse analysis = analysis_result.get("analysis", {}) # Liste des sections pour affichage uniforme sections = [ ("Exigences générales", analysis.get("general_requirements", [])), ("Qualifications", analysis.get("qualifications", [])), ("Expérience en audit", analysis.get("audit_experience", [])), ("Exigences avancées", analysis.get("advanced_requirements", [])) ] # Création d'onglets pour chaque section tabs = st.tabs([section[0] for section in sections]) # Affichage du contenu de chaque section dans son onglet for i, (section_name, section_data) in enumerate(sections): with tabs[i]: if not section_data: st.info(f"Aucune donnée disponible pour la section {section_name}") continue for item in section_data: # Couleur en fonction du statut status_color = { "CONFORME": "#28a745", "NON CONFORME": "#dc3545", "PARTIELLEMENT CONFORME": "#ffc107" }.get(item.get("status", ""), "#6c757d") # Création d'un expander pour chaque exigence with st.expander(f"{item.get('reference', 'REF')} - {item.get('requirement', 'Exigence')} ({item.get('status', 'Non évalué')})"): st.markdown( f"""

Référence: {item.get('reference', 'Non spécifiée')}

Exigence: {item.get('requirement', 'Non spécifiée')}

Statut: {item.get('status', 'Non évalué')}

Éléments justificatifs: {item.get('evidence', 'Aucun')}

Recommandations: {item.get('recommendations', 'Aucune')}

""", unsafe_allow_html=True ) # Génération d'un rapport exportable def generate_exportable_report(analysis_result, referential, cv_text): """ Génère un rapport exportable basé sur l'analyse du CV. Args: analysis_result (dict): Résultats de l'analyse referential (str): Référentiel utilisé cv_text (str): Texte du CV analysé Returns: str: HTML du rapport """ if not analysis_result or "raw_content" in analysis_result: return None summary = analysis_result.get("summary", {}) analysis = analysis_result.get("analysis", {}) html = f""" Rapport d'analyse CV - {referential}

Rapport d'analyse de CV

Référentiel: {referential}

Date: {pd.Timestamp.now().strftime('%d/%m/%Y')}

Résumé de l'analyse

Exigences conformes Exigences non conformes Exigences partiellement conformes Évaluation générale
{summary.get('conformant_count', 0)} {summary.get('non_conformant_count', 0)} {summary.get('partially_conformant_count', 0)} {summary.get('overall_assessment', 'Non déterminée')}

Forces principales

Points d'amélioration

Conclusion

{summary.get('conclusion', 'Aucune conclusion disponible.')}

""" # Ajout des sections détaillées sections = [ ("Exigences générales", analysis.get("general_requirements", [])), ("Qualifications", analysis.get("qualifications", [])), ("Expérience en audit", analysis.get("audit_experience", [])), ("Exigences avancées", analysis.get("advanced_requirements", [])) ] for section_name, section_data in sections: html += f"""

{section_name}

""" if not section_data: html += "

Aucune donnée disponible pour cette section

" else: for item in section_data: status = item.get('status', '') status_class = "" if "CONFORME" in status and "NON" not in status: status_class = "conforme" elif "NON CONFORME" in status: status_class = "non-conforme" elif "PARTIELLEMENT" in status: status_class = "partiel" html += f"""

{item.get('reference', 'REF')} - {item.get('requirement', 'Exigence')}

Statut: {status}

Éléments justificatifs: {item.get('evidence', 'Aucun')}

Recommandations: {item.get('recommendations', 'Aucune')}

""" html += "
" html += """ """ return html # Fonction principale améliorée def main(): """ Interface principale améliorée avec fonctionnalités d'exportation et statistiques. """ # Barre latérale pour configuration with st.sidebar: st.image("https://via.placeholder.com/150x60?text=GFSI+Analyzer", width=150) st.title("Configuration") # Gestion de la clé API avec sauvegarde dans session_state if "api_key" not in st.session_state: st.session_state.api_key = "" api_key = st.text_input( "Clé API Groq :", value=st.session_state.api_key, type="password", help="La clé API commence par 'gsk_'" ) if api_key != st.session_state.api_key: st.session_state.api_key = api_key st.session_state.groq_client = None if not api_key else get_groq_client(api_key) if "groq_client" not in st.session_state: st.session_state.groq_client = None if not api_key else get_groq_client(api_key) # Sélection du modèle model_options = { "openai/gpt-oss-120b": "openai/gpt-oss-120b (Haute précision)", "llama-3.2-11b-versatile": "Llama 3.2 11B (Équilibré)", "llama-3.1-8b-instant": "Llama 3.1 8B (Rapide)" } selected_model = st.selectbox( "Modèle d'IA:", options=list(model_options.keys()), format_func=lambda x: model_options[x], help="Balance entre précision d'analyse et vitesse" ) st.markdown("---") st.markdown("### Options d'analyse") # Options de debug pour développement show_debug = st.checkbox("Afficher les données brutes (Debug)", False) # Informations supplémentaires st.markdown("---") st.markdown("### À propos") st.info(""" **Outil d'analyse CV - GFSI** Version 25.12 Référentiels supportés : BRCGS, FSSC 22000, IFS """) # Contenu principal st.title("Analyse de CV selon les Référentiels GFSI") st.markdown("---") # Panneau d'information avec des instructions complètes with st.expander("ℹ️ Guide d'utilisation", expanded=True): st.markdown(""" ### Comment utiliser cet outil? 1. **Configuration**: - Saisissez votre clé API Groq dans le panneau latéral - Sélectionnez le modèle d'IA à utiliser 2. **Analyse**: - Téléchargez un CV au format PDF - Sélectionnez le référentiel GFSI applicable - Lancez l'analyse 3. **Résultats**: - Consultez le rapport détaillé avec références aux exigences - Explorez les détails par section (Général, Qualifications, etc.) - Exportez les résultats au format HTML **Astuce**: Plus le document est clairement formaté, meilleure sera l'analyse. """) # Contenu principal - Interface d'analyse st.markdown("## Analyse de CV") # Vérification de la présence du client Groq if not st.session_state.groq_client: st.warning("⚠️ Veuillez configurer votre clé API Groq dans le panneau latéral pour continuer.") return # Chargement du fichier PDF uploaded_file = st.file_uploader( "📄 Téléchargez un fichier PDF (CV)", type="pdf", help="Formats supportés : PDF. Taille maximale: 10MB" ) if not uploaded_file: st.info("Veuillez télécharger un fichier PDF pour commencer l'analyse.") # Affichage d'exemples des référentiels st.markdown("### Aperçu des référentiels supportés") for ref_name, ref_data in REFERENTIALS.items(): with st.expander(f"Référentiel {ref_name}"): st.json(ref_data) return # Sélection du référentiel referential = st.selectbox( "📋 Sélectionnez un référentiel", list(REFERENTIALS.keys()), help="Choisissez le standard GFSI applicable pour ce candidat" ) # Vérification que tous les éléments nécessaires sont présents if uploaded_file and referential and st.session_state.groq_client: # Bouton pour lancer l'analyse if st.button("🔍 Analyser le CV", type="primary"): # Extraction du texte du CV cv_text = extract_text_from_pdf(uploaded_file) # Afficher un extrait du texte extrait en mode debug if show_debug and cv_text: with st.expander("Aperçu du texte extrait", expanded=False): st.text(cv_text[:1000] + "..." if len(cv_text) > 1000 else cv_text) if cv_text: # Analyse du CV avec références systématiques analysis_result = analyze_cv_with_groq( cv_text, referential, st.session_state.groq_client, model=selected_model ) # Affichage des données brutes en mode debug if show_debug and analysis_result: with st.expander("Données brutes de l'analyse (Debug)", expanded=False): st.json(analysis_result) # Affichage des résultats if analysis_result: # Affichage structuré des résultats display_analysis_results(analysis_result, referential) # Génération et téléchargement du rapport report_html = generate_exportable_report(analysis_result, referential, cv_text) if report_html: st.download_button( label="📥 Télécharger le rapport complet", data=report_html, file_name=f"analyse_cv_{referential}_{pd.Timestamp.now().strftime('%Y%m%d_%H%M')}.html", mime="text/html", ) if __name__ == "__main__": main()