Spaces:
Runtime error
Runtime error
| """ | |
| 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) | |
| # ============================================================================ | |
| 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 | |
| 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." | |
| ) | |