geomind-ai 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
geomind/__init__.py CHANGED
@@ -3,7 +3,7 @@ GeoMind - Geospatial AI Agent
3
3
 
4
4
  """
5
5
 
6
- __version__ = "1.1.0"
6
+ __version__ = "1.2.0"
7
7
  __author__ = "Harsh Shinde, Rajat Shinde"
8
8
 
9
9
  from .agent import GeoMindAgent
geomind/agent.py CHANGED
@@ -1,144 +1,50 @@
1
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.
2
+ GeoMind Agent - Simplified AI agent for satellite imagery analysis.
6
3
  """
7
4
 
8
5
  import json
9
- import re
10
- from typing import Optional, Callable, Any
6
+ from typing import Optional
11
7
  from datetime import datetime
12
-
13
8
  from openai import OpenAI
14
9
 
15
- from .config import (
16
- OPENROUTER_API_KEY,
17
- OPENROUTER_API_URL,
18
- OPENROUTER_MODEL,
19
- )
10
+ from .config import OPENROUTER_API_KEY, OPENROUTER_API_URL, OPENROUTER_MODEL
20
11
  from .tools import (
21
- geocode_location,
22
- get_bbox_from_location,
23
- search_imagery,
24
- get_item_details,
25
12
  list_recent_imagery,
26
13
  create_rgb_composite,
27
14
  calculate_ndvi,
15
+ search_imagery,
16
+ get_bbox_from_location,
17
+ geocode_location,
18
+ get_item_details,
28
19
  get_band_statistics,
29
20
  )
30
21
 
31
-
32
- # Map tool names to functions
22
+ # Tool function mapping
33
23
  TOOL_FUNCTIONS = {
34
- "geocode_location": geocode_location,
35
- "get_bbox_from_location": get_bbox_from_location,
36
- "search_imagery": search_imagery,
37
24
  "list_recent_imagery": list_recent_imagery,
38
- "get_item_details": get_item_details,
39
25
  "create_rgb_composite": create_rgb_composite,
40
26
  "calculate_ndvi": calculate_ndvi,
27
+ "search_imagery": search_imagery,
28
+ "get_bbox_from_location": get_bbox_from_location,
29
+ "geocode_location": geocode_location,
30
+ "get_item_details": get_item_details,
41
31
  "get_band_statistics": get_band_statistics,
42
32
  }
43
33
 
