geomind-ai 1.0.9__tar.gz → 1.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geomind-ai
3
- Version: 1.0.9
3
+ Version: 1.1.1
4
4
  Summary: AI agent for geospatial analysis with Sentinel-2 satellite imagery
5
5
  Author: Harsh Shinde, Rajat Shinde
6
6
  License-Expression: MIT
@@ -3,7 +3,7 @@ GeoMind - Geospatial AI Agent
3
3
 
4
4
  """
5
5
 
6
- __version__ = "1.0.9"
6
+ __version__ = "1.1.0"
7
7
  __author__ = "Harsh Shinde, Rajat Shinde"
8
8
 
9
9
  from .agent import GeoMindAgent
@@ -245,7 +245,7 @@ class GeoMindAgent:
245
245
  Initialize the GeoMind agent.
246
246
 
247
247
  Args:
248
- model: Model name (default: xiaomi/mimo-v2-flash:free)
248
+ model: Model name (default: nvidia/nemotron-3-nano-30b-a3b:free)
249
249
  api_key: OpenRouter API key (required).
250
250
  """
251
251
  self.provider = "openrouter"
@@ -263,7 +263,7 @@ class GeoMindAgent:
263
263
  "3. Create .env file with: OPENROUTER_API_KEY=YOUR_KEY"
264
264
  )
265
265
 
266
- print(f"🚀 GeoMind Agent initialized with {self.model_name}")
266
+ print(f"GeoMind Agent initialized with {self.model_name}")
267
267
 
268
268
  # Create OpenAI-compatible client
269
269
  self.client = OpenAI(base_url=self.base_url, api_key=self.api_key)
@@ -292,10 +292,19 @@ Key information:
292
292
  - Bands available: B01-B12 at 10m, 20m, or 60m resolution
293
293
  - Current date: {datetime.now().strftime('%Y-%m-%d')}
294
294
 
295
+ IMPORTANT - Zarr URL usage:
296
+ - STAC search results include both SR_10m (base URL) and individual band assets (B02_10m, B03_10m, B04_10m, B08_10m)
297
+ - EITHER type of URL works for create_rgb_composite and calculate_ndvi:
298
+ * SR_10m URL: Points to .../measurements/reflectance/r10m (contains all bands as subdirectories)
299
+ * Individual band URLs: Point directly to specific bands like .../r10m/b02
300
+ - Prefer using SR_10m URL as it's simpler and works for all bands
301
+ - The processing functions automatically handle the correct path structure
302
+
295
303
  When users ask for imagery:
296
304
  1. First use get_bbox_from_location or list_recent_imagery to search
297
- 2. Present the results clearly with key metadata
305
+ 2. Present the results clearly with key metadata (ID, date, cloud cover)
298
306
  3. Offer to create visualizations if data is found
307
+ 4. For visualizations, use the SR_10m asset URL from search results
299
308
 
300
309
  Always explain what you're doing and interpret results in a helpful way."""
301
310
 
@@ -312,7 +321,7 @@ Always explain what you're doing and interpret results in a helpful way."""
312
321
 
313
322
  def _execute_function(self, name: str, args: dict) -> dict:
314
323
  """Execute a function call and return the result."""
315
- print(f" 🔧 Executing: {name}({args})")
324
+ print(f" Executing: {name}({args})")
316
325
 
317
326
  if name not in TOOL_FUNCTIONS:
318
327
  return {"error": f"Unknown function: {name}"}
@@ -328,8 +337,8 @@ Always explain what you're doing and interpret results in a helpful way."""
328
337
  Send a message to the agent and get a response.
329
338
  """
330
339
  if verbose:
331
- print(f"\n💬 User: {message}")
332
- print("🤔 Processing...")
340
+ print(f"\nUser: {message}")
341
+ print("Processing...")
333
342
 
334
343
  # Add user message to history
335
344
  self.history.append({"role": "user", "content": message})
@@ -395,7 +404,7 @@ Always explain what you're doing and interpret results in a helpful way."""
395
404
  self.history.append({"role": "assistant", "content": final_text})
396
405
 
397
406
  if verbose:
398
- print(f"\n🌍 GeoMind: {final_text}")
407
+ print(f"\nGeoMind: {final_text}")
399
408
 
