"""Gradio UI for Agent4EO.""" import sys, os sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import shutil import json import gradio as gr from pathlib import Path from typing import Dict, Any, Tuple, Optional from agent.schema import SampleSpec from agent.router import route_query from agent.interpreter import interpret_results from agent.tools import run_ndvi, run_lst_landsat, run_dnbr_s2pair, load_sample_registry from huggingface_hub import snapshot_download snapshot_download( repo_id="Th-Olive/Agent4EO-samples", repo_type="dataset", local_dir="./data" ) def format_metrics_table(metrics: Dict[str, float]) -> str: """Format metrics as HTML table.""" rows = [] for key, value in metrics.items(): if isinstance(value, float): rows.append(f"{key}{value:.4f}") else: rows.append(f"{key}{value}") return f"{''.join(rows)}
" def format_interpretation(headline: str, bullets: list, caveats: list) -> str: """Format interpretation as HTML.""" html = f"

{headline}

" if bullets: html += "" if caveats: html += "

Caveats:

" return html def run_analysis( sample_title: str, user_query: Optional[str] = "" ) -> Tuple[Optional[str], Optional[str], Optional[str], str, str]: """ Execute EO analysis pipeline. Args: sample_title: Selected sample title user_query: User's natural language query Returns: Tuple of (preview_path, histogram_path, metrics_html, interpretation_html, error_msg) """ try: # Load sample samples = load_sample_registry() if sample_title not in samples: return None, None, None, "", f"❌ Sample '{sample_title}' not found in registry." sample = samples[sample_title] # Route query tool_name, error_msg = route_query(sample, user_query) if error_msg: print('error msg') return None, None, None, "", f"❌ {error_msg}" # Execute tool tool_map = { "run_ndvi": run_ndvi, "run_lst_landsat": run_lst_landsat, "run_dnbr_s2pair": run_dnbr_s2pair } tool_func = tool_map.get(tool_name) if not tool_func: return None, None, None, "", f"❌ Unknown tool: {tool_name}" # Run tool result_dict = tool_func.invoke(sample_title) if result_dict.get("histogram_png") is None: hist_out = gr.update(value=None, visible=False) else: hist_out = gr.update(value=result_dict.get("histogram_png"), visible=True) # Generate interpretation interpretation = interpret_results( tool_name=tool_name, sample_title=sample.title, metrics=result_dict["metrics"], extras=result_dict.get("extras", {}) ) # Format outputs metrics_html = format_metrics_table(result_dict["metrics"]) interpretation_html = format_interpretation( interpretation.headline, interpretation.bullets, interpretation.caveats ) return ( result_dict["preview_png"], hist_out, metrics_html, interpretation_html, "✅ Analysis completed successfully!" ) except Exception as e: return None, None, None, "", f"❌ Error: {str(e)}" about_md = """ --- ## Challenge - **Decision-makers** don’t need raw imagery; they need clear, **operational signals**, still many Earth Observation (EO) use-cases fail to cross the gap from geospatial experts to end-users, **from research to deployment**. - Staying current with a **fast-moving field** while making **geospatial reasoning** explicit : As a **curiosity-driven AI Research Engineer**, I had to **investigate agentic workflows** and the pipeline structures behind groundbreaking initiatives capturing attention: [Google Earth AI](https://ai.google/earth-ai/), [Axion Planetary MCP](https://github.com/Dhenenjay/axion-planetary-mcp), [Ageospatial](https://ageospatial.com/), [GeoRetina](https://www.georetina.com/) ## Solution - Curated samples are fed to a **routing agent** that selects the appropriate analysis, like a geospatial expert would. - Implemented tools: **Vegetation condition analysis** (NDVI), **Urban Heat Island monitoring** (LST), and **burn scars charcterization** (dNBR). - **OpenStreetMap context** (API call) is attached to computed metrics to anchor results to places. - A second **LLM** pass turns numbers + context into a concise operational narrative. - The interface displays a **quicklook**, **histogram**, quantitative **metrics**, and a short, number-grounded **explanation**. - The agent is powered by **Ministral-3B** for efficiency; orchestration uses **LangChain**. EO I/O uses **Rasterio** with proper CRS/transform and not-relevant data handling. ## Results - A **live, accessible project** : I believe in public demos over private perfection. - **Clear visual and textual outputs** understandable by non-experts, revealing Earth Observation's added value. - A concrete **exploration of agentic orchestration** for EO that other practitioners can reuse as a reference pattern. ## Possible extensions - Move to true **prompt-driven analysis**: users specify topic, period, and region; the agent fetches data on demand (from a way larger range of sources) and selects tools accordingly. - **Richer narratives** tailored to roles (territorial planning, insurers...) with domain-aware capacity. - Expand the **toolset**: multi-temporal change maps, flood risk, landslide tracking, building characterization, oil-spill detection, SAR flood mapping, and embeddings-based heads with integration of **Foundation Models** - Enable **on-the-fly tool drafting** (code synthesis) when safe and auditable. - Be **deliberate with LLMs**: for such a small range of tools, selection via LLM isn’t necessary, and with such low context models can hallucinate. Agents should be used where they add real leverage, beyond conventional programming capacities. """ def create_ui(): """Create Gradio interface.""" samples = load_sample_registry() sample_choices = [s.title for s in samples.values()] with gr.Blocks(title="Agent4EO") as demo: gr.Markdown("# 🛰️ Agent4EO - A demo by Thomas OLIVE") with gr.Accordion("Agent4EO is a demo that investigates and demystifies geospatial reasoning by using a lightweight agent to turn curated satellite samples into clear, operational insights with concise narratives.", open=True): gr.Markdown(about_md) with gr.Row(): with gr.Column(scale=1): sample_dropdown = gr.Dropdown( choices=sample_choices, label="Select Sample", value=sample_choices[0][1] if sample_choices else None ) run_button = gr.Button("Run Analysis", variant="primary") with gr.Column(scale=2): status_msg = gr.Markdown("") preview_img = gr.Image(label="Preview", type="filepath") histogram_img = gr.Image(label="Histogram", type="filepath", visible=False) metrics_html = gr.HTML(label="Metrics") interpretation_html = gr.HTML(label="Interpretation") run_button.click( fn=run_analysis, inputs=[sample_dropdown], outputs=[preview_img, histogram_img, metrics_html, interpretation_html, status_msg] ) for foldername in os.listdir("outputs"): shutil.rmtree(os.path.join("outputs", foldername)) return demo if __name__ == "__main__": # Ensure output directory exists Path("outputs").mkdir(exist_ok=True) demo = create_ui() demo.launch(share=True) # demo.launch(server_name="127.0.0.1", server_port=7860)