| """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"<tr><td><b>{key}</b></td><td>{value:.4f}</td></tr>") |
| else: |
| rows.append(f"<tr><td><b>{key}</b></td><td>{value}</td></tr>") |
| return f"<table style='width:100%; border-collapse: collapse;'>{''.join(rows)}</table>" |
|
|
|
|
| def format_interpretation(headline: str, bullets: list, caveats: list) -> str: |
| """Format interpretation as HTML.""" |
| html = f"<h3>{headline}</h3>" |
|
|
| if bullets: |
| html += "<ul>" |
| for bullet in bullets: |
| html += f"<li>{bullet}</li>" |
| html += "</ul>" |
|
|
| if caveats: |
| html += "<p><b>Caveats:</b></p><ul>" |
| for caveat in caveats: |
| html += f"<li><i>{caveat}</i></li>" |
| html += "</ul>" |
|
|
| 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: |
| |
| 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] |
|
|
| |
| tool_name, error_msg = route_query(sample, user_query) |
|
|
| if error_msg: |
| print('error msg') |
| return None, None, None, "", f"❌ {error_msg}" |
|
|
| |
| 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}" |
|
|
|
|
| |
| 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) |
|
|
| |
| interpretation = interpret_results( |
| tool_name=tool_name, |
| sample_title=sample.title, |
| metrics=result_dict["metrics"], |
| extras=result_dict.get("extras", {}) |
| ) |
|
|
| |
| 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__": |
| |
| Path("outputs").mkdir(exist_ok=True) |
|
|
| demo = create_ui() |
| demo.launch(share=True) |
| |
|
|