nharshavardhana commited on
Commit
6ab97fb
Β·
1 Parent(s): 2077da5
Files changed (2) hide show
  1. app.py +330 -0
  2. requirements.txt +23 -0
app.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ee
2
+ import os
3
+ import json
4
+ from google.oauth2 import service_account
5
+
6
+ from typing import Dict
7
+ from datetime import datetime, timedelta
8
+ from geopy.distance import geodesic
9
+ import requests
10
+ import pandas as pd
11
+ import folium
12
+
13
+ import requests
14
+ from langchain_core.tools import tool
15
+ from langgraph.prebuilt import create_react_agent
16
+ from typing_extensions import TypedDict, Literal, Annotated
17
+
18
+ from typing import Optional
19
+
20
+ from collections import Counter
21
+ from langgraph.graph import StateGraph, END
22
+ from typing import TypedDict, Annotated
23
+ import operator
24
+ from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage, AIMessage
25
+ from langchain_community.tools.tavily_search import TavilySearchResults
26
+ from langchain.chat_models import init_chat_model
27
+ from langchain.schema import HumanMessage
28
+ from langchain.tools import tool
29
+ import gradio as gr
30
+
31
+
32
+ # Load JSON string from Hugging Face secret (as env variable)
33
+ SERVICE_KEY_JSON = os.environ.get("GEE_SERVICE_KEY")
34
+ MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
35
+ FIRMS_API_KEY = os.getenv("FIRMS_API_KEY")
36
+ OPENCAGE_API_KEY = os.getenv("OPENCAGE_API_KEY")
37
+
38
+
39
+ if SERVICE_KEY_JSON is None:
40
+ raise RuntimeError("Missing Earth Engine service account key in environment.")
41
+
42
+ # Parse the JSON and create credentials
43
+ SERVICE_KEY_DICT = json.loads(SERVICE_KEY_JSON)
44
+ credentials = service_account.Credentials.from_service_account_info(
45
+ SERVICE_KEY_DICT,
46
+ scopes=['https://www.googleapis.com/auth/cloud-platform']
47
+ )
48
+
49
+ # Initialize EE
50
+ ee.Initialize(credentials=credentials)
51
+
52
+
53
+ @tool
54
+ def get_fire_risk_map(place: str, opencage_key: str, firms_key: str, min_brightness: int = 300, min_confidence: int = 60) -> Optional[str]:
55
+ """
56
+ Returns an HTML string of a folium map showing fire locations, nearest water bodies, and fire stations.
57
+ Args:
58
+ place: Name of the place to fetch the bounding box for.
59
+ opencage_key: opencage API key.
60
+ firms_key: FIRMS API key.
61
+ min_brightness (int, optional): Minimum fire brightness to filter on (e.g., 300). Defaults to 300.
62
+ min_confidence (int, optional): Minimum confidence level (0-100) to filter fire data. Defaults to 60.
63
+ Returns:
64
+ Returns an HTML string of a folium map showing fire locations, nearest water bodies, and fire stations.
65
+ """
66
+ try:
67
+ opencage_key = os.environ["OPENCAGE_API_KEY"]
68
+ firms_key = os.environ["FIRMS_API_KEY"]
69
+
70
+ # Step 1: Get bounding box from OpenCage
71
+ url = f"https://api.opencagedata.com/geocode/v1/json?q={place}&key={opencage_key}"
72
+ resp = requests.get(url)
73
+ resp.raise_for_status()
74
+ data = resp.json()
75
+ bounds = data['results'][0]['bounds']
76
+ bbox = {
77
+ 'north': bounds['northeast']['lat'],
78
+ 'south': bounds['southwest']['lat'],
79
+ 'east': bounds['northeast']['lng'],
80
+ 'west': bounds['southwest']['lng']
81
+ }
82
+ bbox_str = f"{bbox['west']},{bbox['south']},{bbox['east']},{bbox['north']}"
83
+
84
+ # Step 2: Get FIRMS fire data
85
+ source = 'MODIS_NRT'
86
+ url = f'https://firms.modaps.eosdis.nasa.gov/api/area/csv/{firms_key}/{source}/{bbox_str}/3'
87
+ df = pd.read_csv(url)
88
+ now = datetime.utcnow()
89
+ df['acq_datetime'] = pd.to_datetime(
90
+ df['acq_date'] + ' ' + df['acq_time'].astype(str).str.zfill(4),
91
+ format="%Y-%m-%d %H%M"
92
+ )
93
+ df_fires_filtered = df[
94
+ (df['brightness'] >= min_brightness) &
95
+ (df['confidence'] > min_confidence) &
96
+ (df['acq_datetime'] > (now - timedelta(hours=24)))
97
+ ].dropna(subset=['latitude', 'longitude']).reset_index(drop=True)
98
+
99
+ if df_fires_filtered.empty:
100
+ return "<b>No significant fire activity detected in the past 24 hours for the specified location.</b>"
101
+
102
+
103
+ # Step 3: Water bodies (Earth Engine)
104
+ west, south, east, north = map(float, bbox_str.split(','))
105
+ geom = ee.Geometry.BBox(west, south, east, north)
106
+ water = ee.Image('JRC/GSW1_4/GlobalSurfaceWater').select('occurrence')
107
+ water_mask = water.gt(50).selfMask()
108
+ water_clipped = water_mask.clip(geom)
109
+ sampled_points = water_clipped.stratifiedSample(
110
+ numPoints=500,
111
+ classBand='occurrence',
112
+ region=geom,
113
+ scale=500,
114
+ geometries=True,
115
+ seed=42
116
+ ).getInfo()
117
+ water_points = [
118
+ {
119
+ 'latitude': f['geometry']['coordinates'][1],
120
+ 'longitude': f['geometry']['coordinates'][0]
121
+ }
122
+ for f in sampled_points['features']
123
+ ]
124
+ df_water = pd.DataFrame(water_points).dropna().reset_index(drop=True)
125
+
126
+ # Step 4: Compute nearest water body
127
+ def get_nearest_water(fire_lat, fire_lon):
128
+ fire_point = (fire_lat, fire_lon)
129
+ min_dist = float('inf')
130
+ nearest = None
131
+ for _, row in df_water.iterrows():
132
+ dist = geodesic(fire_point, (row['latitude'], row['longitude'])).km
133
+ if dist < min_dist:
134
+ min_dist = dist
135
+ nearest = row
136
+ return pd.Series({
137
+ 'nearest_water_lat': nearest['latitude'],
138
+ 'nearest_water_lon': nearest['longitude'],
139
+ 'distance_km': min_dist
140
+ })
141
+ nearest_water = df_fires_filtered.apply(
142
+ lambda r: get_nearest_water(r['latitude'], r['longitude']), axis=1)
143
+
144
+ # Step 5: Fire stations using Overpass API
145
+ overpass_query = f"""
146
+ [out:json][timeout:25];
147
+ (
148
+ node["amenity"="fire_station"]({south},{west},{north},{east});
149
+ );
150
+ out body;
151
+ """
152
+ res = requests.get("http://overpass-api.de/api/interpreter", params={"data": overpass_query})
153
+ data = res.json()
154
+ stations = [
155
+ {
156
+ 'name': e.get('tags', {}).get('name', 'Unnamed Station'),
157
+ 'lat': e['lat'],
158
+ 'lon': e['lon']
159
+ }
160
+ for e in data.get('elements', []) if 'lat' in e and 'lon' in e
161
+ ]
162
+ df_stations = pd.DataFrame(stations).dropna()
163
+
164
+ def get_nearest_station(fire_lat, fire_lon):
165
+ fire_point = (fire_lat, fire_lon)
166
+ min_dist = float('inf')
167
+ nearest = None
168
+ for _, row in df_stations.iterrows():
169
+ dist = geodesic(fire_point, (row['lat'], row['lon'])).km
170
+ if dist < min_dist:
171
+ min_dist = dist
172
+ nearest = row
173
+ return pd.Series({
174
+ 'nearest_station_name': nearest['name'],
175
+ 'nearest_station_lat': nearest['lat'],
176
+ 'nearest_station_lon': nearest['lon'],
177
+ 'station_distance_km': min_dist
178
+ })
179
+ nearest_station = df_fires_filtered.apply(
180
+ lambda r: get_nearest_station(r['latitude'], r['longitude']), axis=1)
181
+
182
+ # Final dataframe
183
+ df_final = pd.concat([df_fires_filtered, nearest_water, nearest_station], axis=1)
184
+
185
+ # Step 6: Create a folium map
186
+ center_lat = (bbox["north"] + bbox["south"]) / 2
187
+ center_lon = (bbox["east"] + bbox["west"]) / 2
188
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=6)
189
+
190
+ for _, row in df_final.iterrows():
191
+ fire_loc = [row['latitude'], row['longitude']]
192
+ water_loc = [row['nearest_water_lat'], row['nearest_water_lon']]
193
+ station_loc = [row['nearest_station_lat'], row['nearest_station_lon']]
194
+
195
+ # πŸ”₯ 1. Uncertainty Zone (large faint circle) FIRST
196
+ folium.Circle(
197
+ location=fire_loc,
198
+ radius=500, # 3 km
199
+ color='orange',
200
+ fill=True,
201
+ fill_opacity=0.2,
202
+ popup="⚠️ Possible spread zone (~3 km radius)",
203
+ ).add_to(m)
204
+
205
+ # πŸ”΄ 2. Fire point (small red circle) SECOND
206
+ folium.CircleMarker(
207
+ location=fire_loc,
208
+ radius=6,
209
+ color='red',
210
+ fill=True,
211
+ fill_color='red',
212
+ popup=f"πŸ”₯ Brightness: {row['brightness']}, Confidence: {row['confidence']}, DateTime: {row['acq_datetime']} Latitude: {row['latitude']:.3f}, Longitude: {row['longitude']:.3f}",
213
+ ).add_to(m)
214
+
215
+ # πŸ’§ 3. Nearest water point
216
+ folium.Marker(
217
+ location=water_loc,
218
+ icon=folium.Icon(color='blue', icon='tint', prefix='fa'),
219
+ popup=f"πŸ’§ Nearest Water\nDistance: {row['distance_km']:.2f} km\nLat: {row['nearest_water_lat']:.3f}\nLon: {row['nearest_water_lon']:.3f}",
220
+ ).add_to(m)
221
+
222
+ # 🟒 4. Line between fire and water
223
+ folium.PolyLine(
224
+ locations=[fire_loc, water_loc],
225
+ color='green',
226
+ weight=2,
227
+ ).add_to(m)
228
+
229
+ # πŸš’ 5. Nearest fire station (NEW)
230
+ folium.Marker(
231
+ location=station_loc,
232
+ icon=folium.Icon(color='darkred', icon='fire-extinguisher', prefix='fa'),
233
+ popup=f"πŸš’ Nearest Fire Station\nDistance: {row['station_distance_km']:.2f} km\nLat: {row['nearest_station_lat']:.3f}\nLon: {row['nearest_station_lon']:.3f}",
234
+ ).add_to(m)
235
+
236
+ # 🧯 6. Line between fire and station (NEW)
237
+ folium.PolyLine(
238
+ locations=[fire_loc, station_loc],
239
+ color='purple',
240
+ weight=2,
241
+ dash_array='5,10' # dashed line
242
+ ).add_to(m)
243
+
244
+ return m._repr_html_()
245
+
246
+
247
+
248
+ except Exception as e:
249
+ return f"<b>Error generating fire risk map:</b> {str(e)}"
250
+
251
+
252
+
253
+ class AgentState(TypedDict):
254
+ messages: Annotated[list[AnyMessage], operator.add]
255
+
256
+ class Agent:
257
+
258
+ def __init__(self, model, tools, system=""):
259
+ self.system = system
260
+ graph = StateGraph(AgentState)
261
+ graph.add_node("llm", self.call_mistral_ai)
262
+ graph.add_node("action", self.take_action)
263
+ graph.add_node("final", self.final_answer)
264
+ graph.add_conditional_edges(
265
+ "llm",
266
+ self.exists_action,
267
+ {True: "action", False: END}
268
+ )
269
+ graph.add_edge("action", "final") # πŸ†•
270
+ graph.add_edge("final", END) # πŸ†•
271
+ graph.set_entry_point("llm")
272
+ self.graph = graph.compile()
273
+ self.tools = {t.name: t for t in tools}
274
+ self.model = model.bind_tools(tools)
275
+
276
+ def exists_action(self, state: AgentState):
277
+ result = state['messages'][-1]
278
+ return len(result.tool_calls) > 0
279
+
280
+ def call_mistral_ai(self, state: AgentState):
281
+ messages = state['messages']
282
+ if self.system:
283
+ messages = [SystemMessage(content=self.system)] + messages
284
+ message = self.model.invoke(messages)
285
+ return {'messages': [message]}
286
+
287
+ def take_action(self, state: AgentState):
288
+ tool_calls = state['messages'][-1].tool_calls
289
+ results = []
290
+ for t in tool_calls:
291
+ print(f"Calling: {t}")
292
+ if not t['name'] in self.tools: # check for bad tool name from LLM
293
+ print("\n ....bad tool name....")
294
+ result = "bad tool name, retry" # instruct LLM to retry if bad
295
+ else:
296
+ result = self.tools[t['name']].invoke(t['args'])
297
+ results.append(ToolMessage(tool_call_id=t['id'], name=t['name'], content=str(result)))
298
+ return {'messages': results}
299
+
300
+ def final_answer(self, state: AgentState):
301
+ """Return the final tool output cleanly."""
302
+ return {"messages": [AIMessage(content=state['messages'][-1].content.strip())]}
303
+
304
+
305
+ prompt = """ You are a Wildfire Detection Assistant.
306
+ When users ask about fire spots, wildfires, or fire mapping in any region,
307
+ use the firespot_summary function to analyze the area and generate a fire map or status update.
308
+ """
309
+
310
+ model = init_chat_model("mistral-large-latest", model_provider="mistralai")
311
+ abot = Agent(model, [get_fire_risk_map], system=prompt)
312
+
313
+
314
+
315
+
316
+ def process_prompt(prompt):
317
+ messages = [HumanMessage(content=prompt)]
318
+ result = abot.graph.invoke({"messages": messages})
319
+ html_map = result['messages'][-1].content # assuming tool returns HTML string
320
+ return html_map
321
+
322
+ iface = gr.Interface(
323
+ fn=process_prompt,
324
+ inputs=gr.Textbox(lines=2, placeholder="e.g., Check for fires in California..."),
325
+ outputs=gr.HTML(label="Fire Risk Map"),
326
+ title="Fire Risk Mapper",
327
+ description="Ask about fire risks in a region and view results as an interactive map."
328
+ )
329
+
330
+ iface.launch(share = True)
requirements.txt ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core Python libraries
2
+ pandas
3
+ requests
4
+ folium
5
+ geopy
6
+ google-auth
7
+ google-auth-oauthlib
8
+ google-auth-httplib2
9
+
10
+ # Earth Engine (GEE)
11
+ earthengine-api
12
+
13
+ # LangChain ecosystem
14
+ langchain
15
+ langgraph
16
+ langchain-community
17
+ langchain-core
18
+
19
+ # For Gradio UI
20
+ gradio
21
+
22
+ # Typing / Compatibility
23
+ typing-extensions