|
import gradio as gr |
|
from datetime import datetime |
|
import sys |
|
import threading |
|
from agents.orchestrator import ClimateRiskOrchestrator |
|
from tools.mapping_utils import ( |
|
COUNTRIES_AND_CITIES, |
|
US_STATES, |
|
get_coordinates_from_dropdown, |
|
create_risk_map, |
|
get_city_suggestions, |
|
) |
|
|
|
|
|
class LogCatcher: |
|
def __init__(self): |
|
self.buffer = "" |
|
self.lock = threading.Lock() |
|
self._stdout = sys.stdout |
|
self._stderr = sys.stderr |
|
|
|
def write(self, msg): |
|
with self.lock: |
|
self.buffer += msg |
|
self._stdout.write(msg) |
|
|
|
def flush(self): |
|
pass |
|
|
|
def get_logs(self): |
|
with self.lock: |
|
return self.buffer |
|
|
|
def clear(self): |
|
with self.lock: |
|
self.buffer = "" |
|
|
|
def redirect(self): |
|
sys.stdout = self |
|
sys.stderr = self |
|
|
|
def restore(self): |
|
sys.stdout = self._stdout |
|
sys.stderr = self._stderr |
|
|
|
def isatty(self): |
|
return False |
|
|
|
def fileno(self): |
|
return self._stdout.fileno() |
|
|
|
logcatcher = LogCatcher() |
|
logcatcher.redirect() |
|
|
|
class ClimateRiskUI: |
|
"""User interface for the climate risk system with dropdown and map functionality.""" |
|
|
|
def __init__(self, model): |
|
self.orchestrator = ClimateRiskOrchestrator(model) |
|
self.theme = gr.themes.Soft( |
|
primary_hue="blue", secondary_hue="gray", neutral_hue="slate" |
|
) |
|
|
|
def update_business_visibility(self, profile_type): |
|
show_business = profile_type == "Business Owner" |
|
return gr.Dropdown(visible=show_business) |
|
|
|
def analyze_with_dropdown( |
|
self, |
|
country, |
|
city, |
|
state, |
|
profile_type, |
|
business_type, |
|
vulnerable_groups, |
|
): |
|
logcatcher.clear() |
|
|
|
if not country or not city: |
|
return ( |
|
"Please select both country and city.", |
|
"", |
|
"", |
|
) |
|
|
|
coords_result, validation_message = get_coordinates_from_dropdown(country, city, state) |
|
if coords_result is None: |
|
return validation_message, "", "" |
|
|
|
lat, lon = coords_result |
|
|
|
state_info = f", {state}" if state else "" |
|
location_full = f"{city}{state_info}, {country}" |
|
|
|
base_query = f"Perform a comprehensive climate risk assessment for {location_full}." |
|
|
|
profile_context = "" |
|
if profile_type.lower() == "business owner": |
|
business_detail = f" as a {business_type}" if business_type else "" |
|
profile_context = ( |
|
f" Focus on business continuity risks{business_detail}, including supply chain vulnerabilities, operational disruptions, infrastructure threats, customer safety, inventory protection, and revenue continuity. Consider industry-specific vulnerabilities and regulatory compliance requirements." |
|
) |
|
elif profile_type.lower() == "farmer/agriculture": |
|
profile_context = " Emphasize agricultural risks including crop threats, soil conditions, water availability, extreme weather impacts on farming operations, and seasonal climate patterns." |
|
elif profile_type.lower() == "emergency manager": |
|
profile_context = " Prioritize emergency management perspectives including evacuation planning, critical infrastructure vulnerabilities, community preparedness needs, and multi-hazard scenarios." |
|
else: |
|
profile_context = " Focus on residential safety, household preparedness, health impacts, and community-level risks." |
|
|
|
vulnerable_context = "" |
|
if vulnerable_groups: |
|
groups_text = ", ".join(vulnerable_groups) |
|
vulnerable_context = f" Pay special attention to impacts on vulnerable populations: {groups_text}." |
|
|
|
analysis_requirements = ( |
|
" Analyze earthquake, wildfire, flood, and extreme weather risks. Provide specific risk levels (0-100 scale), contributing factors, time horizons, and confidence levels. Include recent data and current conditions." |
|
) |
|
|
|
user_query = base_query + profile_context + vulnerable_context + analysis_requirements |
|
|
|
user_profile = { |
|
"type": profile_type.lower(), |
|
"business_type": business_type if profile_type.lower() == "business owner" else None, |
|
"vulnerable_groups": vulnerable_groups or [], |
|
} |
|
|
|
print(f"[{datetime.now()}] Analyse : {user_query}") |
|
result = self.orchestrator.analyze_and_recommend(user_query, user_profile) |
|
|
|
if "error" in result: |
|
print(f"[ERROR] {result['error']}") |
|
return f"Error: {result['error']}", "", "" |
|
|
|
risk_summary = self._format_risk_analysis(result["risk_analysis"]) |
|
recommendations_text = self._format_recommendations(result["recommendations"], profile_type) |
|
enhanced_map = create_risk_map(lat, lon, city, country, result["risk_analysis"]) |
|
|
|
return risk_summary, recommendations_text, enhanced_map |
|
|
|
def update_map_from_location(self, country, city, state=None): |
|
if not country or not city: |
|
return "Please select both country and city.", "" |
|
coords_result, validation_message = get_coordinates_from_dropdown(country, city, state) |
|
if coords_result is None: |
|
return validation_message, "" |
|
lat, lon = coords_result |
|
risk_map = create_risk_map(lat, lon, city, country) |
|
return validation_message, risk_map |
|
|
|
def update_cities(self, country): |
|
suggestions = get_city_suggestions(country) |
|
show_state = country == "United States" |
|
country_centers = { |
|
"France": (48.8566, 2.3522), |
|
"United States": (39.8283, -98.5795), |
|
"United Kingdom": (51.5074, -0.1278), |
|
"Germany": (52.5200, 13.4050), |
|
"Japan": (35.6762, 139.6503), |
|
"Canada": (45.4215, -75.7040), |
|
"Australia": (-35.2809, 149.1300), |
|
"Italy": (41.9028, 12.4964), |
|
"Spain": (40.4168, -3.7038), |
|
"China": (39.9042, 116.4074), |
|
"India": (28.6139, 77.2090), |
|
"Brazil": (-15.7975, -47.8919), |
|
} |
|
lat, lon = country_centers.get(country, (48.8566, 2.3522)) |
|
basic_map = create_risk_map(lat, lon, f"Select a city in {country}", country) |
|
return suggestions, gr.Dropdown(visible=show_state), basic_map |
|
|
|
def analyze_user_input( |
|
self, |
|
user_query: str, |
|
profile_type: str, |
|
business_type: str, |
|
vulnerable_groups: list = None, |
|
): |
|
logcatcher.clear() |
|
|
|
if not user_query.strip(): |
|
return ( |
|
"Please enter your climate risk question or location.", |
|
"", |
|
"<div style='text-align: center; padding: 50px; background-color: #f0f0f0; border-radius: 10px;'>Map will appear here after analysis.</div>", |
|
) |
|
|
|
user_profile = { |
|
"type": profile_type.lower(), |
|
"business_type": business_type if profile_type.lower() == "business owner" else None, |
|
"vulnerable_groups": vulnerable_groups or [], |
|
} |
|
|
|
print(f"[{datetime.now()}] Analyse: {user_query}") |
|
result = self.orchestrator.analyze_and_recommend(user_query, user_profile) |
|
|
|
if "error" in result: |
|
print(f"[ERROR] {result['error']}") |
|
return f"Error: {result['error']}", "", "" |
|
|
|
risk_summary = self._format_risk_analysis(result["risk_analysis"]) |
|
recommendations_text = self._format_recommendations(result["recommendations"], profile_type) |
|
|
|
location = result["risk_analysis"].get("location", {}) |
|
lat = location.get("lat", 0) |
|
lon = location.get("lon", 0) |
|
city = location.get("city", "Unknown") |
|
country = location.get("country", "Unknown") |
|
|
|
enhanced_map = create_risk_map(lat, lon, city, country, result["risk_analysis"]) |
|
|
|
return risk_summary, recommendations_text, enhanced_map |
|
|
|
def _format_risk_analysis(self, risk_analysis: dict) -> str: |
|
if not risk_analysis or "error" in risk_analysis: |
|
return "Risk analysis not available or failed." |
|
|
|
formatted = f"# 🌍 Climate Risk Analysis\n\n" |
|
|
|
location = risk_analysis.get("location", {}) |
|
if location: |
|
formatted += f"**Location:** {location.get('city', 'Unknown')}, {location.get('country', '')}\n" |
|
formatted += f"**Coordinates:** {location.get('lat', 0):.4f}°N, {location.get('lon', 0):.4f}°E\n\n" |
|
|
|
formatted += f"**Analysis Date:** {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n" |
|
|
|
overall = risk_analysis.get("overall_assessment", "No overall assessment available.") |
|
formatted += f"## 📊 Overall Assessment\n{overall}\n\n" |
|
|
|
risks = risk_analysis.get("risk_analysis", {}) |
|
if risks: |
|
formatted += "## 🎯 Individual Risk Assessment\n\n" |
|
for risk_name, risk_data in risks.items(): |
|
if isinstance(risk_data, dict): |
|
risk_level = risk_data.get("risk_level", 0) |
|
if risk_level > 80: |
|
emoji = "🔴" |
|
level_text = "VERY HIGH" |
|
elif risk_level > 60: |
|
emoji = "🟠" |
|
level_text = "HIGH" |
|
elif risk_level > 40: |
|
emoji = "🟡" |
|
level_text = "MODERATE" |
|
elif risk_level > 20: |
|
emoji = "🟢" |
|
level_text = "LOW" |
|
else: |
|
emoji = "⚪" |
|
level_text = "MINIMAL" |
|
formatted += f"### {emoji} {risk_name.title()} Risk\n" |
|
formatted += f"**Risk Level:** {level_text} ({risk_level}/100)\n" |
|
formatted += f"**Time Horizon:** {risk_data.get('time_horizon', 'Unknown')}\n" |
|
formatted += f"**Confidence:** {risk_data.get('confidence', 'Unknown')}\n\n" |
|
if risk_data.get("key_insights"): |
|
formatted += f"**Analysis:** {risk_data['key_insights']}\n\n" |
|
factors = risk_data.get("contributing_factors", []) |
|
if factors: |
|
formatted += f"**Key Factors:** {', '.join(factors)}\n\n" |
|
return formatted |
|
|
|
def _format_recommendations(self, recommendations: dict, profile_type: str) -> str: |
|
if not recommendations: |
|
return "No recommendations available." |
|
formatted = f"# 🎯 Personalized Recommendations for {profile_type} **[survivalist mode]**\n\n" |
|
if "emergency" in recommendations: |
|
formatted += "## 🚨 Emergency Preparedness\n" |
|
for rec in recommendations["emergency"]: |
|
formatted += f"- {rec}\n" |
|
formatted += "\n" |
|
if "household" in recommendations: |
|
formatted += "## 🏠 Household Adaptations\n" |
|
for rec in recommendations["household"]: |
|
formatted += f"- {rec}\n" |
|
formatted += "\n" |
|
if "business" in recommendations: |
|
formatted += "## 🏢 Business Continuity\n" |
|
for rec in recommendations["business"]: |
|
formatted += f"- {rec}\n" |
|
formatted += "\n" |
|
if "financial" in recommendations: |
|
formatted += "## 💰 Financial Planning\n" |
|
for rec in recommendations["financial"]: |
|
formatted += f"- {rec}\n" |
|
formatted += "\n" |
|
formatted += "---\n" |
|
formatted += "*Recommendations generated by AI agents based on current risk analysis and your profile.*" |
|
return formatted |
|
|
|
def create_interface(self): |
|
def get_logs(): |
|
return logcatcher.get_logs() |
|
|
|
with gr.Blocks( |
|
theme=self.theme, title="🛰️ SentinelO – Climate Risk Evaluation MultiAgents" |
|
) as app: |
|
|
|
gr.Markdown( |
|
""" |
|
# 🛰️ SentinelO – Climate Risk Evaluation MultiAgents |
|
|
|
<div style='background: linear-gradient(90deg, #f6f8fa 0%, #e2eafc 100%); border-radius: 10px; padding: 16px 18px; font-size: 16px; margin-bottom: 10px;'> |
|
<b>🤖 What does SentinelO do?</b> |
|
<br><br> |
|
SentinelO's AI agents instantly analyze climate risks <b>( |
|
🌪️ Weather, |
|
🌊 Flood, |
|
🌍 Earthquake, |
|
🔥 Wildfire, |
|
🌫️ Air quality, |
|
📈 Climate trends, |
|
☀️ Solar radiation, |
|
🌊 Marine forecast |
|
)</b> for any location, providing you with clear, actionable recommendations. |
|
<br><br> |
|
<i>Analysis is fully automated, always up to date, and based on leading data sources: OpenStreetMap 🗺️, Open-Meteo 🌦️, USGS 🌎, NASA FIRMS 🔥.</i> |
|
<br><br> |
|
<b>How to use SentinelO?</b><br> |
|
Use the <b>quick location selection</b> (dropdowns and map) 🌍, or ask complex, personalized questions in <b>natural language</b> 💬. |
|
</div> |
|
""" |
|
) |
|
|
|
with gr.Tabs(): |
|
with gr.TabItem("📍 Quick Location Selection"): |
|
with gr.Row(): |
|
with gr.Column(): |
|
country_dropdown = gr.Dropdown( |
|
choices=list(COUNTRIES_AND_CITIES.keys()), |
|
label="Select Country", |
|
value="France", |
|
interactive=True, |
|
) |
|
city_input = gr.Textbox( |
|
label="Enter City Name", |
|
placeholder="e.g., Bordeaux, Lyon, Marseille, ...", |
|
value="Lorient", |
|
interactive=True, |
|
info="Enter any city name in the selected country", |
|
) |
|
state_dropdown = gr.Dropdown( |
|
choices=US_STATES, |
|
label="Select State (US only)", |
|
value="California", |
|
visible=False, |
|
interactive=True, |
|
info="Select state for US locations", |
|
) |
|
city_suggestions = gr.Markdown( |
|
get_city_suggestions("France"), visible=True |
|
) |
|
|
|
with gr.Column(): |
|
profile_dropdown = gr.Dropdown( |
|
choices=[ |
|
"General Public", |
|
"Business Owner", |
|
"Farmer/Agriculture", |
|
"Emergency Manager", |
|
], |
|
label="Your Profile", |
|
value="General Public", |
|
) |
|
vulnerable_groups = gr.CheckboxGroup( |
|
choices=[ |
|
"Elderly", |
|
"Children", |
|
"Chronic Health Conditions", |
|
"Pregnant", |
|
], |
|
label="Vulnerable Groups in Household", |
|
) |
|
business_type_dropdown = gr.Dropdown( |
|
choices=[ |
|
"Restaurant/Food Service", |
|
"Retail Store", |
|
"Manufacturing", |
|
"Construction", |
|
"Healthcare Facility", |
|
"Educational Institution", |
|
"Technology/Software", |
|
"Transportation/Logistics", |
|
"Tourism/Hospitality", |
|
"Financial Services", |
|
"Real Estate", |
|
"Agriculture/Farming", |
|
"Energy/Utilities", |
|
"Entertainment/Events", |
|
"Professional Services", |
|
"Small Office", |
|
"Warehouse/Distribution", |
|
"Other", |
|
], |
|
label="Business Type", |
|
value="Retail Store", |
|
visible=False, |
|
interactive=True, |
|
info="Select your business type for specialized recommendations", |
|
) |
|
|
|
with gr.Row(): |
|
analyze_location_btn = gr.Button( |
|
"🔍 Analyze This Location", variant="primary", size="lg" |
|
) |
|
|
|
with gr.Row(): |
|
gr.HTML(""" |
|
<div style="display: flex; align-items: center; gap: 10px;"> |
|
<h3 style="margin: 0;">🛰️ Agentic Logs</h3> |
|
</div> |
|
""") |
|
|
|
with gr.Row(): |
|
logs_box = gr.Textbox( |
|
value=logcatcher.get_logs(), |
|
label="Logs", |
|
lines=17, |
|
max_lines=25, |
|
interactive=False, |
|
elem_id="terminal_logs", |
|
show_copy_button=True, |
|
container=False, |
|
) |
|
logs_timer = gr.Timer(0.5) |
|
logs_timer.tick(get_logs, None, logs_box) |
|
|
|
with gr.Row(): |
|
location_map = gr.HTML( |
|
create_risk_map(47.7486, -3.3667, "Lorient", "France"), |
|
label="Interactive Risk Map", |
|
) |
|
|
|
with gr.Row(): |
|
location_status = gr.Markdown("", visible=True) |
|
|
|
|
|
with gr.Row(): |
|
dropdown_risk_summary = gr.Markdown( |
|
"Select a location above to begin analysis.", |
|
label="Risk Assessment Summary", |
|
elem_id="risk_summary_box", |
|
) |
|
|
|
|
|
with gr.Row(): |
|
dropdown_recommendations = gr.Markdown( |
|
"Recommendations will appear here after analysis.", |
|
label="AI-Generated Recommendations", |
|
elem_id="recommendations_box", |
|
) |
|
|
|
with gr.TabItem("💬 Natural Language Query"): |
|
with gr.Row(): |
|
with gr.Column(scale=2): |
|
user_query = gr.Textbox( |
|
label="Your Climate Risk Question", |
|
placeholder="Will New York get flooded tomorrow if we don't win the Hackaton ?", |
|
lines=3, |
|
info="Be as specific as possible about location, timeframe, and what you're concerned about.", |
|
) |
|
gr.Markdown( |
|
""" |
|
**Examples:** |
|
- "What are the wildfire risks in Los Angeles this week?" |
|
- "I live in Lorient (Bretagne), can I run outside this evening ?" |
|
- "I'm planning to move to Miami, what climate risks should I be aware of?" |
|
- "How should my farm in Iowa prepare for climate change?" |
|
- "What emergency preparations should my business in Tokyo make for earthquakes?" |
|
""" |
|
) |
|
|
|
with gr.Column(scale=1): |
|
nl_profile_type = gr.Dropdown( |
|
choices=[ |
|
"General Public", |
|
"Business Owner", |
|
"Farmer/Agriculture", |
|
"Emergency Manager", |
|
], |
|
label="Your Profile", |
|
value="General Public", |
|
) |
|
|
|
nl_business_type_dropdown = gr.Dropdown( |
|
choices=[ |
|
"Restaurant/Food Service", |
|
"Retail Store", |
|
"Manufacturing", |
|
"Construction", |
|
"Healthcare Facility", |
|
"Educational Institution", |
|
"Technology/Software", |
|
"Transportation/Logistics", |
|
"Tourism/Hospitality", |
|
"Financial Services", |
|
"Real Estate", |
|
"Agriculture/Farming", |
|
"Energy/Utilities", |
|
"Entertainment/Events", |
|
"Professional Services", |
|
"Small Office", |
|
"Warehouse/Distribution", |
|
"Other", |
|
], |
|
label="Business Type", |
|
value="Retail Store", |
|
visible=False, |
|
interactive=True, |
|
info="Select your business type for specialized recommendations", |
|
) |
|
|
|
nl_vulnerable_groups = gr.CheckboxGroup( |
|
choices=[ |
|
"Elderly", |
|
"Children", |
|
"Chronic Health Conditions", |
|
"Pregnant", |
|
], |
|
label="Vulnerable Groups in Household", |
|
) |
|
|
|
analyze_btn = gr.Button( |
|
"🔍 Analyze Query & Get Recommendations", |
|
variant="primary", |
|
size="lg", |
|
) |
|
|
|
with gr.Row(): |
|
gr.HTML(""" |
|
<div style="display: flex; align-items: center; gap: 10px;"> |
|
<h3 style="margin: 0;">🛰️ Agentic Logs</h3> |
|
</div> |
|
""") |
|
|
|
with gr.Row(): |
|
nl_logs_box = gr.Textbox( |
|
value=logcatcher.get_logs(), |
|
label="Logs", |
|
lines=17, |
|
max_lines=25, |
|
interactive=False, |
|
elem_id="nl_terminal_logs", |
|
show_copy_button=True, |
|
container=False, |
|
) |
|
nl_logs_timer = gr.Timer(0.5) |
|
nl_logs_timer.tick(get_logs, None, nl_logs_box) |
|
|
|
with gr.Row(): |
|
nl_location_map = gr.HTML( |
|
"<div style='text-align: center; padding: 50px; background-color: #f0f0f0; border-radius: 10px;'>Map will appear here after analysis.</div>", |
|
label="Interactive Risk Map", |
|
) |
|
|
|
|
|
with gr.Row(): |
|
risk_analysis_output = gr.Markdown( |
|
"Enter your question above to get started.", |
|
label="Risk Analysis", |
|
elem_id="nl_risk_box", |
|
) |
|
|
|
|
|
with gr.Row(): |
|
recommendations_output = gr.Markdown( |
|
"Personalized recommendations will appear here.", |
|
label="AI-Generated Recommendations", |
|
elem_id="nl_rec_box", |
|
) |
|
|
|
|
|
gr.HTML(""" |
|
<style> |
|
#risk_summary_box, #recommendations_box, #nl_risk_box, #nl_rec_box { |
|
border: 2px solid #007aff; |
|
border-radius: 13px; |
|
background: #fafdff; |
|
box-shadow: 0 2px 12px rgba(80,140,255,0.08); |
|
padding: 20px 15px; |
|
margin-top: 10px; |
|
margin-bottom: 18px; |
|
} |
|
#terminal_logs textarea, #nl_terminal_logs textarea { |
|
background-color: #181a1b !important; |
|
color: #00ff66 !important; |
|
font-family: 'Fira Mono', 'Consolas', monospace !important; |
|
font-size: 15px; |
|
border-radius: 9px !important; |
|
border: 2px solid #31343a !important; |
|
box-shadow: 0 2px 6px rgba(0,0,0,0.19); |
|
padding: 12px 10px !important; |
|
min-height: 320px !important; |
|
max-height: 420px !important; |
|
letter-spacing: 0.5px; |
|
line-height: 1.5; |
|
overflow-y: auto !important; |
|
resize: vertical !important; |
|
scrollbar-width: thin; |
|
scrollbar-color: #6cf97c #282c34; |
|
} |
|
#terminal_logs, #nl_terminal_logs { |
|
width: 100% !important; |
|
} |
|
</style> |
|
""") |
|
|
|
profile_dropdown.change( |
|
fn=self.update_business_visibility, |
|
inputs=[profile_dropdown], |
|
outputs=[business_type_dropdown], |
|
) |
|
nl_profile_type.change( |
|
fn=self.update_business_visibility, |
|
inputs=[nl_profile_type], |
|
outputs=[nl_business_type_dropdown], |
|
) |
|
country_dropdown.change( |
|
fn=self.update_cities, |
|
inputs=[country_dropdown], |
|
outputs=[city_suggestions, state_dropdown, location_map], |
|
) |
|
city_input.change( |
|
fn=self.update_map_from_location, |
|
inputs=[country_dropdown, city_input, state_dropdown], |
|
outputs=[location_status, location_map], |
|
) |
|
analyze_location_btn.click( |
|
fn=self.analyze_with_dropdown, |
|
inputs=[ |
|
country_dropdown, |
|
city_input, |
|
state_dropdown, |
|
profile_dropdown, |
|
business_type_dropdown, |
|
vulnerable_groups, |
|
], |
|
outputs=[dropdown_risk_summary, dropdown_recommendations, location_map], |
|
show_progress="full", |
|
) |
|
analyze_btn.click( |
|
fn=self.analyze_user_input, |
|
inputs=[ |
|
user_query, |
|
nl_profile_type, |
|
nl_business_type_dropdown, |
|
nl_vulnerable_groups, |
|
], |
|
outputs=[ |
|
risk_analysis_output, |
|
recommendations_output, |
|
nl_location_map, |
|
], |
|
show_progress="full", |
|
) |
|
|
|
return app |