geomind-ai 1.0.0__tar.gz → 1.0.1__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.
Files changed (22) hide show
  1. {geomind_ai-1.0.0/geomind_ai.egg-info → geomind_ai-1.0.1}/PKG-INFO +10 -17
  2. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/README.md +7 -14
  3. geomind_ai-1.0.1/geomind/__init__.py +11 -0
  4. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind/agent.py +127 -127
  5. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind/cli.py +12 -14
  6. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind/tools/geocoding.py +21 -18
  7. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind/tools/processing.py +80 -82
  8. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind/tools/stac_search.py +35 -33
  9. {geomind_ai-1.0.0 → geomind_ai-1.0.1/geomind_ai.egg-info}/PKG-INFO +10 -17
  10. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/pyproject.toml +3 -3
  11. geomind_ai-1.0.0/geomind/__init__.py +0 -11
  12. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/LICENSE +0 -0
  13. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/MANIFEST.in +0 -0
  14. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind/config.py +0 -0
  15. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind/tools/__init__.py +0 -0
  16. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind_ai.egg-info/SOURCES.txt +0 -0
  17. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind_ai.egg-info/dependency_links.txt +0 -0
  18. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind_ai.egg-info/entry_points.txt +0 -0
  19. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind_ai.egg-info/requires.txt +0 -0
  20. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/geomind_ai.egg-info/top_level.txt +0 -0
  21. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/requirements.txt +0 -0
  22. {geomind_ai-1.0.0 → geomind_ai-1.0.1}/setup.cfg +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geomind-ai
3
- Version: 1.0.0
4
- Summary: AI agent for geospatial analysis
3
+ Version: 1.0.1
4
+ Summary: AI agent for geospatial analysis with Sentinel-2 satellite imagery
5
5
  Author: Harsh Shinde
6
6
  License-Expression: MIT
7
- Project-URL: Homepage, https://harshshinde0.github.io/GeoMind/
7
+ Project-URL: Homepage, https://github.com/HarshShinde0/GeoMind
8
8
  Project-URL: Repository, https://github.com/HarshShinde0/GeoMind
9
9
  Project-URL: Documentation, https://github.com/HarshShinde0/GeoMind#readme
10
10
  Project-URL: Issues, https://github.com/HarshShinde0/GeoMind/issues
@@ -36,27 +36,20 @@ Requires-Dist: numpy>=1.26.0
36
36
  Requires-Dist: python-dotenv>=1.0.0
37
37
  Dynamic: license-file
38
38
 
39
- ### 1. Install Dependencies
39
+ ### Install Dependencies
40
40
 
41
41
  ```bash
42
42
  pip install -r requirements.txt
43
43
  ```
44
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
45
+ ### Run the Agent
57
46
 
58
47
  ```bash
59
- python main.py
48
+ # Interactive mode
49
+ geomind
50
+
51
+ # Single query
52
+ geomind --query "Find recent imagery of Paris"
60
53
  ```
61
54
 
62
55
  ## Example Queries
@@ -1,24 +1,17 @@
1
- ### 1. Install Dependencies
1
+ ### Install Dependencies
2
2
 
3
3
  ```bash
4
4
  pip install -r requirements.txt
5
5
  ```
6
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
7
+ ### Run the Agent
19
8
 
20
9
  ```bash
21
- python main.py
10
+ # Interactive mode
11
+ geomind
12
+
13
+ # Single query
14
+ geomind --query "Find recent imagery of Paris"
22
15
  ```
23
16
 
24
17
  ## Example Queries