44
- # Tool definitions for the LLM
34
+ # Simplified tool definitions
45
35
  TOOLS = [
46
- {
47
- "type": "function",
48
- "function": {
49
- "name": "geocode_location",
50
- "description": "Convert a place name to geographic coordinates (latitude, longitude). Use this when you need to find coordinates for a location.",
51
- "parameters": {
52
- "type": "object",
53
- "properties": {
54
- "place_name": {
55
- "type": "string",
56
- "description": "The name of the place to geocode (e.g., 'New York City', 'Paris, France')",
57
- }
58
- },
59
- "required": ["place_name"],
60
- },
61
- },
62
- },
63
- {
64
- "type": "function",
65
- "function": {
66
- "name": "get_bbox_from_location",
67
- "description": "Get a bounding box for a location, suitable for searching satellite imagery.",
68
- "parameters": {
69
- "type": "object",
70
- "properties": {
71
- "place_name": {
72
- "type": "string",
73
- "description": "The name of the place",
74
- },
75
- "buffer_km": {
76
- "type": "number",
77
- "description": "Buffer distance in kilometers (default: 10)",
78
- },
79
- },
80
- "required": ["place_name"],
81
- },
82
- },
83
- },
84
- {
85
- "type": "function",
86
- "function": {
87
- "name": "search_imagery",
88
- "description": "Search for Sentinel-2 satellite imagery in the EOPF catalog. Returns available scenes.",
89
- "parameters": {
90
- "type": "object",
91
- "properties": {
92
- "bbox": {
93
- "type": "array",
94
- "items": {"type": "number"},
95
- "description": "Bounding box as [min_lon, min_lat, max_lon, max_lat]",
96
- },
97
- "start_date": {
98
- "type": "string",
99
- "description": "Start date in YYYY-MM-DD format",
100
- },
101
- "end_date": {
102
- "type": "string",
103
- "description": "End date in YYYY-MM-DD format",
104
- },
105
- "max_cloud_cover": {
106
- "type": "number",
107
- "description": "Maximum cloud cover percentage (0-100)",
108
- },
109
- "max_items": {
110
- "type": "integer",
111
- "description": "Maximum number of results",
112
- },
113
- },
114
- "required": [],
115
- },
116
- },
117
- },
118
36
  {
119
37
  "type": "function",
120
38
  "function": {
121
39
  "name": "list_recent_imagery",
122
- "description": "List recent Sentinel-2 imagery for a location. Combines geocoding and search.",
40
+ "description": "Find recent Sentinel-2 imagery for a location",
123
41
  "parameters": {
124
42
  "type": "object",
125
43
  "properties": {
126
- "location_name": {
127
- "type": "string",
128
- "description": "Name of the location to search",
129
- },
130
- "days": {
131
- "type": "integer",
132
- "description": "Number of days to look back (default: 7)",
133
- },
134
- "max_cloud_cover": {
135
- "type": "number",
136
- "description": "Maximum cloud cover percentage",
137
- },
138
- "max_items": {
139
- "type": "integer",
140
- "description": "Maximum number of results",
141
- },
44
+ "location_name": {"type": "string", "description": "Location name"},
45
+ "days": {"type": "integer", "description": "Days to look back (default: 14)"},
46
+ "max_cloud_cover": {"type": "number", "description": "Max cloud cover %"},
47
+ "max_items": {"type": "integer", "description": "Max results"},
142
48
  },
143
49
  "required": [],
144
50
  },
@@ -147,37 +53,30 @@ TOOLS = [
147
53
  {
148
54
  "type": "function",
149
55
  "function": {
150
- "name": "get_item_details",
151
- "description": "Get detailed information about a specific Sentinel-2 scene by its ID.",
56
+ "name": "create_rgb_composite",
57
+ "description": "Create RGB composite from Sentinel-2 data",
152
58
  "parameters": {
153
59
  "type": "object",
154
60
  "properties": {
155
- "item_id": {"type": "string", "description": "The STAC item ID"}
61
+ "zarr_url": {"type": "string", "description": "SR_10m Zarr URL"},
62
+ "location_name": {"type": "string", "description": "Location for title"},
63
+ "subset_size": {"type": "integer", "description": "Image size (default: 1000)"},
156
64
  },
157
- "required": ["item_id"],
65
+ "required": ["zarr_url"],
158
66
  },
159
67
  },
160
68
  },
161
69
  {
162
70
  "type": "function",
163
71
  "function": {
164
- "name": "create_rgb_composite",
165
- "description": "Create an RGB true-color composite image from Sentinel-2 data.",
72
+ "name": "calculate_ndvi",
73
+ "description": "Calculate NDVI vegetation index",
166
74
  "parameters": {
167
75
  "type": "object",
168
76
  "properties": {
169
- "zarr_url": {
170
- "type": "string",
171
- "description": "URL to the SR_10m Zarr asset from a STAC item",
172
- },
173
- "output_path": {
174
- "type": "string",
175
- "description": "Optional path to save the output image",
176
- },
177
- "subset_size": {
178
- "type": "integer",
179
- "description": "Size to subset the image (default: 1000 pixels)",
180
- },
77
+ "zarr_url": {"type": "string", "description": "SR_10m Zarr URL"},
78
+ "location_name": {"type": "string", "description": "Location for title"},
79
+ "subset_size": {"type": "integer", "description": "Image size (default: 1000)"},
181
80
  },
182
81
  "required": ["zarr_url"],
183
82
  },
@@ -186,47 +85,33 @@ TOOLS = [
186
85
  {
187
86
  "type": "function",
188
87
  "function": {
189
- "name": "calculate_ndvi",
190
- "description": "Calculate NDVI (vegetation index) from Sentinel-2 data.",
88
+ "name": "search_imagery",
89
+ "description": "Search Sentinel-2 imagery by parameters",
191
90
  "parameters": {
192
91
  "type": "object",
193
92
  "properties": {
194
- "zarr_url": {
195
- "type": "string",
196
- "description": "URL to the SR_10m Zarr asset",
197
- },
198
- "output_path": {
199
- "type": "string",
200
- "description": "Optional path to save the NDVI image",
201
- },
202
- "subset_size": {
203
- "type": "integer",
204
- "description": "Size to subset the image",
205
- },
93
+ "bbox": {"type": "array", "items": {"type": "number"}, "description": "Bounding box"},
94
+ "start_date": {"type": "string", "description": "Start date YYYY-MM-DD"},
95
+ "end_date": {"type": "string", "description": "End date YYYY-MM-DD"},
96
+ "max_cloud_cover": {"type": "number", "description": "Max cloud cover %"},
97
+ "max_items": {"type": "integer", "description": "Max results"},
206
98
  },
207
- "required": ["zarr_url"],
99
+ "required": [],
208
100
  },
209
101
  },
210
102
  },
211
103
  {
212
104
  "type": "function",
213
105
  "function": {
214
- "name": "get_band_statistics",
215
- "description": "Get statistics (min, max, mean) for spectral bands.",
106
+ "name": "get_bbox_from_location",
107
+ "description": "Get bounding box for a location",
216
108
  "parameters": {
217
109
  "type": "object",
218
110
  "properties": {
219
- "zarr_url": {
220
- "type": "string",
221
- "description": "URL to the Zarr asset",
222
- },
223
- "bands": {
224
- "type": "array",
225
- "items": {"type": "string"},
226
- "description": "List of band names to analyze",
227
- },
111
+ "place_name": {"type": "string", "description": "Location name"},
112
+ "buffer_km": {"type": "number", "description": "Buffer distance in km"},
228
113
  },
229
- "required": ["zarr_url"],
114
+ "required": ["place_name"],
230
115
  },
231
116
  },
232
117
  },
@@ -234,224 +119,100 @@ TOOLS = [
234
119
 
235
120
 
236
121
  class GeoMindAgent:
237
- """
238
- GeoMind - An AI agent for geospatial analysis with Sentinel-2 imagery.
239
-
240
- Uses OpenRouter API for access to multiple AI models.
241
- """
122
+ """Simplified GeoMind agent for satellite imagery analysis."""
242
123
 
243
124
  def __init__(self, model: Optional[str] = None, api_key: Optional[str] = None):
244
- """
245
- Initialize the GeoMind agent.
246
-
247
- Args:
248
- model: Model name (default: nvidia/nemotron-3-nano-30b-a3b:free)
249
- api_key: OpenRouter API key (required).
250
- """
251
- self.provider = "openrouter"
252
125
  self.api_key = api_key or OPENROUTER_API_KEY
253
126
  self.model_name = model or OPENROUTER_MODEL
254
- self.base_url = OPENROUTER_API_URL
255
-
127
+
256
128
  if not self.api_key:
257
- raise ValueError(
258
- "OpenRouter API key required.\n"
259
- "Get your FREE API key at: https://openrouter.ai/settings/keys\n\n"
260
- "Then provide it in one of these ways:\n"
261
- "1. Run: geomind --api-key YOUR_KEY\n"
262
- "2. Set environment variable: OPENROUTER_API_KEY=YOUR_KEY\n"
263
- "3. Create .env file with: OPENROUTER_API_KEY=YOUR_KEY"
264
- )
265
-
266
- print(f"🚀 GeoMind Agent initialized with {self.model_name}")
267
-
268
- # Create OpenAI-compatible client
269
- self.client = OpenAI(base_url=self.base_url, api_key=self.api_key)
270
-
271
- # Chat history
129
+ raise ValueError("OpenRouter API key required")
130
+
131
+ print(f"GeoMind Agent initialized with {self.model_name}")
132
+ self.client = OpenAI(base_url=OPENROUTER_API_URL, api_key=self.api_key)
272
133
  self.history = []
273
134
 
274
- # Add system message
275
- self.system_prompt = self._get_system_prompt()
276
-
277
135
  def _get_system_prompt(self) -> str:
278
- """Get the system prompt for the agent."""
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
281
- from the EOPF (ESA Earth Observation Processing Framework) catalog.
282
-
283
- Your capabilities include:
284
- 1. **Search**: Find Sentinel-2 L2A imagery by location, date, and cloud cover
285
- 2. **Geocoding**: Convert place names to coordinates for searching
286
- 3. **Visualization**: Create RGB composites and NDVI maps from imagery
287
- 4. **Analysis**: Calculate spectral indices and band statistics
288
-
289
- Key information:
290
- - Data source: EOPF STAC API (https://stac.core.eopf.eodc.eu)
291
- - Satellite: Sentinel-2 (L2A surface reflectance products)
292
- - Bands available: B01-B12 at 10m, 20m, or 60m resolution
293
- - Current date: {datetime.now().strftime('%Y-%m-%d')}
294
-
295
- When users ask for imagery:
296
- 1. First use get_bbox_from_location or list_recent_imagery to search
297
- 2. Present the results clearly with key metadata
298
- 3. Offer to create visualizations if data is found
299
-
300
- Always explain what you're doing and interpret results in a helpful way."""
301
-
302
- def _call_llm(self, messages: list, tools: list) -> dict:
303
- """Call LLM via OpenAI-compatible client."""
304
- response = self.client.chat.completions.create(
305
- model=self.model_name,
306
- messages=messages,
307
- tools=tools,
308
- tool_choice="auto",
309
- max_tokens=4096,
310
- )
311
- return response.model_dump()
136
+ """Short, focused system prompt."""
137
+ return f"""You are GeoMind, an AI assistant for satellite imagery analysis using Sentinel-2 data.
312
138
 
313
- def _execute_function(self, name: str, args: dict) -> dict:
314
- """Execute a function call and return the result."""
315
- print(f" 🔧 Executing: {name}({args})")
139
+ Current date: {datetime.now().strftime('%Y-%m-%d')}
316
140
 
317
- if name not in TOOL_FUNCTIONS:
318
- return {"error": f"Unknown function: {name}"}
141
+ KEY RULES:
142
+ 1. Use list_recent_imagery to find satellite data for locations
143
+ 2. Extract Zarr URLs from results: item['zarr_assets']['SR_10m']['href']
144
+ 3. Always include location_name parameter when creating images
145
+ 4. Use actual function response data in your answers (don't make up paths/stats)
146
+ 5. Let functions use default output paths (saves to outputs/ folder as .png)
319
147
 
320
- try:
321
- result = TOOL_FUNCTIONS[name](**args)
322
- return result
323
- except Exception as e:
324
- return {"error": str(e)}
148
+ Be helpful and accurate - always use real data from function responses."""
325
149
 
326
150
  def chat(self, message: str, verbose: bool = True) -> str:
327
- """
328
- Send a message to the agent and get a response.
329
- """
151
+ """Send message to agent and get response."""
330
152
  if verbose:
331
- print(f"\n💬 User: {message}")
332
- print("🤔 Processing...")
153
+ print(f"\nUser: {message}")
154
+ print("Processing...")
333
155
 
334
- # Add user message to history
335
156
  self.history.append({"role": "user", "content": message})
157
+ messages = [{"role": "system", "content": self._get_system_prompt()}] + self.history
158
+
159
+ for _ in range(10): # Max 10 iterations
160
+ response = self.client.chat.completions.create(
161
+ model=self.model_name,
162
+ messages=messages,
163
+ tools=TOOLS,
164
+ tool_choice="auto",
165
+ max_tokens=4096,
166
+ )
336
167
 
337
- # Build messages with system prompt
338
- messages = [{"role": "system", "content": self.system_prompt}] + self.history
339
-
340
- max_iterations = 10
341
- iteration = 0
342
-
343
- while iteration < max_iterations:
344
- iteration += 1
345
-
346
- # Call the model (via proxy or direct)
347
- response_data = self._call_llm(messages, TOOLS)
348
-
349
- # Extract assistant message from response
350
- choice = response_data["choices"][0]
351
- assistant_message = choice["message"]
168
+ choice = response.choices[0]
169
+ assistant_message = choice.message
352
170
 
353
- # Check if there are tool calls
354
- tool_calls = assistant_message.get("tool_calls", [])
171
+ tool_calls = assistant_message.tool_calls
355
172
  if tool_calls:
356
- # Add assistant message with tool calls to messages
357
- messages.append(
358
- {
359
- "role": "assistant",
360
- "content": assistant_message.get("content") or "",
361
- "tool_calls": [
362
- {
363
- "id": tc["id"],
364
- "type": "function",
365
- "function": {
366
- "name": tc["function"]["name"],
367
- "arguments": tc["function"]["arguments"],
368
- },
369
- }
370
- for tc in tool_calls
371
- ],
372
- }
373
- )
374
-
375
- # Execute each tool call
376
- for tool_call in tool_calls:
377
- func_name = tool_call["function"]["name"]
378
- func_args = json.loads(tool_call["function"]["arguments"])
379
-
380
- result = self._execute_function(func_name, func_args)
381
-
382
- # Add tool result to messages
383
- messages.append(
173
+ # Add assistant message with tool calls
174
+ messages.append({
175
+ "role": "assistant",
176
+ "content": assistant_message.content or "",
177
+ "tool_calls": [
384
178
  {
385
- "role": "tool",
386
- "tool_call_id": tool_call["id"],
387
- "content": json.dumps(result, default=str),
179
+ "id": tc.id,
180
+ "type": "function",
181
+ "function": {"name": tc.function.name, "arguments": tc.function.arguments},
388
182
  }
389
- )
390
- else:
391
- # No tool calls, we have a final response
392
- final_text = assistant_message.get("content") or ""
183
+ for tc in tool_calls
184
+ ],
185
+ })
393
186
 
394
- # Add to history
187
+ # Execute tool calls
188
+ for tool_call in tool_calls:
189
+ func_name = tool_call.function.name
190
+ func_args = json.loads(tool_call.function.arguments)
191
+
192
+ print(f" Executing: {func_name}({func_args})")
193
+
194
+ try:
195
+ result = TOOL_FUNCTIONS[func_name](**func_args)
196
+ except Exception as e:
197
+ result = {"error": str(e)}
198
+
199
+ messages.append({
200
+ "role": "tool",
201
+ "tool_call_id": tool_call.id,
202
+ "content": json.dumps(result, default=str),
203
+ })
204
+ else:
205
+ # Final response
206
+ final_text = assistant_message.content or ""
395
207
  self.history.append({"role": "assistant", "content": final_text})
396
-
208
+
397
209
  if verbose:
398
- print(f"\n🌍 GeoMind: {final_text}")
399
-
210
+ print(f"\nGeoMind: {final_text}")
211
+
400
212
  return final_text
401
213
 
402
- return "Max iterations reached."
214
+ return "Max iterations reached"
403
215
 
404
216
  def reset(self):
405
- """Reset the chat session."""
217
+ """Reset chat history."""
406
218
  self.history = []
407
- print("🔄 Chat session reset")
408
-
409
-
410
- def main(model: Optional[str] = None):
411
- """Main entry point for CLI usage."""
412
- import sys
413
-
414
- print("=" * 60)
415
- print("🌍 GeoMind - Geospatial AI Agent")
416
- print("=" * 60)
417
- print("Powered by OpenRouter | Sentinel-2 Imagery")
418
- print("Type 'quit' or 'exit' to end the session")
419
- print("Type 'reset' to start a new conversation")
420
- print("=" * 60)
421
-
422
- try:
423
- agent = GeoMindAgent(model=model)
424
- except ValueError as e:
425
- print(f"\n❌ Error: {e}")
426
- sys.exit(1)
427
- except Exception as e:
428
- print(f"\n❌ Error: {e}")
429
- print("\nPlease check your API key and internet connection.")
430
- sys.exit(1)
431
-
432
- while True:
433
- try:
434
- user_input = input("\n💬 You: ").strip()
435
-
436
- if not user_input:
437
- continue
438
-
439
- if user_input.lower() in ["quit", "exit", "q"]:
440
- print("\n👋 Goodbye!")
441
- break
442
-
443
- if user_input.lower() == "reset":
444
- agent.reset()
445
- continue
446
-
447
- agent.chat(user_input)
448
-
449
- except KeyboardInterrupt:
450
- print("\n\n👋 Goodbye!")
451
- break
452
- except Exception as e:
453
- print(f"\n❌ Error: {e}")
454
-
455
-
456
- if __name__ == "__main__":
457
- main()
geomind/cli.py CHANGED
@@ -7,6 +7,10 @@ import os
7
7
  import argparse
8
8
  from pathlib import Path
9
9
  from typing import Optional
10
+ import subprocess
11
+ import platform
12
+ import threading
13
+ import time
10
14
 
11
15
  from .agent import GeoMindAgent
12
16
 
@@ -36,6 +40,130 @@ def save_api_key(api_key: str) -> bool:
36
40
  return False
37
41
 
38
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
+
39
167
  def main():
40
168
  """Main CLI entry point for the geomind package."""
41
169
  parser = argparse.ArgumentParser(
@@ -93,9 +221,9 @@ Environment Variables:
93
221
  if args.clear_key:
94
222
  if CONFIG_FILE.exists():
95
223
  CONFIG_FILE.unlink()
96
- print("Saved API key cleared.")
224
+ print("Saved API key cleared.")
97
225
  else:
98
- print("ℹ️ No saved API key found.")
226
+ print("No saved API key found.")
99
227
  sys.exit(0)
100
228
 
101
229
  if args.version:
@@ -112,7 +240,7 @@ Environment Variables:
112
240
 
113
241
  api_key = args.api_key or OPENROUTER_API_KEY or get_saved_api_key()
114
242
  if not api_key:
115
- print(" No API key found. Run 'geomind' first to set up.")
243
+ print("Error: No API key found. Run 'geomind' first to set up.")
116
244
  sys.exit(1)
117
245
  agent = GeoMindAgent(model=args.model, api_key=api_key)
118
246
  agent.chat(args.query)
@@ -120,27 +248,64 @@ Environment Variables:
120
248
  # Interactive mode
121
249
  run_interactive(model=args.model, api_key=args.api_key)
122
250
  except ValueError as e:
123
- print(f"\n❌ Error: {e}")
251
+ print(f"\nError: {e}")
124
252
  sys.exit(1)
125
253
  except KeyboardInterrupt:
126
- print("\n\n👋 Goodbye!")
254
+ print("\n\nGoodbye!")
127
255
  sys.exit(0)
128
256
  except Exception as e:
129
- print(f"\n❌ Unexpected error: {e}")
257
+ print(f"\nUnexpected error: {e}")
130
258
  sys.exit(1)
131
259
 
132
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
+
133
304
  def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
134
305
  """Run interactive CLI mode."""
135
306
  from . import __version__
136
307
 
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")
308
+ print_banner()
144
309
 
145
310
  # Check for API key in order: argument > env > saved file
146
311
  from .config import OPENROUTER_API_KEY
@@ -155,44 +320,72 @@ def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
155
320
  api_key = get_saved_api_key()
156
321
 
157
322
  if not api_key:
158
- print("\n🔑 OpenRouter API key required (FREE)")
323
+ print("\nOpenRouter API key required (FREE)")
159
324
  print(" Get yours at: https://openrouter.ai/settings/keys\n")
160
325
  api_key = input(" Enter your API key: ").strip()
161
326
 
162
327
  if not api_key:
163
- print("\n❌ No API key provided. Exiting.")
328
+ print("\nNo API key provided. Exiting.")
164
329
  return
165
330
 
166
331
  # Save the key for future use
167
332
  if save_api_key(api_key):
168
- print(" API key saved! You won't need to enter it again.\n")
333
+ print(" API key saved! You won't need to enter it again.\n")
169
334
  else:
170
- print(" ⚠️ Could not save API key. You'll need to enter it next time.\n")
335
+ print(" Warning: Could not save API key. You'll need to enter it next time.\n")
171
336
 
172
337
  agent = GeoMindAgent(model=model, api_key=api_key)
173
338
 
339
+ # Claude Code style color scheme
340
+ CYAN = '\033[96m'
341
+ DIM = '\033[2m'
342
+ BOLD = '\033[1m'
343
+ RESET = '\033[0m'
344
+
174
345
  while True:
175
346
  try:
176
- user_input = input("\n💬 You: ").strip()
347
+ # Simple prompt like Claude Code
348
+ user_input = input(f"\n{CYAN}>{RESET} ").strip()
177
349
 
178
350
  if not user_input:
179
351
  continue
180
352
 
181
353
  if user_input.lower() in ["quit", "exit", "q"]:
182
- print("\n👋 Goodbye!")
354
+ print(f"\n{DIM}Goodbye!{RESET}")
183
355
  break
184
356
 
185
357
  if user_input.lower() == "reset":
186
358
  agent.reset()
359
+ print(f"{DIM}Started new conversation{RESET}")
187
360
  continue
188
361
 
189
- agent.chat(user_input)
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
190
383
 
191
384
  except KeyboardInterrupt:
192
- print("\n\n👋 Goodbye!")
385
+ print(f"\n\n{DIM}Goodbye!{RESET}")
193
386
  break
194
387
  except Exception as e:
195
- print(f"\nError: {e}")
388
+ print(f"\n{DIM}Error: {e}{RESET}")
196
389
 
197
390
 
198
391
  if __name__ == "__main__":
geomind/config.py CHANGED
@@ -37,9 +37,9 @@ REFLECTANCE_OFFSET = -0.1
37
37
  RGB_BANDS = {"red": "b04", "green": "b03", "blue": "b02"}
38
38
 
39
39
  # Default search parameters
40
- DEFAULT_MAX_CLOUD_COVER = 20 # percent
41
- DEFAULT_BUFFER_KM = 10 # km buffer around point for bbox
42
- DEFAULT_MAX_ITEMS = 10
40
+ DEFAULT_MAX_CLOUD_COVER = 50 # percent (increased for better results)
41
+ DEFAULT_BUFFER_KM = 15 # km buffer around point for bbox (increased coverage)
42
+ DEFAULT_MAX_ITEMS = 20 # increased to find more options
43
43
 
44
44
  # Output directory for saved images
45
45
  OUTPUT_DIR = Path("outputs")
@@ -99,6 +99,7 @@ def create_rgb_composite(
99
99
  zarr_url: str,
100
100
  output_path: Optional[str] = None,
101
101
  subset_size: Optional[int] = 1000,
102
+ location_name: Optional[str] = None,
102
103
  ) -> dict:
103
104
  """
104
105
  Create an RGB composite image from Sentinel-2 10m bands.
@@ -106,9 +107,10 @@ def create_rgb_composite(
106
107
  Uses B04 (Red), B03 (Green), B02 (Blue) bands.
107
108
 
108
109
  Args:
109
- zarr_url: URL to the SR_10m Zarr asset
110
+ zarr_url: URL to the SR_10m Zarr asset or individual band asset URL
110
111
  output_path: Optional path to save the image
111
112
  subset_size: Size to subset the image (for faster processing)
113
+ location_name: Optional location name to include in the title
112
114
 
113
115
  Returns:
114
116
  Dictionary with path to saved image and metadata
@@ -116,30 +118,49 @@ def create_rgb_composite(
116
118
  try:
117
119
  import xarray as xr
118
120
  import zarr
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"])
121
+ import re
122
+
123
+ # Determine if this is a band-specific URL or base SR_10m URL
124
+ # Band-specific URLs end with /b01, /b02, etc. (Sentinel-2 band pattern)
125
+ url_parts = zarr_url.rstrip('/').split('/')
126
+ last_part = url_parts[-1]
127
+ is_band_url = re.match(r'^b(0[1-9]|1[0-2]|8a)(_\d+m)?$', last_part.lower()) is not None
128
+
129
+ if is_band_url:
130
+ # Individual band URL provided - need to construct URLs for each band
131
+ base_url = '/'.join(zarr_url.rstrip('/').split('/')[:-1])
132
+ red_zarr = zarr.open(f"{base_url}/b04", mode="r")
133
+ green_zarr = zarr.open(f"{base_url}/b03", mode="r")
134
+ blue_zarr = zarr.open(f"{base_url}/b02", mode="r")
135
+ else:
136
+ # Base SR_10m URL - bands are subdirectories
137
+ base_url = zarr_url.rstrip('/')
138
+ red_zarr = zarr.open(f"{base_url}/b04", mode="r")
139
+ green_zarr = zarr.open(f"{base_url}/b03", mode="r")
140
+ blue_zarr = zarr.open(f"{base_url}/b02", mode="r")
141
+
142
+ # Determine subset region before loading data
143
+ full_shape = red_zarr.shape
144
+ if subset_size and full_shape[0] > subset_size:
145
+ # Calculate center subset
146
+ h, w = full_shape
147
+ start_h = (h - subset_size) // 2
148
+ start_w = (w - subset_size) // 2
149
+
150
+ # Load only the subset (memory efficient)
151
+ red = np.array(red_zarr[start_h:start_h + subset_size, start_w:start_w + subset_size])
152
+ green = np.array(green_zarr[start_h:start_h + subset_size, start_w:start_w + subset_size])
153
+ blue = np.array(blue_zarr[start_h:start_h + subset_size, start_w:start_w + subset_size])
154
+ else:
155
+ # Load full arrays (be careful with large datasets)
156
+ red = np.array(red_zarr)
157
+ green = np.array(green_zarr)
158
+ blue = np.array(blue_zarr)
129
159
 
130
160
  # Subset if requested (for faster processing)
131
161
  if subset_size and red.shape[0] > subset_size:
132
- # Take center subset
133
- h, w = red.shape
134
- start_h = (h - subset_size) // 2
135
- start_w = (w - subset_size) // 2
136
- red = red[start_h : start_h + subset_size, start_w : start_w + subset_size]
137
- green = green[
138
- start_h : start_h + subset_size, start_w : start_w + subset_size
139
- ]
140
- blue = blue[
141
- start_h : start_h + subset_size, start_w : start_w + subset_size
142
- ]
162
+ # Already subsetted during loading - this section can be removed
163
+ pass
143
164
 
144
165
  # Apply scale and offset
145
166
  red = _apply_scale_offset(red)
@@ -163,7 +184,12 @@ def create_rgb_composite(
163
184
  # Create figure
164
185
  fig, ax = plt.subplots(figsize=(10, 10))
165
186
  ax.imshow(rgb)
166
- ax.set_title("Sentinel-2 RGB Composite (B4/B3/B2)")
187
+
188
+ # Create dynamic title with location
189
+ title = "Sentinel-2 RGB Composite (B4/B3/B2)"
190
+ if location_name:
191
+ title += f" - {location_name}"
192
+ ax.set_title(title)
167
193
  ax.axis("off")
168
194
 
169
195
  # Save
@@ -189,6 +215,7 @@ def calculate_ndvi(
189
215
  zarr_url: str,
190
216
  output_path: Optional[str] = None,
191
217
  subset_size: Optional[int] = 1000,
218
+ location_name: Optional[str] = None,
192
219
  ) -> dict:
193
220
  """
194
221
  Calculate NDVI (Normalized Difference Vegetation Index) from Sentinel-2 data.
@@ -197,9 +224,10 @@ def calculate_ndvi(
197
224
  Uses B08 (NIR) and B04 (Red) bands.
198
225
 
199
226
  Args:
200
- zarr_url: URL to the SR_10m Zarr asset
227
+ zarr_url: URL to the SR_10m Zarr asset or individual band asset URL
201
228
  output_path: Optional path to save the NDVI image
202
229
  subset_size: Size to subset the image
230
+ location_name: Optional location name to include in the title
203
231
 
204
232
  Returns:
205
233
  Dictionary with NDVI statistics and output path
@@ -207,21 +235,40 @@ def calculate_ndvi(
207
235
  try:
208
236
  import zarr
209
237
  from matplotlib.colors import LinearSegmentedColormap
210
-
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
217
-
218
- # Subset if requested
219
- if subset_size and nir.shape[0] > subset_size:
220
- h, w = nir.shape
238
+ import re
239
+
240
+ # Determine if this is a band-specific URL or base SR_10m URL
241
+ url_parts = zarr_url.rstrip('/').split('/')
242
+ last_part = url_parts[-1]
243
+ is_band_url = re.match(r'^b(0[1-9]|1[0-2]|8a)(_\d+m)?$', last_part.lower()) is not None
244
+
245
+ if is_band_url:
246
+ # Individual band URL provided
247
+ base_url = '/'.join(zarr_url.rstrip('/').split('/')[:-1])
248
+ nir_zarr = zarr.open(f"{base_url}/b08", mode="r")
249
+ red_zarr = zarr.open(f"{base_url}/b04", mode="r")
250
+ else:
251
+ # Base SR_10m URL
252
+ base_url = zarr_url.rstrip('/')
253
+ nir_zarr = zarr.open(f"{base_url}/b08", mode="r")
254
+ red_zarr = zarr.open(f"{base_url}/b04", mode="r")
255
+
256
+ # Determine subset region and load efficiently
257
+ full_shape = nir_zarr.shape
258
+ if subset_size and full_shape[0] > subset_size:
259
+ h, w = full_shape
221
260
  start_h = (h - subset_size) // 2
222
261
  start_w = (w - subset_size) // 2
223
- nir = nir[start_h : start_h + subset_size, start_w : start_w + subset_size]
224
- red = red[start_h : start_h + subset_size, start_w : start_w + subset_size]
262
+ nir = np.array(nir_zarr[start_h:start_h + subset_size, start_w:start_w + subset_size])
263
+ red = np.array(red_zarr[start_h:start_h + subset_size, start_w:start_w + subset_size])
264
+ else:
265
+ nir = np.array(nir_zarr)
266
+ red = np.array(red_zarr)
267
+
268
+ # Subset if requested - already handled during loading
269
+ if subset_size and nir.shape[0] > subset_size:
270
+ # Already subsetted during loading
271
+ pass
225
272
 
226
273
  # Apply scale and offset
227
274
  nir = _apply_scale_offset(nir)
@@ -256,7 +303,12 @@ def calculate_ndvi(
256
303
  # Create figure
257
304
  fig, ax = plt.subplots(figsize=(10, 10))
258
305
  im = ax.imshow(ndvi, cmap=ndvi_cmap, vmin=-1, vmax=1)
259
- ax.set_title("NDVI - Normalized Difference Vegetation Index")
306
+
307
+ # Create dynamic title with location
308
+ title = "NDVI - Normalized Difference Vegetation Index"
309
+ if location_name:
310
+ title += f" - {location_name}"
311
+ ax.set_title(title)
260
312
  ax.axis("off")
261
313
 
262
314
  # Add colorbar
@@ -25,6 +25,33 @@ def _format_item(item) -> dict:
25
25
  """Format a STAC item into a simplified dictionary."""
26
26
  props = item.properties
27
27
 
28
+ # Extract ALL assets, with special handling for Zarr assets
29
+ assets = {}
30
+ zarr_assets = {}
31
+ other_assets = {}
32
+
33
+ for key, asset in item.assets.items():
34
+ asset_info = {
35
+ "title": asset.title,
36
+ "href": asset.href,
37
+ "type": asset.media_type,
38
+ }
39
+
40
+ # Add band information for individual band assets
41
+ if "_" in key and key.split("_")[0] in ["B01", "B02", "B03", "B04", "B05", "B06", "B07", "B08", "B8A", "B09", "B11", "B12"]:
42
+ band_name = key.split("_")[0].lower()
43
+ asset_info["band"] = band_name
44
+ asset_info["resolution"] = key.split("_")[1] if "_" in key else "unknown"
45
+
46
+ # Categorize assets by type
47
+ if asset.media_type and "zarr" in asset.media_type.lower():
48
+ zarr_assets[key] = asset_info
49
+ else:
50
+ other_assets[key] = asset_info
51
+
52
+ # Include all assets in main assets dict
53
+ assets[key] = asset_info
54
+
28
55
  return {
29
56
  "id": item.id,
30
57
  "datetime": props.get("datetime"),
@@ -32,16 +59,15 @@ def _format_item(item) -> dict:
32
59
  "platform": props.get("platform"),
33
60
  "bbox": item.bbox,
34
61
  "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
- },
62
+ "assets": assets,
63
+ "zarr_assets": zarr_assets, # Separate zarr assets for easy access
64
+ "other_assets": other_assets, # Non-zarr assets
44
65
  "stac_url": f"{STAC_API_URL}/collections/{STAC_COLLECTION}/items/{item.id}",
66
+ "asset_summary": {
67
+ "total_assets": len(assets),
68
+ "zarr_assets": len(zarr_assets),
69
+ "other_assets": len(other_assets),
70
+ }
45
71
  }
46
72
 
47
73
 
@@ -129,6 +155,10 @@ def search_imagery(
129
155
  "datetime": datetime_str,
130
156
  "max_cloud_cover": max_cloud_cover,
131
157
  },
158
+ "quality_info": {
159
+ "avg_cloud_cover": sum(item.properties.get("eo:cloud_cover", 100) for item in items) / len(items) if items else None,
160
+ "best_cloud_cover": min(item.properties.get("eo:cloud_cover", 100) for item in items) if items else None,
161
+ } if items else {},
132
162
  }
133
163
 
134
164
  except Exception as e:
@@ -176,7 +206,7 @@ def get_item_details(item_id: str) -> dict:
176
206
 
177
207
  def list_recent_imagery(
178
208
  location_name: Optional[str] = None,
179
- days: int = 7,
209
+ days: int = 14, # Changed from 7 to 14 days default
180
210
  max_cloud_cover: Optional[float] = None,
181
211
  max_items: Optional[int] = None,
182
212
  ) -> dict:
@@ -184,10 +214,11 @@ def list_recent_imagery(
184
214
  List recent Sentinel-2 imagery, optionally for a specific location.
185
215
 
186
216
  This is a convenience function that combines geocoding and search.
217
+ If no good quality results found, automatically extends search period.
187
218
 
188
219
  Args:
189
220
  location_name: Optional place name to search around
190
- days: Number of days to look back (default: 7)
221
+ days: Number of days to look back (default: 14)
191
222
  max_cloud_cover: Maximum cloud cover percentage
192
223
  max_items: Maximum items to return
193
224
 
@@ -228,6 +259,25 @@ def list_recent_imagery(
228
259
  max_items=max_items,
229
260
  )
230
261
 
262
+ # If no results found and we used a restrictive cloud cover, try extending time and relaxing cloud cover
263
+ if result.get("success") and result.get("filtered_count", 0) == 0 and days < 30:
264
+ # Try 30 days with more permissive cloud cover
265
+ extended_start = end_date - timedelta(days=30)
266
+ relaxed_cloud = min((max_cloud_cover or DEFAULT_MAX_CLOUD_COVER) + 30, 80)
267
+
268
+ extended_result = search_imagery(
269
+ bbox=bbox,
270
+ start_date=extended_start.strftime("%Y-%m-%d"),
271
+ end_date=end_date.strftime("%Y-%m-%d"),
272
+ max_cloud_cover=relaxed_cloud,
273
+ max_items=max_items,
274
+ )
275
+
276
+ if extended_result.get("success") and extended_result.get("filtered_count", 0) > 0:
277
+ result = extended_result
278
+ result["extended_search"] = True
279
+ result["note"] = f"Extended search to 30 days with {relaxed_cloud}% cloud cover for better results"
280
+
231
281
  if location_info:
232
282
  result["location"] = location_info
233
283
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geomind-ai
3
- Version: 1.1.0
3
+ Version: 1.2.0
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
@@ -0,0 +1,14 @@
1
+ geomind/__init__.py,sha256=LBwe1KUfm0hXjes2HDyeaN4LZb8tXlRnXqBUDRx3QoI,165
2
+ geomind/agent.py,sha256=My9OikB0RnZulztSML_sPY3-0-dpVvGyfmbzD42Ppp0,8108
3
+ geomind/cli.py,sha256=ipuMF0ZPseHHNKDD5VUMUG4fJDNmG_RQXNvAvT8Gbww,12085
4
+ geomind/config.py,sha256=pgHir8A5tSLbIYz9zOMMwEXhGyECEA0JzStPjQOQfpM,2116
5
+ geomind/tools/__init__.py,sha256=8iumGwIFHh8Bj1VJNgZtmKnEBqCy6_cRkzYENDUH7x4,720
6
+ geomind/tools/geocoding.py,sha256=hiLpzHpkJP6IgWAUtZMnHL6qpkWcYWVLpGe0yfYxXv8,3007
7
+ geomind/tools/processing.py,sha256=4sUrLGqxGqlVETMgHLxOeY-b1_kQ5bstKOjIHh2PbeU,12817
8
+ geomind/tools/stac_search.py,sha256=a0OrxSe4bDNtD_cNHKBnQZHeCtx9B6S65HWxsCBaFxY,8854
9
+ geomind_ai-1.2.0.dist-info/licenses/LICENSE,sha256=aveu0ERm7I3NnIu8rtpKdvd0eyRpmktXKU0PBABtSN0,1069
10
+ geomind_ai-1.2.0.dist-info/METADATA,sha256=-gHApQirq64EklvWj8j7_B-SQjxcsXDCt_HjFe7dJpc,2405
11
+ geomind_ai-1.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
+ geomind_ai-1.2.0.dist-info/entry_points.txt,sha256=2nPR3faYKl0-1epccvzMJ2xdi-Q1Vt7aOSvA84oIWnw,45
13
+ geomind_ai-1.2.0.dist-info/top_level.txt,sha256=rjKWNSNRhq4R9xJoZGsG-eAaH7BmTVNvfrrbcaJMIIs,8
14
+ geomind_ai-1.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,14 +0,0 @@
1
- geomind/__init__.py,sha256=MZ0Zr2vGCJ816ilSApbwhA6iEfCwEBk40etbvIGfpqs,165
2
- geomind/agent.py,sha256=2VDhPK9kvxf80KVoJLXbh8JGKbB8a3EiayojXd7ODOM,15661
3
- geomind/cli.py,sha256=hUONvWUW2QTVawMxpDtFonIAqbS-SiV-bMFthgqnr2g,5614
4
- geomind/config.py,sha256=7zPr0OKvK2SqQArUbjMvf5GVLQsmHcbn7e-BsSB1yv0,2030
5
- geomind/tools/__init__.py,sha256=8iumGwIFHh8Bj1VJNgZtmKnEBqCy6_cRkzYENDUH7x4,720
6
- geomind/tools/geocoding.py,sha256=hiLpzHpkJP6IgWAUtZMnHL6qpkWcYWVLpGe0yfYxXv8,3007
7
- geomind/tools/processing.py,sha256=mGu20uvpAVil2w2BEqj-0zYhKhCFuZHr1-3-vq0VzqM,10152
8
- geomind/tools/stac_search.py,sha256=V6230l4aHjedPWXu-3Cjmfc6diSFh5zsycewUko0W8k,6452
9
- geomind_ai-1.1.0.dist-info/licenses/LICENSE,sha256=aveu0ERm7I3NnIu8rtpKdvd0eyRpmktXKU0PBABtSN0,1069
10
- geomind_ai-1.1.0.dist-info/METADATA,sha256=EJROiPf8W_iht6qEaSyOzosyVob_hoodOe98ayFXAUE,2405
11
- geomind_ai-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- geomind_ai-1.1.0.dist-info/entry_points.txt,sha256=2nPR3faYKl0-1epccvzMJ2xdi-Q1Vt7aOSvA84oIWnw,45
13
- geomind_ai-1.1.0.dist-info/top_level.txt,sha256=rjKWNSNRhq4R9xJoZGsG-eAaH7BmTVNvfrrbcaJMIIs,8
14
- geomind_ai-1.1.0.dist-info/RECORD,,