geomind-ai 1.1.1__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 +1 -1
- geomind/agent.py +106 -354
- geomind/config.py +3 -3
- geomind/tools/processing.py +69 -32
- geomind/tools/stac_search.py +57 -17
- {geomind_ai-1.1.1.dist-info → geomind_ai-1.2.0.dist-info}/METADATA +1 -1
- geomind_ai-1.2.0.dist-info/RECORD +14 -0
- geomind_ai-1.1.1.dist-info/RECORD +0 -14
- {geomind_ai-1.1.1.dist-info → geomind_ai-1.2.0.dist-info}/WHEEL +0 -0
- {geomind_ai-1.1.1.dist-info → geomind_ai-1.2.0.dist-info}/entry_points.txt +0 -0
- {geomind_ai-1.1.1.dist-info → geomind_ai-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {geomind_ai-1.1.1.dist-info → geomind_ai-1.2.0.dist-info}/top_level.txt +0 -0
geomind/__init__.py
CHANGED
geomind/agent.py
CHANGED
|
@@ -1,144 +1,50 @@
|
|
|
1
1
|
"""
|
|
2
|
-
GeoMind Agent -
|
|
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
|
|
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
|
-
#
|
|
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": "
|
|
40
|
+
"description": "Find recent Sentinel-2 imagery for a location",
|
|
123
41
|
"parameters": {
|
|
124
42
|
"type": "object",
|
|
125
43
|
"properties": {
|
|
126
|
-
"location_name": {
|
|
127
|
-
|
|
128
|
-
|
|
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": "
|
|
151
|
-
"description": "
|
|
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
|
-
"
|
|
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": ["
|
|
65
|
+
"required": ["zarr_url"],
|
|
158
66
|
},
|
|
159
67
|
},
|
|
160
68
|
},
|
|
161
69
|
{
|
|
162
70
|
"type": "function",
|
|
163
71
|
"function": {
|
|
164
|
-
"name": "
|
|
165
|
-
"description": "
|
|
72
|
+
"name": "calculate_ndvi",
|
|
73
|
+
"description": "Calculate NDVI vegetation index",
|
|
166
74
|
"parameters": {
|
|
167
75
|
"type": "object",
|
|
168
76
|
"properties": {
|
|
169
|
-
"zarr_url": {
|
|
170
|
-
|
|
171
|
-
|
|
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": "
|
|
190
|
-
"description": "
|
|
88
|
+
"name": "search_imagery",
|
|
89
|
+
"description": "Search Sentinel-2 imagery by parameters",
|
|
191
90
|
"parameters": {
|
|
192
91
|
"type": "object",
|
|
193
92
|
"properties": {
|
|
194
|
-
"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
},
|
|
198
|
-
"
|
|
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": [
|
|
99
|
+
"required": [],
|
|
208
100
|
},
|
|
209
101
|
},
|
|
210
102
|
},
|
|
211
103
|
{
|
|
212
104
|
"type": "function",
|
|
213
105
|
"function": {
|
|
214
|
-
"name": "
|
|
215
|
-
"description": "Get
|
|
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
|
-
"
|
|
220
|
-
|
|
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": ["
|
|
114
|
+
"required": ["place_name"],
|
|
230
115
|
},
|
|
231
116
|
},
|
|
232
117
|
},
|
|
@@ -234,233 +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
|
-
|
|
255
|
-
|
|
127
|
+
|
|
256
128
|
if not self.api_key:
|
|
257
|
-
raise ValueError(
|
|
258
|
-
|
|
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
|
-
|
|
129
|
+
raise ValueError("OpenRouter API key required")
|
|
130
|
+
|
|
266
131
|
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
|
|
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
|
-
"""
|
|
279
|
-
return f"""You are GeoMind, an
|
|
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
|
-
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
|
-
|
|
303
|
-
When users ask for imagery:
|
|
304
|
-
1. First use get_bbox_from_location or list_recent_imagery to search
|
|
305
|
-
2. Present the results clearly with key metadata (ID, date, cloud cover)
|
|
306
|
-
3. Offer to create visualizations if data is found
|
|
307
|
-
4. For visualizations, use the SR_10m asset URL from search results
|
|
308
|
-
|
|
309
|
-
Always explain what you're doing and interpret results in a helpful way."""
|
|
310
|
-
|
|
311
|
-
def _call_llm(self, messages: list, tools: list) -> dict:
|
|
312
|
-
"""Call LLM via OpenAI-compatible client."""
|
|
313
|
-
response = self.client.chat.completions.create(
|
|
314
|
-
model=self.model_name,
|
|
315
|
-
messages=messages,
|
|
316
|
-
tools=tools,
|
|
317
|
-
tool_choice="auto",
|
|
318
|
-
max_tokens=4096,
|
|
319
|
-
)
|
|
320
|
-
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.
|
|
321
138
|
|
|
322
|
-
|
|
323
|
-
"""Execute a function call and return the result."""
|
|
324
|
-
print(f" Executing: {name}({args})")
|
|
139
|
+
Current date: {datetime.now().strftime('%Y-%m-%d')}
|
|
325
140
|
|
|
326
|
-
|
|
327
|
-
|
|
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)
|
|
328
147
|
|
|
329
|
-
|
|
330
|
-
result = TOOL_FUNCTIONS[name](**args)
|
|
331
|
-
return result
|
|
332
|
-
except Exception as e:
|
|
333
|
-
return {"error": str(e)}
|
|
148
|
+
Be helpful and accurate - always use real data from function responses."""
|
|
334
149
|
|
|
335
150
|
def chat(self, message: str, verbose: bool = True) -> str:
|
|
336
|
-
"""
|
|
337
|
-
Send a message to the agent and get a response.
|
|
338
|
-
"""
|
|
151
|
+
"""Send message to agent and get response."""
|
|
339
152
|
if verbose:
|
|
340
153
|
print(f"\nUser: {message}")
|
|
341
154
|
print("Processing...")
|
|
342
155
|
|
|
343
|
-
# Add user message to history
|
|
344
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
|
+
)
|
|
345
167
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
max_iterations = 10
|
|
350
|
-
iteration = 0
|
|
351
|
-
|
|
352
|
-
while iteration < max_iterations:
|
|
353
|
-
iteration += 1
|
|
354
|
-
|
|
355
|
-
# Call the model (via proxy or direct)
|
|
356
|
-
response_data = self._call_llm(messages, TOOLS)
|
|
357
|
-
|
|
358
|
-
# Extract assistant message from response
|
|
359
|
-
choice = response_data["choices"][0]
|
|
360
|
-
assistant_message = choice["message"]
|
|
168
|
+
choice = response.choices[0]
|
|
169
|
+
assistant_message = choice.message
|
|
361
170
|
|
|
362
|
-
|
|
363
|
-
tool_calls = assistant_message.get("tool_calls", [])
|
|
171
|
+
tool_calls = assistant_message.tool_calls
|
|
364
172
|
if tool_calls:
|
|
365
|
-
# Add assistant message with tool calls
|
|
366
|
-
messages.append(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
"tool_calls": [
|
|
371
|
-
{
|
|
372
|
-
"id": tc["id"],
|
|
373
|
-
"type": "function",
|
|
374
|
-
"function": {
|
|
375
|
-
"name": tc["function"]["name"],
|
|
376
|
-
"arguments": tc["function"]["arguments"],
|
|
377
|
-
},
|
|
378
|
-
}
|
|
379
|
-
for tc in tool_calls
|
|
380
|
-
],
|
|
381
|
-
}
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
# Execute each tool call
|
|
385
|
-
for tool_call in tool_calls:
|
|
386
|
-
func_name = tool_call["function"]["name"]
|
|
387
|
-
func_args = json.loads(tool_call["function"]["arguments"])
|
|
388
|
-
|
|
389
|
-
result = self._execute_function(func_name, func_args)
|
|
390
|
-
|
|
391
|
-
# Add tool result to messages
|
|
392
|
-
messages.append(
|
|
173
|
+
# Add assistant message with tool calls
|
|
174
|
+
messages.append({
|
|
175
|
+
"role": "assistant",
|
|
176
|
+
"content": assistant_message.content or "",
|
|
177
|
+
"tool_calls": [
|
|
393
178
|
{
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
"
|
|
179
|
+
"id": tc.id,
|
|
180
|
+
"type": "function",
|
|
181
|
+
"function": {"name": tc.function.name, "arguments": tc.function.arguments},
|
|
397
182
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
final_text = assistant_message.get("content") or ""
|
|
183
|
+
for tc in tool_calls
|
|
184
|
+
],
|
|
185
|
+
})
|
|
402
186
|
|
|
403
|
-
#
|
|
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 ""
|
|
404
207
|
self.history.append({"role": "assistant", "content": final_text})
|
|
405
|
-
|
|
208
|
+
|
|
406
209
|
if verbose:
|
|
407
210
|
print(f"\nGeoMind: {final_text}")
|
|
408
|
-
|
|
211
|
+
|
|
409
212
|
return final_text
|
|
410
213
|
|
|
411
|
-
return "Max iterations reached
|
|
214
|
+
return "Max iterations reached"
|
|
412
215
|
|
|
413
216
|
def reset(self):
|
|
414
|
-
"""Reset
|
|
217
|
+
"""Reset chat history."""
|
|
415
218
|
self.history = []
|
|
416
|
-
print("Chat session reset")
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def main(model: Optional[str] = None):
|
|
420
|
-
"""Main entry point for CLI usage."""
|
|
421
|
-
import sys
|
|
422
|
-
|
|
423
|
-
print("=" * 60)
|
|
424
|
-
print("GeoMind - Geospatial AI Agent")
|
|
425
|
-
print("=" * 60)
|
|
426
|
-
print("Powered by OpenRouter | Sentinel-2 Imagery")
|
|
427
|
-
print("Type 'quit' or 'exit' to end the session")
|
|
428
|
-
print("Type 'reset' to start a new conversation")
|
|
429
|
-
print("=" * 60)
|
|
430
|
-
|
|
431
|
-
try:
|
|
432
|
-
agent = GeoMindAgent(model=model)
|
|
433
|
-
except ValueError as e:
|
|
434
|
-
print(f"\nError: {e}")
|
|
435
|
-
sys.exit(1)
|
|
436
|
-
except Exception as e:
|
|
437
|
-
print(f"\nError: {e}")
|
|
438
|
-
print("\nPlease check your API key and internet connection.")
|
|
439
|
-
sys.exit(1)
|
|
440
|
-
|
|
441
|
-
while True:
|
|
442
|
-
try:
|
|
443
|
-
user_input = input("\nYou: ").strip()
|
|
444
|
-
|
|
445
|
-
if not user_input:
|
|
446
|
-
continue
|
|
447
|
-
|
|
448
|
-
if user_input.lower() in ["quit", "exit", "q"]:
|
|
449
|
-
print("\nGoodbye!")
|
|
450
|
-
break
|
|
451
|
-
|
|
452
|
-
if user_input.lower() == "reset":
|
|
453
|
-
agent.reset()
|
|
454
|
-
continue
|
|
455
|
-
|
|
456
|
-
agent.chat(user_input)
|
|
457
|
-
|
|
458
|
-
except KeyboardInterrupt:
|
|
459
|
-
print("\n\nGoodbye!")
|
|
460
|
-
break
|
|
461
|
-
except Exception as e:
|
|
462
|
-
print(f"\nError: {e}")
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if __name__ == "__main__":
|
|
466
|
-
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 =
|
|
41
|
-
DEFAULT_BUFFER_KM =
|
|
42
|
-
DEFAULT_MAX_ITEMS =
|
|
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")
|
geomind/tools/processing.py
CHANGED
|
@@ -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.
|
|
@@ -109,6 +110,7 @@ def create_rgb_composite(
|
|
|
109
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,38 +118,49 @@ def create_rgb_composite(
|
|
|
116
118
|
try:
|
|
117
119
|
import xarray as xr
|
|
118
120
|
import zarr
|
|
121
|
+
import re
|
|
119
122
|
|
|
120
123
|
# Determine if this is a band-specific URL or base SR_10m URL
|
|
121
|
-
# Band-specific URLs end with /
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
124
128
|
|
|
125
129
|
if is_band_url:
|
|
126
130
|
# Individual band URL provided - need to construct URLs for each band
|
|
127
131
|
base_url = '/'.join(zarr_url.rstrip('/').split('/')[:-1])
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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")
|
|
131
135
|
else:
|
|
132
136
|
# Base SR_10m URL - bands are subdirectories
|
|
133
137
|
base_url = zarr_url.rstrip('/')
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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)
|
|
137
159
|
|
|
138
160
|
# Subset if requested (for faster processing)
|
|
139
161
|
if subset_size and red.shape[0] > subset_size:
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
start_h = (h - subset_size) // 2
|
|
143
|
-
start_w = (w - subset_size) // 2
|
|
144
|
-
red = red[start_h : start_h + subset_size, start_w : start_w + subset_size]
|
|
145
|
-
green = green[
|
|
146
|
-
start_h : start_h + subset_size, start_w : start_w + subset_size
|
|
147
|
-
]
|
|
148
|
-
blue = blue[
|
|
149
|
-
start_h : start_h + subset_size, start_w : start_w + subset_size
|
|
150
|
-
]
|
|
162
|
+
# Already subsetted during loading - this section can be removed
|
|
163
|
+
pass
|
|
151
164
|
|
|
152
165
|
# Apply scale and offset
|
|
153
166
|
red = _apply_scale_offset(red)
|
|
@@ -171,7 +184,12 @@ def create_rgb_composite(
|
|
|
171
184
|
# Create figure
|
|
172
185
|
fig, ax = plt.subplots(figsize=(10, 10))
|
|
173
186
|
ax.imshow(rgb)
|
|
174
|
-
|
|
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)
|
|
175
193
|
ax.axis("off")
|
|
176
194
|
|
|
177
195
|
# Save
|
|
@@ -197,6 +215,7 @@ def calculate_ndvi(
|
|
|
197
215
|
zarr_url: str,
|
|
198
216
|
output_path: Optional[str] = None,
|
|
199
217
|
subset_size: Optional[int] = 1000,
|
|
218
|
+
location_name: Optional[str] = None,
|
|
200
219
|
) -> dict:
|
|
201
220
|
"""
|
|
202
221
|
Calculate NDVI (Normalized Difference Vegetation Index) from Sentinel-2 data.
|
|
@@ -208,6 +227,7 @@ def calculate_ndvi(
|
|
|
208
227
|
zarr_url: URL to the SR_10m Zarr asset or individual band asset URL
|
|
209
228
|
output_path: Optional path to save the NDVI image
|
|
210
229
|
subset_size: Size to subset the image
|
|
230
|
+
location_name: Optional location name to include in the title
|
|
211
231
|
|
|
212
232
|
Returns:
|
|
213
233
|
Dictionary with NDVI statistics and output path
|
|
@@ -215,28 +235,40 @@ def calculate_ndvi(
|
|
|
215
235
|
try:
|
|
216
236
|
import zarr
|
|
217
237
|
from matplotlib.colors import LinearSegmentedColormap
|
|
238
|
+
import re
|
|
218
239
|
|
|
219
240
|
# Determine if this is a band-specific URL or base SR_10m URL
|
|
220
|
-
|
|
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
|
|
221
244
|
|
|
222
245
|
if is_band_url:
|
|
223
246
|
# Individual band URL provided
|
|
224
247
|
base_url = '/'.join(zarr_url.rstrip('/').split('/')[:-1])
|
|
225
|
-
|
|
226
|
-
|
|
248
|
+
nir_zarr = zarr.open(f"{base_url}/b08", mode="r")
|
|
249
|
+
red_zarr = zarr.open(f"{base_url}/b04", mode="r")
|
|
227
250
|
else:
|
|
228
251
|
# Base SR_10m URL
|
|
229
252
|
base_url = zarr_url.rstrip('/')
|
|
230
|
-
|
|
231
|
-
|
|
253
|
+
nir_zarr = zarr.open(f"{base_url}/b08", mode="r")
|
|
254
|
+
red_zarr = zarr.open(f"{base_url}/b04", mode="r")
|
|
232
255
|
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
236
260
|
start_h = (h - subset_size) // 2
|
|
237
261
|
start_w = (w - subset_size) // 2
|
|
238
|
-
nir =
|
|
239
|
-
red =
|
|
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
|
|
240
272
|
|
|
241
273
|
# Apply scale and offset
|
|
242
274
|
nir = _apply_scale_offset(nir)
|
|
@@ -271,7 +303,12 @@ def calculate_ndvi(
|
|
|
271
303
|
# Create figure
|
|
272
304
|
fig, ax = plt.subplots(figsize=(10, 10))
|
|
273
305
|
im = ax.imshow(ndvi, cmap=ndvi_cmap, vmin=-1, vmax=1)
|
|
274
|
-
|
|
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)
|
|
275
312
|
ax.axis("off")
|
|
276
313
|
|
|
277
314
|
# Add colorbar
|
geomind/tools/stac_search.py
CHANGED
|
@@ -25,23 +25,32 @@ def _format_item(item) -> dict:
|
|
|
25
25
|
"""Format a STAC item into a simplified dictionary."""
|
|
26
26
|
props = item.properties
|
|
27
27
|
|
|
28
|
-
# Extract
|
|
28
|
+
# Extract ALL assets, with special handling for Zarr assets
|
|
29
29
|
assets = {}
|
|
30
|
+
zarr_assets = {}
|
|
31
|
+
other_assets = {}
|
|
32
|
+
|
|
30
33
|
for key, asset in item.assets.items():
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
45
54
|
|
|
46
55
|
return {
|
|
47
56
|
"id": item.id,
|
|
@@ -51,7 +60,14 @@ def _format_item(item) -> dict:
|
|
|
51
60
|
"bbox": item.bbox,
|
|
52
61
|
"geometry": item.geometry,
|
|
53
62
|
"assets": assets,
|
|
63
|
+
"zarr_assets": zarr_assets, # Separate zarr assets for easy access
|
|
64
|
+
"other_assets": other_assets, # Non-zarr assets
|
|
54
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
|
+
}
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
|
|
@@ -139,6 +155,10 @@ def search_imagery(
|
|
|
139
155
|
"datetime": datetime_str,
|
|
140
156
|
"max_cloud_cover": max_cloud_cover,
|
|
141
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 {},
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
except Exception as e:
|
|
@@ -186,7 +206,7 @@ def get_item_details(item_id: str) -> dict:
|
|
|
186
206
|
|
|
187
207
|
def list_recent_imagery(
|
|
188
208
|
location_name: Optional[str] = None,
|
|
189
|
-
days: int = 7
|
|
209
|
+
days: int = 14, # Changed from 7 to 14 days default
|
|
190
210
|
max_cloud_cover: Optional[float] = None,
|
|
191
211
|
max_items: Optional[int] = None,
|
|
192
212
|
) -> dict:
|
|
@@ -194,10 +214,11 @@ def list_recent_imagery(
|
|
|
194
214
|
List recent Sentinel-2 imagery, optionally for a specific location.
|
|
195
215
|
|
|
196
216
|
This is a convenience function that combines geocoding and search.
|
|
217
|
+
If no good quality results found, automatically extends search period.
|
|
197
218
|
|
|
198
219
|
Args:
|
|
199
220
|
location_name: Optional place name to search around
|
|
200
|
-
days: Number of days to look back (default:
|
|
221
|
+
days: Number of days to look back (default: 14)
|
|
201
222
|
max_cloud_cover: Maximum cloud cover percentage
|
|
202
223
|
max_items: Maximum items to return
|
|
203
224
|
|
|
@@ -238,6 +259,25 @@ def list_recent_imagery(
|
|
|
238
259
|
max_items=max_items,
|
|
239
260
|
)
|
|
240
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
|
+
|
|
241
281
|
if location_info:
|
|
242
282
|
result["location"] = location_info
|
|
243
283
|
|
|
@@ -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,14 +0,0 @@
|
|
|
1
|
-
geomind/__init__.py,sha256=MZ0Zr2vGCJ816ilSApbwhA6iEfCwEBk40etbvIGfpqs,165
|
|
2
|
-
geomind/agent.py,sha256=-TF2VqOyTAJDrBPMbPKFoUwAHahBUUYOGtzqAKoNiUs,16226
|
|
3
|
-
geomind/cli.py,sha256=ipuMF0ZPseHHNKDD5VUMUG4fJDNmG_RQXNvAvT8Gbww,12085
|
|
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=hb_Y9JV6o8XxS8_HI9DuW-hjzkv8S5ZdBpm45NpWwXQ,11287
|
|
8
|
-
geomind/tools/stac_search.py,sha256=3W9iZRObY6EkDn9K06Sp9ME-fYmubZRjPNDKDLEBUmM,6898
|
|
9
|
-
geomind_ai-1.1.1.dist-info/licenses/LICENSE,sha256=aveu0ERm7I3NnIu8rtpKdvd0eyRpmktXKU0PBABtSN0,1069
|
|
10
|
-
geomind_ai-1.1.1.dist-info/METADATA,sha256=3QKoizXhvfTGs6Bi73RdRFtx748Wzb7jGtbe39DObGw,2405
|
|
11
|
-
geomind_ai-1.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
-
geomind_ai-1.1.1.dist-info/entry_points.txt,sha256=2nPR3faYKl0-1epccvzMJ2xdi-Q1Vt7aOSvA84oIWnw,45
|
|
13
|
-
geomind_ai-1.1.1.dist-info/top_level.txt,sha256=rjKWNSNRhq4R9xJoZGsG-eAaH7BmTVNvfrrbcaJMIIs,8
|
|
14
|
-
geomind_ai-1.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|