400
409
  return final_text
401
410
 
@@ -404,7 +413,7 @@ Always explain what you're doing and interpret results in a helpful way."""
404
413
  def reset(self):
405
414
  """Reset the chat session."""
406
415
  self.history = []
407
- print("🔄 Chat session reset")
416
+ print("Chat session reset")
408
417
 
409
418
 
410
419
  def main(model: Optional[str] = None):
@@ -412,7 +421,7 @@ def main(model: Optional[str] = None):
412
421
  import sys
413
422
 
414
423
  print("=" * 60)
415
- print("🌍 GeoMind - Geospatial AI Agent")
424
+ print("GeoMind - Geospatial AI Agent")
416
425
  print("=" * 60)
417
426
  print("Powered by OpenRouter | Sentinel-2 Imagery")
418
427
  print("Type 'quit' or 'exit' to end the session")
@@ -422,22 +431,22 @@ def main(model: Optional[str] = None):
422
431
  try:
423
432
  agent = GeoMindAgent(model=model)
424
433
  except ValueError as e:
425
- print(f"\n❌ Error: {e}")
434
+ print(f"\nError: {e}")
426
435
  sys.exit(1)
427
436
  except Exception as e:
428
- print(f"\n❌ Error: {e}")
437
+ print(f"\nError: {e}")
429
438
  print("\nPlease check your API key and internet connection.")
430
439
  sys.exit(1)
431
440
 
432
441
  while True:
433
442
  try:
434
- user_input = input("\n💬 You: ").strip()
443
+ user_input = input("\nYou: ").strip()
435
444
 
436
445
  if not user_input:
437
446
  continue
438
447
 
439
448
  if user_input.lower() in ["quit", "exit", "q"]:
440
- print("\n👋 Goodbye!")
449
+ print("\nGoodbye!")
441
450
  break
442
451
 
443
452
  if user_input.lower() == "reset":
@@ -447,10 +456,10 @@ def main(model: Optional[str] = None):
447
456
  agent.chat(user_input)
448
457
 
449
458
  except KeyboardInterrupt:
450
- print("\n\n👋 Goodbye!")
459
+ print("\n\nGoodbye!")
451
460
  break
452
461
  except Exception as e:
453
- print(f"\n❌ Error: {e}")
462
+ print(f"\nError: {e}")
454
463
 
455
464
 
456
465
  if __name__ == "__main__":
