Spaces:
Sleeping
Sleeping
Update app, config, README; add tests
Browse files- .gitignore +5 -0
- LOCALTEST.md +18 -0
- README.md +38 -39
- app.py +50 -50
- config.py +49 -99
- pytest.ini +3 -0
- test_app.py +349 -0
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
.pytest_cache/
|
| 5 |
+
.vscode/
|
LOCALTEST.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Manual Setup
|
| 2 |
+
|
| 3 |
+
```bash
|
| 4 |
+
python3 -m venv .venv # Windows: python -m venv .venv
|
| 5 |
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
| 6 |
+
pip install --upgrade pip
|
| 7 |
+
pip install -r requirements.txt
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+
# Test Application
|
| 11 |
+
|
| 12 |
+
```bash
|
| 13 |
+
python3 test_app.py # Windows: python test_app.py
|
| 14 |
+
# for full test with LLM for Huggingface Spaces deployment
|
| 15 |
+
python3 app.py # Windows: python app.py
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
Open browser: http://localhost:7860
|
README.md
CHANGED
|
@@ -28,37 +28,23 @@ short_description: GeoAI Coding Agent - Geospatial AI Coding Assistant"
|
|
| 28 |
|
| 29 |
# 🌍 GeoAI Coding Agent
|
| 30 |
|
| 31 |
-
A **Geospatial AI Coding Assistant** powered by
|
| 32 |
|
| 33 |
-
Specialized in **GDAL/OGR**, **Rasterio**, **GeoPandas**, **xarray**, and geospatial development across multiple programming languages.
|
| 34 |
|
| 35 |

|
| 36 |
-