@@ -0,0 +1,11 @@
1
+ """
2
+ GeoMind - Geospatial AI Agent
3
+
4
+ """
5
+
6
+ __version__ = "1.0.1"
7
+ __author__ = "Harsh Shinde, Rajat Shinde"
8
+
9
+ from .agent import GeoMindAgent
10
+
11
+ __all__ = ["GeoMindAgent"]
@@ -6,12 +6,15 @@ queries about satellite imagery and execute the appropriate tools.
6
6
  """
7
7
 
8
8
  import json
9
- from typing import Optional
9
+ import re
10
+ from typing import Optional, Callable, Any
10
11
  from datetime import datetime
11
12
 
12
13
  from openai import OpenAI
13
14
 
14
- from .config import OPENROUTER_API_KEY, OPENROUTER_API_URL, OPENROUTER_MODEL
15
+ from .config import (
16
+ OPENROUTER_API_KEY, OPENROUTER_API_URL, OPENROUTER_MODEL
17
+ )
15
18
  from .tools import (
16
19
  geocode_location,
17
20
  get_bbox_from_location,
@@ -48,12 +51,12 @@ TOOLS = [
48
51
  "properties": {
49
52
  "place_name": {
50
53
  "type": "string",
51
- "description": "The name of the place to geocode (e.g., 'New York City', 'Paris, France')",
54
+ "description": "The name of the place to geocode (e.g., 'New York City', 'Paris, France')"
52
55
  }
53
56
  },
54
- "required": ["place_name"],
55
- },
56
- },
57
+ "required": ["place_name"]
58
+ }
59
+ }
57
60
  },
58
61
  {
59
62
  "type": "function",
@@ -65,16 +68,16 @@ TOOLS = [
65
68
  "properties": {
66
69
  "place_name": {
67
70
  "type": "string",
68
- "description": "The name of the place",
71
+ "description": "The name of the place"
69
72
  },
70
73
  "buffer_km": {
71
74
  "type": "number",
72
- "description": "Buffer distance in kilometers (default: 10)",
73
- },
75
+ "description": "Buffer distance in kilometers (default: 10)"
76
+ }
74
77
  },
75
- "required": ["place_name"],
76
- },
77
- },
78
+ "required": ["place_name"]
79
+ }
80
+ }
78
81
  },
79
82
  {
80
83
  "type": "function",
@@ -87,28 +90,28 @@ TOOLS = [
87
90
  "bbox": {
88
91
  "type": "array",
89
92
  "items": {"type": "number"},
90
- "description": "Bounding box as [min_lon, min_lat, max_lon, max_lat]",
93
+ "description": "Bounding box as [min_lon, min_lat, max_lon, max_lat]"
91
94
  },
92
95
  "start_date": {
93
96
  "type": "string",
94
- "description": "Start date in YYYY-MM-DD format",
97
+ "description": "Start date in YYYY-MM-DD format"
95
98
  },
96
99
  "end_date": {
97
100
  "type": "string",
98
- "description": "End date in YYYY-MM-DD format",
101
+ "description": "End date in YYYY-MM-DD format"
99
102
  },
100
103
  "max_cloud_cover": {
101
104
  "type": "number",
102
- "description": "Maximum cloud cover percentage (0-100)",
105
+ "description": "Maximum cloud cover percentage (0-100)"
103
106
  },
104
107
  "max_items": {
105
108
  "type": "integer",
106
- "description": "Maximum number of results",
107
- },
109
+ "description": "Maximum number of results"
110
+ }
108
111
  },
109
- "required": [],
110
- },
111
- },
112
+ "required": []
113
+ }
114
+ }
112
115
  },
113
116
  {
114
117
  "type": "function",
@@ -120,24 +123,24 @@ TOOLS = [
120
123
  "properties": {
121
124
  "location_name": {
122
125
  "type": "string",
123
- "description": "Name of the location to search",
126
+ "description": "Name of the location to search"
124
127
  },
125
128
  "days": {
126
129
  "type": "integer",
127
- "description": "Number of days to look back (default: 7)",
130
+ "description": "Number of days to look back (default: 7)"
128
131
  },
129
132
  "max_cloud_cover": {
130
133
  "type": "number",
131
- "description": "Maximum cloud cover percentage",
134
+ "description": "Maximum cloud cover percentage"
132
135
  },
133
136
  "max_items": {
134
137
  "type": "integer",
135
- "description": "Maximum number of results",
136
- },
138
+ "description": "Maximum number of results"
139
+ }
137
140
  },
138
- "required": [],
139
- },
140
- },
141
+ "required": []
142
+ }
143
+ }
141
144
  },
142
145
  {
143
146
  "type": "function",
@@ -147,11 +150,14 @@ TOOLS = [
147
150
  "parameters": {
148
151
  "type": "object",
149
152
  "properties": {
150
- "item_id": {"type": "string", "description": "The STAC item ID"}
153
+ "item_id": {
154
+ "type": "string",
155
+ "description": "The STAC item ID"
156
+ }
151
157
  },
152
- "required": ["item_id"],
153
- },
154
- },
158
+ "required": ["item_id"]
159
+ }
160
+ }
155
161
  },
156
162
  {
157
163
  "type": "function",
@@ -163,20 +169,20 @@ TOOLS = [
163
169
  "properties": {
164
170
  "zarr_url": {
165
171
  "type": "string",
166
- "description": "URL to the SR_10m Zarr asset from a STAC item",
172
+ "description": "URL to the SR_10m Zarr asset from a STAC item"
167
173
  },
168
174
  "output_path": {
169
175
  "type": "string",
170
- "description": "Optional path to save the output image",
176
+ "description": "Optional path to save the output image"
171
177
  },
172
178
  "subset_size": {
173
179
  "type": "integer",
174
- "description": "Size to subset the image (default: 1000 pixels)",
175
- },
180
+ "description": "Size to subset the image (default: 1000 pixels)"
181
+ }
176
182
  },
177
- "required": ["zarr_url"],
178
- },
179
- },
183
+ "required": ["zarr_url"]
184
+ }
185
+ }
180
186
  },
181
187
  {
182
188
  "type": "function",
@@ -188,20 +194,20 @@ TOOLS = [
188
194
  "properties": {
189
195
  "zarr_url": {
190
196
  "type": "string",
191
- "description": "URL to the SR_10m Zarr asset",
197
+ "description": "URL to the SR_10m Zarr asset"
192
198
  },
193
199
  "output_path": {
194
200
  "type": "string",
195
- "description": "Optional path to save the NDVI image",
201
+ "description": "Optional path to save the NDVI image"
196
202
  },
197
203
  "subset_size": {
198
204
  "type": "integer",
199
- "description": "Size to subset the image",
200
- },
205
+ "description": "Size to subset the image"
206
+ }
201
207
  },
202
- "required": ["zarr_url"],
203
- },
204
- },
208
+ "required": ["zarr_url"]
209
+ }
210
+ }
205
211
  },
206
212
  {
207
213
  "type": "function",
@@ -213,67 +219,65 @@ TOOLS = [
213
219
  "properties": {
214
220
  "zarr_url": {
215
221
  "type": "string",
216
- "description": "URL to the Zarr asset",
222
+ "description": "URL to the Zarr asset"
217
223
  },
218
224
  "bands": {
219
225
  "type": "array",
220
226
  "items": {"type": "string"},
221
- "description": "List of band names to analyze",
222
- },
227
+ "description": "List of band names to analyze"
228
+ }
223
229
  },
224
- "required": ["zarr_url"],
225
- },
226
- },
227
- },
230
+ "required": ["zarr_url"]
231
+ }
232
+ }
233
+ }
228
234
  ]
229
235
 
230
236
 
231
237
  class GeoMindAgent:
232
238
  """
