geomind-ai 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Harsh Shinde
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include README.md
2
+ include LICENSE
3
+ include requirements.txt
4
+ recursive-include geomind *.py
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: geomind-ai
3
+ Version: 1.0.0
4
+ Summary: AI agent for geospatial analysis
5
+ Author: Harsh Shinde
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://harshshinde0.github.io/GeoMind/
8
+ Project-URL: Repository, https://github.com/HarshShinde0/GeoMind
9
+ Project-URL: Documentation, https://github.com/HarshShinde0/GeoMind#readme
10
+ Project-URL: Issues, https://github.com/HarshShinde0/GeoMind/issues
11
+ Keywords: geospatial,satellite-imagery,sentinel-2,ai-agent,remote-sensing,earth-observation
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: openai>=1.0.0
24
+ Requires-Dist: pystac-client>=0.8.0
25
+ Requires-Dist: pystac>=1.10.0
26
+ Requires-Dist: xarray>=2024.1.0
27
+ Requires-Dist: zarr>=2.18.0
28
+ Requires-Dist: dask>=2024.1.0
29
+ Requires-Dist: geopy>=2.4.0
30
+ Requires-Dist: fsspec>=2024.1.0
31
+ Requires-Dist: aiohttp>=3.9.0
32
+ Requires-Dist: requests>=2.31.0
33
+ Requires-Dist: s3fs>=2024.1.0
34
+ Requires-Dist: matplotlib>=3.8.0
35
+ Requires-Dist: numpy>=1.26.0
36
+ Requires-Dist: python-dotenv>=1.0.0
37
+ Dynamic: license-file
38
+
39
+ ### 1. Install Dependencies
40
+
41
+ ```bash
42
+ pip install -r requirements.txt
43
+ ```
44
+
45
+ ### 2. Set Up API Key
46
+
47
+ Set your HuggingFace API key in the environment or update `config.py`:
48
+
49
+ ```python
50
+ # In geomind/config.py
51
+ HF_API_KEY = "your_huggingface_api_key"
52
+ ```
53
+
54
+ Get a free API key from [HuggingFace](https://huggingface.co/settings/tokens).
55
+
56
+ ### 3. Run the Agent
57
+
58
+ ```bash
59
+ python main.py
60
+ ```
61
+
62
+ ## Example Queries
63
+
64
+ ```
65
+
66
+ šŸ’¬ "Create an RGB composite for the most recent image of London"
67
+
68
+ šŸ’¬ "Calculate NDVI for Central Park, New York"
69
+
70
+ šŸ’¬ "What images are available for Tokyo with less than 10% cloud cover?"
71
+ ```
72
+
73
+ ## Approach
74
+
75
+ ### Traditional Approach
76
+ ```
77
+ Full Scene Download → Local Storage → Process → Result
78
+ ~720 MB Disk I/O Slow
79
+ ```
80
+
81
+ ### GeoMind Approach (Zarr + fsspec)
82
+ ```
83
+ HTTP Range Request → Stream Chunks → Process in Memory → Result
84
+ ~1-5 MB No disk Fast
85
+ ```
@@ -0,0 +1,47 @@
1
+ ### 1. Install Dependencies
2
+
3
+ ```bash
4
+ pip install -r requirements.txt
5
+ ```
6
+
7
+ ### 2. Set Up API Key
8
+
9
+ Set your HuggingFace API key in the environment or update `config.py`:
10
+
11
+ ```python
12
+ # In geomind/config.py
13
+ HF_API_KEY = "your_huggingface_api_key"
14
+ ```
15
+
16
+ Get a free API key from [HuggingFace](https://huggingface.co/settings/tokens).
17
+
18
+ ### 3. Run the Agent
19
+
20
+ ```bash
21
+ python main.py
22
+ ```
23
+
24
+ ## Example Queries
25
+
26
+ ```
27
+
28
+ šŸ’¬ "Create an RGB composite for the most recent image of London"
29
+
30
+ šŸ’¬ "Calculate NDVI for Central Park, New York"
31
+
32
+ šŸ’¬ "What images are available for Tokyo with less than 10% cloud cover?"
33
+ ```
34
+
35
+ ## Approach
36
+
37
+ ### Traditional Approach
38
+ ```
39
+ Full Scene Download → Local Storage → Process → Result
40
+ ~720 MB Disk I/O Slow
41
+ ```
42
+
43
+ ### GeoMind Approach (Zarr + fsspec)
44
+ ```
45
+ HTTP Range Request → Stream Chunks → Process in Memory → Result
46
+ ~1-5 MB No disk Fast
47
+ ```
@@ -0,0 +1,11 @@
1
+ """
2
+ GeoMind - Geospatial AI Agent
3
+
4
+ """
5
+
6
+ __version__ = "1.0.0"
7
+ __author__ = "Harsh Shinde"
8
+
9
+ from .agent import GeoMindAgent
10
+
11
+ __all__ = ["GeoMindAgent"]
@@ -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()