rifatSDAS commited on
Commit
2a6ffd6
·
1 Parent(s): 00fa073

Update app, config, README; add tests

Browse files
Files changed (7) hide show
  1. .gitignore +5 -0
  2. LOCALTEST.md +18 -0
  3. README.md +38 -39
  4. app.py +50 -50
  5. config.py +49 -99
  6. pytest.ini +3 -0
  7. 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 multiple LLMs via HuggingFace Inference Providers.
32
 
33
- Specialized in **GDAL/OGR**, **Rasterio**, **GeoPandas**, **xarray**, and geospatial development across multiple programming languages. Designed for **beginner and intermediate learners** with educational explanations.
34
 
35
  ![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)
36
- ![Gradio](https://img.shields.io/badge/Gradio-6.3+-orange.svg)
37
  ![License](https://img.shields.io/badge/License-AGPL--3.0-green.svg)
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 | Default | Description |
140
- | ------------------ | ----------------- | ------------------------------------------- |
141
- | `DEFAULT_MODEL` | `DeepSeek V3.2` | Default model selection |
142
- | `MAX_NEW_TOKENS` | `2048` | Maximum response length |
143
- | `TEMPERATURE` | `0.3` | Sampling temperature (lower = more focused) |
144
- | `TOP_P` | `0.9` | Nucleus sampling threshold |
145
- | `AVAILABLE_MODELS` | (see table above) | Dictionary of available models & providers |
146
 
147
  ## 🔑 API Token
148
 
149
- This application requires a **HuggingFace API token** with Inference Provider permissions.
150
-
151
- **Required permissions**:
152
- - ✅ Make calls to Inference Providers
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 multiple LLMs via [HuggingFace Inference Providers](https://huggingface.co/docs/huggingface_hub/guides/inference)
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, Co-founder and Technical Lead for Satellite Data Services business in Space sector.
 
 
 
 
 
 
184
 
185
- ## Contributing
 
186
 
187
- Contributions welcome! Please feel free to submit issues and pull requests.
 
188
 
189
- ## Contact
190
 
191
- 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!
192
 
193
- Portfolio at https://rifatsdas.github.io/
 
 
 
 
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
  ![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)
36
+ ![Gradio](https://img.shields.io/badge/Gradio-4.19+-orange.svg)
37
  ![License](https://img.shields.io/badge/License-AGPL--3.0-green.svg)
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
- AVAILABLE_MODELS,
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(provider: str) -> Optional[InferenceClient]:
36
- """Create HuggingFace Inference client with specified provider."""
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
- # Get model config
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=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 f"⚠️ **Authentication Error**: The model API returned 401. This could mean:\n\n1. The model `{model_id}` may require accepting terms at the model page\n2. The model may have been gated or moved\n3. Token permissions issue\n\n**Debug info**: {error_msg[:200]}"
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, selected_model, max_tokens):
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, model_selector, max_tokens_slider],
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, model_selector, max_tokens_slider],
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
- Supports multiple models via HuggingFace Inference Providers.
6
  """
7
 
8
- # Available Models with their Inference Providers
9
- # Format: {display_name: {"model_id": str, "provider": str}}
10
- AVAILABLE_MODELS = {
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 and educator specialized in:
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
- Your audience includes beginner and intermediate learners. Provide educational yet practical responses.
77
-
78
- 1. **Start with a brief overview**: Explain what the code will accomplish in 1-2 sentences
79
- 2. **Provide complete, working code**: Include imports, error handling, and type hints (Python)
80
- 3. **Add inline comments**: Explain key lines, especially geospatial-specific operations
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
- # Well-commented code here
95
  ```
96
 
97
- ### How It Works
98
- 1. **Step 1 name**: Explanation of what this step does and why
99
- 2. **Step 2 name**: Continue with key steps...
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
- Be thorough but not overwhelming. Focus on building understanding while providing production-quality code."""
109
 
110
  # Example prompts for UI
111
  EXAMPLE_PROMPTS = [
@@ -121,40 +71,40 @@ EXAMPLE_PROMPTS = [
121
 
122
  # File download templates
123
  NOTEBOOK_TEMPLATE = '''{{
124
- "cells": [
125
- {{
126
- "cell_type": "markdown",
127
- "metadata": {{}},
128
- "source": [
129
- "# GeoAI Generated Code\\n",
130
- "\\n",
131
- "Generated by GeoAI Coding Agent\\n",
132
- "\\n",
133
- "## Query\\n",
134
- "{query}"
135
- ]
136
- }},
137
- {{
138
- "cell_type": "code",
139
- "execution_count": null,
140
- "metadata": {{}},
141
- "outputs": [],
142
- "source": {code_cells}
143
- }}
144
- ],
145
- "metadata": {{
146
- "kernelspec": {{
147
- "display_name": "Python 3",
148
- "language": "python",
149
- "name": "python3"
150
- }},
151
- "language_info": {{
152
- "name": "python",
153
- "version": "3.10.0"
154
- }}
155
- }},
156
- "nbformat": 4,
157
- "nbformat_minor": 4
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"])