FamineGuard_project / streamlit_app.py
Fatimasane26's picture
Update streamlit_app.py
5ace1d2 verified
Raw
History Blame Contribute Delete
9.98 kB
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 ---
@st.cache_resource
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 ---
@tool
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."
@tool
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