Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| from pocketgroq import GroqProvider | |
| import time | |
| import json | |
| import uuid | |
| # Configuration de la page | |
| st.set_page_config( | |
| page_title="VisiPilot IFS Food 8", | |
| page_icon="🔍", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Styles CSS | |
| st.markdown(""" | |
| <style> | |
| .main-header { | |
| font-size: 28px; | |
| font-weight: bold; | |
| color: #0066cc; | |
| text-align: center; | |
| margin-bottom: 20px; | |
| padding: 10px 0; | |
| border-bottom: 2px solid #e0f7fa; | |
| } | |
| .banner { | |
| background-image: url('https://github.com/M00N69/BUSCAR/blob/main/logo%2002%20copie.jpg?raw=true'); | |
| background-size: cover; | |
| height: 150px; | |
| background-position: center; | |
| margin-bottom: 20px; | |
| border-radius: 10px; | |
| } | |
| .card { | |
| padding: 15px; | |
| border-radius: 10px; | |
| background-color: #f9f9f9; | |
| margin-bottom: 15px; | |
| border-left: 5px solid #0066cc; | |
| } | |
| .status-badge { | |
| display: inline-block; | |
| padding: 3px 8px; | |
| font-size: 12px; | |
| font-weight: bold; | |
| border-radius: 10px; | |
| margin-left: 10px; | |
| } | |
| .status-completed { | |
| background-color: #28a745; | |
| color: white; | |
| } | |
| .status-progress { | |
| background-color: #ffc107; | |
| color: black; | |
| } | |
| .status-pending { | |
| background-color: #dc3545; | |
| color: white; | |
| } | |
| .button-container { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| /* Style personnalisé pour les expanders de recommandation */ | |
| .recommendation-expander { | |
| background-color: #e6f2ff !important; | |
| border-radius: 8px !important; | |
| border: 1px solid #b3d9ff !important; | |
| margin-top: 10px !important; | |
| } | |
| /* Modification du style des éléments d'expander de Streamlit */ | |
| .st-emotion-cache-1abe2ax, .st-emotion-cache-ue6h4q, .st-emotion-cache-1y4p8pa { | |
| background-color: #e6f2ff !important; | |
| } | |
| /* Modification pour le header d'expander */ | |
| .st-emotion-cache-19rxjzo { | |
| background-color: #cce5ff !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Initialiser les états de session | |
| if 'api_key' not in st.session_state: | |
| st.session_state['api_key'] = "" | |
| if 'action_plan_df' not in st.session_state: | |
| st.session_state['action_plan_df'] = None | |
| if 'recommendations' not in st.session_state: | |
| st.session_state['recommendations'] = {} | |
| if 'responses' not in st.session_state: | |
| st.session_state['responses'] = {} | |
| if 'active_item' not in st.session_state: | |
| st.session_state['active_item'] = None | |
| if 'ask_questions' not in st.session_state: | |
| st.session_state['ask_questions'] = {} | |
| if 'session_id' not in st.session_state: | |
| st.session_state['session_id'] = str(uuid.uuid4()) | |
| # Initialiser GroqProvider | |
| def get_groq_provider(): | |
| if not st.session_state.get('api_key'): | |
| st.error("⚠️ Veuillez entrer votre clé API Groq.") | |
| return None | |
| return GroqProvider(api_key=st.session_state.api_key) | |
| # Charger le fichier Excel avec le plan d'action | |
| def load_action_plan(uploaded_file): | |
| try: | |
| # Utiliser header=11 pour sauter les lignes d'en-tête | |
| action_plan_df = pd.read_excel(uploaded_file, header=11) | |
| action_plan_df = action_plan_df[["requirementNo", "requirementText", "requirementExplanation"]] | |
| action_plan_df.columns = ["Numéro d'exigence", "Exigence IFS Food 8", "Explication (par l'auditeur/l'évaluateur)"] | |
| # Ajouter une colonne de statut | |
| action_plan_df["Statut"] = "Non traité" | |
| # Supprimer les lignes vides (où le numéro d'exigence est NaN ou vide) | |
| action_plan_df = action_plan_df.dropna(subset=["Numéro d'exigence"]) | |
| action_plan_df = action_plan_df[action_plan_df["Numéro d'exigence"].astype(str).str.strip() != ""] | |
| return action_plan_df | |
| except Exception as e: | |
| st.error(f"⚠️ Erreur lors de la lecture du fichier: {str(e)}") | |
| return None | |
| # Générer des questions adaptées à la non-conformité | |
| def generate_questions(non_conformity): | |
| req_text = non_conformity["Exigence IFS Food 8"] | |
| audit_comment = non_conformity["Explication (par l'auditeur/l'évaluateur)"] | |
| # Détecter les mots-clés pour personnaliser les questions | |
| keywords = { | |
| "formation": ["formation", "compétence", "qualification", "personnel"], | |
| "documentation": ["document", "procédure", "enregistrement", "contrôle"], | |
| "équipement": ["équipement", "matériel", "maintenance", "installation"], | |
| "hygiène": ["hygiène", "nettoyage", "désinfection", "contamination"], | |
| "traçabilité": ["traçabilité", "lot", "identification", "rappel"] | |
| } | |
| # Questions de base | |
| questions = [ | |
| { | |
| "id": "context", | |
| "question": "Décrivez brièvement le contexte actuel lié à cette exigence dans votre entreprise." | |
| }, | |
| { | |
| "id": "cause", | |
| "question": "Selon vous, quelle est la cause principale de cette non-conformité ?" | |
| } | |
| ] | |
| # Ajouter des questions spécifiques en fonction des mots-clés | |
| for category, terms in keywords.items(): | |
| for term in terms: | |
| if term.lower() in req_text.lower() or term.lower() in audit_comment.lower(): | |
| if category == "formation": | |
| questions.append({ | |
| "id": "training", | |
| "question": "Le personnel a-t-il reçu une formation spécifique sur ce sujet ? Si oui, quand était la dernière formation ?" | |
| }) | |
| break | |
| elif category == "documentation": | |
| questions.append({ | |
| "id": "documentation", | |
| "question": "Disposez-vous d'une procédure ou d'instructions pour ce processus ? Est-elle à jour ?" | |
| }) | |
| break | |
| elif category == "équipement": | |
| questions.append({ | |
| "id": "equipment", | |
| "question": "Les équipements concernés sont-ils adaptés et correctement entretenus ?" | |
| }) | |
| break | |
| elif category == "hygiène": | |
| questions.append({ | |
| "id": "hygiene", | |
| "question": "Quelles sont vos pratiques actuelles de nettoyage/désinfection dans cette zone ?" | |
| }) | |
| break | |
| elif category == "traçabilité": | |
| questions.append({ | |
| "id": "traceability", | |
| "question": "Comment assurez-vous actuellement la traçabilité dans ce processus ?" | |
| }) | |
| break | |
| # Limiter à 3 questions maximum | |
| return questions[:3] | |
| # Détecter la langue du texte | |
| def detect_language(text): | |
| # Liste de mots français courants pour détection simple | |
| french_words = ["et", "les", "des", "dans", "pour", "avec", "par", "sur", "en", "au", "aux", | |
| "de", "la", "le", "du", "un", "une", "cette", "est", "sont", "ont", "qui", | |
| "non", "conformité", "exigence", "auditeur"] | |
| # Compter les mots français | |
| text_lower = text.lower() | |
| french_count = sum(1 for word in french_words if f" {word} " in f" {text_lower} ") | |
| # Si au moins 3 mots français sont détectés, considérer comme français | |
| return "fr" if french_count >= 3 else "en" | |
| # Générer une recommandation avec Groq | |
| def generate_ai_recommendation(non_conformity, responses=None, direct=False): | |
| groq = get_groq_provider() | |
| if not groq: | |
| return "Erreur: clé API non fournie." | |
| # Détecter la langue | |
| language = detect_language(non_conformity["Exigence IFS Food 8"] + " " + | |
| non_conformity["Explication (par l'auditeur/l'évaluateur)"]) | |
| # Construire le prompt selon la langue détectée | |
| if language == "fr": | |
| if direct: | |
| prompt = f""" | |
| En tant qu'expert en IFS Food 8 et en sécurité alimentaire, analysez cette non-conformité et proposez un plan d'action complet. | |
| # NON-CONFORMITÉ | |
| - Exigence N°: {non_conformity["Numéro d'exigence"]} | |
| - Exigence IFS Food 8: {non_conformity["Exigence IFS Food 8"]} | |
| - Constat de l'auditeur: {non_conformity["Explication (par l'auditeur/l'évaluateur)"]} | |
| # VOTRE MISSION | |
| Fournir une analyse et un plan d'action structuré avec les sections suivantes: | |
| 1. ANALYSE DE LA NON-CONFORMITÉ | |
| Analysez la situation et identifiez clairement le problème. | |
| 2. ANALYSE DES CAUSES | |
| Identifiez et détaillez les causes racines probables. | |
| 3. PLAN D'ACTION | |
| a) Actions immédiates (corrections) | |
| b) Type de preuves à fournir | |
| c) Actions correctives à long terme | |
| 4. MÉTHODES DE VALIDATION DE L'EFFICACITÉ | |
| Comment vérifier que les actions mises en place sont efficaces. | |
| 5. RECOMMANDATIONS COMPLÉMENTAIRES | |
| Rédigez l'ensemble de l'analyse en français et fournissez des recommandations spécifiques, réalistes et conformes à l'IFS Food 8. | |
| """ | |
| else: | |
| prompt = f""" | |
| En tant qu'expert en IFS Food 8 et en sécurité alimentaire, analysez cette non-conformité et les informations fournies pour proposer un plan d'action adapté. | |
| # NON-CONFORMITÉ | |
| - Exigence N°: {non_conformity["Numéro d'exigence"]} | |
| - Exigence IFS Food 8: {non_conformity["Exigence IFS Food 8"]} | |
| - Constat de l'auditeur: {non_conformity["Explication (par l'auditeur/l'évaluateur)"]} | |
| # INFORMATIONS FOURNIES PAR L'UTILISATEUR | |
| {json.dumps(responses, indent=2)} | |
| # VOTRE MISSION | |
| Fournir une analyse et un plan d'action structuré avec les sections suivantes: | |
| 1. ANALYSE DE LA NON-CONFORMITÉ | |
| Analysez la situation et identifiez clairement le problème en tenant compte des informations fournies. | |
| 2. ANALYSE DES CAUSES | |
| Identifiez et détaillez les causes racines probables. | |
| 3. PLAN D'ACTION | |
| a) Actions immédiates (corrections) | |
| b) Type de preuves à fournir | |
| c) Actions correctives à long terme | |
| 4. MÉTHODES DE VALIDATION DE L'EFFICACITÉ | |
| Comment vérifier que les actions mises en place sont efficaces. | |
| 5. RECOMMANDATIONS COMPLÉMENTAIRES | |
| Rédigez l'ensemble de l'analyse en français et fournissez des recommandations spécifiques, réalistes et conformes à l'IFS Food 8. | |
| """ | |
| else: | |
| # En anglais | |
| if direct: | |
| prompt = f""" | |
| As an IFS Food 8 and food safety expert, analyze this non-conformity and provide a comprehensive action plan. | |
| # NON-CONFORMITY | |
| - Requirement No.: {non_conformity["Numéro d'exigence"]} | |
| - IFS Food 8 Requirement: {non_conformity["Exigence IFS Food 8"]} | |
| - Auditor's finding: {non_conformity["Explication (par l'auditeur/l'évaluateur)"]} | |
| # YOUR MISSION | |
| Provide an analysis and structured action plan with the following sections: | |
| 1. ANALYSIS OF THE NON-CONFORMITY | |
| Analyze the situation and clearly identify the problem. | |
| 2. ROOT CAUSE ANALYSIS | |
| Identify and detail the probable root causes. | |
| 3. ACTION PLAN | |
| a) Immediate actions (corrections) | |
| b) Type of evidence to be provided | |
| c) Long-term corrective actions | |
| 4. METHODS FOR VALIDATING EFFECTIVENESS | |
| How to verify that the implemented actions are effective. | |
| 5. ADDITIONAL RECOMMENDATIONS | |
| Write the entire analysis in English and provide specific, realistic recommendations that comply with IFS Food 8. | |
| """ | |
| else: | |
| prompt = f""" | |
| As an IFS Food 8 and food safety expert, analyze this non-conformity and the information provided to propose an adapted action plan. | |
| # NON-CONFORMITY | |
| - Requirement No.: {non_conformity["Numéro d'exigence"]} | |
| - IFS Food 8 Requirement: {non_conformity["Exigence IFS Food 8"]} | |
| - Auditor's finding: {non_conformity["Explication (par l'auditeur/l'évaluateur)"]} | |
| # INFORMATION PROVIDED BY THE USER | |
| {json.dumps(responses, indent=2)} | |
| # YOUR MISSION | |
| Provide an analysis and structured action plan with the following sections: | |
| 1. ANALYSIS OF THE NON-CONFORMITY | |
| Analyze the situation and clearly identify the problem, taking into account the information provided. | |
| 2. ROOT CAUSE ANALYSIS | |
| Identify and detail the probable root causes. | |
| 3. ACTION PLAN | |
| a) Immediate actions (corrections) | |
| b) Type of evidence to be provided | |
| c) Long-term corrective actions | |
| 4. METHODS FOR VALIDATING EFFECTIVENESS | |
| How to verify that the implemented actions are effective. | |
| 5. ADDITIONAL RECOMMENDATIONS | |
| Write the entire analysis in English and provide specific, realistic recommendations that comply with IFS Food 8. | |
| """ | |
| try: | |
| # Option: Utilisez Chain of Thought pour une analyse plus approfondie | |
| return groq.generate(prompt, max_tokens=1500, temperature=0.2, use_cot=True) | |
| except Exception as e: | |
| st.error(f"⚠️ Erreur lors de la génération de la recommandation : {str(e)}") | |
| return None | |
| # Sauvegarde et chargement de session | |
| def save_session_data(): | |
| session_data = { | |
| "recommendations": st.session_state['recommendations'], | |
| "responses": st.session_state['responses'], | |
| "action_plan_status": st.session_state['action_plan_df']["Statut"].to_dict() if st.session_state['action_plan_df'] is not None else {} | |
| } | |
| return json.dumps(session_data) | |
| def load_session_data(session_json): | |
| try: | |
| data = json.loads(session_json) | |
| st.session_state['recommendations'] = data.get("recommendations", {}) | |
| st.session_state['responses'] = data.get("responses", {}) | |
| # Mettre à jour les statuts si le plan d'action est déjà chargé | |
| if st.session_state['action_plan_df'] is not None and "action_plan_status" in data: | |
| for idx, status in data["action_plan_status"].items(): | |
| if int(idx) in st.session_state['action_plan_df'].index: | |
| st.session_state['action_plan_df'].loc[int(idx), "Statut"] = status | |
| return True | |
| except Exception as e: | |
| st.error(f"⚠️ Erreur lors du chargement de la session : {str(e)}") | |
| return False | |
| # Interface principale | |
| def main(): | |
| # Ajouter la bannière VisiPilot | |
| st.markdown('<div class="banner"></div>', unsafe_allow_html=True) | |
| st.markdown('<div class="main-header">📊 VisiPilot - Assistant Plan d\'Actions IFS Food 8</div>', unsafe_allow_html=True) | |
| # Sidebar pour la configuration | |
| with st.sidebar: | |
| st.markdown("### ⚙️ Configuration") | |
| # API Key Groq | |
| api_key = st.text_input("Clé API Groq :", type="password", value=st.session_state.get('api_key', '')) | |
| if api_key: | |
| st.session_state.api_key = api_key | |
| st.markdown("---") | |
| # Téléchargement du fichier Excel | |
| st.markdown("### 📤 Plan d'Action") | |
| uploaded_file = st.file_uploader("Fichier Excel du plan d'action :", type=["xlsx"]) | |
| if uploaded_file: | |
| action_plan_df = load_action_plan(uploaded_file) | |
| if action_plan_df is not None: | |
| st.session_state['action_plan_df'] = action_plan_df | |
| st.success("✅ Plan d'action chargé avec succès") | |
| st.markdown("---") | |
| # Sauvegarde/Chargement de session | |
| st.markdown("### 💾 Session") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("💾 Télécharger sauvegarde"): | |
| if 'action_plan_df' in st.session_state and st.session_state['action_plan_df'] is not None: | |
| session_data = save_session_data() | |
| # Créer un lien de téléchargement pour le fichier JSON | |
| timestamp = time.strftime("%Y%m%d-%H%M%S") | |
| st.download_button( | |
| label="📥 Télécharger le fichier", | |
| data=session_data, | |
| file_name=f"visipilot_session_{timestamp}.json", | |
| mime="application/json" | |
| ) | |
| else: | |
| st.warning("Chargez d'abord un plan d'action.") | |
| with col2: | |
| session_file = st.file_uploader("📂 Charger une sauvegarde", type=["json"]) | |
| if session_file is not None: | |
| session_content = session_file.getvalue().decode("utf-8") | |
| if load_session_data(session_content): | |
| st.success("✅ Session restaurée avec succès") | |
| st.markdown("---") | |
| # Statistiques | |
| if 'action_plan_df' in st.session_state and st.session_state['action_plan_df'] is not None: | |
| st.markdown("### 📈 Progression") | |
| total = len(st.session_state['action_plan_df']) | |
| completed = sum(st.session_state['action_plan_df']["Statut"] == "Complété") | |
| in_progress = sum(st.session_state['action_plan_df']["Statut"] == "En cours") | |
| st.progress(completed / total if total > 0 else 0) | |
| st.markdown(f"**{completed}/{total}** complétées ({int((completed/total)*100) if total > 0 else 0}%)") | |
| # Section principale | |
| if 'action_plan_df' not in st.session_state or st.session_state['action_plan_df'] is None: | |
| st.info("👈 Commencez par configurer votre clé API Groq et charger votre plan d'action dans la barre latérale.") | |
| else: | |
| # Filtres | |
| col1, col2 = st.columns([1, 2]) | |
| with col1: | |
| status_filter = st.multiselect("Filtrer par statut", | |
| options=["Tous", "Non traité", "En cours", "Complété"], | |
| default=["Tous"]) | |
| with col2: | |
| search_term = st.text_input("Rechercher une exigence", "") | |
| # Appliquer les filtres | |
| filtered_df = st.session_state['action_plan_df'].copy() | |
| if "Tous" not in status_filter: | |
| filtered_df = filtered_df[filtered_df["Statut"].isin(status_filter)] | |
| if search_term: | |
| filtered_df = filtered_df[ | |
| filtered_df["Exigence IFS Food 8"].str.contains(search_term, case=False) | | |
| filtered_df["Numéro d'exigence"].astype(str).str.contains(search_term, case=False) | |
| ] | |
| # Afficher les non-conformités | |
| for index, row in filtered_df.iterrows(): | |
| # Déterminer la classe du badge de statut | |
| status_class = "status-pending" | |
| if row["Statut"] == "Complété": | |
| status_class = "status-completed" | |
| elif row["Statut"] == "En cours": | |
| status_class = "status-progress" | |
| # Afficher la carte de non-conformité | |
| with st.container(): | |
| st.markdown(f""" | |
| <div class="card"> | |
| <div> | |
| <strong>Exigence {row["Numéro d'exigence"]}</strong> | |
| <span class="status-badge {status_class}">{row["Statut"]}</span> | |
| </div> | |
| <div style="margin-top: 5px;"> | |
| {row["Exigence IFS Food 8"]} | |
| </div> | |
| <div style="font-style: italic; margin-top: 5px; color: #666;"> | |
| <strong>Constat de l'auditeur:</strong> {row["Explication (par l'auditeur/l'évaluateur)"]} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Options d'action | |
| col1, col2, col3 = st.columns([1, 1, 1]) | |
| with col1: | |
| if st.button("💡 Questions ciblées", key=f"questions_{index}"): | |
| st.session_state['active_item'] = index | |
| st.session_state['ask_questions'][index] = True | |
| with col2: | |
| if st.button("🤖 Recommandation directe", key=f"direct_{index}"): | |
| with st.spinner("Génération en cours..."): | |
| recommendation = generate_ai_recommendation(row, direct=True) | |
| if recommendation: | |
| st.session_state['recommendations'][index] = recommendation | |
| st.session_state['active_item'] = index | |
| # Mettre à jour le statut | |
| st.session_state['action_plan_df'].loc[index, "Statut"] = "En cours" | |
| st.rerun() | |
| with col3: | |
| status_options = ["Non traité", "En cours", "Complété"] | |
| new_status = st.selectbox("Statut", options=status_options, | |
| index=status_options.index(row["Statut"]), | |
| key=f"status_{index}") | |
| if new_status != row["Statut"]: | |
| st.session_state['action_plan_df'].loc[index, "Statut"] = new_status | |
| # Si cet élément est actif pour les questions | |
| if index == st.session_state.get('active_item') and st.session_state['ask_questions'].get(index, False): | |
| with st.container(): | |
| st.markdown("#### Questions pour analyse ciblée") | |
| # Générer des questions adaptées | |
| questions = generate_questions(row) | |
| with st.form(key=f"questions_form_{index}"): | |
| responses = {} | |
| for q in questions: | |
| responses[q["id"]] = st.text_area(q["question"], key=f"q_{index}_{q['id']}") | |
| submit_btn = st.form_submit_button("Générer recommandation") | |
| if submit_btn: | |
| with st.spinner("Génération en cours..."): | |
| recommendation = generate_ai_recommendation(row, responses) | |
| if recommendation: | |
| st.session_state['recommendations'][index] = recommendation | |
| st.session_state['responses'][index] = responses | |
| # Mettre à jour le statut | |
| st.session_state['action_plan_df'].loc[index, "Statut"] = "En cours" | |
| st.rerun() | |
| # Afficher la recommandation si elle existe | |
| if index == st.session_state.get('active_item') or index in st.session_state.get('recommendations', {}): | |
| if index in st.session_state.get('recommendations', {}): | |
| with st.expander("📋 Recommandation IA", expanded=True if index == st.session_state.get('active_item') else False): | |
| # Appliquer la classe personnalisée à l'expander via HTML | |
| st.markdown('<div class="recommendation-expander">', unsafe_allow_html=True) | |
| st.markdown(st.session_state['recommendations'][index]) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| if st.button("✅ Marquer comme complété", key=f"complete_{index}"): | |
| st.session_state['action_plan_df'].loc[index, "Statut"] = "Complété" | |
| st.rerun() | |
| st.markdown("---") | |
| if __name__ == "__main__": | |
| main() |