|
| 38 |
|
| 39 |
## ✨ Features
|
| 40 |
|
| 41 |
-
- **🤖 Multiple AI Models**: Choose from 6 different models (DeepSeek, Kimi, Llama, MiniMax, GLM, Mistral)
|
| 42 |
- **🗺️ Geospatial Expertise**: Deep knowledge of GDAL, Rasterio, GeoPandas, PyProj, xarray, and rioxarray
|
| 43 |
-
- **📚 Educational Focus**: Responses include code explanations, step-by-step logic, and tips for beginners
|
| 44 |
- **💻 Multi-Language Support**: Python, Java, C/C++, JavaScript, TypeScript, Rust
|
| 45 |
-
- **📥 Download Options**: Export responses as Markdown or code files
|
| 46 |
- **🎨 Code Editor Style UI**: Syntax highlighting with dark theme
|
|
|
|
| 47 |
- **⚡ Streaming Responses**: Real-time token streaming for better UX
|
| 48 |
|
| 49 |
-
## 🤖 Available Models
|
| 50 |
-
|
| 51 |
-
| Model | Provider | Speed | Best For |
|
| 52 |
-
|-------|----------|-------|----------|
|
| 53 |
-
| **DeepSeek V3.2** | Novita | Medium | Complex geospatial tasks, detailed explanations |
|
| 54 |
-
| **Kimi K2 Instruct** | Novita | Medium | General coding, good balance |
|
| 55 |
-
| **Llama 3.1 8B** | Novita | Fast | Quick answers, simple tasks |
|
| 56 |
-
| **MiniMax M2.1** | Novita | Medium | Creative solutions |
|
| 57 |
-
| **GLM 4.7 Flash** | Novita | Fast | Fast responses, basic tasks |
|
| 58 |
-
| **Mistral 7B v0.2** | Featherless | Fast | Quick prototyping |
|
| 59 |
-
|
| 60 |
-
> 💡 **Tip**: Larger models provide better results but take longer. Smaller models are faster for simple queries.
|
| 61 |
-
|
| 62 |
## 🚀 Quick Start
|
| 63 |
|
| 64 |
### Local Development
|
|
@@ -136,25 +122,22 @@ geoai-coding-agent/
|
|
| 136 |
|
| 137 |
Key settings in `config.py`:
|
| 138 |
|
| 139 |
-
| Parameter
|
| 140 |
-
| ----------------
|
| 141 |
-
| `
|
| 142 |
-
| `MAX_NEW_TOKENS`
|
| 143 |
-
| `TEMPERATURE`
|
| 144 |
-
| `TOP_P`
|
| 145 |
-
| `AVAILABLE_MODELS` | (see table above) | Dictionary of available models & providers |
|
| 146 |
|
| 147 |
## 🔑 API Token
|
| 148 |
|
| 149 |
-
This application requires a **HuggingFace API token**
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
- ✅
|
| 153 |
|
| 154 |
Get your free token at: [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)
|
| 155 |
|
| 156 |
-
For HuggingFace Spaces deployment, add the token as a **Secret** named `HF_TOKEN` in Space Settings.
|
| 157 |
-
|
| 158 |
## 🛠️ Supported Languages
|
| 159 |
|
| 160 |
| Language | Extension | Use Case |
|
|
@@ -175,22 +158,34 @@ For HuggingFace Spaces deployment, add the token as a **Secret** named `HF_TOKEN
|
|
| 175 |
## 🙏 Acknowledgments
|
| 176 |
|
| 177 |
- Inspired by [GeoAI-Assistance-Ext](https://github.com/rifatSDAS/GeoAI-Assistance-Ext) VS Code Extension
|
| 178 |
-
- Powered by
|
| 179 |
- Built with [Gradio](https://gradio.app/)
|
| 180 |
|
| 181 |
## About the Developer
|
| 182 |
|
| 183 |
-
Built by Dr. Kazi Rifat Ahmed, a Full Stack Geospatial AI Engineer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
-
##
|
|
|
|
| 186 |
|
| 187 |
-
|
|
|
|
| 188 |
|
| 189 |
-
##
|
| 190 |
|
| 191 |
-
|
| 192 |
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
## 📄 License
|
| 196 |
|
|
@@ -198,6 +193,10 @@ This project is licensed under the **GNU Affero General Public License v3.0** (A
|
|
| 198 |
|
| 199 |
See [LICENSE](LICENSE.md) for details.
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
---
|
| 202 |
|
| 203 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 28 |
|
| 29 |
# 🌍 GeoAI Coding Agent
|
| 30 |
|
| 31 |
+
A **Geospatial AI Coding Assistant** powered by `Qwen/Qwen2.5-Coder-7B-Instruct` via HuggingFace Inference API.
|
| 32 |
|
| 33 |
+
Specialized in **GDAL/OGR**, **Rasterio**, **GeoPandas**, **xarray**, and geospatial development across multiple programming languages.
|
| 34 |
|
| 35 |

|
| 36 |
+

|
| 37 |

|
| 38 |
|
| 39 |
## ✨ Features
|
| 40 |
|
|
|
|
| 41 |
- **🗺️ Geospatial Expertise**: Deep knowledge of GDAL, Rasterio, GeoPandas, PyProj, xarray, and rioxarray
|
|
|
|
| 42 |
- **💻 Multi-Language Support**: Python, Java, C/C++, JavaScript, TypeScript, Rust
|
| 43 |
+
- **📥 Download Options**: Export responses as Markdown, Jupyter Notebooks, or code files
|
| 44 |
- **🎨 Code Editor Style UI**: Syntax highlighting with dark theme
|
| 45 |
+
- **🔒 BYOT (Bring Your Own Token)**: Uses your HuggingFace API token for inference
|
| 46 |
- **⚡ Streaming Responses**: Real-time token streaming for better UX
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
## 🚀 Quick Start
|
| 49 |
|
| 50 |
### Local Development
|
|
|
|
| 122 |
|
| 123 |
Key settings in `config.py`:
|
| 124 |
|
| 125 |
+
| Parameter | Default | Description |
|
| 126 |
+
| ---------------- | -------------------------------- | ------------------------------------------- |
|
| 127 |
+
| `MODEL_ID` | `Qwen/Qwen2.5-Coder-7B-Instruct` | HuggingFace model |
|
| 128 |
+
| `MAX_NEW_TOKENS` | `2048` | Maximum response length |
|
| 129 |
+
| `TEMPERATURE` | `0.3` | Sampling temperature (lower = more focused) |
|
| 130 |
+
| `TOP_P` | `0.9` | Nucleus sampling threshold |
|
|
|
|
| 131 |
|
| 132 |
## 🔑 API Token
|
| 133 |
|
| 134 |
+
This application requires a **HuggingFace API token** for inference. This design:
|
| 135 |
+
- ✅ Keeps the Space free (no server-side costs)
|
| 136 |
+
- ✅ Prevents token limit overflow
|
| 137 |
+
- ✅ Gives users control over their usage
|
| 138 |
|
| 139 |
Get your free token at: [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)
|
| 140 |
|
|
|
|
|
|
|
| 141 |
## 🛠️ Supported Languages
|
| 142 |
|
| 143 |
| Language | Extension | Use Case |
|
|
|
|
| 158 |
## 🙏 Acknowledgments
|
| 159 |
|
| 160 |
- Inspired by [GeoAI-Assistance-Ext](https://github.com/rifatSDAS/GeoAI-Assistance-Ext) VS Code Extension
|
| 161 |
+
- Powered by [Qwen2.5-Coder](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct)
|
| 162 |
- Built with [Gradio](https://gradio.app/)
|
| 163 |
|
| 164 |
## About the Developer
|
| 165 |
|
| 166 |
+
Built by Dr. Kazi Rifat Ahmed, a **Full Stack Geospatial AI Engineer** specializing in:
|
| 167 |
+
- AI/ML-DL for geospatial applications
|
| 168 |
+
- Cloud-native geospatial software engineering & architecture
|
| 169 |
+
- Large-scale Satellite/Earth Observation data data analysis, processing, analytics, and visualization
|
| 170 |
+
- Blockchain and Quantum Computing for geospatial applications
|
| 171 |
+
- Research Advanced Geospatial Science, Technology, and Applications
|
| 172 |
+
- Co-founder and Technical Lead for Satellite Data Services business in Space sector, i.e., QuentuED (https://quentued.de) and Sensor Aktor (https://sensor-aktor.de)
|
| 173 |
|
| 174 |
+
### Tech Stack Proficiency
|
| 175 |
+
Python | Java | JavaScript | TypeScript | C/C++ | Bash | Cloud-Native Architecture (kubernetes) | DevOps | AI/ML/DL | MLOps | LLM Integration | Blockchain | Remote Sensing Science & Technology | Geospatial Data Science & Engineering
|
| 176 |
|
| 177 |
+
### Research Interests
|
| 178 |
+
Geospatial AI | Satellite Data Engineering | Drone Sensors | Geospatial Big Data Analytics | Earth Observation Systems & Sensors | Advanced Remote Sensing Techniques | Space Technology | Quantum Computing | Blockchain | | Satellite Data Services | Planetary Science & Exploration
|
| 179 |
|
| 180 |
+
## 🤝 Contributing
|
| 181 |
|
| 182 |
+
Contributions are welcome! Please:
|
| 183 |
|
| 184 |
+
1. Fork the repository
|
| 185 |
+
2. Create a feature branch
|
| 186 |
+
3. Make your changes
|
| 187 |
+
4. Run tests: `pytest test_app.py -v`
|
| 188 |
+
5. Submit a pull request
|
| 189 |
|
| 190 |
## 📄 License
|
| 191 |
|
|
|
|
| 193 |
|
| 194 |
See [LICENSE](LICENSE.md) for details.
|
| 195 |
|
| 196 |
+
## Contact
|
| 197 |
+
|
| 198 |
+
For collaboration opportunities in satellite data services & applications, large-scale satellite data analytics, geospatial AI, blockchain & quantum computing for geospatial applications, or advanced geospatial science, technology & applications, feel free to reach out!
|
| 199 |
+
|
| 200 |
---
|
| 201 |
|
| 202 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
CHANGED
|
@@ -18,29 +18,25 @@ from typing import Generator, Tuple, Optional
|
|
| 18 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 19 |
|
| 20 |
from config import (
|
| 21 |
-
|
| 22 |
-
DEFAULT_MODEL,
|
| 23 |
MAX_NEW_TOKENS,
|
| 24 |
-
MAX_TOKENS_UI_LIMIT,
|
| 25 |
TEMPERATURE,
|
| 26 |
TOP_P,
|
| 27 |
REPETITION_PENALTY,
|
| 28 |
SUPPORTED_LANGUAGES,
|
| 29 |
SYSTEM_PROMPT,
|
| 30 |
EXAMPLE_PROMPTS,
|
|
|
|
| 31 |
MARKDOWN_TEMPLATE,
|
| 32 |
)
|
| 33 |
|
| 34 |
|
| 35 |
-
def create_client(
|
| 36 |
-
"""Create HuggingFace Inference client with
|
| 37 |
if not HF_TOKEN:
|
| 38 |
return None
|
| 39 |
try:
|
| 40 |
-
return InferenceClient(
|
| 41 |
-
provider=provider,
|
| 42 |
-
api_key=HF_TOKEN,
|
| 43 |
-
)
|
| 44 |
except Exception as e:
|
| 45 |
print(f"Error creating client: {e}")
|
| 46 |
return None
|
|
@@ -73,8 +69,6 @@ def extract_code_blocks(text: str) -> list[dict]:
|
|
| 73 |
def generate_response(
|
| 74 |
message: str,
|
| 75 |
history: list,
|
| 76 |
-
model_name: str = DEFAULT_MODEL,
|
| 77 |
-
max_tokens: int = MAX_NEW_TOKENS,
|
| 78 |
) -> Generator[str, None, None]:
|
| 79 |
"""Generate streaming response from the model."""
|
| 80 |
|
|
@@ -82,12 +76,7 @@ def generate_response(
|
|
| 82 |
yield "⚠️ **Error**: Server configuration error. Please contact the administrator."
|
| 83 |
return
|
| 84 |
|
| 85 |
-
|
| 86 |
-
model_config = AVAILABLE_MODELS.get(model_name, AVAILABLE_MODELS[DEFAULT_MODEL])
|
| 87 |
-
model_id = model_config["model_id"]
|
| 88 |
-
provider = model_config["provider"]
|
| 89 |
-
|
| 90 |
-
client = create_client(provider)
|
| 91 |
if client is None:
|
| 92 |
yield "⚠️ **Error**: Failed to initialize the model client. Please try again later."
|
| 93 |
return
|
|
@@ -106,9 +95,8 @@ def generate_response(
|
|
| 106 |
try:
|
| 107 |
response_text = ""
|
| 108 |
stream = client.chat_completion(
|
| 109 |
-
model=model_id,
|
| 110 |
messages=messages,
|
| 111 |
-
max_tokens=
|
| 112 |
temperature=TEMPERATURE,
|
| 113 |
top_p=TOP_P,
|
| 114 |
stream=True,
|
|
@@ -122,15 +110,10 @@ def generate_response(
|
|
| 122 |
|
| 123 |
except Exception as e:
|
| 124 |
error_msg = str(e)
|
| 125 |
-
print(f"[DEBUG] Full error: {error_msg}") # Log full error for debugging
|
| 126 |
if "401" in error_msg or "unauthorized" in error_msg.lower():
|
| 127 |
-
yield
|
| 128 |
elif "429" in error_msg or "rate" in error_msg.lower():
|
| 129 |
yield "⚠️ **Rate Limit**: Too many requests. Please wait a moment and try again."
|
| 130 |
-
elif "503" in error_msg or "loading" in error_msg.lower():
|
| 131 |
-
yield "⚠️ **Model Loading**: The model is currently loading. Please try again in a few seconds."
|
| 132 |
-
elif "404" in error_msg:
|
| 133 |
-
yield f"⚠️ **Model Not Found**: The model `{model_id}` is not available on provider `{provider}`. Try selecting a different model."
|
| 134 |
else:
|
| 135 |
yield f"⚠️ **Error**: {error_msg}"
|
| 136 |
|
|
@@ -152,6 +135,24 @@ def create_download_file(
|
|
| 152 |
content = MARKDOWN_TEMPLATE.format(query=query, response=response)
|
| 153 |
filename = f"geoai_response_{timestamp}.md"
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
elif file_format == "code":
|
| 156 |
code_blocks = extract_code_blocks(response)
|
| 157 |
if not code_blocks:
|
|
@@ -183,6 +184,11 @@ def download_as_markdown(response: str, query: str) -> Optional[str]:
|
|
| 183 |
return create_download_file(response, query, "markdown")
|
| 184 |
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
def download_as_code(response: str, query: str) -> Optional[str]:
|
| 187 |
"""Download response as code file."""
|
| 188 |
return create_download_file(response, query, "code")
|
|
@@ -247,36 +253,18 @@ def create_app():
|
|
| 247 |
gr.Markdown(
|
| 248 |
"""
|
| 249 |
# 🌍 GeoAI Coding Agent
|
| 250 |
-
### Geospatial AI Coding Assistant
|
| 251 |
|
| 252 |
Expert in **GDAL/OGR**, **Rasterio**, **GeoPandas**, **xarray**, and geospatial development.
|
| 253 |
Fluent in Python, Java, C/C++, JavaScript, TypeScript, and Rust.
|
| 254 |
"""
|
| 255 |
)
|
| 256 |
|
| 257 |
-
# Model Selection
|
| 258 |
-
with gr.Row():
|
| 259 |
-
model_selector = gr.Dropdown(
|
| 260 |
-
choices=list(AVAILABLE_MODELS.keys()),
|
| 261 |
-
value=DEFAULT_MODEL,
|
| 262 |
-
label="🤖 Select Model",
|
| 263 |
-
info="💡 Larger models (DeepSeek, Kimi, MiniMax) take longer than smaller models (Llama, Mistral, GLM) which are faster.",
|
| 264 |
-
scale=2,
|
| 265 |
-
)
|
| 266 |
-
max_tokens_slider = gr.Slider(
|
| 267 |
-
minimum=1024,
|
| 268 |
-
maximum=MAX_TOKENS_UI_LIMIT,
|
| 269 |
-
value=MAX_NEW_TOKENS,
|
| 270 |
-
step=1024,
|
| 271 |
-
label="📝 Max Tokens",
|
| 272 |
-
info="Increase for longer responses from reasoning models",
|
| 273 |
-
scale=1,
|
| 274 |
-
)
|
| 275 |
-
|
| 276 |
# Main Chat Interface
|
| 277 |
chatbot = gr.Chatbot(
|
| 278 |
label="GeoAI Conversation",
|
| 279 |
height=500,
|
|
|
|
| 280 |
)
|
| 281 |
|
| 282 |
# Input Row
|
|
@@ -314,6 +302,7 @@ def create_app():
|
|
| 314 |
gr.Markdown("### 📥 Download Response")
|
| 315 |
with gr.Row():
|
| 316 |
download_md_btn = gr.Button("📄 Markdown", size="sm")
|
|
|
|
| 317 |
download_code_btn = gr.Button("💻 Code File", size="sm")
|
| 318 |
|
| 319 |
download_file = gr.File(label="Download", visible=False)
|
|
@@ -329,7 +318,7 @@ def create_app():
|
|
| 329 |
return "", history
|
| 330 |
return "", history + [{"role": "user", "content": message}]
|
| 331 |
|
| 332 |
-
def bot_response(history
|
| 333 |
"""Generate bot response with streaming."""
|
| 334 |
if not history:
|
| 335 |
return history, "", ""
|
|
@@ -339,8 +328,6 @@ def create_app():
|
|
| 339 |
for response in generate_response(
|
| 340 |
user_msg,
|
| 341 |
history[:-1],
|
| 342 |
-
selected_model,
|
| 343 |
-
int(max_tokens),
|
| 344 |
):
|
| 345 |
yield history + [{"role": "assistant", "content": response}], response, user_msg
|
| 346 |
|
|
@@ -357,6 +344,13 @@ def create_app():
|
|
| 357 |
return gr.File(value=filepath, visible=True)
|
| 358 |
return gr.File(visible=False)
|
| 359 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
def handle_download_code(response, query):
|
| 361 |
"""Handle code file download."""
|
| 362 |
filepath = download_as_code(response, query)
|
|
@@ -372,7 +366,7 @@ def create_app():
|
|
| 372 |
queue=False,
|
| 373 |
).then(
|
| 374 |
bot_response,
|
| 375 |
-
[chatbot
|
| 376 |
[chatbot, last_response, last_query],
|
| 377 |
)
|
| 378 |
|
|
@@ -383,7 +377,7 @@ def create_app():
|
|
| 383 |
queue=False,
|
| 384 |
).then(
|
| 385 |
bot_response,
|
| 386 |
-
[chatbot
|
| 387 |
[chatbot, last_response, last_query],
|
| 388 |
)
|
| 389 |
|
|
@@ -398,6 +392,12 @@ def create_app():
|
|
| 398 |
[download_file],
|
| 399 |
)
|
| 400 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
download_code_btn.click(
|
| 402 |
handle_download_code,
|
| 403 |
[last_response, last_query],
|
|
|
|
| 18 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 19 |
|
| 20 |
from config import (
|
| 21 |
+
MODEL_ID,
|
|
|
|
| 22 |
MAX_NEW_TOKENS,
|
|
|
|
| 23 |
TEMPERATURE,
|
| 24 |
TOP_P,
|
| 25 |
REPETITION_PENALTY,
|
| 26 |
SUPPORTED_LANGUAGES,
|
| 27 |
SYSTEM_PROMPT,
|
| 28 |
EXAMPLE_PROMPTS,
|
| 29 |
+
NOTEBOOK_TEMPLATE,
|
| 30 |
MARKDOWN_TEMPLATE,
|
| 31 |
)
|
| 32 |
|
| 33 |
|
| 34 |
+
def create_client() -> Optional[InferenceClient]:
|
| 35 |
+
"""Create HuggingFace Inference client with environment token."""
|
| 36 |
if not HF_TOKEN:
|
| 37 |
return None
|
| 38 |
try:
|
| 39 |
+
return InferenceClient(model=MODEL_ID, token=HF_TOKEN)
|
|
|
|
|
|
|
|
|
|
| 40 |
except Exception as e:
|
| 41 |
print(f"Error creating client: {e}")
|
| 42 |
return None
|
|
|
|
| 69 |
def generate_response(
|
| 70 |
message: str,
|
| 71 |
history: list,
|
|
|
|
|
|
|
| 72 |
) -> Generator[str, None, None]:
|
| 73 |
"""Generate streaming response from the model."""
|
| 74 |
|
|
|
|
| 76 |
yield "⚠️ **Error**: Server configuration error. Please contact the administrator."
|
| 77 |
return
|
| 78 |
|
| 79 |
+
client = create_client()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
if client is None:
|
| 81 |
yield "⚠️ **Error**: Failed to initialize the model client. Please try again later."
|
| 82 |
return
|
|
|
|
| 95 |
try:
|
| 96 |
response_text = ""
|
| 97 |
stream = client.chat_completion(
|
|
|
|
| 98 |
messages=messages,
|
| 99 |
+
max_tokens=MAX_NEW_TOKENS,
|
| 100 |
temperature=TEMPERATURE,
|
| 101 |
top_p=TOP_P,
|
| 102 |
stream=True,
|
|
|
|
| 110 |
|
| 111 |
except Exception as e:
|
| 112 |
error_msg = str(e)
|
|
|
|
| 113 |
if "401" in error_msg or "unauthorized" in error_msg.lower():
|
| 114 |
+
yield "⚠️ **Authentication Error**: Invalid HuggingFace token. Please check your API token."
|
| 115 |
elif "429" in error_msg or "rate" in error_msg.lower():
|
| 116 |
yield "⚠️ **Rate Limit**: Too many requests. Please wait a moment and try again."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
else:
|
| 118 |
yield f"⚠️ **Error**: {error_msg}"
|
| 119 |
|
|
|
|
| 135 |
content = MARKDOWN_TEMPLATE.format(query=query, response=response)
|
| 136 |
filename = f"geoai_response_{timestamp}.md"
|
| 137 |
|
| 138 |
+
elif file_format == "notebook":
|
| 139 |
+
code_blocks = extract_code_blocks(response)
|
| 140 |
+
if not code_blocks:
|
| 141 |
+
# If no code blocks, create a markdown cell
|
| 142 |
+
code_cells = json.dumps([f"# No code blocks found\\n# Response:\\n# {response[:500]}..."])
|
| 143 |
+
else:
|
| 144 |
+
# Create code cells from extracted blocks
|
| 145 |
+
cells = [block["code"] for block in code_blocks if block["language"] == "python"]
|
| 146 |
+
if not cells:
|
| 147 |
+
cells = [code_blocks[0]["code"]] if code_blocks else ["# No Python code found"]
|
| 148 |
+
code_cells = json.dumps(cells)
|
| 149 |
+
|
| 150 |
+
content = NOTEBOOK_TEMPLATE.format(
|
| 151 |
+
query=query.replace('"', '\\"').replace('\n', '\\n'),
|
| 152 |
+
code_cells=code_cells
|
| 153 |
+
)
|
| 154 |
+
filename = f"geoai_notebook_{timestamp}.ipynb"
|
| 155 |
+
|
| 156 |
elif file_format == "code":
|
| 157 |
code_blocks = extract_code_blocks(response)
|
| 158 |
if not code_blocks:
|
|
|
|
| 184 |
return create_download_file(response, query, "markdown")
|
| 185 |
|
| 186 |
|
| 187 |
+
def download_as_notebook(response: str, query: str) -> Optional[str]:
|
| 188 |
+
"""Download response as Jupyter Notebook."""
|
| 189 |
+
return create_download_file(response, query, "notebook")
|
| 190 |
+
|
| 191 |
+
|
| 192 |
def download_as_code(response: str, query: str) -> Optional[str]:
|
| 193 |
"""Download response as code file."""
|
| 194 |
return create_download_file(response, query, "code")
|
|
|
|
| 253 |
gr.Markdown(
|
| 254 |
"""
|
| 255 |
# 🌍 GeoAI Coding Agent
|
| 256 |
+
### Geospatial AI Coding Assistant powered by Qwen2.5-Coder-7B
|
| 257 |
|
| 258 |
Expert in **GDAL/OGR**, **Rasterio**, **GeoPandas**, **xarray**, and geospatial development.
|
| 259 |
Fluent in Python, Java, C/C++, JavaScript, TypeScript, and Rust.
|
| 260 |
"""
|
| 261 |
)
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
# Main Chat Interface
|
| 264 |
chatbot = gr.Chatbot(
|
| 265 |
label="GeoAI Conversation",
|
| 266 |
height=500,
|
| 267 |
+
type="messages",
|
| 268 |
)
|
| 269 |
|
| 270 |
# Input Row
|
|
|
|
| 302 |
gr.Markdown("### 📥 Download Response")
|
| 303 |
with gr.Row():
|
| 304 |
download_md_btn = gr.Button("📄 Markdown", size="sm")
|
| 305 |
+
download_nb_btn = gr.Button("📓 Notebook", size="sm")
|
| 306 |
download_code_btn = gr.Button("💻 Code File", size="sm")
|
| 307 |
|
| 308 |
download_file = gr.File(label="Download", visible=False)
|
|
|
|
| 318 |
return "", history
|
| 319 |
return "", history + [{"role": "user", "content": message}]
|
| 320 |
|
| 321 |
+
def bot_response(history):
|
| 322 |
"""Generate bot response with streaming."""
|
| 323 |
if not history:
|
| 324 |
return history, "", ""
|
|
|
|
| 328 |
for response in generate_response(
|
| 329 |
user_msg,
|
| 330 |
history[:-1],
|
|
|
|
|
|
|
| 331 |
):
|
| 332 |
yield history + [{"role": "assistant", "content": response}], response, user_msg
|
| 333 |
|
|
|
|
| 344 |
return gr.File(value=filepath, visible=True)
|
| 345 |
return gr.File(visible=False)
|
| 346 |
|
| 347 |
+
def handle_download_nb(response, query):
|
| 348 |
+
"""Handle notebook download."""
|
| 349 |
+
filepath = download_as_notebook(response, query)
|
| 350 |
+
if filepath:
|
| 351 |
+
return gr.File(value=filepath, visible=True)
|
| 352 |
+
return gr.File(visible=False)
|
| 353 |
+
|
| 354 |
def handle_download_code(response, query):
|
| 355 |
"""Handle code file download."""
|
| 356 |
filepath = download_as_code(response, query)
|
|
|
|
| 366 |
queue=False,
|
| 367 |
).then(
|
| 368 |
bot_response,
|
| 369 |
+
[chatbot],
|
| 370 |
[chatbot, last_response, last_query],
|
| 371 |
)
|
| 372 |
|
|
|
|
| 377 |
queue=False,
|
| 378 |
).then(
|
| 379 |
bot_response,
|
| 380 |
+
[chatbot],
|
| 381 |
[chatbot, last_response, last_query],
|
| 382 |
)
|
| 383 |
|
|
|
|
| 392 |
[download_file],
|
| 393 |
)
|
| 394 |
|
| 395 |
+
download_nb_btn.click(
|
| 396 |
+
handle_download_nb,
|
| 397 |
+
[last_response, last_query],
|
| 398 |
+
[download_file],
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
download_code_btn.click(
|
| 402 |
handle_download_code,
|
| 403 |
[last_response, last_query],
|
config.py
CHANGED
|
@@ -2,45 +2,12 @@
|
|
| 2 |
GeoAI Coding Agent - Configuration
|
| 3 |
===================================
|
| 4 |
Configuration settings for the Geospatial AI Coding Assistant.
|
| 5 |
-
|
| 6 |
"""
|
| 7 |
|
| 8 |
-
#
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
"DeepSeek V3.2": {
|
| 12 |
-
"model_id": "deepseek-ai/DeepSeek-V3.2",
|
| 13 |
-
"provider": "novita",
|
| 14 |
-
},
|
| 15 |
-
"Kimi K2 Instruct": {
|
| 16 |
-
"model_id": "moonshotai/Kimi-K2-Instruct",
|
| 17 |
-
"provider": "novita",
|
| 18 |
-
},
|
| 19 |
-
"Llama 3.1 8B": {
|
| 20 |
-
"model_id": "meta-llama/Llama-3.1-8B-Instruct",
|
| 21 |
-
"provider": "novita",
|
| 22 |
-
},
|
| 23 |
-
"MiniMax M2.1": {
|
| 24 |
-
"model_id": "MiniMaxAI/MiniMax-M2.1",
|
| 25 |
-
"provider": "novita",
|
| 26 |
-
},
|
| 27 |
-
"GLM 4.7 Flash": {
|
| 28 |
-
"model_id": "zai-org/GLM-4.7-Flash",
|
| 29 |
-
"provider": "novita",
|
| 30 |
-
},
|
| 31 |
-
"Mistral 7B v0.2": {
|
| 32 |
-
"model_id": "mistralai/Mistral-7B-Instruct-v0.2",
|
| 33 |
-
"provider": "featherless-ai",
|
| 34 |
-
},
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
# Default model selection
|
| 38 |
-
DEFAULT_MODEL = "DeepSeek V3.2"
|
| 39 |
-
|
| 40 |
-
# Generation Parameters
|
| 41 |
-
# Increased for reasoning models that need more tokens for thinking + answer
|
| 42 |
-
MAX_NEW_TOKENS = 16384
|
| 43 |
-
MAX_TOKENS_UI_LIMIT = 32768 # Maximum allowed via UI slider
|
| 44 |
TEMPERATURE = 0.3
|
| 45 |
TOP_P = 0.9
|
| 46 |
REPETITION_PENALTY = 1.1
|
|
@@ -60,7 +27,7 @@ SUPPORTED_LANGUAGES = {
|
|
| 60 |
}
|
| 61 |
|
| 62 |
# Geospatial System Prompt
|
| 63 |
-
SYSTEM_PROMPT = """You are GeoAI Coding Agent, an expert Geospatial Software Engineer
|
| 64 |
|
| 65 |
## Core Expertise
|
| 66 |
- **GDAL/OGR**: Raster/vector processing, format conversions, coordinate transformations
|
|
@@ -73,39 +40,22 @@ SYSTEM_PROMPT = """You are GeoAI Coding Agent, an expert Geospatial Software Eng
|
|
| 73 |
Python, Java, C/C++, JavaScript, TypeScript, Rust
|
| 74 |
|
| 75 |
## Response Guidelines
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
4. **Explain the logic**: After the code, provide a "How It Works" section explaining:
|
| 82 |
-
- The overall approach/algorithm
|
| 83 |
-
- Why specific libraries or functions are used
|
| 84 |
-
- What each major step does
|
| 85 |
-
5. **Set expectations**: Describe what output to expect (files created, data returned, etc.)
|
| 86 |
-
6. **Include tips**: Add beginner-friendly tips, common pitfalls, or alternatives when relevant
|
| 87 |
-
7. **Format code blocks** with language specification: ```python, ```java, ```cpp, etc.
|
| 88 |
|
| 89 |
## Response Structure
|
| 90 |
-
### What This Code Does
|
| 91 |
-
Brief overview of the solution.
|
| 92 |
-
|
| 93 |
```[language]
|
| 94 |
-
#
|
| 95 |
```
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
### What to Expect
|
| 102 |
-
- Describe the output (files, data structures, visualizations)
|
| 103 |
-
- Any dependencies to install
|
| 104 |
-
|
| 105 |
-
### 💡 Tips for Beginners
|
| 106 |
-
- Helpful tips, common mistakes to avoid, or alternative approaches
|
| 107 |
|
| 108 |
-
|
| 109 |
|
| 110 |
# Example prompts for UI
|
| 111 |
EXAMPLE_PROMPTS = [
|
|
@@ -121,40 +71,40 @@ EXAMPLE_PROMPTS = [
|
|
| 121 |
|
| 122 |
# File download templates
|
| 123 |
NOTEBOOK_TEMPLATE = '''{{
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
}}'''
|
| 159 |
|
| 160 |
MARKDOWN_TEMPLATE = """# GeoAI Generated Code
|
|
|
|
| 2 |
GeoAI Coding Agent - Configuration
|
| 3 |
===================================
|
| 4 |
Configuration settings for the Geospatial AI Coding Assistant.
|
| 5 |
+
Uses Qwen/Qwen2.5-Coder-7B-Instruct via HuggingFace Inference API.
|
| 6 |
"""
|
| 7 |
|
| 8 |
+
# Model Configuration
|
| 9 |
+
MODEL_ID = "Qwen/Qwen2.5-Coder-7B-Instruct"
|
| 10 |
+
MAX_NEW_TOKENS = 2048
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
TEMPERATURE = 0.3
|
| 12 |
TOP_P = 0.9
|
| 13 |
REPETITION_PENALTY = 1.1
|
|
|
|
| 27 |
}
|
| 28 |
|
| 29 |
# Geospatial System Prompt
|
| 30 |
+
SYSTEM_PROMPT = """You are GeoAI Coding Agent, an expert Geospatial Software Engineer specialized in:
|
| 31 |
|
| 32 |
## Core Expertise
|
| 33 |
- **GDAL/OGR**: Raster/vector processing, format conversions, coordinate transformations
|
|
|
|
| 40 |
Python, Java, C/C++, JavaScript, TypeScript, Rust
|
| 41 |
|
| 42 |
## Response Guidelines
|
| 43 |
+
1. **Be concise**: Provide working code with minimal but meaningful explanations
|
| 44 |
+
2. **Code first**: Lead with the solution, explain key points after
|
| 45 |
+
3. **Best practices**: Include error handling, type hints (Python), and memory efficiency
|
| 46 |
+
4. **Geospatial focus**: Optimize for large Earth Observation datasets
|
| 47 |
+
5. **Format code blocks** with language specification: ```python, ```java, ```cpp, etc.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
## Response Structure
|
|
|
|
|
|
|
|
|
|
| 50 |
```[language]
|
| 51 |
+
# Your code here
|
| 52 |
```
|
| 53 |
|
| 54 |
+
**Key Points:**
|
| 55 |
+
- Brief explanation of the approach (2-3 sentences max)
|
| 56 |
+
- Any important caveats or dependencies
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
+
Do NOT include lengthy introductions or excessive explanations. Users are professionals who need working code quickly."""
|
| 59 |
|
| 60 |
# Example prompts for UI
|
| 61 |
EXAMPLE_PROMPTS = [
|
|
|
|
| 71 |
|
| 72 |
# File download templates
|
| 73 |
NOTEBOOK_TEMPLATE = '''{{
|
| 74 |
+
"cells": [
|
| 75 |
+
{{
|
| 76 |
+
"cell_type": "markdown",
|
| 77 |
+
"metadata": {{}},
|
| 78 |
+
"source": [
|
| 79 |
+
"# GeoAI Generated Code\\n",
|
| 80 |
+
"\\n",
|
| 81 |
+
"Generated by GeoAI Coding Agent\\n",
|
| 82 |
+
"\\n",
|
| 83 |
+
"## Query\\n",
|
| 84 |
+
"{query}"
|
| 85 |
+
]
|
| 86 |
+
}},
|
| 87 |
+
{{
|
| 88 |
+
"cell_type": "code",
|
| 89 |
+
"execution_count": null,
|
| 90 |
+
"metadata": {{}},
|
| 91 |
+
"outputs": [],
|
| 92 |
+
"source": {code_cells}
|
| 93 |
+
}}
|
| 94 |
+
],
|
| 95 |
+
"metadata": {{
|
| 96 |
+
"kernelspec": {{
|
| 97 |
+
"display_name": "Python 3",
|
| 98 |
+
"language": "python",
|
| 99 |
+
"name": "python3"
|
| 100 |
+
}},
|
| 101 |
+
"language_info": {{
|
| 102 |
+
"name": "python",
|
| 103 |
+
"version": "3.10.0"
|
| 104 |
+
}}
|
| 105 |
+
}},
|
| 106 |
+
"nbformat": 4,
|
| 107 |
+
"nbformat_minor": 4
|
| 108 |
}}'''
|
| 109 |
|
| 110 |
MARKDOWN_TEMPLATE = """# GeoAI Generated Code
|
pytest.ini
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[pytest]
|
| 2 |
+
filterwarnings =
|
| 3 |
+
ignore::pytest.PytestAssertRewriteWarning
|
test_app.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GeoAI Coding Agent - Test Suite
|
| 3 |
+
================================
|
| 4 |
+
Unit tests for the GeoAI Coding Agent application.
|
| 5 |
+
Run with: pytest test_app.py -v
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import pytest
|
| 9 |
+
import json
|
| 10 |
+
import os
|
| 11 |
+
import tempfile
|
| 12 |
+
from unittest.mock import Mock, patch, MagicMock
|
| 13 |
+
|
| 14 |
+
# Import modules to test
|
| 15 |
+
from config import (
|
| 16 |
+
MODEL_ID,
|
| 17 |
+
SYSTEM_PROMPT,
|
| 18 |
+
SUPPORTED_LANGUAGES,
|
| 19 |
+
EXAMPLE_PROMPTS,
|
| 20 |
+
NOTEBOOK_TEMPLATE,
|
| 21 |
+
MARKDOWN_TEMPLATE,
|
| 22 |
+
)
|
| 23 |
+
from app import (
|
| 24 |
+
create_client,
|
| 25 |
+
extract_code_blocks,
|
| 26 |
+
create_download_file,
|
| 27 |
+
download_as_markdown,
|
| 28 |
+
download_as_notebook,
|
| 29 |
+
download_as_code,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class TestConfig:
|
| 34 |
+
"""Test configuration settings."""
|
| 35 |
+
|
| 36 |
+
def test_model_id_is_set(self):
|
| 37 |
+
"""Verify model ID is configured."""
|
| 38 |
+
assert MODEL_ID == "Qwen/Qwen2.5-Coder-7B-Instruct"
|
| 39 |
+
|
| 40 |
+
def test_system_prompt_contains_geospatial_keywords(self):
|
| 41 |
+
"""Verify system prompt includes geospatial expertise."""
|
| 42 |
+
keywords = ["GDAL", "Rasterio", "GeoPandas", "PyProj", "xarray"]
|
| 43 |
+
for keyword in keywords:
|
| 44 |
+
assert keyword in SYSTEM_PROMPT, f"Missing keyword: {keyword}"
|
| 45 |
+
|
| 46 |
+
def test_supported_languages(self):
|
| 47 |
+
"""Verify all required languages are supported."""
|
| 48 |
+
required_languages = ["python", "java", "cpp", "javascript", "typescript", "rust"]
|
| 49 |
+
for lang in required_languages:
|
| 50 |
+
assert lang in SUPPORTED_LANGUAGES, f"Missing language: {lang}"
|
| 51 |
+
assert "extension" in SUPPORTED_LANGUAGES[lang]
|
| 52 |
+
assert "highlight" in SUPPORTED_LANGUAGES[lang]
|
| 53 |
+
|
| 54 |
+
def test_example_prompts_not_empty(self):
|
| 55 |
+
"""Verify example prompts are provided."""
|
| 56 |
+
assert len(EXAMPLE_PROMPTS) > 0
|
| 57 |
+
for prompt in EXAMPLE_PROMPTS:
|
| 58 |
+
assert len(prompt) > 10, "Example prompt too short"
|
| 59 |
+
|
| 60 |
+
def test_notebook_template_valid_json_structure(self):
|
| 61 |
+
"""Verify notebook template has valid structure."""
|
| 62 |
+
# Test with sample data
|
| 63 |
+
test_template = NOTEBOOK_TEMPLATE.format(
|
| 64 |
+
query="Test query",
|
| 65 |
+
code_cells='["print(\\"hello\\")"]'
|
| 66 |
+
)
|
| 67 |
+
# Should be valid JSON
|
| 68 |
+
parsed = json.loads(test_template)
|
| 69 |
+
assert "cells" in parsed
|
| 70 |
+
assert "nbformat" in parsed
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class TestCodeExtraction:
|
| 74 |
+
"""Test code block extraction functionality."""
|
| 75 |
+
|
| 76 |
+
def test_extract_single_python_block(self):
|
| 77 |
+
"""Extract a single Python code block."""
|
| 78 |
+
text = """Here's the code:
|
| 79 |
+
```python
|
| 80 |
+
import rasterio
|
| 81 |
+
with rasterio.open('test.tif') as src:
|
| 82 |
+
data = src.read()
|
| 83 |
+
```
|
| 84 |
+
That's it!"""
|
| 85 |
+
|
| 86 |
+
blocks = extract_code_blocks(text)
|
| 87 |
+
assert len(blocks) == 1
|
| 88 |
+
assert blocks[0]["language"] == "python"
|
| 89 |
+
assert "import rasterio" in blocks[0]["code"]
|
| 90 |
+
assert blocks[0]["extension"] == ".py"
|
| 91 |
+
|
| 92 |
+
def test_extract_multiple_blocks(self):
|
| 93 |
+
"""Extract multiple code blocks with different languages."""
|
| 94 |
+
text = """Python version:
|
| 95 |
+
```python
|
| 96 |
+
import gdal
|
| 97 |
+
ds = gdal.Open('file.tif')
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
JavaScript version:
|
| 101 |
+
```javascript
|
| 102 |
+
const fs = require('fs');
|
| 103 |
+
console.log('Hello');
|
| 104 |
+
```
|
| 105 |
+
"""
|
| 106 |
+
blocks = extract_code_blocks(text)
|
| 107 |
+
assert len(blocks) == 2
|
| 108 |
+
assert blocks[0]["language"] == "python"
|
| 109 |
+
assert blocks[1]["language"] == "javascript"
|
| 110 |
+
|
| 111 |
+
def test_extract_cpp_block(self):
|
| 112 |
+
"""Extract C++ code block."""
|
| 113 |
+
text = """```cpp
|
| 114 |
+
#include <gdal/gdal.h>
|
| 115 |
+
int main() {
|
| 116 |
+
GDALAllRegister();
|
| 117 |
+
return 0;
|
| 118 |
+
}
|
| 119 |
+
```"""
|
| 120 |
+
blocks = extract_code_blocks(text)
|
| 121 |
+
assert len(blocks) == 1
|
| 122 |
+
assert blocks[0]["language"] == "cpp"
|
| 123 |
+
assert blocks[0]["extension"] == ".cpp"
|
| 124 |
+
|
| 125 |
+
def test_extract_rust_block(self):
|
| 126 |
+
"""Extract Rust code block."""
|
| 127 |
+
text = """```rust
|
| 128 |
+
fn main() {
|
| 129 |
+
println!("Hello, GeoAI!");
|
| 130 |
+
}
|
| 131 |
+
```"""
|
| 132 |
+
blocks = extract_code_blocks(text)
|
| 133 |
+
assert len(blocks) == 1
|
| 134 |
+
assert blocks[0]["language"] == "rust"
|
| 135 |
+
assert blocks[0]["extension"] == ".rs"
|
| 136 |
+
|
| 137 |
+
def test_extract_no_language_defaults_to_python(self):
|
| 138 |
+
"""Code block without language specification defaults to python."""
|
| 139 |
+
text = """```
|
| 140 |
+
print("hello")
|
| 141 |
+
```"""
|
| 142 |
+
blocks = extract_code_blocks(text)
|
| 143 |
+
assert len(blocks) == 1
|
| 144 |
+
assert blocks[0]["language"] == "python"
|
| 145 |
+
|
| 146 |
+
def test_extract_empty_text(self):
|
| 147 |
+
"""Handle empty text gracefully."""
|
| 148 |
+
blocks = extract_code_blocks("")
|
| 149 |
+
assert len(blocks) == 0
|
| 150 |
+
|
| 151 |
+
def test_extract_no_code_blocks(self):
|
| 152 |
+
"""Handle text without code blocks."""
|
| 153 |
+
blocks = extract_code_blocks("Just some plain text without any code.")
|
| 154 |
+
assert len(blocks) == 0
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
class TestDownloadFunctions:
|
| 158 |
+
"""Test file download generation."""
|
| 159 |
+
|
| 160 |
+
def test_download_as_markdown(self):
|
| 161 |
+
"""Test markdown file generation."""
|
| 162 |
+
response = "Here's some code:\n```python\nprint('hello')\n```"
|
| 163 |
+
query = "Test query"
|
| 164 |
+
|
| 165 |
+
filepath = download_as_markdown(response, query)
|
| 166 |
+
|
| 167 |
+
assert filepath is not None
|
| 168 |
+
assert filepath.endswith(".md")
|
| 169 |
+
assert os.path.exists(filepath)
|
| 170 |
+
|
| 171 |
+
with open(filepath, "r") as f:
|
| 172 |
+
content = f.read()
|
| 173 |
+
|
| 174 |
+
assert "Test query" in content
|
| 175 |
+
assert "GeoAI" in content
|
| 176 |
+
|
| 177 |
+
# Cleanup
|
| 178 |
+
os.remove(filepath)
|
| 179 |
+
|
| 180 |
+
def test_download_as_notebook(self):
|
| 181 |
+
"""Test Jupyter notebook file generation."""
|
| 182 |
+
response = "```python\nimport rasterio\nprint('hello')\n```"
|
| 183 |
+
query = "Test notebook"
|
| 184 |
+
|
| 185 |
+
filepath = download_as_notebook(response, query)
|
| 186 |
+
|
| 187 |
+
assert filepath is not None
|
| 188 |
+
assert filepath.endswith(".ipynb")
|
| 189 |
+
assert os.path.exists(filepath)
|
| 190 |
+
|
| 191 |
+
with open(filepath, "r") as f:
|
| 192 |
+
content = json.load(f)
|
| 193 |
+
|
| 194 |
+
assert "cells" in content
|
| 195 |
+
assert content["nbformat"] == 4
|
| 196 |
+
|
| 197 |
+
# Cleanup
|
| 198 |
+
os.remove(filepath)
|
| 199 |
+
|
| 200 |
+
def test_download_as_code_python(self):
|
| 201 |
+
"""Test Python code file generation."""
|
| 202 |
+
response = "```python\nimport gdal\nds = gdal.Open('test.tif')\n```"
|
| 203 |
+
query = "Open a GeoTIFF"
|
| 204 |
+
|
| 205 |
+
filepath = download_as_code(response, query)
|
| 206 |
+
|
| 207 |
+
assert filepath is not None
|
| 208 |
+
assert filepath.endswith(".py")
|
| 209 |
+
assert os.path.exists(filepath)
|
| 210 |
+
|
| 211 |
+
with open(filepath, "r") as f:
|
| 212 |
+
content = f.read()
|
| 213 |
+
|
| 214 |
+
assert "import gdal" in content
|
| 215 |
+
|
| 216 |
+
# Cleanup
|
| 217 |
+
os.remove(filepath)
|
| 218 |
+
|
| 219 |
+
def test_download_as_code_cpp(self):
|
| 220 |
+
"""Test C++ code file generation."""
|
| 221 |
+
response = "```cpp\n#include <iostream>\nint main() { return 0; }\n```"
|
| 222 |
+
query = "C++ example"
|
| 223 |
+
|
| 224 |
+
filepath = download_as_code(response, query)
|
| 225 |
+
|
| 226 |
+
assert filepath is not None
|
| 227 |
+
assert filepath.endswith(".cpp")
|
| 228 |
+
|
| 229 |
+
# Cleanup
|
| 230 |
+
os.remove(filepath)
|
| 231 |
+
|
| 232 |
+
def test_download_empty_response(self):
|
| 233 |
+
"""Handle empty response gracefully."""
|
| 234 |
+
filepath = download_as_markdown("", "query")
|
| 235 |
+
assert filepath is None
|
| 236 |
+
|
| 237 |
+
def test_download_no_code_blocks(self):
|
| 238 |
+
"""Handle response without code blocks for code download."""
|
| 239 |
+
filepath = download_as_code("Just text, no code", "query")
|
| 240 |
+
assert filepath is None
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
class TestClientCreation:
|
| 244 |
+
"""Test HuggingFace client creation."""
|
| 245 |
+
|
| 246 |
+
def test_create_client_no_env_token(self):
|
| 247 |
+
"""No HF_TOKEN env var should return None."""
|
| 248 |
+
import app
|
| 249 |
+
original_token = app.HF_TOKEN
|
| 250 |
+
app.HF_TOKEN = ""
|
| 251 |
+
client = create_client()
|
| 252 |
+
app.HF_TOKEN = original_token
|
| 253 |
+
assert client is None
|
| 254 |
+
|
| 255 |
+
@patch('app.InferenceClient')
|
| 256 |
+
def test_create_client_with_env_token(self, mock_client):
|
| 257 |
+
"""With HF_TOKEN env var should create client."""
|
| 258 |
+
import app
|
| 259 |
+
original_token = app.HF_TOKEN
|
| 260 |
+
app.HF_TOKEN = "hf_validtoken123"
|
| 261 |
+
|
| 262 |
+
mock_instance = MagicMock()
|
| 263 |
+
mock_client.return_value = mock_instance
|
| 264 |
+
|
| 265 |
+
client = create_client()
|
| 266 |
+
|
| 267 |
+
app.HF_TOKEN = original_token
|
| 268 |
+
assert client is not None
|
| 269 |
+
mock_client.assert_called_once()
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
class TestGeospatialPrompts:
|
| 273 |
+
"""Test geospatial-specific functionality."""
|
| 274 |
+
|
| 275 |
+
def test_gdal_keywords_in_examples(self):
|
| 276 |
+
"""Verify GDAL-related examples exist."""
|
| 277 |
+
gdal_found = any("GDAL" in p or "gdal" in p.lower() for p in EXAMPLE_PROMPTS)
|
| 278 |
+
assert gdal_found, "No GDAL examples found"
|
| 279 |
+
|
| 280 |
+
def test_rasterio_keywords_in_examples(self):
|
| 281 |
+
"""Verify Rasterio examples exist."""
|
| 282 |
+
rasterio_found = any("rasterio" in p.lower() for p in EXAMPLE_PROMPTS)
|
| 283 |
+
assert rasterio_found, "No Rasterio examples found"
|
| 284 |
+
|
| 285 |
+
def test_geopandas_keywords_in_examples(self):
|
| 286 |
+
"""Verify GeoPandas examples exist."""
|
| 287 |
+
geopandas_found = any(
|
| 288 |
+
"geopandas" in p.lower() or "geodataframe" in p.lower()
|
| 289 |
+
for p in EXAMPLE_PROMPTS
|
| 290 |
+
)
|
| 291 |
+
assert geopandas_found, "No GeoPandas examples found"
|
| 292 |
+
|
| 293 |
+
def test_cog_or_cloud_native_in_examples(self):
|
| 294 |
+
"""Verify cloud-native geospatial examples exist."""
|
| 295 |
+
cog_found = any(
|
| 296 |
+
"cog" in p.lower() or "cloud" in p.lower() or "xarray" in p.lower()
|
| 297 |
+
for p in EXAMPLE_PROMPTS
|
| 298 |
+
)
|
| 299 |
+
assert cog_found, "No cloud-native geospatial examples found"
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
class TestIntegration:
|
| 303 |
+
"""Integration tests (require mocking or real API)."""
|
| 304 |
+
|
| 305 |
+
def test_full_workflow_mock(self):
|
| 306 |
+
"""Test full workflow with mocked API."""
|
| 307 |
+
# Simulate a response
|
| 308 |
+
mock_response = """Here's how to read a GeoTIFF:
|
| 309 |
+
|
| 310 |
+
```python
|
| 311 |
+
import rasterio
|
| 312 |
+
|
| 313 |
+
with rasterio.open('image.tif') as src:
|
| 314 |
+
data = src.read()
|
| 315 |
+
profile = src.profile
|
| 316 |
+
|
| 317 |
+
print(f"Shape: {data.shape}")
|
| 318 |
+
print(f"CRS: {src.crs}")
|
| 319 |
+
```
|
| 320 |
+
|
| 321 |
+
**Key Points:**
|
| 322 |
+
- Uses context manager for safe file handling
|
| 323 |
+
- Reads all bands into numpy array
|
| 324 |
+
"""
|
| 325 |
+
|
| 326 |
+
# Test extraction
|
| 327 |
+
blocks = extract_code_blocks(mock_response)
|
| 328 |
+
assert len(blocks) == 1
|
| 329 |
+
assert blocks[0]["language"] == "python"
|
| 330 |
+
|
| 331 |
+
# Test markdown download
|
| 332 |
+
md_path = download_as_markdown(mock_response, "Read GeoTIFF")
|
| 333 |
+
assert md_path is not None
|
| 334 |
+
os.remove(md_path)
|
| 335 |
+
|
| 336 |
+
# Test notebook download
|
| 337 |
+
nb_path = download_as_notebook(mock_response, "Read GeoTIFF")
|
| 338 |
+
assert nb_path is not None
|
| 339 |
+
os.remove(nb_path)
|
| 340 |
+
|
| 341 |
+
# Test code download
|
| 342 |
+
code_path = download_as_code(mock_response, "Read GeoTIFF")
|
| 343 |
+
assert code_path is not None
|
| 344 |
+
os.remove(code_path)
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
# Run tests
|
| 348 |
+
if __name__ == "__main__":
|
| 349 |
+
pytest.main([__file__, "-v"])
|