""" app.py - Application Streamlit pour la détermination de l'impact carbone des matières premières pour aliments composés. Lancement : streamlit run app.py """ import streamlit as st import pandas as pd from flowchart_engine import evaluate_carbon_impact, CarbonResult import data_loader import config # ============================================================================ # Configuration de la page # ============================================================================ st.set_page_config( page_title="POC GAIA - Impact Carbone MP", page_icon="🌿", layout="wide", ) st.title("🌿 POC GAIA — Impact Carbone des Matières Premières") st.markdown(""" **Logigramme d'aide pour faciliter l'application du Guide de calcul de l'impact carbone des aliments composés** *(GT Carbone — hors produits dérivés du soja)* Renseignez les informations sur votre matière première ci-dessous, puis lancez l'évaluation. """) st.divider() # ============================================================================ # Listes de suggestions (chargées une fois, mises en cache par Streamlit) # ============================================================================ @st.cache_data def get_country_list(): """Retourne la liste des noms de pays disponibles (clés du mapping FR→ISO).""" noms = sorted(set(k.title() for k in config.PAYS_FR_TO_ISO.keys())) return noms @st.cache_data def get_matiere_list(): """Retourne la liste des matières premières disponibles dans EcoALIM.""" return data_loader.get_ecoalim_matieres() # ============================================================================ # Composant autocomplete maison # ============================================================================ def autocomplete_input(label: str, options: list[str], key: str, placeholder: str = "", help_text: str = "", max_suggestions: int = 8) -> str: """ Champ texte libre avec suggestions filtrées cliquables en dessous. L'utilisateur peut taper librement ou cliquer une suggestion. """ # Si une suggestion a été cliquée au tour précédent, injecter la valeur sel_key = f"{key}__sel" if sel_key in st.session_state: st.session_state[key] = st.session_state.pop(sel_key) typed = st.text_input(label, key=key, placeholder=placeholder, help=help_text) if typed and len(typed) >= 2: typed_lower = typed.lower().strip() # Filtrer + trier : commence par > contient, puis longueur croissante matches = [ o for o in options if typed_lower in o.lower() ] matches.sort(key=lambda x: (not x.lower().startswith(typed_lower), len(x))) matches = matches[:max_suggestions] # Ne pas afficher si l'input est déjà un match exact if matches and typed not in matches: cols = st.columns(min(len(matches), 4)) for i, sugg in enumerate(matches): with cols[i % 4]: if st.button(f"➜ {sugg}", key=f"{key}__sug_{i}", use_container_width=True): st.session_state[sel_key] = sugg st.rerun() return typed or "" # ============================================================================ # Formulaire d'entrée # ============================================================================ col_form, col_info = st.columns([2, 1]) with col_form: st.subheader("📝 Formulaire de saisie") matiere = autocomplete_input( "Nom de la matière première", get_matiere_list(), key="input_matiere", placeholder="Ex : BLE, T.TNSL DEC., ORGE, T. COLZA, LUZERNE…", help_text="Entrez le nom usuel de la matière première. Des suggestions apparaîtront.", ) provenance_connue = st.radio( "Connaissez-vous la provenance de la matière première ?", options=["Oui", "Non"], horizontal=True, ) pays_production = None pays_transformation = None if provenance_connue == "Oui": pays_production = autocomplete_input( "Pays de production primaire de la MP brute", get_country_list(), key="input_pays_prod", placeholder="Ex : France, Brésil, Ukraine…", help_text="Tapez les premières lettres et cliquez sur une suggestion, ou saisissez librement.", ) pays_transformation = autocomplete_input( "Pays de transformation (laisser vide si MP brute)", get_country_list(), key="input_pays_transfo", placeholder="Ex : France, Allemagne… (vide si non transformée)", help_text="Pays où la transformation a eu lieu. Laissez vide si la matière est brute.", ) st.markdown("---") run_button = st.button("🚀 Évaluer l'impact carbone", type="primary", use_container_width=True) with col_info: st.subheader("ℹ️ Règles générales") st.info(""" - Le choix des valeurs par défaut relève de la responsabilité des entreprises. - Il faut attribuer un facteur d'émission à **tous** les intrants. - Si provenance inconnue → valeur la **plus défavorable**. - Si provenance connue mais donnée inexistante → donnée générique la **plus pertinente**. - Bases : **GFLI** et **ECOALIM** en priorité. """) st.warning("⚠️ **Hors produits dérivés du soja** — Ce logigramme ne s'applique pas aux dérivés de soja.") # ============================================================================ # Exécution et affichage des résultats # ============================================================================ if run_button: if not matiere: st.error("❌ Veuillez saisir un nom de matière première.") st.stop() # Nettoyage des entrées pays_prod = pays_production.strip() if pays_production and pays_production.strip() else None pays_transfo = pays_transformation.strip() if pays_transformation and pays_transformation.strip() else None with st.spinner("🔄 Évaluation en cours… (classification LLM + recherche dans les bases de données)"): result: CarbonResult = evaluate_carbon_impact( matiere_premiere=matiere.strip(), pays_production=pays_prod, pays_transformation=pays_transfo, ) st.divider() # ------------------------------------------------------------------ # Section 1 : Résultat principal # ------------------------------------------------------------------ st.subheader("📊 Résultat de l'impact carbone") if result.erreur: st.error(f"❌ {result.erreur}") else: col1, col2, col3 = st.columns(3) with col1: if result.impact_kg_co2_eq is not None: if "tonne" in result.unite_source: # GFLI : valeur en kg CO2 / tonne st.metric( label="Impact carbone", value=f"{result.impact_kg_co2_eq:.2f}", delta=f"kg CO2 eq / tonne", ) st.metric( label="Soit en tonnes CO2 eq / tonne produit", value=f"{result.impact_tonne_co2_eq:.4f}", delta="t CO2 eq / t produit", ) else: # EcoALIM : valeur en kg CO2 / kg st.metric( label="Impact carbone", value=f"{result.impact_kg_co2_eq:.4f}", delta=f"kg CO2 eq / kg", ) st.metric( label="Soit en tonnes CO2 eq / tonne produit", value=f"{result.impact_kg_co2_eq:.4f}", delta="t CO2 eq / t produit (même valeur numérique)", ) with col2: st.markdown(f"**Source :** {result.source_db}") st.markdown(f"**Intrant utilisé :** {result.intrant_utilise}") st.markdown(f"**Match exact :** {'✅ Oui' if result.match_exact else '⚠️ Non — valeur approchée'}") with col3: st.markdown(f"**Classification :** {'🏭 Transformé' if result.classification == 'transforme' else '🌾 Brut'}") st.markdown(f"**Node résultat :** `{result.node_resultat}`") if result.pays_production: st.markdown(f"**Pays production :** {result.pays_production}") if result.pays_transformation: st.markdown(f"**Pays transformation :** {result.pays_transformation}") # ------------------------------------------------------------------ # Section 2 : Parcours de logique (logigramme) # ------------------------------------------------------------------ st.subheader("🔀 Parcours du logigramme") for i, step in enumerate(result.parcours): with st.expander(f"Étape {i+1} — {step.node_id}", expanded=(i == 0)): if step.question: st.markdown(f"**Question :** {step.question}") if step.answer: st.markdown(f"**Réponse :** {step.answer}") if step.action: st.markdown(f"**Action :** {step.action}") # ------------------------------------------------------------------ # Section 3 : Actions appliquées (recherche dans les BDD) # ------------------------------------------------------------------ st.subheader("🔍 Détail des recherches effectuées") for action in result.actions_appliquees: if action.startswith(" →"): st.success(action) else: st.markdown(f"- {action}") # ------------------------------------------------------------------ # Section 4 : Justification si valeur alternative # ------------------------------------------------------------------ if result.justification_alternative: st.subheader("💡 Justification du choix de valeur") st.info(result.justification_alternative) # ------------------------------------------------------------------ # Section 5 : Candidats alternatifs # ------------------------------------------------------------------ if result.candidats_alternatifs: st.subheader("📋 Produits candidats (triés par pertinence)") if not result.match_exact: st.warning("⚠️ Pas de correspondance exacte — voici les produits les plus proches avec leur impact carbone.") else: st.info("ℹ️ Autres produits correspondant à la recherche dans les bases de données.") # Construire un DataFrame pour un affichage clair df_candidates = pd.DataFrame(result.candidats_alternatifs) df_candidates = df_candidates.rename(columns={ "nom": "Intrant", "impact": "Impact carbone", "unite": "Unité", "source": "Base", }) # Formater les colonnes st.dataframe( df_candidates, use_container_width=True, hide_index=True, column_config={ "Impact carbone": st.column_config.NumberColumn(format="%.4f"), }, ) # ------------------------------------------------------------------ # Section 6 : Classification détaillée # ------------------------------------------------------------------ with st.expander("📋 Détail de la classification brut/transformé"): st.markdown(f"**Classification :** {result.classification}") st.markdown(f"**Justification :** {result.classification_justification}") # ============================================================================ # Mode batch (facultatif) # ============================================================================ st.divider() st.subheader("📁 Évaluation par lot (import fichier)") st.markdown("Importez un fichier Excel avec les colonnes : `Matière première`, `Pays de la production primaire de la MP brute`, `Pays de la transformation`") uploaded_file = st.file_uploader("Choisir un fichier Excel", type=["xlsx", "xls"]) if uploaded_file is not None: try: df_input = pd.read_excel(uploaded_file) st.dataframe(df_input, use_container_width=True) # Chercher les colonnes par nom approximatif col_mapping = {} for col in df_input.columns: col_lower = str(col).lower() if "matière" in col_lower or "matiere" in col_lower or "mp" in col_lower: col_mapping["matiere"] = col elif "production" in col_lower or "pays" in col_lower and "transf" not in col_lower: if "matiere" not in col_lower: col_mapping["pays_prod"] = col elif "transf" in col_lower: col_mapping["pays_transfo"] = col if "matiere" not in col_mapping: st.warning("⚠️ Colonne 'Matière première' non détectée. Vérifiez les noms de colonnes.") else: if st.button("🚀 Lancer l'évaluation par lot", type="primary"): results_list = [] progress_bar = st.progress(0) for idx, row in df_input.iterrows(): mp = str(row.get(col_mapping["matiere"], "")).strip() if not mp or mp == "nan": continue pays_p = str(row.get(col_mapping.get("pays_prod", ""), "")).strip() pays_t = str(row.get(col_mapping.get("pays_transfo", ""), "")).strip() pays_p = pays_p if pays_p and pays_p != "nan" else None pays_t = pays_t if pays_t and pays_t != "nan" else None with st.spinner(f"Évaluation de {mp}..."): res = evaluate_carbon_impact(mp, pays_p, pays_t) results_list.append({ "Matière première": mp, "Pays production": pays_p or "", "Pays transformation": pays_t or "", "Classification": res.classification, "Impact (kg CO2 eq)": res.impact_kg_co2_eq, "Unité": res.unite_source, "Source": res.source_db, "Intrant utilisé": res.intrant_utilise, "Match exact": "Oui" if res.match_exact else "Non", "Node résultat": res.node_resultat, "Justification": res.justification_alternative or "", "Erreur": res.erreur or "", }) progress_bar.progress((idx + 1) / len(df_input)) if results_list: df_results = pd.DataFrame(results_list) st.subheader("📊 Résultats du lot") st.dataframe(df_results, use_container_width=True) # Téléchargement csv = df_results.to_csv(index=False).encode("utf-8") st.download_button( label="📥 Télécharger les résultats (CSV)", data=csv, file_name="resultats_impact_carbone.csv", mime="text/csv", ) except Exception as e: st.error(f"Erreur lors de la lecture du fichier : {e}") # ============================================================================ # Footer # ============================================================================ st.divider() st.caption( "POC GAIA — Outil d'aide à la détermination de l'impact carbone des matières premières " "pour aliments composés. Basé sur le Guide GT Carbone, les bases ECOALIM v9 et GFLI 2.0. " "Classification brut/transformé assistée par Mistral AI et le Catalogue UE des Matières Premières." )