233
239
  GeoMind - An AI agent for geospatial analysis with Sentinel-2 imagery.
234
-
240
+
235
241
  Uses OpenRouter API for access to multiple AI models.
236
242
  """
237
-
238
- def __init__(self, model: Optional[str] = None, api_key: Optional[str] = None):
243
+
244
+ def __init__(self, model: Optional[str] = None):
239
245
  """
240
246
  Initialize the GeoMind agent.
241
-
247
+
242
248
  Args:
243
249
  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
250
  """
246
251
  self.provider = "openrouter"
247
- self.api_key = api_key or OPENROUTER_API_KEY
252
+ self.api_key = OPENROUTER_API_KEY
248
253
  self.model_name = model or OPENROUTER_MODEL
249
254
  self.base_url = OPENROUTER_API_URL
250
-
255
+
251
256
  if not self.api_key:
252
257
  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"
258
+ "OpenRouter API key required. Set OPENROUTER_API_KEY in .env file.\n"
259
+ "Get your API key at: https://openrouter.ai/settings/keys"
259
260
  )
260
-
261
+
261
262
  print(f"🚀 GeoMind Agent initialized with {self.model_name} (OpenRouter)")
262
263
  print(f" API URL: {self.base_url}")
263
-
264
+
264
265
  # Create OpenAI-compatible client
265
- self.client = OpenAI(base_url=self.base_url, api_key=self.api_key)
266
-
266
+ self.client = OpenAI(
267
+ base_url=self.base_url,
268
+ api_key=self.api_key
269
+ )
270
+
267
271
  # Chat history
268
272
  self.history = []
269
-
273
+
270
274
  # Add system message
271
275
  self.system_prompt = self._get_system_prompt()
272
-
276
+
273
277
  def _get_system_prompt(self) -> str:
274
278
  """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
