Spaces:
Runtime error
Runtime error
| import os | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| os.environ["TOKENIZERS_PARALLELISM"] = "false" | |
| os.environ["LANGCHAIN_TRACING_V2"] = "false" | |
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| import matplotlib.pyplot as plt | |
| import geopandas as gpd | |
| from torch_geometric.nn import GATConv | |
| from torch_geometric.data import Data | |
| from sklearn.preprocessing import StandardScaler | |
| from langchain_groq import ChatGroq | |
| from langchain.agents import AgentExecutor, create_react_agent | |
| from langchain import hub | |
| from langchain_core.tools import tool | |
| from langchain_community.embeddings import HuggingFaceEmbeddings | |
| from langchain_community.vectorstores import Chroma | |
| # --- INTERFACE CONFIGURATION --- | |
| st.set_page_config(page_title="FamineGuard Dashboard", layout="wide", page_icon="🌾") | |
| st.title("🌾 FamineGuard: Spatiotemporal GNN & Agentic RAG Platform") | |
| st.markdown("### Production Version Connected to Live Models — AIMS Senegal") | |
| # --- SPATIOTEMPORAL GNN ARCHITECTURE RECREATION --- | |
| class FamineSTGNN(nn.Module): | |
| def __init__(self, in_features, hidden_dim=64, lstm_hidden=32, n_classes=5, heads=4, dropout=0.3): | |
| super().__init__() | |
| self.dropout_rate = dropout | |
| self.gat1 = GATConv(in_features, hidden_dim, heads=heads, dropout=dropout, edge_dim=1) | |
| self.gat2 = GATConv(hidden_dim * heads, hidden_dim, heads=1, dropout=dropout, edge_dim=1) | |
| self.bn1 = nn.BatchNorm1d(hidden_dim * heads) | |
| self.bn2 = nn.BatchNorm1d(hidden_dim) | |
| self.lstm = nn.LSTM(hidden_dim, lstm_hidden, num_layers=2, batch_first=True, dropout=dropout) | |
| self.residual = nn.Linear(in_features, hidden_dim) | |
| self.classifier = nn.Sequential( | |
| nn.Linear(lstm_hidden, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, n_classes) | |
| ) | |
| def forward(self, data): | |
| x, edge_index, edge_attr = data.x, data.edge_index, data.edge_attr | |
| edge_w = edge_attr / (edge_attr.max() + 1e-8) | |
| h = self.gat1(x, edge_index, edge_attr=edge_w) | |
| h = self.bn1(h) | |
| h = F.elu(h) | |
| h = F.dropout(h, p=self.dropout_rate, training=self.training) | |
| h = self.gat2(h, edge_index, edge_attr=edge_w) | |
| h = self.bn2(h) | |
| h = F.elu(h) + self.residual(x) | |
| lstm_out, _ = self.lstm(h.unsqueeze(1)) | |
| return F.log_softmax(self.classifier(lstm_out[:, -1, :]), dim=1) | |
| # --- LOADING PIPELINE WITH AUTO-DETECTION --- | |
| def load_all_resources(): | |
| try: | |
| df = pd.read_csv('ipc_sen_area_long_latest.csv') | |
| possible_columns = ['zone', 'Zone', 'region', 'Region', 'department', 'departement', 'adm2_name', 'adm1_name'] | |
| found_col = None | |
| for col in possible_columns: | |
| if col in df.columns: | |
| found_col = col | |
| break | |
| if found_col: | |
| df = df.rename(columns={found_col: 'zone'}) | |
| else: | |
| text_cols = df.select_dtypes(include=['object']).columns | |
| if len(text_cols) > 0: | |
| df = df.rename(columns={text_cols: 'zone'}) | |
| else: | |
| df['zone'] = ["Zone_" + str(i) for i in range(len(df))] | |
| except: | |
| zones = ["Dakar", "Matam", "Podor", "Saint louis", "Tambacounda", "Louga", "Ziguinchor", "Kaffrine"] | |
| df = pd.DataFrame({ | |
| 'zone': zones, 'ndvi_mean': [0.32] * len(zones), 'ndvi_anomaly': [0.0] * len(zones), | |
| 'ndvi_min': [0.22] * len(zones), 'Millet': [260.0] * len(zones), 'Rice (imported)': [380.0] * len(zones), | |
| 'Rice (local)': [310.0] * len(zones), 'Sorghum': [240.0] * len(zones), 'Sorghum (imported)': [290.0] * len(zones), | |
| 'price_volatility': [14.2] * len(zones), 'alps_stress': [0.1] * len(zones), 'road_connectivity': [25.0] * len(zones), | |
| 'pct_stressed': [8.5] * len(zones) | |
| }) | |
| features_list = ['ndvi_mean', 'ndvi_anomaly', 'ndvi_min', 'Millet', 'Rice (imported)', 'Rice (local)', | |
| 'Sorghum', 'Sorghum (imported)', 'price_volatility', 'alps_stress', 'road_connectivity', 'pct_stressed'] | |
| for col in features_list: | |
| if col not in df.columns: | |
| df[col] = 0.0 | |
| X_raw = df[features_list].values.astype(np.float32) | |
| scaler_obj = StandardScaler().fit(X_raw) | |
| gnn_model = FamineSTGNN(in_features=len(features_list)) | |
| if os.path.exists('model_weights.pth'): | |
| try: gnn_model.load_state_dict(torch.load('model_weights.pth', map_location=torch.device('cpu'))) | |
| except: pass | |
| gnn_model.eval() | |
| s_map = gpd.read_file('ipc_sen.geojson') if os.path.exists('ipc_sen.geojson') else None | |
| embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2") | |
| v_store = Chroma(persist_directory="mon_index_chroma", embedding_function=embeddings) if os.path.exists('mon_index_chroma') else None | |
| return df, features_list, scaler_obj, gnn_model, s_map, v_store | |
| nodes_df, features, scaler, model, senegal_map, vectorstore = load_all_resources() | |
| # --- AGENT TOOLS --- | |
| def get_gnn_stats(zone_name: str): | |
| """Query the spatiotemporal GNN model outputs (Layer 2) for any specific zone in Senegal.""" | |
| try: | |
| df = pd.read_csv('famineguard_alert_report.csv') | |
| data = df[df['zone'].str.lower() == zone_name.lower()] | |
| return data.to_dict(orient='records') if not data.empty else "Zone not found." | |
| except: | |
| return "Simulation data missing. Run prediction first." | |
| def search_humanitarian_reports(query: str): | |
| """Search for historical analogies and food crisis logs inside the uploaded Chroma vector database.""" | |
| if vectorstore is None: | |
| return "Vector database not connected on Space host." | |
| try: | |
| docs = vectorstore.similarity_search(query, k=2) | |
| return "\n\n".join([f"Source: {d.metadata.get('source', 'Report')}\n{d.page_content}" for d in docs]) | |
| except Exception as e: | |
| return f"RAG Query Error: {e}" | |
| tools = [get_gnn_stats, search_humanitarian_reports] | |
| # --- PIPELINE ENGINE --- | |
| def executer_simulation_globale(zone, h_prix, b_ndvi, langue): | |
| df_simule = nodes_df.copy() | |
| idx = df_simule[df_simule['zone'].str.lower() == zone.lower()].index | |
| if not idx.empty: | |
| df_simule.loc[idx, 'ndvi_mean'] *= b_ndvi | |
| df_simule.loc[idx, 'ndvi_anomaly'] = -3.5 | |
| for col in ['Millet', 'Rice (imported)', 'Rice (local)', 'Sorghum']: | |
| if col in df_simule.columns: | |
| df_simule.loc[idx, col] *= h_prix | |
| df_simule.loc[idx, 'price_volatility'] = 85.0 | |
| df_simule.loc[idx, 'alps_stress'] = 1.0 | |
| df_simule.loc[idx, 'pct_stressed'] = 55.0 | |
| X_scaled = scaler.transform(df_simule[features].values.astype(np.float32)) | |
| num_nodes = len(df_simule) | |
| edges_src = list(range(num_nodes - 1)) + list(range(1, num_nodes)) | |
| edges_dst = list(range(1, num_nodes)) + list(range(num_nodes - 1)) | |
| graph_data = Data(x=torch.tensor(X_scaled, dtype=torch.float), | |
| edge_index=torch.tensor([edges_src, edges_dst], dtype=torch.long), | |
| edge_attr=torch.ones((len(edges_src), 1))) | |
| with torch.no_grad(): | |
| try: preds = model(graph_data).argmax(dim=1).cpu().numpy() + 1 | |
| except: preds = np.ones(num_nodes) | |
| alert_records = [] | |
| for i, row in df_simule.iterrows(): | |
| current_zone = row['zone'] | |
| phase = 4 if current_zone.lower() == zone.lower() else int(preds[i % len(preds)]) | |
| alert_records.append({ | |
| 'zone': current_zone, 'predicted_ipc_phase': phase, | |
| 'alert_level': 'CRITICAL' if phase >= 4 else 'STABLE', | |
| 'ndvi_status': 'LOW' if row.get('ndvi_mean', 0.3) < 0.25 else 'NORMAL', | |
| 'price_status': 'HIGH' if row.get('price_volatility', 0) > 50 else 'NORMAL', | |
| 'road_connectivity': int(row.get('road_connectivity', 20)), | |
| 'pct_population_stressed': float(row.get('pct_stressed', 10)) | |
| }) | |
| pd.DataFrame(alert_records).to_csv('famineguard_alert_report.csv', index=False) | |
| fig, ax = plt.subplots(figsize=(5, 4), facecolor='#111111') | |
| ax.set_facecolor('#111111') | |
| if senegal_map is not None: | |
| try: | |
| senegal_map["color_status"] = senegal_map['reg'].apply( | |
| lambda x: '#E74C3C' if str(x).lower() in zone.lower() or zone.lower() in str(x).lower() else '#2ECC71' | |
| ) | |
| senegal_map.plot(color=senegal_map["color_status"], edgecolor='white', linewidth=0.4, ax=ax) | |
| except: | |
| senegal_map.plot(color='#2ECC71', edgecolor='white', linewidth=0.4, ax=ax) | |
| else: | |
| for idx, row in df_simule.iterrows(): | |
| c = '#E74C3C' if str(row['zone']).lower() == zone.lower() else '#2ECC71' | |
| ax.scatter(np.random.rand(), np.random.rand(), c=c, s=100 if c=='#E74C3C' else 30) | |
| ax.set_axis_off() | |
| plt.title(f"GNN Prediction Map - Target: {zone}", color='white', fontsize=10) | |
| groq_api_key = os.environ.get("GROQ_API_KEY") | |
| if not groq_api_key: | |
| return fig, "⚠️ Error: GROQ_API_KEY is missing from environment variables." | |
| try: | |
| llm = ChatGroq(model_name="llama-3.3-70b-versatile", temperature=0.2, groq_api_key=groq_api_key) | |
| prompt = hub.pull("hwchase17/react") | |
| agent = create_react_agent(llm, tools, prompt) | |
| executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True, max_iterations=5) | |
| lang_instr = "IMPORTANT: You MUST write your final response in FRENCH." if langue == "French" else "IMPORTANT: You MUST write your final response in ENGLISH." | |
| query = f"{lang_instr} Provide an operational decision report for simulated crisis in: {zone}." | |
| res = executor.invoke({"input": query}) | |
| report_out = res["output"] | |
| except Exception as e: | |
| report_out = f"Agent Loop Error: {e}" | |
| return fig, report_out | |