geomind-ai 1.1.0__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.
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/PKG-INFO +1 -1
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind/agent.py +23 -14
- geomind_ai-1.1.1/geomind/cli.py +392 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind/tools/processing.py +32 -17
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind/tools/stac_search.py +19 -9
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind_ai.egg-info/PKG-INFO +1 -1
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/pyproject.toml +1 -1
- geomind_ai-1.1.0/geomind/cli.py +0 -199
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/LICENSE +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/MANIFEST.in +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/README.md +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind/__init__.py +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind/config.py +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind/tools/__init__.py +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind/tools/geocoding.py +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind_ai.egg-info/SOURCES.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind_ai.egg-info/dependency_links.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind_ai.egg-info/entry_points.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind_ai.egg-info/requires.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/geomind_ai.egg-info/top_level.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/requirements.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.1.1}/setup.cfg +0 -0
|
@@ -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"
|
|
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"
|
|
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"\
|
|
332
|
-
print("
|
|
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"\
|
|
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("
|
|
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("
|
|
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"\
|
|
434
|
+
print(f"\nError: {e}")
|
|
426
435
|
sys.exit(1)
|
|
427
436
|
except Exception as e:
|
|
428
|
-
print(f"\
|
|
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("\
|
|
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("\
|
|
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\
|
|
459
|
+
print("\n\nGoodbye!")
|
|
451
460
|
break
|
|
452
461
|
except Exception as e:
|
|
453
|
-
print(f"\
|
|
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()
|
|
@@ -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
|
-
#
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
geomind_ai-1.1.0/geomind/cli.py
DELETED
|
@@ -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: nvidia/nemotron-3-nano-30b-a3b: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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|