@@ -0,0 +1,392 @@
1
+ """
2
+ Command-line interface for GeoMind.
3
+ """
4
+
5
+ import sys
6
+ import os
7
+ import argparse
8
+ from pathlib import Path
9
+ from typing import Optional
10
+ import subprocess
11
+ import platform
12
+ import threading
13
+ import time
14
+
15
+ from .agent import GeoMindAgent
16
+
17
+
18
+ # Config file path for storing API key
19
+ CONFIG_DIR = Path.home() / ".geomind"
20
+ CONFIG_FILE = CONFIG_DIR / "config"
21
+
22
+
23
+ def get_saved_api_key() -> Optional[str]:
24
+ """Get API key saved on user's PC."""
25
+ if CONFIG_FILE.exists():
26
+ try:
27
+ return CONFIG_FILE.read_text().strip()
28
+ except Exception:
29
+ return None
30
+ return None
31
+
32
+
33
+ def save_api_key(api_key: str) -> bool:
34
+ """Save API key to user's PC."""
35
+ try:
36
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
37
+ CONFIG_FILE.write_text(api_key)
38
+ return True
39
+ except Exception:
40
+ return False
41
+
42
+
43
+ def display_recent_images():
44
+ """Display recently created images if any exist."""
45
+ outputs_dir = Path("outputs")
46
+ if not outputs_dir.exists():
47
+ return
48
+
49
+ # Get recent image files (created in last few seconds)
50
+ import time
51
+ recent_threshold = time.time() - 30 # 30 seconds ago
52
+
53
+ recent_images = []
54
+ for ext in ['*.png', '*.jpg', '*.jpeg', '*.tiff']:
55
+ for img_file in outputs_dir.glob(ext):
56
+ if img_file.stat().st_mtime > recent_threshold:
57
+ recent_images.append(img_file)
58
+
59
+ if recent_images:
60
+ print("\n" + "="*60)
61
+ print("Generated Images:")
62
+ for img in recent_images:
63
+ print(f" • {img.name} ({img.stat().st_size // 1024}KB)")
64
+
65
+ # Try to open the most recent image
66
+ if recent_images:
67
+ latest_image = max(recent_images, key=lambda x: x.stat().st_mtime)
68
+ open_image_viewer(latest_image)
69
+ print("="*60)
70
+
71
+
72
+ def open_image_viewer(image_path: Path):
73
+ """Open image in default viewer."""
74
+ try:
75
+ system = platform.system()
76
+ if system == "Windows":
77
+ os.startfile(str(image_path))
78
+ elif system == "Darwin": # macOS
79
+ subprocess.run(["open", str(image_path)], check=False)
80
+ else: # Linux
81
+ subprocess.run(["xdg-open", str(image_path)], check=False)
82
+ print(f" -> Opened {image_path.name} in default viewer")
83
+ except Exception:
84
+ print(f" -> Saved to: {image_path}")
85
+
86
+
87
+ def format_response_box(title: str, content: str, color_code: str = "\033[94m") -> str:
88
+ """Format response in an attractive box."""
89
+ RESET = "\033[0m"
90
+ lines = content.split('\n')
91
+ max_width = max(len(line) for line in lines) if lines else 0
92
+ max_width = max(max_width, len(title) + 4)
93
+ width = min(max_width + 4, 80)
94
+
95
+ box = f"{color_code}"
96
+ box += "┌" + "─" * (width - 2) + "┐\n"
97
+ box += f"│ {title:<{width-4}} │\n"
98
+ box += "├" + "─" * (width - 2) + "┤\n"
99
+
100
+ for line in lines:
101
+ if line.strip():
102
+ box += f"│ {line:<{width-4}} │\n"
103
+ else:
104
+ box += f"│{' ' * (width-2)}│\n"
105
+
106
+ box += "└" + "─" * (width - 2) + "┘"
107
+ box += RESET
108
+ return box
109
+
110
+
111
+ class ThinkingIndicator:
112
+ """Claude Code style thinking animation."""
113
+
114
+ def __init__(self):
115
+ self.is_thinking = False
116
+ self.thread = None
117
+ self.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
118
+ self.thinking_messages = [
119
+ "Thinking",
120
+ "Analyzing satellite data",
121
+ "Processing request",
122
+ "Searching imagery"
123
+ ]
124
+
125
+ def start(self):
126
+ """Start the thinking animation."""
127
+ self.is_thinking = True
128
+ self.thread = threading.Thread(target=self._animate)
129
+ self.thread.daemon = True
130
+ self.thread.start()
131
+
132
+ def stop(self):
133
+ """Stop the thinking animation."""
134
+ self.is_thinking = False
135
+ if self.thread:
136
+ self.thread.join(timeout=0.1)
137
+ # Clear the line
138
+ print("\r" + " " * 60 + "\r", end="", flush=True)
139
+
140
+ def _animate(self):
141
+ """Run the thinking animation."""
142
+ frame_idx = 0
143
+ message_idx = 0
144
+ message_counter = 0
145
+
146
+ # Colors like Claude Code
147
+ DIM = '\033[2m'
148
+ RESET = '\033[0m'
149
+
150
+ while self.is_thinking:
151
+ spinner = self.frames[frame_idx % len(self.frames)]
152
+
153
+ # Cycle through thinking messages every 30 frames (3 seconds)
154
+ if message_counter % 30 == 0:
155
+ message_idx = (message_idx + 1) % len(self.thinking_messages)
156
+
157
+ message = self.thinking_messages[message_idx]
158
+
159
+ # Show thinking with shimmer effect like Claude Code
160
+ print(f"\r{DIM}{spinner} {message}...{RESET}", end="", flush=True)
161
+
162
+ time.sleep(0.1)
163
+ frame_idx += 1
164
+ message_counter += 1
165
+
166
+
167
+ def main():
168
+ """Main CLI entry point for the geomind package."""
169
+ parser = argparse.ArgumentParser(
170
+ description="GeoMind - AI agent for geospatial analysis with Sentinel-2 imagery",
171
+ formatter_class=argparse.RawDescriptionHelpFormatter,
172
+ epilog="""
173
+ Examples:
174
+ # Interactive mode
175
+ geomind
176
+
177
+ # Single query
178
+ geomind --query "Find recent imagery of Paris"
179
+
180
+ # With custom model
181
+ geomind --model "anthropic/claude-3.5-sonnet"
182
+
183
+ # With API key
184
+ geomind --api-key "your-openrouter-api-key"
185
+
186
+ # Clear saved API key
187
+ geomind --clear-key
188
+
189
+ Environment Variables:
190
+ OPENROUTER_API_KEY Your OpenRouter API key
191
+ OPENROUTER_MODEL Model to use (default: nvidia/nemotron-3-nano-30b-a3b:free)
192
+ OPENROUTER_API_URL API endpoint (default: https://openrouter.ai/api/v1)
193
+ """,
194
+ )
195
+
196
+ parser.add_argument(
197
+ "--query",
198
+ "-q",
199
+ type=str,
200
+ help="Single query to run (if not provided, starts interactive mode)",
201
+ )
202
+ parser.add_argument(
203
+ "--model",
204
+ "-m",
205
+ type=str,
206
+ help="Model name to use (e.g., 'anthropic/claude-3.5-sonnet')",
207
+ )
208
+ parser.add_argument(
209
+ "--api-key",
210
+ "-k",
211
+ type=str,
212
+ help="OpenRouter API key (or set OPENROUTER_API_KEY env variable)",
213
+ )
214
+ parser.add_argument(
215
+ "--version", "-v", action="store_true", help="Show version and exit"
216
+ )
217
+ parser.add_argument("--clear-key", action="store_true", help="Clear saved API key")
218
+
219
+ args = parser.parse_args()
220
+
221
+ if args.clear_key:
222
+ if CONFIG_FILE.exists():
223
+ CONFIG_FILE.unlink()
224
+ print("Saved API key cleared.")
225
+ else:
226
+ print("No saved API key found.")
227
+ sys.exit(0)
228
+
229
+ if args.version:
230
+ from . import __version__
231
+
232
+ print(f"GeoMind version {__version__}")
233
+ sys.exit(0)
234
+
235
+ # Start interactive or single-query mode
236
+ try:
237
+ if args.query:
238
+ # Single query mode - check API key in order: argument > env > saved file
239
+ from .config import OPENROUTER_API_KEY
240
+
241
+ api_key = args.api_key or OPENROUTER_API_KEY or get_saved_api_key()
242
+ if not api_key:
243
+ print("Error: No API key found. Run 'geomind' first to set up.")
244
+ sys.exit(1)
245
+ agent = GeoMindAgent(model=args.model, api_key=api_key)
246
+ agent.chat(args.query)
247
+ else:
248
+ # Interactive mode
249
+ run_interactive(model=args.model, api_key=args.api_key)
250
+ except ValueError as e:
251
+ print(f"\nError: {e}")
252
+ sys.exit(1)
253
+ except KeyboardInterrupt:
254
+ print("\n\nGoodbye!")
255
+ sys.exit(0)
256
+ except Exception as e:
257
+ print(f"\nUnexpected error: {e}")
258
+ sys.exit(1)
259
+
260
+
261
+ def print_banner():
262
+ from . import __version__
263
+
264
+ # ANSI color codes
265
+ BOLD = '\033[1m'
266
+ DIM = '\033[2m'
267
+ RESET = '\033[0m'
268
+
269
+ banner = f"""
270
+ ┌──────────────────────────────────────────────────────────────────────┐
271
+ │ {BOLD}>_ GeoMind{RESET} (v{__version__}) │
272
+ │ │
273
+ │ model: nvidia/nemotron-3-nano-30b-a3b:free │
274
+ │ docs: https://harshshinde0.github.io/GeoMind │
275
+ │ authors: Harsh Shinde, Rajat Shinde │
276
+ │ official: https://harshshinde0.github.io/GeoMind │
277
+ │ │
278
+ │ Type "?" for help, "quit" to exit. │
279
+ └──────────────────────────────────────────────────────────────────────┘
280
+ """
281
+ print(banner)
282
+ print()
283
+
284
+
285
+ def print_help():
286
+ """Print interactive session help."""
287
+ help_text = """
288
+ Interactive Commands:
289
+ help, ? Show this help
290
+ reset Reset conversation
291
+ exit, quit, q Exit GeoMind
292
+
293
+ Query Examples:
294
+ > Find recent Sentinel-2 imagery of Paris
295
+ > Show me NDVI data for the Amazon rainforest
296
+ > Search for images with less than 10% cloud cover in London
297
+ > Get satellite data for coordinates 40.7128, -74.0060
298
+
299
+ For CLI options, run: geomind --help
300
+ """
301
+ print(help_text)
302
+
303
+
304
+ def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
305
+ """Run interactive CLI mode."""
306
+ from . import __version__
307
+
308
+ print_banner()
309
+
310
+ # Check for API key in order: argument > env > saved file
311
+ from .config import OPENROUTER_API_KEY
312
+
313
+ # Priority: command line arg > env variable > saved file
314
+ if api_key:
315
+ # Use provided argument
316
+ pass
317
+ elif OPENROUTER_API_KEY:
318
+ api_key = OPENROUTER_API_KEY
319
+ else:
320
+ api_key = get_saved_api_key()
321
+
322
+ if not api_key:
323
+ print("\nOpenRouter API key required (FREE)")
324
+ print(" Get yours at: https://openrouter.ai/settings/keys\n")
325
+ api_key = input(" Enter your API key: ").strip()
326
+
327
+ if not api_key:
328
+ print("\nNo API key provided. Exiting.")
329
+ return
330
+
331
+ # Save the key for future use
332
+ if save_api_key(api_key):
333
+ print(" API key saved! You won't need to enter it again.\n")
334
+ else:
335
+ print(" Warning: Could not save API key. You'll need to enter it next time.\n")
336
+
337
+ agent = GeoMindAgent(model=model, api_key=api_key)
338
+
339
+ # Claude Code style color scheme
340
+ CYAN = '\033[96m'
341
+ DIM = '\033[2m'
342
+ BOLD = '\033[1m'
343
+ RESET = '\033[0m'
344
+
345
+ while True:
346
+ try:
347
+ # Simple prompt like Claude Code
348
+ user_input = input(f"\n{CYAN}>{RESET} ").strip()
349
+
350
+ if not user_input:
351
+ continue
352
+
353
+ if user_input.lower() in ["quit", "exit", "q"]:
354
+ print(f"\n{DIM}Goodbye!{RESET}")
355
+ break
356
+
357
+ if user_input.lower() == "reset":
358
+ agent.reset()
359
+ print(f"{DIM}Started new conversation{RESET}")
360
+ continue
361
+
362
+ if user_input.lower() in ["help", "?"]:
363
+ print_help()
364
+ continue
365
+
366
+ # Start thinking animation
367
+ thinking = ThinkingIndicator()
368
+ thinking.start()
369
+
370
+ try:
371
+ # Get response from agent
372
+ response = agent.chat(user_input, verbose=False)
373
+
374
+ # Stop thinking animation
375
+ thinking.stop()
376
+
377
+ # Display response cleanly like Claude Code
378
+ print(f"\n{response}")
379
+
380
+ except Exception as chat_error:
381
+ thinking.stop()
382
+ raise chat_error
383
+
384
+ except KeyboardInterrupt:
385
+ print(f"\n\n{DIM}Goodbye!{RESET}")
386
+ break
387
+ except Exception as e:
388
+ print(f"\n{DIM}Error: {e}{RESET}")
389
+
390
+
391
+ if __name__ == "__main__":
392
+ main()
@@ -52,4 +52,4 @@ GEOCODER_USER_AGENT = "geomind_agent_v0.1"
52
52
  # Get your free API key at: https://openrouter.ai/settings/keys
