geomind-ai 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geomind/__init__.py +11 -0
- geomind/agent.py +445 -0
- geomind/cli.py +125 -0
- geomind/config.py +55 -0
- geomind/tools/__init__.py +27 -0
- geomind/tools/geocoding.py +108 -0
- geomind/tools/processing.py +349 -0
- geomind/tools/stac_search.py +231 -0
- geomind_ai-1.0.0.dist-info/METADATA +85 -0
- geomind_ai-1.0.0.dist-info/RECORD +14 -0
- geomind_ai-1.0.0.dist-info/WHEEL +5 -0
- geomind_ai-1.0.0.dist-info/entry_points.txt +2 -0
- geomind_ai-1.0.0.dist-info/licenses/LICENSE +21 -0
- geomind_ai-1.0.0.dist-info/top_level.txt +1 -0
geomind/__init__.py
ADDED
geomind/agent.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeoMind Agent - Main agent class powered by OpenRouter.
|
|
3
|
+
|
|
4
|
+
This module implements the AI agent that can understand natural language
|
|
5
|
+
queries about satellite imagery and execute the appropriate tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from openai import OpenAI
|
|
13
|
+
|
|
14
|
+
from .config import OPENROUTER_API_KEY, OPENROUTER_API_URL, OPENROUTER_MODEL
|
|
15
|
+
from .tools import (
|
|
16
|
+
geocode_location,
|
|
17
|
+
get_bbox_from_location,
|
|
18
|
+
search_imagery,
|
|
19
|
+
get_item_details,
|
|
20
|
+
list_recent_imagery,
|
|
21
|
+
create_rgb_composite,
|
|
22
|
+
calculate_ndvi,
|
|
23
|
+
get_band_statistics,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Map tool names to functions
|
|
28
|
+
TOOL_FUNCTIONS = {
|
|
29
|
+
"geocode_location": geocode_location,
|
|
30
|
+
"get_bbox_from_location": get_bbox_from_location,
|
|
31
|
+
"search_imagery": search_imagery,
|
|
32
|
+
"list_recent_imagery": list_recent_imagery,
|
|
33
|
+
"get_item_details": get_item_details,
|
|
34
|
+
"create_rgb_composite": create_rgb_composite,
|
|
35
|
+
"calculate_ndvi": calculate_ndvi,
|
|
36
|
+
"get_band_statistics": get_band_statistics,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Tool definitions for the LLM
|
|
40
|
+
TOOLS = [
|
|
41
|
+
{
|
|
42
|
+
"type": "function",
|
|
43
|
+
"function": {
|
|
44
|
+
"name": "geocode_location",
|
|
45
|
+
"description": "Convert a place name to geographic coordinates (latitude, longitude). Use this when you need to find coordinates for a location.",
|
|
46
|
+
"parameters": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"place_name": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "The name of the place to geocode (e.g., 'New York City', 'Paris, France')",
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"required": ["place_name"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"type": "function",
|
|
60
|
+
"function": {
|
|
61
|
+
"name": "get_bbox_from_location",
|
|
62
|
+
"description": "Get a bounding box for a location, suitable for searching satellite imagery.",
|
|
63
|
+
"parameters": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"place_name": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "The name of the place",
|
|
69
|
+
},
|
|
70
|
+
"buffer_km": {
|
|
71
|
+
"type": "number",
|
|
72
|
+
"description": "Buffer distance in kilometers (default: 10)",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"required": ["place_name"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"type": "function",
|
|
81
|
+
"function": {
|
|
82
|
+
"name": "search_imagery",
|
|
83
|
+
"description": "Search for Sentinel-2 satellite imagery in the EOPF catalog. Returns available scenes.",
|
|
84
|
+
"parameters": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"properties": {
|
|
87
|
+
"bbox": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"items": {"type": "number"},
|
|
90
|
+
"description": "Bounding box as [min_lon, min_lat, max_lon, max_lat]",
|
|
91
|
+
},
|
|
92
|
+
"start_date": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"description": "Start date in YYYY-MM-DD format",
|
|
95
|
+
},
|
|
96
|
+
"end_date": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"description": "End date in YYYY-MM-DD format",
|
|
99
|
+
},
|
|
100
|
+
"max_cloud_cover": {
|
|
101
|
+
"type": "number",
|
|
102
|
+
"description": "Maximum cloud cover percentage (0-100)",
|
|
103
|
+
},
|
|
104
|
+
"max_items": {
|
|
105
|
+
"type": "integer",
|
|
106
|
+
"description": "Maximum number of results",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
"required": [],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"type": "function",
|
|
115
|
+
"function": {
|
|
116
|
+
"name": "list_recent_imagery",
|
|
117
|
+
"description": "List recent Sentinel-2 imagery for a location. Combines geocoding and search.",
|
|
118
|
+
"parameters": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {
|
|
121
|
+
"location_name": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "Name of the location to search",
|
|
124
|
+
},
|
|
125
|
+
"days": {
|
|
126
|
+
"type": "integer",
|
|
127
|
+
"description": "Number of days to look back (default: 7)",
|
|
128
|
+
},
|
|
129
|
+
"max_cloud_cover": {
|
|
130
|
+
"type": "number",
|
|
131
|
+
"description": "Maximum cloud cover percentage",
|
|
132
|
+
},
|
|
133
|
+
"max_items": {
|
|
134
|
+
"type": "integer",
|
|
135
|
+
"description": "Maximum number of results",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
"required": [],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"type": "function",
|
|
144
|
+
"function": {
|
|
145
|
+
"name": "get_item_details",
|
|
146
|
+
"description": "Get detailed information about a specific Sentinel-2 scene by its ID.",
|
|
147
|
+
"parameters": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": {
|
|
150
|
+
"item_id": {"type": "string", "description": "The STAC item ID"}
|
|
151
|
+
},
|
|
152
|
+
"required": ["item_id"],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"type": "function",
|
|
158
|
+
"function": {
|
|
159
|
+
"name": "create_rgb_composite",
|
|
160
|
+
"description": "Create an RGB true-color composite image from Sentinel-2 data.",
|
|
161
|
+
"parameters": {
|
|
162
|
+
"type": "object",
|
|
163
|
+
"properties": {
|
|
164
|
+
"zarr_url": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"description": "URL to the SR_10m Zarr asset from a STAC item",
|
|
167
|
+
},
|
|
168
|
+
"output_path": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"description": "Optional path to save the output image",
|
|
171
|
+
},
|
|
172
|
+
"subset_size": {
|
|
173
|
+
"type": "integer",
|
|
174
|
+
"description": "Size to subset the image (default: 1000 pixels)",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
"required": ["zarr_url"],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"type": "function",
|
|
183
|
+
"function": {
|
|
184
|
+
"name": "calculate_ndvi",
|
|
185
|
+
"description": "Calculate NDVI (vegetation index) from Sentinel-2 data.",
|
|
186
|
+
"parameters": {
|
|
187
|
+
"type": "object",
|
|
188
|
+
"properties": {
|
|
189
|
+
"zarr_url": {
|
|
190
|
+
"type": "string",
|
|
191
|
+
"description": "URL to the SR_10m Zarr asset",
|
|
192
|
+
},
|
|
193
|
+
"output_path": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"description": "Optional path to save the NDVI image",
|
|
196
|
+
},
|
|
197
|
+
"subset_size": {
|
|
198
|
+
"type": "integer",
|
|
199
|
+
"description": "Size to subset the image",
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
"required": ["zarr_url"],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"type": "function",
|
|
208
|
+
"function": {
|
|
209
|
+
"name": "get_band_statistics",
|
|
210
|
+
"description": "Get statistics (min, max, mean) for spectral bands.",
|
|
211
|
+
"parameters": {
|
|
212
|
+
"type": "object",
|
|
213
|
+
"properties": {
|
|
214
|
+
"zarr_url": {
|
|
215
|
+
"type": "string",
|
|
216
|
+
"description": "URL to the Zarr asset",
|
|
217
|
+
},
|
|
218
|
+
"bands": {
|
|
219
|
+
"type": "array",
|
|
220
|
+
"items": {"type": "string"},
|
|
221
|
+
"description": "List of band names to analyze",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
"required": ["zarr_url"],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class GeoMindAgent:
|
|
232
|
+
"""
|
|
233
|
+
GeoMind - An AI agent for geospatial analysis with Sentinel-2 imagery.
|
|
234
|
+
|
|
235
|
+
Uses OpenRouter API for access to multiple AI models.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
def __init__(self, model: Optional[str] = None, api_key: Optional[str] = None):
|
|
239
|
+
"""
|
|
240
|
+
Initialize the GeoMind agent.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
model: Model name (default: xiaomi/mimo-v2-flash:free)
|
|
244
|
+
api_key: OpenRouter API key. If not provided, looks for OPENROUTER_API_KEY env variable.
|
|
245
|
+
"""
|
|
246
|
+
self.provider = "openrouter"
|
|
247
|
+
self.api_key = api_key or OPENROUTER_API_KEY
|
|
248
|
+
self.model_name = model or OPENROUTER_MODEL
|
|
249
|
+
self.base_url = OPENROUTER_API_URL
|
|
250
|
+
|
|
251
|
+
if not self.api_key:
|
|
252
|
+
raise ValueError(
|
|
253
|
+
"OpenRouter API key required.\n"
|
|
254
|
+
"You can provide it in three ways:\n"
|
|
255
|
+
"1. Pass it to the constructor: GeoMindAgent(api_key='your-key')\n"
|
|
256
|
+
"2. Set OPENROUTER_API_KEY environment variable\n"
|
|
257
|
+
"3. Create a .env file with OPENROUTER_API_KEY=your-key\n"
|
|
258
|
+
"\nGet your API key at: https://openrouter.ai/settings/keys"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
print(f"š GeoMind Agent initialized with {self.model_name} (OpenRouter)")
|
|
262
|
+
print(f" API URL: {self.base_url}")
|
|
263
|
+
|
|
264
|
+
# Create OpenAI-compatible client
|
|
265
|
+
self.client = OpenAI(base_url=self.base_url, api_key=self.api_key)
|
|
266
|
+
|
|
267
|
+
# Chat history
|
|
268
|
+
self.history = []
|
|
269
|
+
|
|
270
|
+
# Add system message
|
|
271
|
+
self.system_prompt = self._get_system_prompt()
|
|
272
|
+
|
|
273
|
+
def _get_system_prompt(self) -> str:
|
|
274
|
+
"""Get the system prompt for the agent."""
|
|
275
|
+
return f"""You are GeoMind, an expert AI assistant specialized in geospatial analysis
|
|
276
|
+
and satellite imagery. You help users find, analyze, and visualize Sentinel-2 satellite data
|
|
277
|
+
from the EOPF (ESA Earth Observation Processing Framework) catalog.
|
|
278
|
+
|
|
279
|
+
Your capabilities include:
|
|
280
|
+
1. **Search**: Find Sentinel-2 L2A imagery by location, date, and cloud cover
|
|
281
|
+
2. **Geocoding**: Convert place names to coordinates for searching
|
|
282
|
+
3. **Visualization**: Create RGB composites and NDVI maps from imagery
|
|
283
|
+
4. **Analysis**: Calculate spectral indices and band statistics
|
|
284
|
+
|
|
285
|
+
Key information:
|
|
286
|
+
- Data source: EOPF STAC API (https://stac.core.eopf.eodc.eu)
|
|
287
|
+
- Satellite: Sentinel-2 (L2A surface reflectance products)
|
|
288
|
+
- Bands available: B01-B12 at 10m, 20m, or 60m resolution
|
|
289
|
+
- Current date: {datetime.now().strftime('%Y-%m-%d')}
|
|
290
|
+
|
|
291
|
+
When users ask for imagery:
|
|
292
|
+
1. First use get_bbox_from_location or list_recent_imagery to search
|
|
293
|
+
2. Present the results clearly with key metadata
|
|
294
|
+
3. Offer to create visualizations if data is found
|
|
295
|
+
|
|
296
|
+
Always explain what you're doing and interpret results in a helpful way."""
|
|
297
|
+
|
|
298
|
+
def _execute_function(self, name: str, args: dict) -> dict:
|
|
299
|
+
"""Execute a function call and return the result."""
|
|
300
|
+
print(f" š§ Executing: {name}({args})")
|
|
301
|
+
|
|
302
|
+
if name not in TOOL_FUNCTIONS:
|
|
303
|
+
return {"error": f"Unknown function: {name}"}
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
result = TOOL_FUNCTIONS[name](**args)
|
|
307
|
+
return result
|
|
308
|
+
except Exception as e:
|
|
309
|
+
return {"error": str(e)}
|
|
310
|
+
|
|
311
|
+
def chat(self, message: str, verbose: bool = True) -> str:
|
|
312
|
+
"""
|
|
313
|
+
Send a message to the agent and get a response.
|
|
314
|
+
"""
|
|
315
|
+
if verbose:
|
|
316
|
+
print(f"\nš¬ User: {message}")
|
|
317
|
+
print("š¤ Processing...")
|
|
318
|
+
|
|
319
|
+
# Add user message to history
|
|
320
|
+
self.history.append({"role": "user", "content": message})
|
|
321
|
+
|
|
322
|
+
# Build messages with system prompt
|
|
323
|
+
messages = [{"role": "system", "content": self.system_prompt}] + self.history
|
|
324
|
+
|
|
325
|
+
max_iterations = 10
|
|
326
|
+
iteration = 0
|
|
327
|
+
|
|
328
|
+
while iteration < max_iterations:
|
|
329
|
+
iteration += 1
|
|
330
|
+
|
|
331
|
+
# Call the model
|
|
332
|
+
response = self.client.chat.completions.create(
|
|
333
|
+
model=self.model_name,
|
|
334
|
+
messages=messages,
|
|
335
|
+
tools=TOOLS,
|
|
336
|
+
tool_choice="auto",
|
|
337
|
+
max_tokens=4096,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
assistant_message = response.choices[0].message
|
|
341
|
+
|
|
342
|
+
# Check if there are tool calls
|
|
343
|
+
if assistant_message.tool_calls:
|
|
344
|
+
# Add assistant message with tool calls to messages
|
|
345
|
+
messages.append(
|
|
346
|
+
{
|
|
347
|
+
"role": "assistant",
|
|
348
|
+
"content": assistant_message.content or "",
|
|
349
|
+
"tool_calls": [
|
|
350
|
+
{
|
|
351
|
+
"id": tc.id,
|
|
352
|
+
"type": "function",
|
|
353
|
+
"function": {
|
|
354
|
+
"name": tc.function.name,
|
|
355
|
+
"arguments": tc.function.arguments,
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
for tc in assistant_message.tool_calls
|
|
359
|
+
],
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Execute each tool call
|
|
364
|
+
for tool_call in assistant_message.tool_calls:
|
|
365
|
+
func_name = tool_call.function.name
|
|
366
|
+
func_args = json.loads(tool_call.function.arguments)
|
|
367
|
+
|
|
368
|
+
result = self._execute_function(func_name, func_args)
|
|
369
|
+
|
|
370
|
+
# Add tool result to messages
|
|
371
|
+
messages.append(
|
|
372
|
+
{
|
|
373
|
+
"role": "tool",
|
|
374
|
+
"tool_call_id": tool_call.id,
|
|
375
|
+
"content": json.dumps(result, default=str),
|
|
376
|
+
}
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
# No tool calls, we have a final response
|
|
380
|
+
final_text = assistant_message.content or ""
|
|
381
|
+
|
|
382
|
+
# Add to history
|
|
383
|
+
self.history.append({"role": "assistant", "content": final_text})
|
|
384
|
+
|
|
385
|
+
if verbose:
|
|
386
|
+
print(f"\nš GeoMind: {final_text}")
|
|
387
|
+
|
|
388
|
+
return final_text
|
|
389
|
+
|
|
390
|
+
return "Max iterations reached."
|
|
391
|
+
|
|
392
|
+
def reset(self):
|
|
393
|
+
"""Reset the chat session."""
|
|
394
|
+
self.history = []
|
|
395
|
+
print("š Chat session reset")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def main(model: Optional[str] = None):
|
|
399
|
+
"""Main entry point for CLI usage."""
|
|
400
|
+
import sys
|
|
401
|
+
|
|
402
|
+
print("=" * 60)
|
|
403
|
+
print("š GeoMind - Geospatial AI Agent")
|
|
404
|
+
print("=" * 60)
|
|
405
|
+
print("Powered by OpenRouter | Sentinel-2 Imagery")
|
|
406
|
+
print("Type 'quit' or 'exit' to end the session")
|
|
407
|
+
print("Type 'reset' to start a new conversation")
|
|
408
|
+
print("=" * 60)
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
agent = GeoMindAgent(model=model)
|
|
412
|
+
except ValueError as e:
|
|
413
|
+
print(f"\nā Error: {e}")
|
|
414
|
+
sys.exit(1)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
print(f"\nā Error: {e}")
|
|
417
|
+
print("\nPlease check your API key and internet connection.")
|
|
418
|
+
sys.exit(1)
|
|
419
|
+
|
|
420
|
+
while True:
|
|
421
|
+
try:
|
|
422
|
+
user_input = input("\nš¬ You: ").strip()
|
|
423
|
+
|
|
424
|
+
if not user_input:
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
if user_input.lower() in ["quit", "exit", "q"]:
|
|
428
|
+
print("\nš Goodbye!")
|
|
429
|
+
break
|
|
430
|
+
|
|
431
|
+
if user_input.lower() == "reset":
|
|
432
|
+
agent.reset()
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
agent.chat(user_input)
|
|
436
|
+
|
|
437
|
+
except KeyboardInterrupt:
|
|
438
|
+
print("\n\nš Goodbye!")
|
|
439
|
+
break
|
|
440
|
+
except Exception as e:
|
|
441
|
+
print(f"\nā Error: {e}")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
if __name__ == "__main__":
|
|
445
|
+
main()
|
geomind/cli.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for GeoMind.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import argparse
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .agent import GeoMindAgent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
"""Main CLI entry point for the geomind package."""
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="GeoMind - AI agent for geospatial analysis with Sentinel-2 imagery",
|
|
16
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
17
|
+
epilog="""
|
|
18
|
+
Examples:
|
|
19
|
+
# Interactive mode
|
|
20
|
+
geomind
|
|
21
|
+
|
|
22
|
+
# Single query
|
|
23
|
+
geomind --query "Find recent imagery of Paris"
|
|
24
|
+
|
|
25
|
+
# With custom model
|
|
26
|
+
geomind --model "anthropic/claude-3.5-sonnet"
|
|
27
|
+
|
|
28
|
+
# With API key
|
|
29
|
+
geomind --api-key "your-openrouter-api-key"
|
|
30
|
+
|
|
31
|
+
Environment Variables:
|
|
32
|
+
OPENROUTER_API_KEY Your OpenRouter API key
|
|
33
|
+
OPENROUTER_MODEL Model to use (default: xiaomi/mimo-v2-flash:free)
|
|
34
|
+
OPENROUTER_API_URL API endpoint (default: https://openrouter.ai/api/v1)
|
|
35
|
+
""",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--query",
|
|
40
|
+
"-q",
|
|
41
|
+
type=str,
|
|
42
|
+
help="Single query to run (if not provided, starts interactive mode)",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--model",
|
|
46
|
+
"-m",
|
|
47
|
+
type=str,
|
|
48
|
+
help="Model name to use (e.g., 'anthropic/claude-3.5-sonnet')",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--api-key",
|
|
52
|
+
"-k",
|
|
53
|
+
type=str,
|
|
54
|
+
help="OpenRouter API key (or set OPENROUTER_API_KEY env variable)",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--version", "-v", action="store_true", help="Show version and exit"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
|
|
62
|
+
if args.version:
|
|
63
|
+
from . import __version__
|
|
64
|
+
|
|
65
|
+
print(f"GeoMind version {__version__}")
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
# Start interactive or single-query mode
|
|
69
|
+
try:
|
|
70
|
+
if args.query:
|
|
71
|
+
# Single query mode
|
|
72
|
+
agent = GeoMindAgent(model=args.model, api_key=args.api_key)
|
|
73
|
+
agent.chat(args.query)
|
|
74
|
+
else:
|
|
75
|
+
# Interactive mode
|
|
76
|
+
run_interactive(model=args.model, api_key=args.api_key)
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
print(f"\nā Error: {e}")
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
except KeyboardInterrupt:
|
|
81
|
+
print("\n\nš Goodbye!")
|
|
82
|
+
sys.exit(0)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"\nā Unexpected error: {e}")
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
|
|
89
|
+
"""Run interactive CLI mode."""
|
|
90
|
+
print("=" * 60)
|
|
91
|
+
print("š GeoMind - Geospatial AI Agent")
|
|
92
|
+
print("=" * 60)
|
|
93
|
+
print("Powered by OpenRouter | Sentinel-2 Imagery")
|
|
94
|
+
print("Type 'quit' or 'exit' to end the session")
|
|
95
|
+
print("Type 'reset' to start a new conversation")
|
|
96
|
+
print("=" * 60)
|
|
97
|
+
|
|
98
|
+
agent = GeoMindAgent(model=model, api_key=api_key)
|
|
99
|
+
|
|
100
|
+
while True:
|
|
101
|
+
try:
|
|
102
|
+
user_input = input("\nš¬ You: ").strip()
|
|
103
|
+
|
|
104
|
+
if not user_input:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if user_input.lower() in ["quit", "exit", "q"]:
|
|
108
|
+
print("\nš Goodbye!")
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
if user_input.lower() == "reset":
|
|
112
|
+
agent.reset()
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
agent.chat(user_input)
|
|
116
|
+
|
|
117
|
+
except KeyboardInterrupt:
|
|
118
|
+
print("\n\nš Goodbye!")
|
|
119
|
+
break
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print(f"\nā Error: {e}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
main()
|
geomind/config.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration settings for GeoMind agent.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
# Load environment variables from .env file
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
# STAC API Configuration
|
|
13
|
+
STAC_API_URL = "https://stac.core.eopf.eodc.eu"
|
|
14
|
+
STAC_COLLECTION = "sentinel-2-l2a"
|
|
15
|
+
|
|
16
|
+
# Sentinel-2 Band Configuration
|
|
17
|
+
SENTINEL2_BANDS = {
|
|
18
|
+
"B01": {"name": "Coastal aerosol", "wavelength": 0.443, "resolution": 60},
|
|
19
|
+
"B02": {"name": "Blue", "wavelength": 0.490, "resolution": 10},
|
|
20
|
+
"B03": {"name": "Green", "wavelength": 0.560, "resolution": 10},
|
|
21
|
+
"B04": {"name": "Red", "wavelength": 0.665, "resolution": 10},
|
|
22
|
+
"B05": {"name": "Red Edge 1", "wavelength": 0.704, "resolution": 20},
|
|
23
|
+
"B06": {"name": "Red Edge 2", "wavelength": 0.740, "resolution": 20},
|
|
24
|
+
"B07": {"name": "Red Edge 3", "wavelength": 0.783, "resolution": 20},
|
|
25
|
+
"B08": {"name": "NIR", "wavelength": 0.842, "resolution": 10},
|
|
26
|
+
"B8A": {"name": "NIR Narrow", "wavelength": 0.865, "resolution": 20},
|
|
27
|
+
"B09": {"name": "Water Vapour", "wavelength": 0.945, "resolution": 60},
|
|
28
|
+
"B11": {"name": "SWIR 1", "wavelength": 1.610, "resolution": 20},
|
|
29
|
+
"B12": {"name": "SWIR 2", "wavelength": 2.190, "resolution": 20},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Reflectance scale and offset (from STAC metadata)
|
|
33
|
+
REFLECTANCE_SCALE = 0.0001
|
|
34
|
+
REFLECTANCE_OFFSET = -0.1
|
|
35
|
+
|
|
36
|
+
# RGB Band Mapping
|
|
37
|
+
RGB_BANDS = {"red": "b04", "green": "b03", "blue": "b02"}
|
|
38
|
+
|
|
39
|
+
# Default search parameters
|
|
40
|
+
DEFAULT_MAX_CLOUD_COVER = 20 # percent
|
|
41
|
+
DEFAULT_BUFFER_KM = 10 # km buffer around point for bbox
|
|
42
|
+
DEFAULT_MAX_ITEMS = 10
|
|
43
|
+
|
|
44
|
+
# Output directory for saved images
|
|
45
|
+
OUTPUT_DIR = Path("outputs")
|
|
46
|
+
OUTPUT_DIR.mkdir(exist_ok=True)
|
|
47
|
+
|
|
48
|
+
# Geocoding configuration
|
|
49
|
+
GEOCODER_USER_AGENT = "geomind_agent_v0.1"
|
|
50
|
+
|
|
51
|
+
# OpenRouter API Configuration
|
|
52
|
+
# OpenRouter provides access to multiple AI models via API
|
|
53
|
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
|
54
|
+
OPENROUTER_API_URL = os.getenv("OPENROUTER_API_URL", "https://openrouter.ai/api/v1")
|
|
55
|
+
OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "xiaomi/mimo-v2-flash:free")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeoMind Tools - Functions available to the AI agent.
|
|
3
|
+
|
|
4
|
+
This module contains all the tools that the agent can call:
|
|
5
|
+
- Geocoding: Convert place names to coordinates
|
|
6
|
+
- STAC Search: Query the Sentinel-2 catalog
|
|
7
|
+
- Processing: Load and process imagery data
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .geocoding import geocode_location, get_bbox_from_location
|
|
11
|
+
from .stac_search import search_imagery, get_item_details, list_recent_imagery
|
|
12
|
+
from .processing import (
|
|
13
|
+
create_rgb_composite,
|
|
14
|
+
calculate_ndvi,
|
|
15
|
+
get_band_statistics,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"geocode_location",
|
|
20
|
+
"get_bbox_from_location",
|
|
21
|
+
"search_imagery",
|
|
22
|
+
"get_item_details",
|
|
23
|
+
"list_recent_imagery",
|
|
24
|
+
"create_rgb_composite",
|
|
25
|
+
"calculate_ndvi",
|
|
26
|
+
"get_band_statistics",
|
|
27
|
+
]
|