279
+ return f"""You are GeoMind, an expert AI assistant specialized in geospatial analysis
280
+ and satellite imagery. You help users find, analyze, and visualize Sentinel-2 satellite data
277
281
  from the EOPF (ESA Earth Observation Processing Framework) catalog.
278
282
 
279
283
  Your capabilities include:
@@ -294,20 +298,20 @@ When users ask for imagery:
294
298
  3. Offer to create visualizations if data is found
295
299
 
296
300
  Always explain what you're doing and interpret results in a helpful way."""
297
-
301
+
298
302
  def _execute_function(self, name: str, args: dict) -> dict:
299
303
  """Execute a function call and return the result."""
300
304
  print(f" 🔧 Executing: {name}({args})")
301
-
305
+
302
306
  if name not in TOOL_FUNCTIONS:
303
307
  return {"error": f"Unknown function: {name}"}
304
-
308
+
305
309
  try:
306
310
  result = TOOL_FUNCTIONS[name](**args)
307
311
  return result
308
312
  except Exception as e:
309
313
  return {"error": str(e)}
310
-
314
+
311
315
  def chat(self, message: str, verbose: bool = True) -> str:
312
316
  """
313
317
  Send a message to the agent and get a response.
@@ -315,19 +319,19 @@ Always explain what you're doing and interpret results in a helpful way."""
315
319
  if verbose:
316
320
  print(f"\n💬 User: {message}")
317
321
  print("🤔 Processing...")
318
-
322
+
319
323
  # Add user message to history
320
324
  self.history.append({"role": "user", "content": message})
321
-
325
+
322
326
  # Build messages with system prompt
323
327
  messages = [{"role": "system", "content": self.system_prompt}] + self.history
324
-
328
+
325
329
  max_iterations = 10
326
330
  iteration = 0
327
-
331
+
328
332
  while iteration < max_iterations:
329
333
  iteration += 1
330
-
334
+
331
335
  # Call the model
332
336
  response = self.client.chat.completions.create(
333
337
  model=self.model_name,
@@ -336,59 +340,55 @@ Always explain what you're doing and interpret results in a helpful way."""
336
340
  tool_choice="auto",
337
341
  max_tokens=4096,
338
342
  )
339
-
343
+
340
344
  assistant_message = response.choices[0].message
341
-
345
+
342
346
  # Check if there are tool calls
343
347
  if assistant_message.tool_calls:
344
348
  # 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
- },
349
+ messages.append({
350
+ "role": "assistant",
351
+ "content": assistant_message.content or "",
352
+ "tool_calls": [
353
+ {
354
+ "id": tc.id,
355
+ "type": "function",
356
+ "function": {
357
+ "name": tc.function.name,
358
+ "arguments": tc.function.arguments
357
359
  }
358
- for tc in assistant_message.tool_calls
359
- ],
360
- }
361
- )
362
-
360
+ }
361
+ for tc in assistant_message.tool_calls
362
+ ]
363
+ })
364
+
363
365
  # Execute each tool call
364
366
  for tool_call in assistant_message.tool_calls:
365
367
  func_name = tool_call.function.name
366
368
  func_args = json.loads(tool_call.function.arguments)
367
-
369
+
368
370
  result = self._execute_function(func_name, func_args)
369
-
371
+
370
372
  # 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
- )
373
+ messages.append({
374
+ "role": "tool",
375
+ "tool_call_id": tool_call.id,
376
+ "content": json.dumps(result, default=str)
377
+ })
378
378
  else:
379
379
  # No tool calls, we have a final response
380
380
  final_text = assistant_message.content or ""
381
-
381
+
382
382
  # Add to history
383
383
  self.history.append({"role": "assistant", "content": final_text})
384
-
384
+
385
385
  if verbose:
386
386
  print(f"\n🌍 GeoMind: {final_text}")
387
-
387
+
388
388
  return final_text