53
53
  OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
54
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")
55
+ OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "nvidia/nemotron-3-nano-30b-a3b:free")
@@ -106,7 +106,7 @@ def create_rgb_composite(
106
106
  Uses B04 (Red), B03 (Green), B02 (Blue) bands.
107
107
 
108
108
  Args:
109
- zarr_url: URL to the SR_10m Zarr asset
109
+ zarr_url: URL to the SR_10m Zarr asset or individual band asset URL
110
110
  output_path: Optional path to save the image
111
111
  subset_size: Size to subset the image (for faster processing)
112
112
 
@@ -117,15 +117,23 @@ def create_rgb_composite(
117
117
  import xarray as xr
118
118
  import zarr
119
119
 
120
- # Open the Zarr store
121
- # The SR_10m asset contains b02, b03, b04, b08
122
- store = zarr.open(zarr_url, mode="r")
123
-
124
- # Read the bands
125
- # Note: Band names are lowercase in the Zarr structure
126
- red = np.array(store["b04"])
127
- green = np.array(store["b03"])
128
- blue = np.array(store["b02"])
120
+ # Determine if this is a band-specific URL or base SR_10m URL
121
+ # Band-specific URLs end with /b02, /b03, /b04, etc.
122
+ # Base SR_10m URLs end with /r10m
123
+ is_band_url = zarr_url.rstrip('/').split('/')[-1].startswith('b')
124
+
125
+ if is_band_url:
126
+ # Individual band URL provided - need to construct URLs for each band
127
+ base_url = '/'.join(zarr_url.rstrip('/').split('/')[:-1])
128
+ red = np.array(zarr.open(f"{base_url}/b04", mode="r"))
129
+ green = np.array(zarr.open(f"{base_url}/b03", mode="r"))
130
+ blue = np.array(zarr.open(f"{base_url}/b02", mode="r"))
131
+ else:
132
+ # Base SR_10m URL - bands are subdirectories
133
+ base_url = zarr_url.rstrip('/')
134
+ red = np.array(zarr.open(f"{base_url}/b04", mode="r"))
135
+ green = np.array(zarr.open(f"{base_url}/b03", mode="r"))
136
+ blue = np.array(zarr.open(f"{base_url}/b02", mode="r"))
129
137
 
130
138
  # Subset if requested (for faster processing)
131
139
  if subset_size and red.shape[0] > subset_size:
@@ -197,7 +205,7 @@ def calculate_ndvi(
197
205
  Uses B08 (NIR) and B04 (Red) bands.
198
206
 
199
207
  Args:
200
- zarr_url: URL to the SR_10m Zarr asset
208
+ zarr_url: URL to the SR_10m Zarr asset or individual band asset URL
201
209
  output_path: Optional path to save the NDVI image
202
210
  subset_size: Size to subset the image
203
211
 
@@ -208,12 +216,19 @@ def calculate_ndvi(
208
216
  import zarr
209
217
  from matplotlib.colors import LinearSegmentedColormap
210
218
 
211
- # Open the Zarr store
212
- store = zarr.open(zarr_url, mode="r")
213
-
214
- # Read the bands
215
- nir = np.array(store["b08"]) # NIR
216
- red = np.array(store["b04"]) # Red
219
+ # Determine if this is a band-specific URL or base SR_10m URL
220
+ is_band_url = zarr_url.rstrip('/').split('/')[-1].startswith('b')
221
+
222
+ if is_band_url:
223
+ # Individual band URL provided
224
+ base_url = '/'.join(zarr_url.rstrip('/').split('/')[:-1])
225
+ nir = np.array(zarr.open(f"{base_url}/b08", mode="r"))
226
+ red = np.array(zarr.open(f"{base_url}/b04", mode="r"))
227
+ else:
228
+ # Base SR_10m URL
229
+ base_url = zarr_url.rstrip('/')
230
+ nir = np.array(zarr.open(f"{base_url}/b08", mode="r"))
231
+ red = np.array(zarr.open(f"{base_url}/b04", mode="r"))
217
232
 
218
233
  # Subset if requested
219
234
  if subset_size and nir.shape[0] > subset_size:
@@ -25,6 +25,24 @@ def _format_item(item) -> dict:
25
25
  """Format a STAC item into a simplified dictionary."""
26
26
  props = item.properties
27
27
 
28
+ # Extract individual band assets for direct access
29
+ assets = {}
30
+ for key, asset in item.assets.items():
31
+ if key in ["SR_10m", "SR_20m", "SR_60m", "TCI_10m", "product"]:
32
+ assets[key] = {
33
+ "title": asset.title,
34
+ "href": asset.href,
35
+ "type": asset.media_type,
36
+ }
37
+ # Include individual 10m band assets for direct access
38
+ elif key in ["B02_10m", "B03_10m", "B04_10m", "B08_10m"]:
39
+ assets[key] = {
40
+ "title": asset.title,
41
+ "href": asset.href,
42
+ "type": asset.media_type,
43
+ "band": key.split("_")[0].lower(), # Extract band name (b02, b03, b04, b08)
44
+ }
45
+
28
46
  return {
29
47
  "id": item.id,
30
48
  "datetime": props.get("datetime"),
@@ -32,15 +50,7 @@ def _format_item(item) -> dict:
32
50
  "platform": props.get("platform"),
33
51
  "bbox": item.bbox,
34
52
  "geometry": item.geometry,
35
- "assets": {
36
- key: {
37
- "title": asset.title,
38
- "href": asset.href,
39
- "type": asset.media_type,
40
- }
41
- for key, asset in item.assets.items()
42
- if key in ["SR_10m", "SR_20m", "SR_60m", "TCI_10m", "product"]
43
- },
53
+ "assets": assets,
44
54
  "stac_url": f"{STAC_API_URL}/collections/{STAC_COLLECTION}/items/{item.id}",
45
55
  }
46
56
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geomind-ai
3
- Version: 1.0.9
3
+ Version: 1.1.1
4
4
  Summary: AI agent for geospatial analysis with Sentinel-2 satellite imagery
5
5
  Author: Harsh Shinde, Rajat Shinde
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "geomind-ai"
7
- version = "1.0.9"
7
+ version = "1.1.1"
8
8
  description = "AI agent for geospatial analysis with Sentinel-2 satellite imagery"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -1,199 +0,0 @@
1
- """
2
- Command-line interface for GeoMind.
3
- """
4
-
5
- import sys
6
- import os
7
- import argparse
8
- from pathlib import Path
9
- from typing import Optional
10
-
11
- from .agent import GeoMindAgent
12
-
13
-
14
- # Config file path for storing API key
15
- CONFIG_DIR = Path.home() / ".geomind"
16
- CONFIG_FILE = CONFIG_DIR / "config"
17
-
18
-
19
- def get_saved_api_key() -> Optional[str]:
20
- """Get API key saved on user's PC."""
21
- if CONFIG_FILE.exists():
22
- try:
23
- return CONFIG_FILE.read_text().strip()
24
- except Exception:
25
- return None
26
- return None
27
-
28
-
29
- def save_api_key(api_key: str) -> bool:
30
- """Save API key to user's PC."""
31
- try:
32
- CONFIG_DIR.mkdir(parents=True, exist_ok=True)
33
- CONFIG_FILE.write_text(api_key)
34
- return True
35
- except Exception:
36
- return False
37
-
38
-
39
- def main():
40
- """Main CLI entry point for the geomind package."""
41
- parser = argparse.ArgumentParser(
42
- description="GeoMind - AI agent for geospatial analysis with Sentinel-2 imagery",
43
- formatter_class=argparse.RawDescriptionHelpFormatter,
44
- epilog="""
45
- Examples:
46
- # Interactive mode
47
- geomind
48
-
49
- # Single query
50
- geomind --query "Find recent imagery of Paris"
51
-
52
- # With custom model
53
- geomind --model "anthropic/claude-3.5-sonnet"
54
-
55
- # With API key
56
- geomind --api-key "your-openrouter-api-key"
57
-
58
- # Clear saved API key
59
- geomind --clear-key
60
-
61
- Environment Variables:
62
- OPENROUTER_API_KEY Your OpenRouter API key
63
- OPENROUTER_MODEL Model to use (default: xiaomi/mimo-v2-flash:free)
64
- OPENROUTER_API_URL API endpoint (default: https://openrouter.ai/api/v1)
65
- """,
66
- )
67
-
68
- parser.add_argument(
69
- "--query",
70
- "-q",
71
- type=str,
72
- help="Single query to run (if not provided, starts interactive mode)",
73
- )
74
- parser.add_argument(
75
- "--model",
76
- "-m",
77
- type=str,
78
- help="Model name to use (e.g., 'anthropic/claude-3.5-sonnet')",
79
- )
80
- parser.add_argument(
81
- "--api-key",
82
- "-k",
83
- type=str,
84
- help="OpenRouter API key (or set OPENROUTER_API_KEY env variable)",
85
- )
86
- parser.add_argument(
87
- "--version", "-v", action="store_true", help="Show version and exit"
88
- )
89
- parser.add_argument("--clear-key", action="store_true", help="Clear saved API key")
90
-
91
- args = parser.parse_args()
92
-
93
- if args.clear_key:
94
- if CONFIG_FILE.exists():
95
- CONFIG_FILE.unlink()
96
- print("✅ Saved API key cleared.")
97
- else:
98
- print("ℹ️ No saved API key found.")
99
- sys.exit(0)
100
-
101
- if args.version:
102
- from . import __version__
103
-
104
- print(f"GeoMind version {__version__}")
105
- sys.exit(0)
106
-
107
- # Start interactive or single-query mode
108
- try:
109
- if args.query:
110
- # Single query mode - check API key in order: argument > env > saved file
111
- from .config import OPENROUTER_API_KEY
112
-
113
- api_key = args.api_key or OPENROUTER_API_KEY or get_saved_api_key()
114
- if not api_key:
115
- print("❌ No API key found. Run 'geomind' first to set up.")
116
- sys.exit(1)
117
- agent = GeoMindAgent(model=args.model, api_key=api_key)
118
- agent.chat(args.query)
119
- else:
120
- # Interactive mode
121
- run_interactive(model=args.model, api_key=args.api_key)
122
- except ValueError as e:
123
- print(f"\n❌ Error: {e}")
124
- sys.exit(1)
125
- except KeyboardInterrupt:
126
- print("\n\n👋 Goodbye!")
127
- sys.exit(0)
128
- except Exception as e:
129
- print(f"\n❌ Unexpected error: {e}")
130
- sys.exit(1)
131
-
132
-
133
- def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
134
- """Run interactive CLI mode."""
135
- from . import __version__
136
-
137
- print("=" * 60)
138
- print("🌍 GeoMind - Geospatial AI Agent")
139
- print("=" * 60)
140
- print(f"Version: {__version__} | Authors: Harsh Shinde, Rajat Shinde")
141
- print("Type 'quit' or 'exit' to end the session")
142
- print("Type 'reset' to start a new conversation")
143
- print("Type 'geomind --help' for more options")
144
-
145
- # Check for API key in order: argument > env > saved file
146
- from .config import OPENROUTER_API_KEY
147
-
148
- # Priority: command line arg > env variable > saved file
149
- if api_key:
150
- # Use provided argument
151
- pass
152
- elif OPENROUTER_API_KEY:
153
- api_key = OPENROUTER_API_KEY
154
- else:
155
- api_key = get_saved_api_key()
156
-
157
- if not api_key:
158
- print("\n🔑 OpenRouter API key required (FREE)")
159
- print(" Get yours at: https://openrouter.ai/settings/keys\n")
160
- api_key = input(" Enter your API key: ").strip()
161
-
162
- if not api_key:
163
- print("\n❌ No API key provided. Exiting.")
164
- return
165
-
166
- # Save the key for future use
167
- if save_api_key(api_key):
168
- print(" ✅ API key saved! You won't need to enter it again.\n")
169
- else:
170
- print(" ⚠️ Could not save API key. You'll need to enter it next time.\n")
171
-
172
- agent = GeoMindAgent(model=model, api_key=api_key)
173
-
174
- while True:
175
- try:
176
- user_input = input("\n💬 You: ").strip()
177
-
178
- if not user_input:
179
- continue
180
-
181
- if user_input.lower() in ["quit", "exit", "q"]:
182
- print("\n👋 Goodbye!")
183
- break
184
-
185
- if user_input.lower() == "reset":
186
- agent.reset()
187
- continue
188
-
189
- agent.chat(user_input)
190
-
191
- except KeyboardInterrupt:
192
- print("\n\n👋 Goodbye!")
193
- break
194
- except Exception as e:
195
- print(f"\n❌ Error: {e}")
196
-
197
-
198
- if __name__ == "__main__":
199
- main()
File without changes
File without changes
File without changes
File without changes
File without changes