geomind-ai 1.1.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/PKG-INFO +1 -1
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/__init__.py +1 -1
- geomind_ai-1.2.0/geomind/agent.py +218 -0
- geomind_ai-1.2.0/geomind/cli.py +392 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/config.py +3 -3
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/tools/processing.py +90 -38
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/tools/stac_search.py +61 -11
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/PKG-INFO +1 -1
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/pyproject.toml +1 -1
- geomind_ai-1.1.0/geomind/agent.py +0 -457
- geomind_ai-1.1.0/geomind/cli.py +0 -199
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/LICENSE +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/MANIFEST.in +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/README.md +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/tools/__init__.py +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/tools/geocoding.py +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/SOURCES.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/dependency_links.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/entry_points.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/requires.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/top_level.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/requirements.txt +0 -0
- {geomind_ai-1.1.0 → geomind_ai-1.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeoMind Agent - Simplified AI agent for satellite imagery analysis.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from openai import OpenAI
|
|
9
|
+
|
|
10
|
+
from .config import OPENROUTER_API_KEY, OPENROUTER_API_URL, OPENROUTER_MODEL
|
|
11
|
+
from .tools import (
|
|
12
|
+
list_recent_imagery,
|
|
13
|
+
create_rgb_composite,
|
|
14
|
+
calculate_ndvi,
|
|
15
|
+
search_imagery,
|
|
16
|
+
get_bbox_from_location,
|
|
17
|
+
geocode_location,
|
|
18
|
+
get_item_details,
|
|
19
|
+
get_band_statistics,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Tool function mapping
|
|
23
|
+
TOOL_FUNCTIONS = {
|
|
24
|
+
"list_recent_imagery": list_recent_imagery,
|
|
25
|
+
"create_rgb_composite": create_rgb_composite,
|
|
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,
|
|
31
|
+
"get_band_statistics": get_band_statistics,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Simplified tool definitions
|
|
35
|
+
TOOLS = [
|
|
36
|
+
{
|
|
37
|
+
"type": "function",
|
|
38
|
+
"function": {
|
|
39
|
+
"name": "list_recent_imagery",
|
|
40
|
+
"description": "Find recent Sentinel-2 imagery for a location",
|
|
41
|
+
"parameters": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
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"},
|
|
48
|
+
},
|
|
49
|
+
"required": [],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"type": "function",
|
|
55
|
+
"function": {
|
|
56
|
+
"name": "create_rgb_composite",
|
|
57
|
+
"description": "Create RGB composite from Sentinel-2 data",
|
|
58
|
+
"parameters": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
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)"},
|
|
64
|
+
},
|
|
65
|
+
"required": ["zarr_url"],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "function",
|
|
71
|
+
"function": {
|
|
72
|
+
"name": "calculate_ndvi",
|
|
73
|
+
"description": "Calculate NDVI vegetation index",
|
|
74
|
+
"parameters": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"properties": {
|
|
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)"},
|
|
80
|
+
},
|
|
81
|
+
"required": ["zarr_url"],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"type": "function",
|
|
87
|
+
"function": {
|
|
88
|
+
"name": "search_imagery",
|
|
89
|
+
"description": "Search Sentinel-2 imagery by parameters",
|
|
90
|
+
"parameters": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": {
|
|
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"},
|
|
98
|
+
},
|
|
99
|
+
"required": [],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"type": "function",
|
|
105
|
+
"function": {
|
|
106
|
+
"name": "get_bbox_from_location",
|
|
107
|
+
"description": "Get bounding box for a location",
|
|
108
|
+
"parameters": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"properties": {
|
|
111
|
+
"place_name": {"type": "string", "description": "Location name"},
|
|
112
|
+
"buffer_km": {"type": "number", "description": "Buffer distance in km"},
|
|
113
|
+
},
|
|
114
|
+
"required": ["place_name"],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class GeoMindAgent:
|
|
122
|
+
"""Simplified GeoMind agent for satellite imagery analysis."""
|
|
123
|
+
|
|
124
|
+
def __init__(self, model: Optional[str] = None, api_key: Optional[str] = None):
|
|
125
|
+
self.api_key = api_key or OPENROUTER_API_KEY
|
|
126
|
+
self.model_name = model or OPENROUTER_MODEL
|
|
127
|
+
|
|
128
|
+
if not self.api_key:
|
|
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)
|
|
133
|
+
self.history = []
|
|
134
|
+
|
|
135
|
+
def _get_system_prompt(self) -> str:
|
|
136
|
+
"""Short, focused system prompt."""
|
|
137
|
+
return f"""You are GeoMind, an AI assistant for satellite imagery analysis using Sentinel-2 data.
|
|
138
|
+
|
|
139
|
+
Current date: {datetime.now().strftime('%Y-%m-%d')}
|
|
140
|
+
|
|
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)
|
|
147
|
+
|
|
148
|
+
Be helpful and accurate - always use real data from function responses."""
|
|
149
|
+
|
|
150
|
+
def chat(self, message: str, verbose: bool = True) -> str:
|
|
151
|
+
"""Send message to agent and get response."""
|
|
152
|
+
if verbose:
|
|
153
|
+
print(f"\nUser: {message}")
|
|
154
|
+
print("Processing...")
|
|
155
|
+
|
|
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
|
+
)
|
|
167
|
+
|
|
168
|
+
choice = response.choices[0]
|
|
169
|
+
assistant_message = choice.message
|
|
170
|
+
|
|
171
|
+
tool_calls = assistant_message.tool_calls
|
|
172
|
+
if tool_calls:
|
|
173
|
+
# Add assistant message with tool calls
|
|
174
|
+
messages.append({
|
|
175
|
+
"role": "assistant",
|
|
176
|
+
"content": assistant_message.content or "",
|
|
177
|
+
"tool_calls": [
|
|
178
|
+
{
|
|
179
|
+
"id": tc.id,
|
|
180
|
+
"type": "function",
|
|
181
|
+
"function": {"name": tc.function.name, "arguments": tc.function.arguments},
|
|
182
|
+
}
|
|
183
|
+
for tc in tool_calls
|
|
184
|
+
],
|
|
185
|
+
})
|
|
186
|
+
|
|
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 ""
|
|
207
|
+
self.history.append({"role": "assistant", "content": final_text})
|
|
208
|
+
|
|
209
|
+
if verbose:
|
|
210
|
+
print(f"\nGeoMind: {final_text}")
|
|
211
|
+
|
|
212
|
+
return final_text
|
|
213
|
+
|
|
214
|
+
return "Max iterations reached"
|
|
215
|
+
|
|
216
|
+
def reset(self):
|
|
217
|
+
"""Reset chat history."""
|
|
218
|
+
self.history = []
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for GeoMind.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
import subprocess
|
|
11
|
+
import platform
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from .agent import GeoMindAgent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Config file path for storing API key
|
|
19
|
+
CONFIG_DIR = Path.home() / ".geomind"
|
|
20
|
+
CONFIG_FILE = CONFIG_DIR / "config"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_saved_api_key() -> Optional[str]:
|
|
24
|
+
"""Get API key saved on user's PC."""
|
|
25
|
+
if CONFIG_FILE.exists():
|
|
26
|
+
try:
|
|
27
|
+
return CONFIG_FILE.read_text().strip()
|
|
28
|
+
except Exception:
|
|
29
|
+
return None
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def save_api_key(api_key: str) -> bool:
|
|
34
|
+
"""Save API key to user's PC."""
|
|
35
|
+
try:
|
|
36
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
CONFIG_FILE.write_text(api_key)
|
|
38
|
+
return True
|
|
39
|
+
except Exception:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def display_recent_images():
|
|
44
|
+
"""Display recently created images if any exist."""
|
|
45
|
+
outputs_dir = Path("outputs")
|
|
46
|
+
if not outputs_dir.exists():
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
# Get recent image files (created in last few seconds)
|
|
50
|
+
import time
|
|
51
|
+
recent_threshold = time.time() - 30 # 30 seconds ago
|
|
52
|
+
|
|
53
|
+
recent_images = []
|
|
54
|
+
for ext in ['*.png', '*.jpg', '*.jpeg', '*.tiff']:
|
|
55
|
+
for img_file in outputs_dir.glob(ext):
|
|
56
|
+
if img_file.stat().st_mtime > recent_threshold:
|
|
57
|
+
recent_images.append(img_file)
|
|
58
|
+
|
|
59
|
+
if recent_images:
|
|
60
|
+
print("\n" + "="*60)
|
|
61
|
+
print("Generated Images:")
|
|
62
|
+
for img in recent_images:
|
|
63
|
+
print(f" • {img.name} ({img.stat().st_size // 1024}KB)")
|
|
64
|
+
|
|
65
|
+
# Try to open the most recent image
|
|
66
|
+
if recent_images:
|
|
67
|
+
latest_image = max(recent_images, key=lambda x: x.stat().st_mtime)
|
|
68
|
+
open_image_viewer(latest_image)
|
|
69
|
+
print("="*60)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def open_image_viewer(image_path: Path):
|
|
73
|
+
"""Open image in default viewer."""
|
|
74
|
+
try:
|
|
75
|
+
system = platform.system()
|
|
76
|
+
if system == "Windows":
|
|
77
|
+
os.startfile(str(image_path))
|
|
78
|
+
elif system == "Darwin": # macOS
|
|
79
|
+
subprocess.run(["open", str(image_path)], check=False)
|
|
80
|
+
else: # Linux
|
|
81
|
+
subprocess.run(["xdg-open", str(image_path)], check=False)
|
|
82
|
+
print(f" -> Opened {image_path.name} in default viewer")
|
|
83
|
+
except Exception:
|
|
84
|
+
print(f" -> Saved to: {image_path}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def format_response_box(title: str, content: str, color_code: str = "\033[94m") -> str:
|
|
88
|
+
"""Format response in an attractive box."""
|
|
89
|
+
RESET = "\033[0m"
|
|
90
|
+
lines = content.split('\n')
|
|
91
|
+
max_width = max(len(line) for line in lines) if lines else 0
|
|
92
|
+
max_width = max(max_width, len(title) + 4)
|
|
93
|
+
width = min(max_width + 4, 80)
|
|
94
|
+
|
|
95
|
+
box = f"{color_code}"
|
|
96
|
+
box += "┌" + "─" * (width - 2) + "┐\n"
|
|
97
|
+
box += f"│ {title:<{width-4}} │\n"
|
|
98
|
+
box += "├" + "─" * (width - 2) + "┤\n"
|
|
99
|
+
|
|
100
|
+
for line in lines:
|
|
101
|
+
if line.strip():
|
|
102
|
+
box += f"│ {line:<{width-4}} │\n"
|
|
103
|
+
else:
|
|
104
|
+
box += f"│{' ' * (width-2)}│\n"
|
|
105
|
+
|
|
106
|
+
box += "└" + "─" * (width - 2) + "┘"
|
|
107
|
+
box += RESET
|
|
108
|
+
return box
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ThinkingIndicator:
|
|
112
|
+
"""Claude Code style thinking animation."""
|
|
113
|
+
|
|
114
|
+
def __init__(self):
|
|
115
|
+
self.is_thinking = False
|
|
116
|
+
self.thread = None
|
|
117
|
+
self.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
118
|
+
self.thinking_messages = [
|
|
119
|
+
"Thinking",
|
|
120
|
+
"Analyzing satellite data",
|
|
121
|
+
"Processing request",
|
|
122
|
+
"Searching imagery"
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
def start(self):
|
|
126
|
+
"""Start the thinking animation."""
|
|
127
|
+
self.is_thinking = True
|
|
128
|
+
self.thread = threading.Thread(target=self._animate)
|
|
129
|
+
self.thread.daemon = True
|
|
130
|
+
self.thread.start()
|
|
131
|
+
|
|
132
|
+
def stop(self):
|
|
133
|
+
"""Stop the thinking animation."""
|
|
134
|
+
self.is_thinking = False
|
|
135
|
+
if self.thread:
|
|
136
|
+
self.thread.join(timeout=0.1)
|
|
137
|
+
# Clear the line
|
|
138
|
+
print("\r" + " " * 60 + "\r", end="", flush=True)
|
|
139
|
+
|
|
140
|
+
def _animate(self):
|
|
141
|
+
"""Run the thinking animation."""
|
|
142
|
+
frame_idx = 0
|
|
143
|
+
message_idx = 0
|
|
144
|
+
message_counter = 0
|
|
145
|
+
|
|
146
|
+
# Colors like Claude Code
|
|
147
|
+
DIM = '\033[2m'
|
|
148
|
+
RESET = '\033[0m'
|
|
149
|
+
|
|
150
|
+
while self.is_thinking:
|
|
151
|
+
spinner = self.frames[frame_idx % len(self.frames)]
|
|
152
|
+
|
|
153
|
+
# Cycle through thinking messages every 30 frames (3 seconds)
|
|
154
|
+
if message_counter % 30 == 0:
|
|
155
|
+
message_idx = (message_idx + 1) % len(self.thinking_messages)
|
|
156
|
+
|
|
157
|
+
message = self.thinking_messages[message_idx]
|
|
158
|
+
|
|
159
|
+
# Show thinking with shimmer effect like Claude Code
|
|
160
|
+
print(f"\r{DIM}{spinner} {message}...{RESET}", end="", flush=True)
|
|
161
|
+
|
|
162
|
+
time.sleep(0.1)
|
|
163
|
+
frame_idx += 1
|
|
164
|
+
message_counter += 1
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main():
|
|
168
|
+
"""Main CLI entry point for the geomind package."""
|
|
169
|
+
parser = argparse.ArgumentParser(
|
|
170
|
+
description="GeoMind - AI agent for geospatial analysis with Sentinel-2 imagery",
|
|
171
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
172
|
+
epilog="""
|
|
173
|
+
Examples:
|
|
174
|
+
# Interactive mode
|
|
175
|
+
geomind
|
|
176
|
+
|
|
177
|
+
# Single query
|
|
178
|
+
geomind --query "Find recent imagery of Paris"
|
|
179
|
+
|
|
180
|
+
# With custom model
|
|
181
|
+
geomind --model "anthropic/claude-3.5-sonnet"
|
|
182
|
+
|
|
183
|
+
# With API key
|
|
184
|
+
geomind --api-key "your-openrouter-api-key"
|
|
185
|
+
|
|
186
|
+
# Clear saved API key
|
|
187
|
+
geomind --clear-key
|
|
188
|
+
|
|
189
|
+
Environment Variables:
|
|
190
|
+
OPENROUTER_API_KEY Your OpenRouter API key
|
|
191
|
+
OPENROUTER_MODEL Model to use (default: nvidia/nemotron-3-nano-30b-a3b:free)
|
|
192
|
+
OPENROUTER_API_URL API endpoint (default: https://openrouter.ai/api/v1)
|
|
193
|
+
""",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
"--query",
|
|
198
|
+
"-q",
|
|
199
|
+
type=str,
|
|
200
|
+
help="Single query to run (if not provided, starts interactive mode)",
|
|
201
|
+
)
|
|
202
|
+
parser.add_argument(
|
|
203
|
+
"--model",
|
|
204
|
+
"-m",
|
|
205
|
+
type=str,
|
|
206
|
+
help="Model name to use (e.g., 'anthropic/claude-3.5-sonnet')",
|
|
207
|
+
)
|
|
208
|
+
parser.add_argument(
|
|
209
|
+
"--api-key",
|
|
210
|
+
"-k",
|
|
211
|
+
type=str,
|
|
212
|
+
help="OpenRouter API key (or set OPENROUTER_API_KEY env variable)",
|
|
213
|
+
)
|
|
214
|
+
parser.add_argument(
|
|
215
|
+
"--version", "-v", action="store_true", help="Show version and exit"
|
|
216
|
+
)
|
|
217
|
+
parser.add_argument("--clear-key", action="store_true", help="Clear saved API key")
|
|
218
|
+
|
|
219
|
+
args = parser.parse_args()
|
|
220
|
+
|
|
221
|
+
if args.clear_key:
|
|
222
|
+
if CONFIG_FILE.exists():
|
|
223
|
+
CONFIG_FILE.unlink()
|
|
224
|
+
print("Saved API key cleared.")
|
|
225
|
+
else:
|
|
226
|
+
print("No saved API key found.")
|
|
227
|
+
sys.exit(0)
|
|
228
|
+
|
|
229
|
+
if args.version:
|
|
230
|
+
from . import __version__
|
|
231
|
+
|
|
232
|
+
print(f"GeoMind version {__version__}")
|
|
233
|
+
sys.exit(0)
|
|
234
|
+
|
|
235
|
+
# Start interactive or single-query mode
|
|
236
|
+
try:
|
|
237
|
+
if args.query:
|
|
238
|
+
# Single query mode - check API key in order: argument > env > saved file
|
|
239
|
+
from .config import OPENROUTER_API_KEY
|
|
240
|
+
|
|
241
|
+
api_key = args.api_key or OPENROUTER_API_KEY or get_saved_api_key()
|
|
242
|
+
if not api_key:
|
|
243
|
+
print("Error: No API key found. Run 'geomind' first to set up.")
|
|
244
|
+
sys.exit(1)
|
|
245
|
+
agent = GeoMindAgent(model=args.model, api_key=api_key)
|
|
246
|
+
agent.chat(args.query)
|
|
247
|
+
else:
|
|
248
|
+
# Interactive mode
|
|
249
|
+
run_interactive(model=args.model, api_key=args.api_key)
|
|
250
|
+
except ValueError as e:
|
|
251
|
+
print(f"\nError: {e}")
|
|
252
|
+
sys.exit(1)
|
|
253
|
+
except KeyboardInterrupt:
|
|
254
|
+
print("\n\nGoodbye!")
|
|
255
|
+
sys.exit(0)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
print(f"\nUnexpected error: {e}")
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def print_banner():
|
|
262
|
+
from . import __version__
|
|
263
|
+
|
|
264
|
+
# ANSI color codes
|
|
265
|
+
BOLD = '\033[1m'
|
|
266
|
+
DIM = '\033[2m'
|
|
267
|
+
RESET = '\033[0m'
|
|
268
|
+
|
|
269
|
+
banner = f"""
|
|
270
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
271
|
+
│ {BOLD}>_ GeoMind{RESET} (v{__version__}) │
|
|
272
|
+
│ │
|
|
273
|
+
│ model: nvidia/nemotron-3-nano-30b-a3b:free │
|
|
274
|
+
│ docs: https://harshshinde0.github.io/GeoMind │
|
|
275
|
+
│ authors: Harsh Shinde, Rajat Shinde │
|
|
276
|
+
│ official: https://harshshinde0.github.io/GeoMind │
|
|
277
|
+
│ │
|
|
278
|
+
│ Type "?" for help, "quit" to exit. │
|
|
279
|
+
└──────────────────────────────────────────────────────────────────────┘
|
|
280
|
+
"""
|
|
281
|
+
print(banner)
|
|
282
|
+
print()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def print_help():
|
|
286
|
+
"""Print interactive session help."""
|
|
287
|
+
help_text = """
|
|
288
|
+
Interactive Commands:
|
|
289
|
+
help, ? Show this help
|
|
290
|
+
reset Reset conversation
|
|
291
|
+
exit, quit, q Exit GeoMind
|
|
292
|
+
|
|
293
|
+
Query Examples:
|
|
294
|
+
> Find recent Sentinel-2 imagery of Paris
|
|
295
|
+
> Show me NDVI data for the Amazon rainforest
|
|
296
|
+
> Search for images with less than 10% cloud cover in London
|
|
297
|
+
> Get satellite data for coordinates 40.7128, -74.0060
|
|
298
|
+
|
|
299
|
+
For CLI options, run: geomind --help
|
|
300
|
+
"""
|
|
301
|
+
print(help_text)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
|
|
305
|
+
"""Run interactive CLI mode."""
|
|
306
|
+
from . import __version__
|
|
307
|
+
|
|
308
|
+
print_banner()
|
|
309
|
+
|
|
310
|
+
# Check for API key in order: argument > env > saved file
|
|
311
|
+
from .config import OPENROUTER_API_KEY
|
|
312
|
+
|
|
313
|
+
# Priority: command line arg > env variable > saved file
|
|
314
|
+
if api_key:
|
|
315
|
+
# Use provided argument
|
|
316
|
+
pass
|
|
317
|
+
elif OPENROUTER_API_KEY:
|
|
318
|
+
api_key = OPENROUTER_API_KEY
|
|
319
|
+
else:
|
|
320
|
+
api_key = get_saved_api_key()
|
|
321
|
+
|
|
322
|
+
if not api_key:
|
|
323
|
+
print("\nOpenRouter API key required (FREE)")
|
|
324
|
+
print(" Get yours at: https://openrouter.ai/settings/keys\n")
|
|
325
|
+
api_key = input(" Enter your API key: ").strip()
|
|
326
|
+
|
|
327
|
+
if not api_key:
|
|
328
|
+
print("\nNo API key provided. Exiting.")
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
# Save the key for future use
|
|
332
|
+
if save_api_key(api_key):
|
|
333
|
+
print(" API key saved! You won't need to enter it again.\n")
|
|
334
|
+
else:
|
|
335
|
+
print(" Warning: Could not save API key. You'll need to enter it next time.\n")
|
|
336
|
+
|
|
337
|
+
agent = GeoMindAgent(model=model, api_key=api_key)
|
|
338
|
+
|
|
339
|
+
# Claude Code style color scheme
|
|
340
|
+
CYAN = '\033[96m'
|
|
341
|
+
DIM = '\033[2m'
|
|
342
|
+
BOLD = '\033[1m'
|
|
343
|
+
RESET = '\033[0m'
|
|
344
|
+
|
|
345
|
+
while True:
|
|
346
|
+
try:
|
|
347
|
+
# Simple prompt like Claude Code
|
|
348
|
+
user_input = input(f"\n{CYAN}>{RESET} ").strip()
|
|
349
|
+
|
|
350
|
+
if not user_input:
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
if user_input.lower() in ["quit", "exit", "q"]:
|
|
354
|
+
print(f"\n{DIM}Goodbye!{RESET}")
|
|
355
|
+
break
|
|
356
|
+
|
|
357
|
+
if user_input.lower() == "reset":
|
|
358
|
+
agent.reset()
|
|
359
|
+
print(f"{DIM}Started new conversation{RESET}")
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
if user_input.lower() in ["help", "?"]:
|
|
363
|
+
print_help()
|
|
364
|
+
continue
|
|
365
|
+
|
|
366
|
+
# Start thinking animation
|
|
367
|
+
thinking = ThinkingIndicator()
|
|
368
|
+
thinking.start()
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
# Get response from agent
|
|
372
|
+
response = agent.chat(user_input, verbose=False)
|
|
373
|
+
|
|
374
|
+
# Stop thinking animation
|
|
375
|
+
thinking.stop()
|
|
376
|
+
|
|
377
|
+
# Display response cleanly like Claude Code
|
|
378
|
+
print(f"\n{response}")
|
|
379
|
+
|
|
380
|
+
except Exception as chat_error:
|
|
381
|
+
thinking.stop()
|
|
382
|
+
raise chat_error
|
|
383
|
+
|
|
384
|
+
except KeyboardInterrupt:
|
|
385
|
+
print(f"\n\n{DIM}Goodbye!{RESET}")
|
|
386
|
+
break
|
|
387
|
+
except Exception as e:
|
|
388
|
+
print(f"\n{DIM}Error: {e}{RESET}")
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
if __name__ == "__main__":
|
|
392
|
+
main()
|
|
@@ -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")
|