geomind-ai 1.0.1__py3-none-any.whl → 1.0.2__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/agent.py CHANGED
@@ -1,445 +1,441 @@
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.
6
- """
7
-
8
- import json
9
- import re
10
- from typing import Optional, Callable, Any
11
- from datetime import datetime
12
-
13
- from openai import OpenAI
14
-
15
- from .config import (
16
- OPENROUTER_API_KEY, OPENROUTER_API_URL, OPENROUTER_MODEL
17
- )
18
- from .tools import (
19
- geocode_location,
20
- get_bbox_from_location,
21
- search_imagery,
22
- get_item_details,
23
- list_recent_imagery,
24
- create_rgb_composite,
25
- calculate_ndvi,
26
- get_band_statistics,
27
- )
28
-
29
-
30
- # Map tool names to functions
31
- TOOL_FUNCTIONS = {
32
- "geocode_location": geocode_location,
33
- "get_bbox_from_location": get_bbox_from_location,
34
- "search_imagery": search_imagery,
35
- "list_recent_imagery": list_recent_imagery,
36
- "get_item_details": get_item_details,
37
- "create_rgb_composite": create_rgb_composite,
38
- "calculate_ndvi": calculate_ndvi,
39
- "get_band_statistics": get_band_statistics,
40
- }
41
-
42
- # Tool definitions for the LLM
43
- TOOLS = [
44
- {
45
- "type": "function",
46
- "function": {
47
- "name": "geocode_location",
48
- "description": "Convert a place name to geographic coordinates (latitude, longitude). Use this when you need to find coordinates for a location.",
49
- "parameters": {
50
- "type": "object",
51
- "properties": {
52
- "place_name": {
53
- "type": "string",
54
- "description": "The name of the place to geocode (e.g., 'New York City', 'Paris, France')"
55
- }
56
- },
57
- "required": ["place_name"]
58
- }
59
- }
60
- },
61
- {
62
- "type": "function",
63
- "function": {
64
- "name": "get_bbox_from_location",
65
- "description": "Get a bounding box for a location, suitable for searching satellite imagery.",
66
- "parameters": {
67
- "type": "object",
68
- "properties": {
69
- "place_name": {
70
- "type": "string",
71
- "description": "The name of the place"
72
- },
73
- "buffer_km": {
74
- "type": "number",
75
- "description": "Buffer distance in kilometers (default: 10)"
76
- }
77
- },
78
- "required": ["place_name"]
79
- }
80
- }
81
- },
82
- {
83
- "type": "function",
84
- "function": {
85
- "name": "search_imagery",
86
- "description": "Search for Sentinel-2 satellite imagery in the EOPF catalog. Returns available scenes.",
87
- "parameters": {
88
- "type": "object",
89
- "properties": {
90
- "bbox": {
91
- "type": "array",
92
- "items": {"type": "number"},
93
- "description": "Bounding box as [min_lon, min_lat, max_lon, max_lat]"
94
- },
95
- "start_date": {
96
- "type": "string",
97
- "description": "Start date in YYYY-MM-DD format"
98
- },
99
- "end_date": {
100
- "type": "string",
101
- "description": "End date in YYYY-MM-DD format"
102
- },
103
- "max_cloud_cover": {
104
- "type": "number",
105
- "description": "Maximum cloud cover percentage (0-100)"
106
- },
107
- "max_items": {
108
- "type": "integer",
109
- "description": "Maximum number of results"
110
- }
111
- },
112
- "required": []
113
- }
114
- }
115
- },
116
- {
117
- "type": "function",
118
- "function": {
119
- "name": "list_recent_imagery",
120
- "description": "List recent Sentinel-2 imagery for a location. Combines geocoding and search.",
121
- "parameters": {
122
- "type": "object",
123
- "properties": {
124
- "location_name": {
125
- "type": "string",
126
- "description": "Name of the location to search"
127
- },
128
- "days": {
129
- "type": "integer",
130
- "description": "Number of days to look back (default: 7)"
131
- },
132
- "max_cloud_cover": {
133
- "type": "number",
134
- "description": "Maximum cloud cover percentage"
135
- },
136
- "max_items": {
137
- "type": "integer",
138
- "description": "Maximum number of results"
139
- }
140
- },
141
- "required": []
142
- }
143
- }
144
- },
145
- {
146
- "type": "function",
147
- "function": {
148
- "name": "get_item_details",
149
- "description": "Get detailed information about a specific Sentinel-2 scene by its ID.",
150
- "parameters": {
151
- "type": "object",
152
- "properties": {
153
- "item_id": {
154
- "type": "string",
155
- "description": "The STAC item ID"
156
- }
157
- },
158
- "required": ["item_id"]
159
- }
160
- }
161
- },
162
- {
163
- "type": "function",
164
- "function": {
165
- "name": "create_rgb_composite",
166
- "description": "Create an RGB true-color composite image from Sentinel-2 data.",
167
- "parameters": {
168
- "type": "object",
169
- "properties": {
170
- "zarr_url": {
171
- "type": "string",
172
- "description": "URL to the SR_10m Zarr asset from a STAC item"
173
- },
174
- "output_path": {
175
- "type": "string",
176
- "description": "Optional path to save the output image"
177
- },
178
- "subset_size": {
179
- "type": "integer",
180
- "description": "Size to subset the image (default: 1000 pixels)"
181
- }
182
- },
183
- "required": ["zarr_url"]
184
- }
185
- }
186
- },
187
- {
188
- "type": "function",
189
- "function": {
190
- "name": "calculate_ndvi",
191
- "description": "Calculate NDVI (vegetation index) from Sentinel-2 data.",
192
- "parameters": {
193
- "type": "object",
194
- "properties": {
195
- "zarr_url": {
196
- "type": "string",
197
- "description": "URL to the SR_10m Zarr asset"
198
- },
199
- "output_path": {
200
- "type": "string",
201
- "description": "Optional path to save the NDVI image"
202
- },
203
- "subset_size": {
204
- "type": "integer",
205
- "description": "Size to subset the image"
206
- }
207
- },
208
- "required": ["zarr_url"]
209
- }
210
- }
211
- },
212
- {
213
- "type": "function",
214
- "function": {
215
- "name": "get_band_statistics",
216
- "description": "Get statistics (min, max, mean) for spectral bands.",
217
- "parameters": {
218
- "type": "object",
219
- "properties": {
220
- "zarr_url": {
221
- "type": "string",
222
- "description": "URL to the Zarr asset"
223
- },
224
- "bands": {
225
- "type": "array",
226
- "items": {"type": "string"},
227
- "description": "List of band names to analyze"
228
- }
229
- },
230
- "required": ["zarr_url"]
231
- }
232
- }
233
- }
234
- ]
235
-
236
-
237
- class GeoMindAgent:
238
- """
239
- GeoMind - An AI agent for geospatial analysis with Sentinel-2 imagery.
240
-
241
- Uses OpenRouter API for access to multiple AI models.
242
- """
243
-
244
- def __init__(self, model: Optional[str] = None):
245
- """
246
- Initialize the GeoMind agent.
247
-
248
- Args:
249
- model: Model name (default: xiaomi/mimo-v2-flash:free)
250
- """
251
- self.provider = "openrouter"
252
- self.api_key = OPENROUTER_API_KEY
253
- self.model_name = model or OPENROUTER_MODEL
254
- self.base_url = OPENROUTER_API_URL
255
-
256
- if not self.api_key:
257
- raise ValueError(
258
- "OpenRouter API key required. Set OPENROUTER_API_KEY in .env file.\n"
259
- "Get your API key at: https://openrouter.ai/settings/keys"
260
- )
261
-
262
- print(f"🚀 GeoMind Agent initialized with {self.model_name} (OpenRouter)")
263
- print(f" API URL: {self.base_url}")
264
-
265
- # Create OpenAI-compatible client
266
- self.client = OpenAI(
267
- base_url=self.base_url,
268
- api_key=self.api_key
269
- )
270
-
271
- # Chat history
272
- self.history = []
273
-
274
- # Add system message
275
- self.system_prompt = self._get_system_prompt()
276
-
277
- 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 _execute_function(self, name: str, args: dict) -> dict:
303
- """Execute a function call and return the result."""
304
- print(f" 🔧 Executing: {name}({args})")
305
-
306
- if name not in TOOL_FUNCTIONS:
307
- return {"error": f"Unknown function: {name}"}
308
-
309
- try:
310
- result = TOOL_FUNCTIONS[name](**args)
311
- return result
312
- except Exception as e:
313
- return {"error": str(e)}
314
-
315
- def chat(self, message: str, verbose: bool = True) -> str:
316
- """
317
- Send a message to the agent and get a response.
318
- """
319
- if verbose:
320
- print(f"\n💬 User: {message}")
321
- print("🤔 Processing...")
322
-
323
- # Add user message to history
324
- self.history.append({"role": "user", "content": message})
325
-
326
- # Build messages with system prompt
327
- messages = [{"role": "system", "content": self.system_prompt}] + self.history
328
-
329
- max_iterations = 10
330
- iteration = 0
331
-
332
- while iteration < max_iterations:
333
- iteration += 1
334
-
335
- # Call the model
336
- response = self.client.chat.completions.create(
337
- model=self.model_name,
338
- messages=messages,
339
- tools=TOOLS,
340
- tool_choice="auto",
341
- max_tokens=4096,
342
- )
343
-
344
- assistant_message = response.choices[0].message
345
-
346
- # Check if there are tool calls
347
- if assistant_message.tool_calls:
348
- # Add assistant message with tool calls to messages
349
- messages.append({
350
- "role": "assistant",
351
- "content": assistant_message.content or "",
352
- "tool_calls": [
353
- {
354
- "id": tc.id,
355
- "type": "function",
356
- "function": {
357
- "name": tc.function.name,
358
- "arguments": tc.function.arguments
359
- }
360
- }
361
- for tc in assistant_message.tool_calls
362
- ]
363
- })
364
-
365
- # Execute each tool call
366
- for tool_call in assistant_message.tool_calls:
367
- func_name = tool_call.function.name
368
- func_args = json.loads(tool_call.function.arguments)
369
-
370
- result = self._execute_function(func_name, func_args)
371
-
372
- # Add tool result to messages
373
- messages.append({
374
- "role": "tool",
375
- "tool_call_id": tool_call.id,
376
- "content": json.dumps(result, default=str)
377
- })
378
- else:
379
- # No tool calls, we have a final response
380
- final_text = assistant_message.content or ""
381
-
382
- # Add to history
383
- self.history.append({"role": "assistant", "content": final_text})
384
-
385
- if verbose:
386
- print(f"\n🌍 GeoMind: {final_text}")
387
-
388
- return final_text
389
-
390
- return "Max iterations reached."
391
-
392
- def reset(self):
393
- """Reset the chat session."""
394
- self.history = []
395
- print("🔄 Chat session reset")
396
-
397
-
398
- def main(model: Optional[str] = None):
399
- """Main entry point for CLI usage."""
400
- import sys
401
-
402
- print("=" * 60)
403
- print("🌍 GeoMind - Geospatial AI Agent")
404
- print("=" * 60)
405
- print("Powered by OpenRouter | Sentinel-2 Imagery")
406
- print("Type 'quit' or 'exit' to end the session")
407
- print("Type 'reset' to start a new conversation")
408
- print("=" * 60)
409
-
410
- try:
411
- agent = GeoMindAgent(model=model)
412
- except ValueError as e:
413
- print(f"\n❌ Error: {e}")
414
- sys.exit(1)
415
- except Exception as e:
416
- print(f"\n❌ Error: {e}")
417
- print("\nPlease check your API key and internet connection.")
418
- sys.exit(1)
419
-
420
- while True:
421
- try:
422
- user_input = input("\n💬 You: ").strip()
423
-
424
- if not user_input:
425
- continue
426
-
427
- if user_input.lower() in ['quit', 'exit', 'q']:
428
- print("\n👋 Goodbye!")
429
- break
430
-
431
- if user_input.lower() == 'reset':
432
- agent.reset()
433
- continue
434
-
435
- agent.chat(user_input)
436
-
437
- except KeyboardInterrupt:
438
- print("\n\n👋 Goodbye!")
439
- break
440
- except Exception as e:
441
- print(f"\n❌ Error: {e}")
442
-
443
-
444
- if __name__ == "__main__":
445
- main()
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.
6
+ """
7
+
8
+ import json
9
+ import re
10
+ from typing import Optional, Callable, Any
11
+ from datetime import datetime
12
+
13
+ from openai import OpenAI
14
+
15
+ from .config import OPENROUTER_API_KEY, OPENROUTER_API_URL, OPENROUTER_MODEL
16
+ from .tools import (
17
+ geocode_location,
18
+ get_bbox_from_location,
19
+ search_imagery,
20
+ get_item_details,
21
+ list_recent_imagery,
22
+ create_rgb_composite,
23
+ calculate_ndvi,
24
+ get_band_statistics,
25
+ )
26
+
27
+
28
+ # Map tool names to functions
29
+ TOOL_FUNCTIONS = {
30
+ "geocode_location": geocode_location,
31
+ "get_bbox_from_location": get_bbox_from_location,
32
+ "search_imagery": search_imagery,
33
+ "list_recent_imagery": list_recent_imagery,
34
+ "get_item_details": get_item_details,
35
+ "create_rgb_composite": create_rgb_composite,
36
+ "calculate_ndvi": calculate_ndvi,
37
+ "get_band_statistics": get_band_statistics,
38
+ }
39
+
40
+ # Tool definitions for the LLM
41
+ TOOLS = [
42
+ {
43
+ "type": "function",
44
+ "function": {
45
+ "name": "geocode_location",
46
+ "description": "Convert a place name to geographic coordinates (latitude, longitude). Use this when you need to find coordinates for a location.",
47
+ "parameters": {
48
+ "type": "object",
49
+ "properties": {
50
+ "place_name": {
51
+ "type": "string",
52
+ "description": "The name of the place to geocode (e.g., 'New York City', 'Paris, France')",
53
+ }
54
+ },
55
+ "required": ["place_name"],
56
+ },
57
+ },
58
+ },
59
+ {
60
+ "type": "function",
61
+ "function": {
62
+ "name": "get_bbox_from_location",
63
+ "description": "Get a bounding box for a location, suitable for searching satellite imagery.",
64
+ "parameters": {
65
+ "type": "object",
66
+ "properties": {
67
+ "place_name": {
68
+ "type": "string",
69
+ "description": "The name of the place",
70
+ },
71
+ "buffer_km": {
72
+ "type": "number",
73
+ "description": "Buffer distance in kilometers (default: 10)",
74
+ },
75
+ },
76
+ "required": ["place_name"],
77
+ },
78
+ },
79
+ },
80
+ {
81
+ "type": "function",
82
+ "function": {
83
+ "name": "search_imagery",
84
+ "description": "Search for Sentinel-2 satellite imagery in the EOPF catalog. Returns available scenes.",
85
+ "parameters": {
86
+ "type": "object",
87
+ "properties": {
88
+ "bbox": {
89
+ "type": "array",
90
+ "items": {"type": "number"},
91
+ "description": "Bounding box as [min_lon, min_lat, max_lon, max_lat]",
92
+ },
93
+ "start_date": {
94
+ "type": "string",
95
+ "description": "Start date in YYYY-MM-DD format",
96
+ },
97
+ "end_date": {
98
+ "type": "string",
99
+ "description": "End date in YYYY-MM-DD format",
100
+ },
101
+ "max_cloud_cover": {
102
+ "type": "number",
103
+ "description": "Maximum cloud cover percentage (0-100)",
104
+ },
105
+ "max_items": {
106
+ "type": "integer",
107
+ "description": "Maximum number of results",
108
+ },
109
+ },
110
+ "required": [],
111
+ },
112
+ },
113
+ },
114
+ {
115
+ "type": "function",
116
+ "function": {
117
+ "name": "list_recent_imagery",
118
+ "description": "List recent Sentinel-2 imagery for a location. Combines geocoding and search.",
119
+ "parameters": {
120
+ "type": "object",
121
+ "properties": {
122
+ "location_name": {
123
+ "type": "string",
124
+ "description": "Name of the location to search",
125
+ },
126
+ "days": {
127
+ "type": "integer",
128
+ "description": "Number of days to look back (default: 7)",
129
+ },
130
+ "max_cloud_cover": {
131
+ "type": "number",
132
+ "description": "Maximum cloud cover percentage",
133
+ },
134
+ "max_items": {
135
+ "type": "integer",
136
+ "description": "Maximum number of results",
137
+ },
138
+ },
139
+ "required": [],
140
+ },
141
+ },
142
+ },
143
+ {
144
+ "type": "function",
145
+ "function": {
146
+ "name": "get_item_details",
147
+ "description": "Get detailed information about a specific Sentinel-2 scene by its ID.",
148
+ "parameters": {
149
+ "type": "object",
150
+ "properties": {
151
+ "item_id": {"type": "string", "description": "The STAC item ID"}
152
+ },
153
+ "required": ["item_id"],
154
+ },
155
+ },
156
+ },
157
+ {
158
+ "type": "function",
159
+ "function": {
160
+ "name": "create_rgb_composite",
161
+ "description": "Create an RGB true-color composite image from Sentinel-2 data.",
162
+ "parameters": {
163
+ "type": "object",
164
+ "properties": {
165
+ "zarr_url": {
166
+ "type": "string",
167
+ "description": "URL to the SR_10m Zarr asset from a STAC item",
168
+ },
169
+ "output_path": {
170
+ "type": "string",
171
+ "description": "Optional path to save the output image",
172
+ },
173
+ "subset_size": {
174
+ "type": "integer",
175
+ "description": "Size to subset the image (default: 1000 pixels)",
176
+ },
177
+ },
178
+ "required": ["zarr_url"],
179
+ },
180
+ },
181
+ },
182
+ {
183
+ "type": "function",
184
+ "function": {
185
+ "name": "calculate_ndvi",
186
+ "description": "Calculate NDVI (vegetation index) from Sentinel-2 data.",
187
+ "parameters": {
188
+ "type": "object",
189
+ "properties": {
190
+ "zarr_url": {
191
+ "type": "string",
192
+ "description": "URL to the SR_10m Zarr asset",
193
+ },
194
+ "output_path": {
195
+ "type": "string",
196
+ "description": "Optional path to save the NDVI image",
197
+ },
198
+ "subset_size": {
199
+ "type": "integer",
200
+ "description": "Size to subset the image",
201
+ },
202
+ },
203
+ "required": ["zarr_url"],
204
+ },
205
+ },
206
+ },
207
+ {
208
+ "type": "function",
209
+ "function": {
210
+ "name": "get_band_statistics",
211
+ "description": "Get statistics (min, max, mean) for spectral bands.",
212
+ "parameters": {
213
+ "type": "object",
214
+ "properties": {
215
+ "zarr_url": {
216
+ "type": "string",
217
+ "description": "URL to the Zarr asset",
218
+ },
219
+ "bands": {
220
+ "type": "array",
221
+ "items": {"type": "string"},
222
+ "description": "List of band names to analyze",
223
+ },
224
+ },
225
+ "required": ["zarr_url"],
226
+ },
227
+ },
228
+ },
229
+ ]
230
+
231
+
232
+ class GeoMindAgent:
233
+ """
234
+ GeoMind - An AI agent for geospatial analysis with Sentinel-2 imagery.
235
+
236
+ Uses OpenRouter API for access to multiple AI models.
237
+ """
238
+
239
+ def __init__(self, model: Optional[str] = None):
240
+ """
241
+ Initialize the GeoMind agent.
242
+
243
+ Args:
244
+ model: Model name (default: xiaomi/mimo-v2-flash:free)
245
+ """
246
+ self.provider = "openrouter"
247
+ self.api_key = OPENROUTER_API_KEY
248
+ self.model_name = model or OPENROUTER_MODEL
249
+ self.base_url = OPENROUTER_API_URL
250
+
251
+ if not self.api_key:
252
+ raise ValueError(
253
+ "OpenRouter API key required. Set OPENROUTER_API_KEY in .env file.\n"
254
+ "Get your API key at: https://openrouter.ai/settings/keys"
255
+ )
256
+
257
+ print(f"🚀 GeoMind Agent initialized with {self.model_name} (OpenRouter)")
258
+ print(f" API URL: {self.base_url}")
259
+
260
+ # Create OpenAI-compatible client
261
+ self.client = OpenAI(base_url=self.base_url, api_key=self.api_key)
262
+
263
+ # Chat history
264
+ self.history = []
265
+
266
+ # Add system message
267
+ self.system_prompt = self._get_system_prompt()
268
+
269
+ def _get_system_prompt(self) -> str:
270
+ """Get the system prompt for the agent."""
271
+ return f"""You are GeoMind, an expert AI assistant specialized in geospatial analysis
272
+ and satellite imagery. You help users find, analyze, and visualize Sentinel-2 satellite data
273
+ from the EOPF (ESA Earth Observation Processing Framework) catalog.
274
+
275
+ Your capabilities include:
276
+ 1. **Search**: Find Sentinel-2 L2A imagery by location, date, and cloud cover
277
+ 2. **Geocoding**: Convert place names to coordinates for searching
278
+ 3. **Visualization**: Create RGB composites and NDVI maps from imagery
279
+ 4. **Analysis**: Calculate spectral indices and band statistics
280
+
281
+ Key information:
282
+ - Data source: EOPF STAC API (https://stac.core.eopf.eodc.eu)
283
+ - Satellite: Sentinel-2 (L2A surface reflectance products)
284
+ - Bands available: B01-B12 at 10m, 20m, or 60m resolution
285
+ - Current date: {datetime.now().strftime('%Y-%m-%d')}
286
+
287
+ When users ask for imagery:
288
+ 1. First use get_bbox_from_location or list_recent_imagery to search
289
+ 2. Present the results clearly with key metadata
290
+ 3. Offer to create visualizations if data is found
291
+
292
+ Always explain what you're doing and interpret results in a helpful way."""
293
+
294
+ def _execute_function(self, name: str, args: dict) -> dict:
295
+ """Execute a function call and return the result."""
296
+ print(f" 🔧 Executing: {name}({args})")
297
+
298
+ if name not in TOOL_FUNCTIONS:
299
+ return {"error": f"Unknown function: {name}"}
300
+
301
+ try:
302
+ result = TOOL_FUNCTIONS[name](**args)
303
+ return result
304
+ except Exception as e:
305
+ return {"error": str(e)}
306
+
307
+ def chat(self, message: str, verbose: bool = True) -> str:
308
+ """
309
+ Send a message to the agent and get a response.
310
+ """
311
+ if verbose:
312
+ print(f"\n💬 User: {message}")
313
+ print("🤔 Processing...")
314
+
315
+ # Add user message to history
316
+ self.history.append({"role": "user", "content": message})
317
+
318
+ # Build messages with system prompt
319
+ messages = [{"role": "system", "content": self.system_prompt}] + self.history
320
+
321
+ max_iterations = 10
322
+ iteration = 0
323
+
324
+ while iteration < max_iterations:
325
+ iteration += 1
326
+
327
+ # Call the model
328
+ response = self.client.chat.completions.create(
329
+ model=self.model_name,
330
+ messages=messages,
331
+ tools=TOOLS,
332
+ tool_choice="auto",
333
+ max_tokens=4096,
334
+ )
335
+
336
+ assistant_message = response.choices[0].message
337
+
338
+ # Check if there are tool calls
339
+ if assistant_message.tool_calls:
340
+ # Add assistant message with tool calls to messages
341
+ messages.append(
342
+ {
343
+ "role": "assistant",
344
+ "content": assistant_message.content or "",
345
+ "tool_calls": [
346
+ {
347
+ "id": tc.id,
348
+ "type": "function",
349
+ "function": {
350
+ "name": tc.function.name,
351
+ "arguments": tc.function.arguments,
352
+ },
353
+ }
354
+ for tc in assistant_message.tool_calls
355
+ ],
356
+ }
357
+ )
358
+
359
+ # Execute each tool call
360
+ for tool_call in assistant_message.tool_calls:
361
+ func_name = tool_call.function.name
362
+ func_args = json.loads(tool_call.function.arguments)
363
+
364
+ result = self._execute_function(func_name, func_args)
365
+
366
+ # Add tool result to messages
367
+ messages.append(
368
+ {
369
+ "role": "tool",
370
+ "tool_call_id": tool_call.id,
371
+ "content": json.dumps(result, default=str),
372
+ }
373
+ )
374
+ else:
375
+ # No tool calls, we have a final response
376
+ final_text = assistant_message.content or ""
377
+
378
+ # Add to history
379
+ self.history.append({"role": "assistant", "content": final_text})
380
+
381
+ if verbose:
382
+ print(f"\n🌍 GeoMind: {final_text}")
383
+
384
+ return final_text
385
+
386
+ return "Max iterations reached."
387
+
388
+ def reset(self):
389
+ """Reset the chat session."""
390
+ self.history = []
391
+ print("🔄 Chat session reset")
392
+
393
+
394
+ def main(model: Optional[str] = None):
395
+ """Main entry point for CLI usage."""
396
+ import sys
397
+
398
+ print("=" * 60)
399
+ print("🌍 GeoMind - Geospatial AI Agent")
400
+ print("=" * 60)
401
+ print("Powered by OpenRouter | Sentinel-2 Imagery")
402
+ print("Type 'quit' or 'exit' to end the session")
403
+ print("Type 'reset' to start a new conversation")
404
+ print("=" * 60)
405
+
406
+ try:
407
+ agent = GeoMindAgent(model=model)
408
+ except ValueError as e:
409
+ print(f"\n❌ Error: {e}")
410
+ sys.exit(1)
411
+ except Exception as e:
412
+ print(f"\n❌ Error: {e}")
413
+ print("\nPlease check your API key and internet connection.")
414
+ sys.exit(1)
415
+
416
+ while True:
417
+ try:
418
+ user_input = input("\n💬 You: ").strip()
419
+
420
+ if not user_input:
421
+ continue
422
+
423
+ if user_input.lower() in ["quit", "exit", "q"]:
424
+ print("\n👋 Goodbye!")
425
+ break
426
+
427
+ if user_input.lower() == "reset":
428
+ agent.reset()
429
+ continue
430
+
431
+ agent.chat(user_input)
432
+
433
+ except KeyboardInterrupt:
434
+ print("\n\n👋 Goodbye!")
435
+ break
436
+ except Exception as e:
437
+ print(f"\n❌ Error: {e}")
438
+
439
+
440
+ if __name__ == "__main__":
441
+ main()