389
-
389
+
390
390
  return "Max iterations reached."
391
-
391
+
392
392
  def reset(self):
393
393
  """Reset the chat session."""
394
394
  self.history = []
@@ -398,7 +398,7 @@ Always explain what you're doing and interpret results in a helpful way."""
398
398
  def main(model: Optional[str] = None):
399
399
  """Main entry point for CLI usage."""
400
400
  import sys
401
-
401
+
402
402
  print("=" * 60)
403
403
  print("🌍 GeoMind - Geospatial AI Agent")
404
404
  print("=" * 60)
@@ -406,7 +406,7 @@ def main(model: Optional[str] = None):
406
406
  print("Type 'quit' or 'exit' to end the session")
407
407
  print("Type 'reset' to start a new conversation")
408
408
  print("=" * 60)
409
-
409
+
410
410
  try:
411
411
  agent = GeoMindAgent(model=model)
412
412
  except ValueError as e:
@@ -416,24 +416,24 @@ def main(model: Optional[str] = None):
416
416
  print(f"\n❌ Error: {e}")
417
417
  print("\nPlease check your API key and internet connection.")
418
418
  sys.exit(1)
419
-
419
+
420
420
  while True:
421
421
  try:
422
422
  user_input = input("\n💬 You: ").strip()
423
-
423
+
424
424
  if not user_input:
425
425
  continue
426
-
427
- if user_input.lower() in ["quit", "exit", "q"]:
426
+
427
+ if user_input.lower() in ['quit', 'exit', 'q']:
428
428
  print("\n👋 Goodbye!")
429
429
  break
430
-
431
- if user_input.lower() == "reset":
430
+
431
+ if user_input.lower() == 'reset':
432
432
  agent.reset()
433
433
  continue
434
-
434
+
435
435
  agent.chat(user_input)
436
-
436
+
437
437
  except KeyboardInterrupt:
438
438
  print("\n\n👋 Goodbye!")
439
439
  break
@@ -32,36 +32,34 @@ Environment Variables:
32
32
  OPENROUTER_API_KEY Your OpenRouter API key
33
33
  OPENROUTER_MODEL Model to use (default: xiaomi/mimo-v2-flash:free)
34
34
  OPENROUTER_API_URL API endpoint (default: https://openrouter.ai/api/v1)
35
- """,
35
+ """
36
36
  )
37
37
 
38
38
  parser.add_argument(
39
- "--query",
40
- "-q",
39
+ "--query", "-q",
41
40
  type=str,
42
- help="Single query to run (if not provided, starts interactive mode)",
41
+ help="Single query to run (if not provided, starts interactive mode)"
43
42
  )
44
43
  parser.add_argument(
45
- "--model",
46
- "-m",
44
+ "--model", "-m",
47
45
  type=str,
48
- help="Model name to use (e.g., 'anthropic/claude-3.5-sonnet')",
46
+ help="Model name to use (e.g., 'anthropic/claude-3.5-sonnet')"
49
47
  )
50
48
  parser.add_argument(
51
- "--api-key",
52
- "-k",
49
+ "--api-key", "-k",
53
50
  type=str,
54
- help="OpenRouter API key (or set OPENROUTER_API_KEY env variable)",
51
+ help="OpenRouter API key (or set OPENROUTER_API_KEY env variable)"
55
52
  )
56
53
  parser.add_argument(
57
- "--version", "-v", action="store_true", help="Show version and exit"
54
+ "--version", "-v",
55
+ action="store_true",
56
+ help="Show version and exit"
58
57
  )
59
58
 
60
59
  args = parser.parse_args()
61
60
 
62
61
  if args.version:
63
62
  from . import __version__
64
-
65
63
  print(f"GeoMind version {__version__}")
66
64
  sys.exit(0)
67
65
 
@@ -104,11 +102,11 @@ def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
104
102
  if not user_input:
105
103
  continue
106
104
 
107
- if user_input.lower() in ["quit", "exit", "q"]:
105
+ if user_input.lower() in ['quit', 'exit', 'q']:
108
106
  print("\n👋 Goodbye!")
109
107
  break
110
108
 
111
- if user_input.lower() == "reset":
109
+ if user_input.lower() == 'reset':
112
110
  agent.reset()
113
111
  continue
114
112