Added new datasets and fixed gemini models

#9
Files changed (12) hide show
  1. .DS_Store +0 -0
  2. .gitattributes +0 -2
  3. AQ_met_data.csv +0 -3
  4. app.py +814 -1026
  5. ncap_funding_data.csv +0 -118
  6. new_system_prompt.txt +0 -65
  7. questions.txt +28 -30
  8. src.py +421 -249
  9. states_data.csv +0 -32
  10. system_prompt.txt +0 -1
  11. test_image.py +0 -129
  12. vayuchat.mplstyle +0 -93
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
.gitattributes CHANGED
@@ -34,5 +34,3 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  Data.csv filter=lfs diff=lfs merge=lfs -text
37
- CPCB_data.csv filter=lfs diff=lfs merge=lfs -text
38
- AQ_met_data.csv filter=lfs diff=lfs merge=lfs -text
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  Data.csv filter=lfs diff=lfs merge=lfs -text
 
 
AQ_met_data.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:d060bcf1c8cc8bc7c7fa8016fa573202218e3b434885e7022481fd488c5f8198
3
- size 143177747
 
 
 
 
app.py CHANGED
@@ -19,14 +19,6 @@ from datasets import load_dataset, get_dataset_config_info, Dataset
19
  from PIL import Image
20
  import time
21
  import uuid
22
- import asyncio
23
-
24
- # Gemini API requires async
25
- try:
26
- asyncio.get_running_loop()
27
- except RuntimeError:
28
- loop = asyncio.new_event_loop()
29
- asyncio.set_event_loop(loop)
30
 
31
  # Page config with beautiful theme
32
  st.set_page_config(
@@ -36,1117 +28,913 @@ st.set_page_config(
36
  initial_sidebar_state="expanded"
37
  )
38
 
39
- # CRITICAL: CSS must be loaded FIRST for immediate blue message styling
40
  st.markdown("""
41
  <style>
42
- /* User message styling - MUST be defined early */
43
- .user-message {
44
- background: #3b82f6 !important;
45
- color: white !important;
46
- padding: 0.75rem 1rem !important;
47
- border-radius: 7px !important;
48
- max-width: 95% !important;
49
  }
50
 
51
- /* Assistant message styling */
52
- .assistant-message {
53
- background: #f1f5f9 !important;
54
- color: #334155 !important;
55
- padding: 0.75rem 1rem !important;
56
- border-radius: 12px !important;
57
- max-width: 85% !important;
58
  }
59
 
60
- .assistant-info {
61
- font-size: 0.875rem !important;
62
- color: #6b7280 !important;
63
- margin-bottom: 5px !important;
64
  }
65
- </style>
66
- """, unsafe_allow_html=True)
67
-
68
- # JavaScript for interactions
69
- # st.markdown("""
70
- # <script>
71
- # function scrollToBottom() {
72
- # setTimeout(function() {
73
- # const mainContainer = document.querySelector('.main-container');
74
- # if (mainContainer) {
75
- # mainContainer.scrollTop = mainContainer.scrollHeight;
76
- # }
77
- # window.scrollTo(0, document.body.scrollHeight);
78
- # }, 100);
79
- # }
80
-
81
- # function toggleCode(header) {
82
- # const codeBlock = header.nextElementSibling;
83
- # const toggleText = header.querySelector('.toggle-text');
84
-
85
- # if (codeBlock.style.display === 'none') {
86
- # codeBlock.style.display = 'block';
87
- # toggleText.textContent = 'Click to collapse';
88
- # } else {
89
- # codeBlock.style.display = 'none';
90
- # toggleText.textContent = 'Click to expand';
91
- # }
92
- # }
93
- # </script>
94
- # """, unsafe_allow_html=True)
95
-
96
- # FORCE reload environment variables
97
- load_dotenv(override=True)
98
-
99
- # Get API keys
100
- Groq_Token = os.getenv("GROQ_API_KEY")
101
- hf_token = os.getenv("HF_TOKEN")
102
- gemini_token = os.getenv("GEMINI_TOKEN")
103
 
104
- # Model order is decided by this
105
- models = {
106
- "gpt-oss-120b": "openai/gpt-oss-120b",
107
- "qwen3-32b": "qwen/qwen3-32b",
108
- "gpt-oss-20b": "openai/gpt-oss-20b",
109
- "llama4 maverik":"meta-llama/llama-4-maverick-17b-128e-instruct",
110
- "llama3.3": "llama-3.3-70b-versatile",
111
- "deepseek-R1": "deepseek-r1-distill-llama-70b",
112
- "gemini-2.5-flash": "gemini-2.5-flash",
113
- "gemini-2.5-pro": "gemini-2.5-pro",
114
- "gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
115
- "gemini-2.0-flash": "gemini-2.0-flash",
116
- "gemini-2.0-flash-lite": "gemini-2.0-flash-lite",
117
- # "llama4 scout":"meta-llama/llama-4-scout-17b-16e-instruct"
118
- # "llama3.1": "llama-3.1-8b-instant"
119
  }
120
 
121
- self_path = os.path.dirname(os.path.abspath(__file__))
122
-
123
- # Initialize session ID for this session
124
- if "session_id" not in st.session_state:
125
- st.session_state.session_id = str(uuid.uuid4())
126
-
127
- def upload_feedback(feedback, error, output, last_prompt, code, status):
128
- """Enhanced feedback upload function with better logging and error handling"""
129
- try:
130
- if not hf_token or hf_token.strip() == "":
131
- st.warning("Cannot upload feedback - HF_TOKEN not available")
132
- return False
133
-
134
- # Create comprehensive feedback data
135
- feedback_data = {
136
- "timestamp": datetime.now().isoformat(),
137
- "session_id": st.session_state.session_id,
138
- "feedback_score": feedback.get("score", ""),
139
- "feedback_comment": feedback.get("text", ""),
140
- "user_prompt": last_prompt,
141
- "ai_output": str(output),
142
- "generated_code": code or "",
143
- "error_message": error or "",
144
- "is_image_output": status.get("is_image", False),
145
- "success": not bool(error)
146
- }
147
 
148
- # Create unique folder name with timestamp
149
- timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
150
- random_id = str(uuid.uuid4())[:8]
151
- folder_name = f"feedback_{timestamp_str}_{random_id}"
152
-
153
- # Create markdown feedback file
154
- markdown_content = f"""# VayuChat Feedback Report
155
 
156
- ## Session Information
157
- - **Timestamp**: {feedback_data['timestamp']}
158
- - **Session ID**: {feedback_data['session_id']}
159
 
160
- ## User Interaction
161
- **Prompt**: {feedback_data['user_prompt']}
 
 
162
 
163
- ## AI Response
164
- **Output**: {feedback_data['ai_output']}
 
 
165
 
166
- ## Generated Code
167
- ```python
168
- {feedback_data['generated_code']}
169
- ```
170
 
171
- ## Technical Details
172
- - **Error Message**: {feedback_data['error_message']}
173
- - **Is Image Output**: {feedback_data['is_image_output']}
174
- - **Success**: {feedback_data['success']}
 
 
 
 
175
 
176
- ## User Feedback
177
- - **Score**: {feedback_data['feedback_score']}
178
- - **Comments**: {feedback_data['feedback_comment']}
179
- """
 
 
 
180
 
181
- # Save markdown file locally
182
- markdown_filename = f"{folder_name}.md"
183
- markdown_local_path = f"/tmp/{markdown_filename}"
184
-
185
- with open(markdown_local_path, "w", encoding="utf-8") as f:
186
- f.write(markdown_content)
 
 
 
 
187
 
188
- # Upload to Hugging Face
189
- api = HfApi(token=hf_token)
190
-
191
- # Upload markdown feedback
192
- api.upload_file(
193
- path_or_fileobj=markdown_local_path,
194
- path_in_repo=f"data/{markdown_filename}",
195
- repo_id="SustainabilityLabIITGN/VayuChat_Feedback",
196
- repo_type="dataset",
197
- )
198
-
199
- # Upload image if it exists and is an image output
200
- if status.get("is_image", False) and isinstance(output, str) and os.path.exists(output):
201
- try:
202
- image_filename = f"{folder_name}_plot.png"
203
- api.upload_file(
204
- path_or_fileobj=output,
205
- path_in_repo=f"data/{image_filename}",
206
- repo_id="SustainabilityLabIITGN/VayuChat_Feedback",
207
- repo_type="dataset",
208
- )
209
- except Exception as img_error:
210
- print(f"Error uploading image: {img_error}")
211
-
212
- # Clean up local files
213
- if os.path.exists(markdown_local_path):
214
- os.remove(markdown_local_path)
215
-
216
- st.success("Feedback uploaded successfully!")
217
- return True
218
-
219
- except Exception as e:
220
- st.error(f"Error uploading feedback: {e}")
221
- print(f"Feedback upload error: {e}")
222
- return False
223
 
224
- # Filter available models
225
- available_models = []
226
- model_names = list(models.keys())
227
- groq_models = []
228
- gemini_models = []
229
- for model_name in model_names:
230
- if "gemini" not in model_name:
231
- groq_models.append(model_name)
232
- else:
233
- gemini_models.append(model_name)
234
- if Groq_Token and Groq_Token.strip():
235
- available_models.extend(groq_models)
236
- if gemini_token and gemini_token.strip():
237
- available_models.extend(gemini_models)
238
 
239
- if not available_models:
240
- st.error("No API keys available! Please set up your API keys in the .env file")
241
- st.stop()
 
242
 
243
- # Set GPT-OSS-120B as default if available
244
- default_index = 0
245
- if "gpt-oss-120b" in available_models:
246
- default_index = available_models.index("gpt-oss-120b")
247
- elif "deepseek-R1" in available_models:
248
- default_index = available_models.index("deepseek-R1")
 
 
249
 
250
- # Compact header - everything perfectly aligned at same height
251
- st.markdown("""
252
- <style>
253
- .header-container {
254
- display: flex;
255
- align-items: center;
256
- justify-content: center;
257
- gap: 12px;
258
- border-bottom: 1px solid #e5e7eb;
259
  }
260
 
261
- .header-container img {
262
- height: 80px;
 
 
 
 
 
263
  }
264
 
265
- .header-container h1 {
266
- padding: 0.25rem 0;
267
- margin: 0;
268
- font-size: 1.5rem;
269
- font-weight: 700;
270
- color: #2563eb;
271
  }
272
 
273
- /* 🔹 Responsive: On small screens stack vertically */
274
- @media (max-width: 768px) {
275
- .header-container {
276
- flex-direction: column;
277
- text-align: center;
278
- gap: 0;
279
- padding: 0 0 0.40rem;
280
- }
281
- .header-container img {
282
- height: 60px;
283
- }
284
- .header-container h1 {
285
- padding: 0 0;
286
- font-size: 1.25rem;
287
- }
288
  }
289
- </style>
290
- <div class="header-container">
291
- <img src="https://sustainability-lab.github.io/images/logo_light.svg" />
292
- <div style="display: flex; flex-direction: column; line-height: 1.2;">
293
- <h1>VayuChat</h1>
294
- <span>AI Air Quality Analysis • Sustainability Lab, IIT Gandhinagar</span>
295
- </div>
296
- </div>
297
- """, unsafe_allow_html=True)
298
-
299
- # Load data with caching for better performance
300
- @st.cache_data
301
- def load_data():
302
- return preprocess_and_load_df(join(self_path, "Data.csv"))
303
-
304
- try:
305
- df = load_data()
306
- # Data loaded silently - no success message needed
307
- except Exception as e:
308
- st.error(f"Error loading data: {e}")
309
- st.stop()
310
-
311
- inference_server = "https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.2"
312
- image_path = "IITGN_Logo.png"
313
-
314
- # Clean sidebar
315
- with st.sidebar:
316
- # Model selector at top of sidebar for easy access
317
- model_name = st.selectbox(
318
- "🤖 AI Model:",
319
- available_models,
320
- index=default_index,
321
- help="Choose your AI model - easily accessible without scrolling!"
322
- )
323
-
324
- st.markdown("---")
325
-
326
- # Quick Queries Section
327
- st.markdown("### 💭 Quick Queries")
328
-
329
- # Load quick prompts with caching
330
- @st.cache_data
331
- def load_questions():
332
- questions = []
333
- questions_file = join(self_path, "questions.txt")
334
- if os.path.exists(questions_file):
335
- try:
336
- with open(questions_file, 'r', encoding='utf-8') as f:
337
- content = f.read()
338
- questions = [q.strip() for q in content.split("\n") if q.strip()]
339
- except Exception as e:
340
- questions = []
341
- return questions
342
-
343
- questions = load_questions()
344
-
345
- # Add default prompts if file doesn't exist or is empty
346
- if not questions:
347
- questions = [
348
- "Which month had highest pollution?",
349
- "Which city has worst air quality?",
350
- "Show annual PM2.5 average",
351
- "Plot monthly average PM2.5 for 2023",
352
- "List all cities by pollution level",
353
- "Compare winter vs summer pollution",
354
- "Show seasonal pollution patterns",
355
- "Which areas exceed WHO guidelines?",
356
- "What are peak pollution hours?",
357
- "Show PM10 vs PM2.5 comparison",
358
- "Which station records highest variability in PM2.5?",
359
- "Calculate pollution improvement rate year-over-year by city",
360
- "Identify cities with PM2.5 levels consistently above 50 μg/m³ for >6 months",
361
- "Find correlation between PM2.5 and PM10 across different seasons and cities",
362
- "Compare weekday vs weekend levels",
363
- "Plot yearly trend analysis",
364
- "Show pollution distribution by city",
365
- "Create correlation plot between pollutants"
366
- ]
367
-
368
- # Quick query buttons in sidebar
369
- selected_prompt = None
370
-
371
-
372
- # Show all questions but in a scrollable format
373
- if len(questions) > 0:
374
- st.markdown("**Select a question to analyze:**")
375
-
376
- # Getting Started section with simple questions
377
- getting_started_questions = questions[:10] # First 10 simple questions
378
- with st.expander("🚀 Getting Started - Simple Questions", expanded=True):
379
- for i, q in enumerate(getting_started_questions):
380
- if st.button(q, key=f"start_q_{i}", use_container_width=True, help=f"Analyze: {q}"):
381
- selected_prompt = q
382
- st.session_state.last_selected_prompt = q
383
-
384
- # Create expandable sections for better organization
385
- with st.expander("📊 NCAP Funding & Policy Analysis", expanded=False):
386
- for i, q in enumerate([q for q in questions if any(word in q.lower() for word in ['ncap', 'funding', 'investment', 'rupee'])]):
387
- if st.button(q, key=f"ncap_q_{i}", use_container_width=True, help=f"Analyze: {q}"):
388
- selected_prompt = q
389
- st.session_state.last_selected_prompt = q
390
-
391
- with st.expander("🌬️ Meteorology & Environmental Factors", expanded=False):
392
- for i, q in enumerate([q for q in questions if any(word in q.lower() for word in ['wind', 'temperature', 'humidity', 'rainfall', 'meteorological', 'monsoon', 'barometric'])]):
393
- if st.button(q, key=f"met_q_{i}", use_container_width=True, help=f"Analyze: {q}"):
394
- selected_prompt = q
395
- st.session_state.last_selected_prompt = q
396
-
397
- with st.expander("👥 Population & Demographics", expanded=False):
398
- for i, q in enumerate([q for q in questions if any(word in q.lower() for word in ['population', 'capita', 'density', 'exposure'])]):
399
- if st.button(q, key=f"pop_q_{i}", use_container_width=True, help=f"Analyze: {q}"):
400
- selected_prompt = q
401
- st.session_state.last_selected_prompt = q
402
-
403
- with st.expander("🏭 Multi-Pollutant Analysis", expanded=False):
404
- for i, q in enumerate([q for q in questions if any(word in q.lower() for word in ['ozone', 'no2', 'correlation', 'multi-pollutant', 'interaction'])]):
405
- if st.button(q, key=f"multi_q_{i}", use_container_width=True, help=f"Analyze: {q}"):
406
- selected_prompt = q
407
- st.session_state.last_selected_prompt = q
408
-
409
- with st.expander("📈 Other Analysis Questions", expanded=False):
410
- remaining_questions = [q for q in questions if not any(any(word in q.lower() for word in category) for category in [
411
- ['ncap', 'funding', 'investment', 'rupee'],
412
- ['wind', 'temperature', 'humidity', 'rainfall', 'meteorological', 'monsoon', 'barometric'],
413
- ['population', 'capita', 'density', 'exposure'],
414
- ['ozone', 'no2', 'correlation', 'multi-pollutant', 'interaction']
415
- ])]
416
- for i, q in enumerate(remaining_questions):
417
- if st.button(q, key=f"other_q_{i}", use_container_width=True, help=f"Analyze: {q}"):
418
- selected_prompt = q
419
- st.session_state.last_selected_prompt = q
420
-
421
- st.markdown("---")
422
-
423
-
424
- # Clear Chat Button
425
- if st.button("Clear Chat", use_container_width=True):
426
- st.session_state.responses = []
427
- st.session_state.processing = False
428
- st.session_state.session_id = str(uuid.uuid4())
429
- try:
430
- st.rerun()
431
- except AttributeError:
432
- st.experimental_rerun()
433
-
434
- # Initialize session state first
435
- if "responses" not in st.session_state:
436
- st.session_state.responses = []
437
- if "processing" not in st.session_state:
438
- st.session_state.processing = False
439
- if "session_id" not in st.session_state:
440
- st.session_state.session_id = str(uuid.uuid4())
441
-
442
-
443
-
444
-
445
- def show_custom_response(response):
446
- """Custom response display function with improved styling"""
447
- role = response.get("role", "assistant")
448
- content = response.get("content", "")
449
-
450
- if role == "user":
451
- # User message with right alignment - CSS now loaded at top of file
452
- st.markdown(f"""
453
- <div style='display: flex; justify-content: flex-end; margin: 1rem 0;'>
454
- <div class='user-message'>
455
- {content}
456
- </div>
457
- </div>
458
- """, unsafe_allow_html=True)
459
- elif role == "assistant":
460
- # Check if content is an image filename - don't display the filename text
461
- is_image_path = isinstance(content, str) and any(ext in content for ext in ['.png', '.jpg', '.jpeg'])
462
-
463
- # Check if content is a pandas DataFrame
464
- import pandas as pd
465
- is_dataframe = isinstance(content, pd.DataFrame)
466
-
467
- # Check for errors first and display them with special styling
468
- error = response.get("error")
469
- timestamp = response.get("timestamp", "")
470
- timestamp_display = f" • {timestamp}" if timestamp else ""
471
-
472
- if error:
473
- st.markdown(f"""
474
- <div style='display: flex; justify-content: flex-start; margin: 1rem 0;'>
475
- <div class='assistant-message'>
476
- <div class='assistant-info'>VayuChat{timestamp_display}</div>
477
- <div class='error-message'>
478
- ⚠️ <strong>Error:</strong> {error}
479
- <br><br>
480
- <em>💡 Try rephrasing your question or being more specific about what you'd like to analyze.</em>
481
- </div>
482
- </div>
483
- </div>
484
- """, unsafe_allow_html=True)
485
- # Assistant message with left alignment - reduced margins
486
- elif not is_image_path and not is_dataframe:
487
- st.markdown(f"""
488
- <div style='display: flex; justify-content: flex-start; margin: 1rem 0;'>
489
- <div class='assistant-message'>
490
- <div class='assistant-info'>VayuChat{timestamp_display}</div>
491
- {content if isinstance(content, str) else str(content)}
492
- </div>
493
- </div>
494
- """, unsafe_allow_html=True)
495
- elif is_dataframe:
496
- # Display DataFrame with nice formatting
497
- st.markdown(f"""
498
- <div style='display: flex; justify-content: flex-start; margin: 1rem 0;'>
499
- <div class='assistant-message'>
500
- <div class='assistant-info'>VayuChat{timestamp_display}</div>
501
- Here are the results:
502
- </div>
503
- </div>
504
- """, unsafe_allow_html=True)
505
-
506
- # Add context info for dataframes
507
- st.markdown("""
508
- <div class='context-info'>
509
- 💡 This table is interactive - click column headers to sort, or scroll to view all data.
510
- </div>
511
- """, unsafe_allow_html=True)
512
-
513
- # Display dataframe with built-in download functionality
514
- st.dataframe(
515
- content,
516
- use_container_width=True,
517
- hide_index=True,
518
- column_config=None
519
- )
520
-
521
- # Show generated code with Streamlit expander
522
- if response.get("gen_code"):
523
- with st.expander("📋 View Generated Code", expanded=False):
524
- st.code(response["gen_code"], language="python")
525
-
526
- # Check if this is a plot response (plots are now displayed directly via st.pyplot)
527
- is_plot_response = isinstance(content, str) and "Plot displayed successfully" in content
528
-
529
- # Try to display image if content is a file path (for backward compatibility)
530
- try:
531
- if isinstance(content, str) and (content.endswith('.png') or content.endswith('.jpg')):
532
- if os.path.exists(content):
533
- # Display image with better styling and reasonable width
534
- st.markdown("""
535
- <div style='margin: 1rem 0; display: flex; justify-content: center;'>
536
- </div>
537
- """, unsafe_allow_html=True)
538
- st.image(content, width=1080, caption="Generated Visualization")
539
- return {"is_image": True}
540
- # Also handle case where content shows filename but we want to show image
541
- elif isinstance(content, str) and any(ext in content for ext in ['.png', '.jpg']):
542
- # Extract potential filename from content
543
- import re
544
- filename_match = re.search(r'([^/\\]+\.(?:png|jpg|jpeg))', content)
545
- if filename_match:
546
- filename = filename_match.group(1)
547
- if os.path.exists(filename):
548
- st.markdown("""
549
- <div style='margin: 1rem 0; display: flex; justify-content: center;'>
550
- </div>
551
- """, unsafe_allow_html=True)
552
- st.image(filename, width=1080, caption="Generated Visualization")
553
- return {"is_image": True}
554
- except:
555
- pass
556
-
557
- return {"is_image": False}
558
 
 
 
 
 
 
559
 
560
- # Chat history
561
- # Display chat history
562
- for response_id, response in enumerate(st.session_state.responses):
563
- status = show_custom_response(response)
564
-
565
- # Show feedback section for assistant responses
566
- if response["role"] == "assistant":
567
- feedback_key = f"feedback_{int(response_id/2)}"
568
- error = response.get("error", "")
569
- output = response.get("content", "")
570
- last_prompt = response.get("last_prompt", "")
571
- code = response.get("gen_code", "")
572
-
573
 
574
- # Beautiful action bar with feedback and retry
575
- st.markdown('<div style="margin: 1.5rem 0 0.5rem 0;"></div>', unsafe_allow_html=True) # Spacer
576
-
577
- if "feedback" in st.session_state.responses[response_id]:
578
- # Show submitted feedback nicely
579
- feedback_data = st.session_state.responses[response_id]["feedback"]
580
- col1, col2 = st.columns([3, 1])
581
- with col1:
582
- st.markdown(f"""
583
- <div style='
584
- background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
585
- border: 1px solid #a7f3d0;
586
- border-radius: 8px;
587
- padding: 0.75rem 1rem;
588
- display: flex;
589
- align-items: center;
590
- gap: 8px;
591
- '>
592
- <span style='font-size: 1.1rem;'>{feedback_data.get('score', '')}</span>
593
- <span style='color: #059669; font-weight: 500; font-size: 0.9rem;'>
594
- Thanks for your feedback!
595
- </span>
596
- </div>
597
- """, unsafe_allow_html=True)
598
- with col2:
599
- if st.button("🔄 Retry", key=f"retry_{response_id}", use_container_width=True):
600
- user_prompt = ""
601
- if response_id > 0:
602
- user_prompt = st.session_state.responses[response_id-1].get("content", "")
603
- if user_prompt:
604
- if response_id > 0:
605
- retry_prompt = st.session_state.responses[response_id-1].get("content", "")
606
- del st.session_state.responses[response_id]
607
- del st.session_state.responses[response_id-1]
608
- st.session_state.follow_up_prompt = retry_prompt
609
- st.rerun()
610
- else:
611
- # Clean feedback and retry layout
612
- col1, col2, col3, col4 = st.columns([2, 2, 1, 1])
613
-
614
- with col1:
615
- if st.button("✨ Excellent", key=f"{feedback_key}_excellent", use_container_width=True):
616
- feedback = {"score": "✨ Excellent", "text": ""}
617
- st.session_state.responses[response_id]["feedback"] = feedback
618
- st.rerun()
619
-
620
- with col2:
621
- if st.button("🔧 Needs work", key=f"{feedback_key}_poor", use_container_width=True):
622
- feedback = {"score": "🔧 Needs work", "text": ""}
623
- st.session_state.responses[response_id]["feedback"] = feedback
624
- st.rerun()
625
-
626
- with col4:
627
- if st.button("🔄 Retry", key=f"retry_{response_id}", use_container_width=True):
628
- user_prompt = ""
629
- if response_id > 0:
630
- user_prompt = st.session_state.responses[response_id-1].get("content", "")
631
- if user_prompt:
632
- if response_id > 0:
633
- retry_prompt = st.session_state.responses[response_id-1].get("content", "")
634
- del st.session_state.responses[response_id]
635
- del st.session_state.responses[response_id-1]
636
- st.session_state.follow_up_prompt = retry_prompt
637
- st.rerun()
638
 
639
- # Chat input with better guidance
640
- prompt = st.chat_input("💬 Ask about air quality trends, pollution analysis, or city comparisons...", key="main_chat")
 
 
 
 
 
641
 
642
- # Handle selected prompt from quick prompts
643
- if selected_prompt:
644
- prompt = selected_prompt
 
 
 
 
 
645
 
646
- # Handle follow-up prompts from quick action buttons
647
- if st.session_state.get("follow_up_prompt") and not st.session_state.get("processing"):
648
- prompt = st.session_state.follow_up_prompt
649
- st.session_state.follow_up_prompt = None # Clear the follow-up prompt
650
 
651
- # Handle new queries
652
- if prompt and not st.session_state.get("processing"):
653
- # Prevent duplicate processing
654
- if "last_prompt" in st.session_state:
655
- last_prompt = st.session_state["last_prompt"]
656
- last_model_name = st.session_state.get("last_model_name", "")
657
- if (prompt == last_prompt) and (model_name == last_model_name):
658
- prompt = None
 
 
659
 
660
- if prompt:
661
- # Add user input to chat history
662
- user_response = get_from_user(prompt)
663
- st.session_state.responses.append(user_response)
664
-
665
- # Set processing state
666
- st.session_state.processing = True
667
- st.session_state.current_model = model_name
668
- st.session_state.current_question = prompt
669
-
670
- # Rerun to show processing indicator
671
- st.rerun()
672
 
673
- # Process the question if we're in processing state
674
- if st.session_state.get("processing"):
675
- # Enhanced processing indicator like Claude Code
676
- st.markdown("""
677
- <div style='padding: 1rem; text-align: center; background: #f8fafc; border-radius: 8px; margin: 1rem 0;'>
678
- <div style='display: flex; align-items: center; justify-content: center; gap: 0.5rem; color: #475569;'>
679
- <div style='font-weight: 500;'>🤖 Processing with """ + str(st.session_state.get('current_model', 'Unknown')) + """</div>
680
- <div class='dots' style='display: inline-flex; gap: 2px;'>
681
- <div class='dot' style='width: 4px; height: 4px; background: #3b82f6; border-radius: 50%; animation: bounce 1.4s infinite ease-in-out;'></div>
682
- <div class='dot' style='width: 4px; height: 4px; background: #3b82f6; border-radius: 50%; animation: bounce 1.4s infinite ease-in-out; animation-delay: 0.16s;'></div>
683
- <div class='dot' style='width: 4px; height: 4px; background: #3b82f6; border-radius: 50%; animation: bounce 1.4s infinite ease-in-out; animation-delay: 0.32s;'></div>
684
- </div>
685
- </div>
686
- <div style='font-size: 0.75rem; color: #6b7280; margin-top: 0.25rem;'>Analyzing data and generating response...</div>
687
- </div>
688
- <style>
689
- @keyframes bounce {
690
- 0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
691
- 40% { transform: scale(1.2); opacity: 1; }
692
- }
693
- </style>
694
- """, unsafe_allow_html=True)
695
-
696
- prompt = st.session_state.get("current_question")
697
- model_name = st.session_state.get("current_model")
698
-
699
- try:
700
- response = ask_question(model_name=model_name, question=prompt)
701
-
702
- if not isinstance(response, dict):
703
- response = {
704
- "role": "assistant",
705
- "content": "Error: Invalid response format",
706
- "gen_code": "",
707
- "ex_code": "",
708
- "last_prompt": prompt,
709
- "error": "Invalid response format",
710
- "timestamp": datetime.now().strftime("%H:%M")
711
- }
712
-
713
- response.setdefault("role", "assistant")
714
- response.setdefault("content", "No content generated")
715
- response.setdefault("gen_code", "")
716
- response.setdefault("ex_code", "")
717
- response.setdefault("last_prompt", prompt)
718
- response.setdefault("error", None)
719
- response.setdefault("timestamp", datetime.now().strftime("%H:%M"))
720
-
721
- except Exception as e:
722
- response = {
723
- "role": "assistant",
724
- "content": f"Sorry, I encountered an error: {str(e)}",
725
- "gen_code": "",
726
- "ex_code": "",
727
- "last_prompt": prompt,
728
- "error": str(e),
729
- "timestamp": datetime.now().strftime("%H:%M")
730
- }
731
 
732
- st.session_state.responses.append(response)
733
- st.session_state["last_prompt"] = prompt
734
- st.session_state["last_model_name"] = model_name
735
- st.session_state.processing = False
736
-
737
- # Clear processing state
738
- if "current_model" in st.session_state:
739
- del st.session_state.current_model
740
- if "current_question" in st.session_state:
741
- del st.session_state.current_question
742
-
743
- st.rerun()
744
 
745
- # Close chat container
746
- st.markdown("</div>", unsafe_allow_html=True)
 
 
747
 
748
- # Minimal auto-scroll - only scroll when processing
749
- if st.session_state.get("processing"):
750
- st.markdown("<script>scrollToBottom();</script>", unsafe_allow_html=True)
 
 
 
 
 
751
 
752
- # Dataset Info Section (matching mockup)
753
- st.markdown("### Dataset Info")
754
- st.markdown("""
755
- <div style='background: #f1f5f9; border-radius: 8px; padding: 1rem; margin-bottom: 1rem;'>
756
- <h4 style='margin: 0 0 0.5rem 0; color: #1e293b; font-size: 0.9rem;'>PM2.5 Air Quality Data</h4>
757
- <p style='margin: 0; font-size: 0.75rem; color: #475569;'><strong>Time Range:</strong> 2022 - 2023</p>
758
- <p style='margin: 0; font-size: 0.75rem; color: #475569;'><strong>Locations:</strong> 300+ cities across India</p>
759
- <p style='margin: 0; font-size: 0.75rem; color: #475569;'><strong>Records:</strong> 100,000+ measurements</p>
760
- </div>
761
- """, unsafe_allow_html=True)
762
-
763
 
764
- # streamlit adds each markdown's div, so its better to keep this in the last
765
- # Custom CSS for beautiful styling
766
- st.markdown("""
767
- <style>
768
- /* Clean app background */
769
- .stApp {
770
- background-color: #ffffff;
771
- color: #212529;
772
- font-family: 'Segoe UI', sans-serif;
773
  }
774
 
775
- /* Reduce main container padding */
776
- .main .block-container {
777
- padding-top: 0px;
778
- padding-bottom: 3rem;
779
- max-width: 100%;
 
 
780
  }
781
 
782
- /* Remove excessive spacing */
783
- .element-container {
784
- margin-bottom: 0.5rem !important;
785
  }
786
 
787
- /* Fix sidebar spacing */
788
- [data-testid="stSidebar"] .element-container {
789
- margin-bottom: 0.25rem !important;
 
790
  }
791
 
792
- /* Sidebar */
793
- [data-testid="stSidebar"] {
794
- background-color: #f8f9fa;
795
- border-right: 1px solid #dee2e6;
796
- padding: 1rem;
 
 
 
 
797
  }
798
 
799
- /* Optimize sidebar scrolling */
800
- [data-testid="stSidebar"] > div:first-child {
801
- height: 100vh;
802
- overflow-y: auto;
803
- padding-bottom: 2rem;
 
804
  }
805
 
806
- [data-testid="stSidebar"]::-webkit-scrollbar {
807
- width: 6px;
 
 
 
808
  }
809
 
810
- [data-testid="stSidebar"]::-webkit-scrollbar-track {
811
- background: #f1f1f1;
812
- border-radius: 3px;
 
 
 
813
  }
814
 
815
- [data-testid="stSidebar"]::-webkit-scrollbar-thumb {
816
- background: #c1c1c1;
817
- border-radius: 3px;
 
 
 
 
818
  }
819
 
820
- [data-testid="stSidebar"]::-webkit-scrollbar-thumb:hover {
821
- background: #a1a1a1;
 
 
 
 
 
 
 
822
  }
 
 
823
 
824
- /* Main title */
825
- .main-title {
826
- text-align: center;
827
- color: #343a40;
828
- font-size: 2.5rem;
829
- font-weight: 700;
830
- margin-bottom: 0.5rem;
 
 
 
 
831
  }
832
 
833
- /* Subtitle */
834
- .subtitle {
835
- text-align: center;
836
- color: #6c757d;
837
- font-size: 1.1rem;
838
- margin-bottom: 1.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
839
  }
840
 
841
- /* Instructions */
842
- .instructions {
843
- background-color: #f1f3f5;
844
- border-left: 4px solid #0d6efd;
845
- padding: 1rem;
846
- margin-bottom: 1.5rem;
847
- border-radius: 6px;
848
- color: #495057;
849
- text-align: left;
850
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
851
 
852
- /* Quick prompt buttons */
853
- .quick-prompt-container {
854
- display: flex;
855
- flex-wrap: wrap;
856
- gap: 8px;
857
- margin-bottom: 1.5rem;
858
- padding: 1rem;
859
- background-color: #f8f9fa;
860
- border-radius: 10px;
861
- border: 1px solid #dee2e6;
862
- }
863
 
864
- .quick-prompt-btn {
865
- background-color: #0d6efd;
866
- color: white;
867
- border: none;
868
- padding: 8px 16px;
869
- border-radius: 20px;
870
- font-size: 0.9rem;
871
- cursor: pointer;
872
- transition: all 0.2s ease;
873
- white-space: nowrap;
874
- }
875
 
876
- .quick-prompt-btn:hover {
877
- background-color: #0b5ed7;
878
- transform: translateY(-2px);
879
- }
880
 
881
- /* User message styling */
882
- .user-message {
883
- background: #3b82f6;
884
- color: white;
885
- padding: 0.75rem 1rem;
886
- border-radius: 7px;
887
- max-width: 95%;
888
- }
889
 
890
- .user-info {
891
- font-size: 0.875rem;
892
- opacity: 0.9;
893
- margin-bottom: 3px;
894
- }
895
 
896
- /* Assistant message styling */
897
- .assistant-message {
898
- background: #f1f5f9;
899
- color: #334155;
900
- padding: 0.75rem 1rem;
901
- border-radius: 12px;
902
- max-width: 85%;
903
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904
 
905
- .assistant-info {
906
- font-size: 0.875rem;
907
- color: #6b7280;
908
- margin-bottom: 5px;
909
- }
 
 
910
 
911
- /* Processing indicator */
912
- .processing-indicator {
913
- background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
914
- color: #333;
915
- padding: 1rem 1.5rem;
916
- border-radius: 12px;
917
- margin: 1rem 0;
918
- margin-left: 0;
919
- margin-right: auto;
920
- max-width: 70%;
921
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
922
- animation: pulse 2s infinite;
923
- }
924
 
925
- @keyframes pulse {
926
- 0% { opacity: 1; }
927
- 50% { opacity: 0.7; }
928
- 100% { opacity: 1; }
929
- }
930
 
931
- /* Feedback box */
932
- .feedback-section {
933
- background-color: #f8f9fa;
934
- border: 1px solid #dee2e6;
935
- padding: 1rem;
936
- border-radius: 8px;
937
- margin: 1rem 0;
938
- }
939
 
940
- /* Success and error messages */
941
- .success-message {
942
- background-color: #d1e7dd;
943
- color: #0f5132;
944
- padding: 1rem;
945
- border-radius: 6px;
946
- border: 1px solid #badbcc;
947
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
948
 
949
- .error-message {
950
- background-color: #f8d7da;
951
- color: #842029;
952
- padding: 1rem;
953
- border-radius: 6px;
954
- border: 1px solid #f5c2c7;
955
- }
956
 
957
- /* Chat input styling - Fixed alignment */
958
- # .stChatInput {
959
- # border-radius: 12px !important;
960
- # border: 2px solid #e5e7eb !important;
961
- # background: #ffffff !important;
962
- # padding: 0.75rem 1rem !important;
963
- # font-size: 1rem !important;
964
- # width: 100% !important;
965
- # max-width: 70% !important;
966
- # margin: 0 !important;
967
- # box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important;
968
- # transition: all 0.2s ease !important;
969
- # }
970
-
971
- # .stChatInput:focus {
972
- # border-color: #3b82f6 !important;
973
- # box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
974
- # outline: none !important;
975
- # }
976
-
977
- /* Chat input container */
978
- .stChatInput > div {
979
- padding: 0 !important;
980
- margin: 0 !important;
981
- }
982
 
983
- /* Chat input text area */
984
- # .stChatInput textarea {
985
- # border: none !important;
986
- # background: transparent !important;
987
- # padding: 0 !important;
988
- # margin: 0 !important;
989
- # font-size: 1rem !important;
990
- # line-height: 1.5 !important;
991
- # resize: none !important;
992
- # outline: none !important;
993
- # }
994
-
995
- /* Chat input placeholder */
996
- # .stChatInput textarea::placeholder {
997
- # color: #9ca3af !important;
998
- # font-style: normal !important;
999
- # }
1000
-
1001
- .st-emotion-cache-f4ro0r {
1002
- align-items = center;
1003
- }
1004
 
1005
- /* Fix the main chat input container alignment */
1006
- [data-testid="stChatInput"] {
1007
- position: fixed !important;
1008
- bottom: 0.5rem !important;
1009
- left: 6rem !important;
1010
- right: 0 !important;
1011
- background: #ffffff !important;
1012
- width: 65% !important;
1013
- box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1) !important;
1014
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1015
 
1016
- /* Adjust main content to account for fixed chat input */
1017
- .main .block-container {
1018
- padding-bottom: 100px !important;
1019
- }
1020
 
1021
- /* Chat input button styling */
1022
- [data-testid="stChatInput"] button {
1023
- background: #3b82f6 !important;
1024
- color: white !important;
1025
- border: none !important;
1026
- border-radius: 12px !important;
1027
- font-weight: 600 !important;
1028
- transition: background-color 0.2s ease !important;
1029
- }
1030
 
1031
- [data-testid="stChatInput"] button:hover {
1032
- background: #2563eb !important;
1033
- }
 
1034
 
1035
- /* Textarea inside chat input */
1036
- [data-testid="stChatInput"] [data-baseweb="textarea"] {
1037
- border: 2px solid #3b82f6 !important;
1038
- border-radius: 12px !important;
1039
- font-size: 16px !important;
1040
- color: #111 !important;
 
 
1041
 
1042
- width: 100% !important; /* fill the parent container */
1043
- box-sizing: border-box !important;
1044
- }
 
 
 
 
 
 
 
 
 
1045
 
1046
- /* Ensure proper spacing from sidebar */
1047
- @media (min-width: 768px) {
1048
- [data-testid="stChatInput"] {
1049
- margin-left: 21rem !important; /* Account for sidebar width */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1050
  }
1051
- }
1052
-
1053
- /* Code container styling */
1054
- .code-container {
1055
- margin: 1rem 0;
1056
- border: 1px solid #d1d5db;
1057
- border-radius: 12px;
1058
- background: white;
1059
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1060
- }
1061
-
1062
- .code-header {
1063
- display: flex;
1064
- justify-content: space-between;
1065
- align-items: center;
1066
- padding: 0.875rem 1.25rem;
1067
- background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
1068
- border-bottom: 1px solid #e2e8f0;
1069
- cursor: pointer;
1070
- transition: all 0.2s ease;
1071
- border-radius: 12px 12px 0 0;
1072
- }
1073
-
1074
- .code-header:hover {
1075
- background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%);
1076
- }
1077
-
1078
- .code-title {
1079
- font-size: 0.9rem;
1080
- font-weight: 600;
1081
- color: #1e293b;
1082
- display: flex;
1083
- align-items: center;
1084
- gap: 0.5rem;
1085
- }
1086
-
1087
- .code-title:before {
1088
- content: "⚡";
1089
- font-size: 0.8rem;
1090
- }
1091
-
1092
- .toggle-text {
1093
- font-size: 0.75rem;
1094
- color: #64748b;
1095
- font-weight: 500;
1096
- }
1097
-
1098
- .code-block {
1099
- background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
1100
- color: #e2e8f0;
1101
- padding: 1.5rem;
1102
- font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
1103
- font-size: 0.875rem;
1104
- overflow-x: auto;
1105
- line-height: 1.6;
1106
- border-radius: 0 0 12px 12px;
1107
- }
1108
 
1109
- .answer-container {
1110
- background: #f8fafc;
1111
- border: 1px solid #e2e8f0;
1112
- border-radius: 8px;
1113
- padding: 1.5rem;
1114
- margin: 1rem 0;
1115
- }
 
 
 
 
 
1116
 
1117
- .answer-text {
1118
- font-size: 1.125rem;
1119
- color: #1e293b;
1120
- line-height: 1.6;
1121
- margin-bottom: 1rem;
1122
- }
1123
 
1124
- .answer-highlight {
1125
- background: #fef3c7;
1126
- padding: 0.125rem 0.375rem;
1127
- border-radius: 4px;
1128
- font-weight: 600;
1129
- color: #92400e;
1130
- }
1131
 
1132
- .context-info {
1133
- background: #f1f5f9;
1134
- border-left: 4px solid #3b82f6;
1135
- padding: 0.75rem 1rem;
1136
- margin: 1rem 0;
1137
- font-size: 0.875rem;
1138
- color: #475569;
1139
- }
 
 
 
 
 
 
 
 
 
 
 
 
1140
 
1141
- /* Hide default menu and footer */
1142
- #MainMenu {visibility: hidden;}
1143
- footer {visibility: hidden;}
1144
- header {visibility: hidden;}
1145
 
1146
- /* Auto scroll */
1147
- .main-container {
1148
- height: 70vh;
1149
- overflow-y: auto;
1150
- }
1151
- </style>
1152
- """, unsafe_allow_html=True)
 
19
  from PIL import Image
20
  import time
21
  import uuid
 
 
 
 
 
 
 
 
22
 
23
  # Page config with beautiful theme
24
  st.set_page_config(
 
28
  initial_sidebar_state="expanded"
29
  )
30
 
31
+ # Custom CSS for beautiful styling
32
  st.markdown("""
33
  <style>
34
+ /* Clean app background */
35
+ .stApp {
36
+ background-color: #ffffff;
37
+ color: #212529;
38
+ font-family: 'Segoe UI', sans-serif;
 
 
39
  }
40
 
41
+ /* Reduce main container padding */
42
+ .main .block-container {
43
+ padding-top: 0.5rem;
44
+ padding-bottom: 3rem;
45
+ max-width: 100%;
 
 
46
  }
47
 
48
+ /* Remove excessive spacing */
49
+ .element-container {
50
+ margin-bottom: 0.5rem !important;
 
51
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ /* Fix sidebar spacing */
54
+ [data-testid="stSidebar"] .element-container {
55
+ margin-bottom: 0.25rem !important;
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
 
58
+ /* Sidebar */
59
+ [data-testid="stSidebar"] {
60
+ background-color: #f8f9fa;
61
+ border-right: 1px solid #dee2e6;
62
+ padding: 1rem;
63
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ /* Optimize sidebar scrolling */
66
+ [data-testid="stSidebar"] > div:first-child {
67
+ height: 100vh;
68
+ overflow-y: auto;
69
+ padding-bottom: 2rem;
70
+ }
 
71
 
72
+ [data-testid="stSidebar"]::-webkit-scrollbar {
73
+ width: 6px;
74
+ }
75
 
76
+ [data-testid="stSidebar"]::-webkit-scrollbar-track {
77
+ background: #f1f1f1;
78
+ border-radius: 3px;
79
+ }
80
 
81
+ [data-testid="stSidebar"]::-webkit-scrollbar-thumb {
82
+ background: #c1c1c1;
83
+ border-radius: 3px;
84
+ }
85
 
86
+ [data-testid="stSidebar"]::-webkit-scrollbar-thumb:hover {
87
+ background: #a1a1a1;
88
+ }
 
89
 
90
+ /* Main title */
91
+ .main-title {
92
+ text-align: center;
93
+ color: #343a40;
94
+ font-size: 2.5rem;
95
+ font-weight: 700;
96
+ margin-bottom: 0.5rem;
97
+ }
98
 
99
+ /* Subtitle */
100
+ .subtitle {
101
+ text-align: center;
102
+ color: #6c757d;
103
+ font-size: 1.1rem;
104
+ margin-bottom: 1.5rem;
105
+ }
106
 
107
+ /* Instructions */
108
+ .instructions {
109
+ background-color: #f1f3f5;
110
+ border-left: 4px solid #0d6efd;
111
+ padding: 1rem;
112
+ margin-bottom: 1.5rem;
113
+ border-radius: 6px;
114
+ color: #495057;
115
+ text-align: left;
116
+ }
117
 
118
+ /* Quick prompt buttons */
119
+ .quick-prompt-container {
120
+ display: flex;
121
+ flex-wrap: wrap;
122
+ gap: 8px;
123
+ margin-bottom: 1.5rem;
124
+ padding: 1rem;
125
+ background-color: #f8f9fa;
126
+ border-radius: 10px;
127
+ border: 1px solid #dee2e6;
128
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
+ .quick-prompt-btn {
131
+ background-color: #0d6efd;
132
+ color: white;
133
+ border: none;
134
+ padding: 8px 16px;
135
+ border-radius: 20px;
136
+ font-size: 0.9rem;
137
+ cursor: pointer;
138
+ transition: all 0.2s ease;
139
+ white-space: nowrap;
140
+ }
 
 
 
141
 
142
+ .quick-prompt-btn:hover {
143
+ background-color: #0b5ed7;
144
+ transform: translateY(-2px);
145
+ }
146
 
147
+ /* User message styling */
148
+ .user-message {
149
+ background: #3b82f6;
150
+ color: white;
151
+ padding: 0.75rem 1rem;
152
+ border-radius: 12px;
153
+ max-width: 70%;
154
+ }
155
 
156
+ .user-info {
157
+ font-size: 0.875rem;
158
+ opacity: 0.9;
159
+ margin-bottom: 3px;
 
 
 
 
 
160
  }
161
 
162
+ /* Assistant message styling */
163
+ .assistant-message {
164
+ background: #f1f5f9;
165
+ color: #334155;
166
+ padding: 0.75rem 1rem;
167
+ border-radius: 12px;
168
+ max-width: 70%;
169
  }
170
 
171
+ .assistant-info {
172
+ font-size: 0.875rem;
173
+ color: #6b7280;
174
+ margin-bottom: 5px;
 
 
175
  }
176
 
177
+ /* Processing indicator */
178
+ .processing-indicator {
179
+ background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
180
+ color: #333;
181
+ padding: 1rem 1.5rem;
182
+ border-radius: 12px;
183
+ margin: 1rem 0;
184
+ margin-left: 0;
185
+ margin-right: auto;
186
+ max-width: 70%;
187
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
188
+ animation: pulse 2s infinite;
 
 
 
189
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ @keyframes pulse {
192
+ 0% { opacity: 1; }
193
+ 50% { opacity: 0.7; }
194
+ 100% { opacity: 1; }
195
+ }
196
 
197
+ /* Feedback box */
198
+ .feedback-section {
199
+ background-color: #f8f9fa;
200
+ border: 1px solid #dee2e6;
201
+ padding: 1rem;
202
+ border-radius: 8px;
203
+ margin: 1rem 0;
204
+ }
 
 
 
 
 
205
 
206
+ /* Success and error messages */
207
+ .success-message {
208
+ background-color: #d1e7dd;
209
+ color: #0f5132;
210
+ padding: 1rem;
211
+ border-radius: 6px;
212
+ border: 1px solid #badbcc;
213
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
+ .error-message {
216
+ background-color: #f8d7da;
217
+ color: #842029;
218
+ padding: 1rem;
219
+ border-radius: 6px;
220
+ border: 1px solid #f5c2c7;
221
+ }
222
 
223
+ /* Chat input styling like mockup */
224
+ .stChatInput {
225
+ border-radius: 8px;
226
+ border: 1px solid #d1d5db;
227
+ background: #ffffff;
228
+ padding: 0.75rem 1rem;
229
+ font-size: 1rem;
230
+ }
231
 
232
+ .stChatInput:focus {
233
+ border-color: #3b82f6;
234
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
235
+ }
236
 
237
+ /* Button */
238
+ .stButton > button {
239
+ background-color: #0d6efd;
240
+ color: white;
241
+ border-radius: 6px;
242
+ padding: 0.5rem 1.25rem;
243
+ border: none;
244
+ font-weight: 600;
245
+ transition: background-color 0.2s ease;
246
+ }
247
 
248
+ .stButton > button:hover {
249
+ background-color: #0b5ed7;
250
+ }
 
 
 
 
 
 
 
 
 
251
 
252
+ /* Sidebar button styling - smaller, left-aligned */
253
+ [data-testid="stSidebar"] .stButton > button {
254
+ background-color: #f8fafc;
255
+ color: #475569;
256
+ border: 1px solid #e2e8f0;
257
+ padding: 0.375rem 0.75rem;
258
+ font-size: 0.65rem;
259
+ font-weight: normal;
260
+ text-align: left;
261
+ white-space: normal;
262
+ height: auto;
263
+ line-height: 1.2;
264
+ transition: all 0.2s ease;
265
+ cursor: pointer;
266
+ margin-bottom: 0.25rem;
267
+ width: 100%;
268
+ display: flex;
269
+ justify-content: flex-start;
270
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
+ [data-testid="stSidebar"] .stButton > button:hover {
273
+ background-color: #e0f2fe;
274
+ border-color: #0ea5e9;
275
+ color: #0c4a6e;
276
+ }
 
 
 
 
 
 
 
277
 
278
+ [data-testid="stSidebar"] .stButton > button:active {
279
+ transform: translateY(0);
280
+ box-shadow: none;
281
+ }
282
 
283
+ /* Code container styling */
284
+ .code-container {
285
+ margin: 1rem 0;
286
+ border: 1px solid #d1d5db;
287
+ border-radius: 12px;
288
+ background: white;
289
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
290
+ }
291
 
292
+ .code-header {
293
+ display: flex;
294
+ justify-content: space-between;
295
+ align-items: center;
296
+ padding: 0.875rem 1.25rem;
297
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
298
+ border-bottom: 1px solid #e2e8f0;
299
+ cursor: pointer;
300
+ transition: all 0.2s ease;
301
+ border-radius: 12px 12px 0 0;
302
+ }
303
 
304
+ .code-header:hover {
305
+ background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%);
 
 
 
 
 
 
 
306
  }
307
 
308
+ .code-title {
309
+ font-size: 0.9rem;
310
+ font-weight: 600;
311
+ color: #1e293b;
312
+ display: flex;
313
+ align-items: center;
314
+ gap: 0.5rem;
315
  }
316
 
317
+ .code-title:before {
318
+ content: "⚡";
319
+ font-size: 0.8rem;
320
  }
321
 
322
+ .toggle-text {
323
+ font-size: 0.75rem;
324
+ color: #64748b;
325
+ font-weight: 500;
326
  }
327
 
328
+ .code-block {
329
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
330
+ color: #e2e8f0;
331
+ padding: 1.5rem;
332
+ font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
333
+ font-size: 0.875rem;
334
+ overflow-x: auto;
335
+ line-height: 1.6;
336
+ border-radius: 0 0 12px 12px;
337
  }
338
 
339
+ .answer-container {
340
+ background: #f8fafc;
341
+ border: 1px solid #e2e8f0;
342
+ border-radius: 8px;
343
+ padding: 1.5rem;
344
+ margin: 1rem 0;
345
  }
346
 
347
+ .answer-text {
348
+ font-size: 1.125rem;
349
+ color: #1e293b;
350
+ line-height: 1.6;
351
+ margin-bottom: 1rem;
352
  }
353
 
354
+ .answer-highlight {
355
+ background: #fef3c7;
356
+ padding: 0.125rem 0.375rem;
357
+ border-radius: 4px;
358
+ font-weight: 600;
359
+ color: #92400e;
360
  }
361
 
362
+ .context-info {
363
+ background: #f1f5f9;
364
+ border-left: 4px solid #3b82f6;
365
+ padding: 0.75rem 1rem;
366
+ margin: 1rem 0;
367
+ font-size: 0.875rem;
368
+ color: #475569;
369
  }
370
 
371
+ /* Hide default menu and footer */
372
+ #MainMenu {visibility: hidden;}
373
+ footer {visibility: hidden;}
374
+ header {visibility: hidden;}
375
+
376
+ /* Auto scroll */
377
+ .main-container {
378
+ height: 70vh;
379
+ overflow-y: auto;
380
  }
381
+ </style>
382
+ """, unsafe_allow_html=True)
383
 
384
+ # JavaScript for interactions
385
+ st.markdown("""
386
+ <script>
387
+ function scrollToBottom() {
388
+ setTimeout(function() {
389
+ const mainContainer = document.querySelector('.main-container');
390
+ if (mainContainer) {
391
+ mainContainer.scrollTop = mainContainer.scrollHeight;
392
+ }
393
+ window.scrollTo(0, document.body.scrollHeight);
394
+ }, 100);
395
  }
396
 
397
+ function toggleCode(header) {
398
+ const codeBlock = header.nextElementSibling;
399
+ const toggleText = header.querySelector('.toggle-text');
400
+
401
+ if (codeBlock.style.display === 'none') {
402
+ codeBlock.style.display = 'block';
403
+ toggleText.textContent = 'Click to collapse';
404
+ } else {
405
+ codeBlock.style.display = 'none';
406
+ toggleText.textContent = 'Click to expand';
407
+ }
408
+ }
409
+ </script>
410
+ """, unsafe_allow_html=True)
411
+
412
+ # FORCE reload environment variables
413
+ load_dotenv(override=True)
414
+
415
+ # Get API keys
416
+ Groq_Token = os.getenv("GROQ_API_KEY")
417
+ hf_token = os.getenv("HF_TOKEN")
418
+ gemini_token = os.getenv("GEMINI_TOKEN")
419
+
420
+ models = {
421
+ "gpt-oss-20b": "openai/gpt-oss-20b",
422
+ "gpt-oss-120b": "openai/gpt-oss-120b",
423
+ "llama3.1": "llama-3.1-8b-instant",
424
+ "llama3.3": "llama-3.3-70b-versatile",
425
+ "deepseek-R1": "deepseek-r1-distill-llama-70b",
426
+ "llama4 maverik":"meta-llama/llama-4-maverick-17b-128e-instruct",
427
+ "llama4 scout":"meta-llama/llama-4-scout-17b-16e-instruct",
428
+ "gemini-pro": "gemini-1.5-pro"
429
  }
430
 
431
+ self_path = os.path.dirname(os.path.abspath(__file__))
432
+
433
+ # Initialize session ID for this session
434
+ if "session_id" not in st.session_state:
435
+ st.session_state.session_id = str(uuid.uuid4())
436
+
437
+ def upload_feedback(feedback, error, output, last_prompt, code, status):
438
+ """Enhanced feedback upload function with better logging and error handling"""
439
+ try:
440
+ if not hf_token or hf_token.strip() == "":
441
+ st.warning("Cannot upload feedback - HF_TOKEN not available")
442
+ return False
443
+
444
+ # Create comprehensive feedback data
445
+ feedback_data = {
446
+ "timestamp": datetime.now().isoformat(),
447
+ "session_id": st.session_state.session_id,
448
+ "feedback_score": feedback.get("score", ""),
449
+ "feedback_comment": feedback.get("text", ""),
450
+ "user_prompt": last_prompt,
451
+ "ai_output": str(output),
452
+ "generated_code": code or "",
453
+ "error_message": error or "",
454
+ "is_image_output": status.get("is_image", False),
455
+ "success": not bool(error)
456
+ }
457
+
458
+ # Create unique folder name with timestamp
459
+ timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
460
+ random_id = str(uuid.uuid4())[:8]
461
+ folder_name = f"feedback_{timestamp_str}_{random_id}"
462
+
463
+ # Create markdown feedback file
464
+ markdown_content = f"""# VayuChat Feedback Report
465
+
466
+ ## Session Information
467
+ - **Timestamp**: {feedback_data['timestamp']}
468
+ - **Session ID**: {feedback_data['session_id']}
469
+
470
+ ## User Interaction
471
+ **Prompt**: {feedback_data['user_prompt']}
472
+
473
+ ## AI Response
474
+ **Output**: {feedback_data['ai_output']}
475
+
476
+ ## Generated Code
477
+ ```python
478
+ {feedback_data['generated_code']}
479
+ ```
480
+
481
+ ## Technical Details
482
+ - **Error Message**: {feedback_data['error_message']}
483
+ - **Is Image Output**: {feedback_data['is_image_output']}
484
+ - **Success**: {feedback_data['success']}
485
+
486
+ ## User Feedback
487
+ - **Score**: {feedback_data['feedback_score']}
488
+ - **Comments**: {feedback_data['feedback_comment']}
489
+ """
490
+
491
+ # Save markdown file locally
492
+ markdown_filename = f"{folder_name}.md"
493
+ markdown_local_path = f"/tmp/{markdown_filename}"
494
+
495
+ with open(markdown_local_path, "w", encoding="utf-8") as f:
496
+ f.write(markdown_content)
497
+
498
+ # Upload to Hugging Face
499
+ api = HfApi(token=hf_token)
500
+
501
+ # Upload markdown feedback
502
+ api.upload_file(
503
+ path_or_fileobj=markdown_local_path,
504
+ path_in_repo=f"data/{markdown_filename}",
505
+ repo_id="SustainabilityLabIITGN/VayuChat_Feedback",
506
+ repo_type="dataset",
507
+ )
508
+
509
+ # Upload image if it exists and is an image output
510
+ if status.get("is_image", False) and isinstance(output, str) and os.path.exists(output):
511
+ try:
512
+ image_filename = f"{folder_name}_plot.png"
513
+ api.upload_file(
514
+ path_or_fileobj=output,
515
+ path_in_repo=f"data/{image_filename}",
516
+ repo_id="SustainabilityLabIITGN/VayuChat_Feedback",
517
+ repo_type="dataset",
518
+ )
519
+ except Exception as img_error:
520
+ print(f"Error uploading image: {img_error}")
521
+
522
+ # Clean up local files
523
+ if os.path.exists(markdown_local_path):
524
+ os.remove(markdown_local_path)
525
+
526
+ st.success("Feedback uploaded successfully!")
527
+ return True
528
+
529
+ except Exception as e:
530
+ st.error(f"Error uploading feedback: {e}")
531
+ print(f"Feedback upload error: {e}")
532
+ return False
533
+
534
+ # Filter available models
535
+ available_models = []
536
+ model_names = list(models.keys())
537
+ groq_models = []
538
+ gemini_models = []
539
+ for model_name in model_names:
540
+ if "gemini" not in model_name:
541
+ groq_models.append(model_name)
542
+ else:
543
+ gemini_models.append(model_name)
544
+ if Groq_Token and Groq_Token.strip():
545
+ available_models.extend(groq_models)
546
+ if gemini_token and gemini_token.strip():
547
+ available_models.extend(gemini_models)
548
+
549
+ if not available_models:
550
+ st.error("No API keys available! Please set up your API keys in the .env file")
551
+ st.stop()
552
+
553
+ # Set DeepSeek-R1 as default if available
554
+ default_index = 0
555
+ if "deepseek-R1" in available_models:
556
+ default_index = available_models.index("deepseek-R1")
557
 
558
+ # Simple header - just title and model selector
559
+ col1, col2 = st.columns([3, 1])
560
+ with col1:
561
+ st.title("VayuChat")
562
+ with col2:
563
+ model_name = st.selectbox(
564
+ "Model:",
565
+ available_models,
566
+ index=default_index,
567
+ help="Choose your AI model"
568
+ )
569
 
 
 
 
 
 
 
 
 
 
 
 
570
 
571
+ # Load data with caching for better performance
572
+ @st.cache_data
573
+ def load_data():
574
+ return preprocess_and_load_df(join(self_path, "Data.csv"))
575
 
576
+ try:
577
+ df = load_data()
578
+ # Data loaded silently - no success message needed
579
+ except Exception as e:
580
+ st.error(f"Error loading data: {e}")
581
+ st.stop()
 
 
582
 
583
+ inference_server = "https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.2"
584
+ image_path = "IITGN_Logo.png"
 
 
 
585
 
586
+ # Clean sidebar
587
+ with st.sidebar:
588
+ # Quick Queries Section - moved to top
589
+ st.markdown("### Quick Queries")
590
+
591
+ # Load quick prompts with caching
592
+ @st.cache_data
593
+ def load_questions():
594
+ questions = []
595
+ questions_file = join(self_path, "questions.txt")
596
+ if os.path.exists(questions_file):
597
+ try:
598
+ with open(questions_file, 'r', encoding='utf-8') as f:
599
+ content = f.read()
600
+ questions = [q.strip() for q in content.split("\n") if q.strip()]
601
+ except Exception as e:
602
+ questions = []
603
+ return questions
604
+
605
+ questions = load_questions()
606
+
607
+ # Add default prompts if file doesn't exist or is empty
608
+ if not questions:
609
+ questions = [
610
+ "Which month had highest pollution?",
611
+ "Which city has worst air quality?",
612
+ "Show annual PM2.5 average",
613
+ "Plot monthly average PM2.5 for 2023",
614
+ "List all cities by pollution level",
615
+ "Compare winter vs summer pollution",
616
+ "Show seasonal pollution patterns",
617
+ "Which areas exceed WHO guidelines?",
618
+ "What are peak pollution hours?",
619
+ "Show PM10 vs PM2.5 comparison",
620
+ "Which station records highest variability in PM2.5?",
621
+ "Calculate pollution improvement rate year-over-year by city",
622
+ "Identify cities with PM2.5 levels consistently above 50 μg/m³ for >6 months",
623
+ "Find correlation between PM2.5 and PM10 across different seasons and cities",
624
+ "Compare weekday vs weekend levels",
625
+ "Plot yearly trend analysis",
626
+ "Show pollution distribution by city",
627
+ "Create correlation plot between pollutants"
628
+ ]
629
+
630
+ # Quick query buttons in sidebar
631
+ selected_prompt = None
632
+
633
+
634
+ for i, question in enumerate(questions[:20]): # Show more questions including policy-focused ones
635
+ # Simple left-aligned buttons without icons for cleaner look
636
+ if st.button(question, key=f"sidebar_prompt_{i}", use_container_width=True, help=f"Click to analyze: {question}"):
637
+ if question != st.session_state.get("last_selected_prompt"):
638
+ selected_prompt = question
639
+ st.session_state.last_selected_prompt = question
640
+
641
+ st.markdown("---")
642
+
643
+
644
+ # Clear Chat Button
645
+ if st.button("Clear Chat", use_container_width=True):
646
+ st.session_state.responses = []
647
+ st.session_state.processing = False
648
+ st.session_state.session_id = str(uuid.uuid4())
649
+ try:
650
+ st.rerun()
651
+ except AttributeError:
652
+ st.experimental_rerun()
653
 
654
+ # Initialize session state first
655
+ if "responses" not in st.session_state:
656
+ st.session_state.responses = []
657
+ if "processing" not in st.session_state:
658
+ st.session_state.processing = False
659
+ if "session_id" not in st.session_state:
660
+ st.session_state.session_id = str(uuid.uuid4())
661
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
 
 
 
 
 
 
663
 
 
 
 
 
 
 
 
 
664
 
665
+ def show_custom_response(response):
666
+ """Custom response display function with improved styling"""
667
+ role = response.get("role", "assistant")
668
+ content = response.get("content", "")
669
+
670
+ if role == "user":
671
+ # User message with right alignment - reduced margins
672
+ st.markdown(f"""
673
+ <div style='display: flex; justify-content: flex-end; margin: 1rem 0;'>
674
+ <div class='user-message'>
675
+ {content}
676
+ </div>
677
+ </div>
678
+ """, unsafe_allow_html=True)
679
+ elif role == "assistant":
680
+ # Check if content is an image filename - don't display the filename text
681
+ is_image_path = isinstance(content, str) and any(ext in content for ext in ['.png', '.jpg', '.jpeg'])
682
+
683
+ # Check if content is a pandas DataFrame
684
+ import pandas as pd
685
+ is_dataframe = isinstance(content, pd.DataFrame)
686
+
687
+ # Check for errors first and display them with special styling
688
+ error = response.get("error")
689
+ timestamp = response.get("timestamp", "")
690
+ timestamp_display = f" • {timestamp}" if timestamp else ""
691
+
692
+ if error:
693
+ st.markdown(f"""
694
+ <div style='display: flex; justify-content: flex-start; margin: 1rem 0;'>
695
+ <div class='assistant-message'>
696
+ <div class='assistant-info'>VayuChat{timestamp_display}</div>
697
+ <div class='error-message'>
698
+ ⚠️ <strong>Error:</strong> {error}
699
+ <br><br>
700
+ <em>💡 Try rephrasing your question or being more specific about what you'd like to analyze.</em>
701
+ </div>
702
+ </div>
703
+ </div>
704
+ """, unsafe_allow_html=True)
705
+ # Assistant message with left alignment - reduced margins
706
+ elif not is_image_path and not is_dataframe:
707
+ st.markdown(f"""
708
+ <div style='display: flex; justify-content: flex-start; margin: 1rem 0;'>
709
+ <div class='assistant-message'>
710
+ <div class='assistant-info'>VayuChat{timestamp_display}</div>
711
+ {content if isinstance(content, str) else str(content)}
712
+ </div>
713
+ </div>
714
+ """, unsafe_allow_html=True)
715
+ elif is_dataframe:
716
+ # Display DataFrame with nice formatting
717
+ st.markdown(f"""
718
+ <div style='display: flex; justify-content: flex-start; margin: 1rem 0;'>
719
+ <div class='assistant-message'>
720
+ <div class='assistant-info'>VayuChat{timestamp_display}</div>
721
+ Here are the results:
722
+ </div>
723
+ </div>
724
+ """, unsafe_allow_html=True)
725
+
726
+ # Add context info for dataframes
727
+ st.markdown("""
728
+ <div class='context-info'>
729
+ 💡 This table is interactive - click column headers to sort, or scroll to view all data.
730
+ </div>
731
+ """, unsafe_allow_html=True)
732
+
733
+ st.dataframe(content, use_container_width=True)
734
+
735
+ # Show generated code with Streamlit expander
736
+ if response.get("gen_code"):
737
+ with st.expander("📋 View Generated Code", expanded=False):
738
+ st.code(response["gen_code"], language="python")
739
+
740
+ # Try to display image if content is a file path
741
+ try:
742
+ if isinstance(content, str) and (content.endswith('.png') or content.endswith('.jpg')):
743
+ if os.path.exists(content):
744
+ # Display image without showing filename
745
+ st.image(content, use_column_width=True)
746
+ return {"is_image": True}
747
+ # Also handle case where content shows filename but we want to show image
748
+ elif isinstance(content, str) and any(ext in content for ext in ['.png', '.jpg']):
749
+ # Extract potential filename from content
750
+ import re
751
+ filename_match = re.search(r'([^/\\]+\.(?:png|jpg|jpeg))', content)
752
+ if filename_match:
753
+ filename = filename_match.group(1)
754
+ if os.path.exists(filename):
755
+ st.image(filename, use_column_width=True)
756
+ return {"is_image": True}
757
+ except:
758
+ pass
759
+
760
+ return {"is_image": False}
761
 
 
 
 
 
 
 
 
762
 
763
+ # Chat history
764
+ # Display chat history
765
+ for response_id, response in enumerate(st.session_state.responses):
766
+ status = show_custom_response(response)
767
+
768
+ # Show feedback section for assistant responses
769
+ if response["role"] == "assistant":
770
+ feedback_key = f"feedback_{int(response_id/2)}"
771
+ error = response.get("error", "")
772
+ output = response.get("content", "")
773
+ last_prompt = response.get("last_prompt", "")
774
+ code = response.get("gen_code", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
775
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
776
 
777
+ if "feedback" in st.session_state.responses[response_id]:
778
+ feedback_data = st.session_state.responses[response_id]["feedback"]
779
+ st.markdown(f"""
780
+ <div class='feedback-section'>
781
+ <strong>Your Feedback:</strong> {feedback_data.get('score', '')}
782
+ {f"- {feedback_data.get('text', '')}" if feedback_data.get('text') else ""}
783
+ </div>
784
+ """, unsafe_allow_html=True)
785
+ else:
786
+ # Simple feedback
787
+ st.markdown("**Rate this response:**")
788
+ col1, col2 = st.columns(2)
789
+ with col1:
790
+ good = st.button("👍 Good", key=f"{feedback_key}_good")
791
+ with col2:
792
+ poor = st.button("👎 Needs work", key=f"{feedback_key}_poor")
793
+
794
+ if good or poor:
795
+ if good:
796
+ thumbs = "👍 Good"
797
+ else:
798
+ thumbs = "👎 Needs work"
799
+ comments = st.text_input("Optional comment:", key=f"{feedback_key}_comments")
800
+
801
+ feedback = {"score": thumbs, "text": comments}
802
+ st.session_state.responses[response_id]["feedback"] = feedback
803
+ st.success("Thanks for your feedback!")
804
+ st.rerun()
805
 
806
+ # Chat input with better guidance
807
+ prompt = st.chat_input("💬 Ask about air quality trends, compare cities, or request visualizations...", key="main_chat")
 
 
808
 
809
+ # Handle selected prompt from quick prompts
810
+ if selected_prompt:
811
+ prompt = selected_prompt
 
 
 
 
 
 
812
 
813
+ # Handle follow-up prompts from quick action buttons
814
+ if st.session_state.get("follow_up_prompt") and not st.session_state.get("processing"):
815
+ prompt = st.session_state.follow_up_prompt
816
+ st.session_state.follow_up_prompt = None # Clear the follow-up prompt
817
 
818
+ # Handle new queries
819
+ if prompt and not st.session_state.get("processing"):
820
+ # Prevent duplicate processing
821
+ if "last_prompt" in st.session_state:
822
+ last_prompt = st.session_state["last_prompt"]
823
+ last_model_name = st.session_state.get("last_model_name", "")
824
+ if (prompt == last_prompt) and (model_name == last_model_name):
825
+ prompt = None
826
 
827
+ if prompt:
828
+ # Add user input to chat history
829
+ user_response = get_from_user(prompt)
830
+ st.session_state.responses.append(user_response)
831
+
832
+ # Set processing state
833
+ st.session_state.processing = True
834
+ st.session_state.current_model = model_name
835
+ st.session_state.current_question = prompt
836
+
837
+ # Rerun to show processing indicator
838
+ st.rerun()
839
 
840
+ # Process the question if we're in processing state
841
+ if st.session_state.get("processing"):
842
+ # Enhanced processing indicator like Claude Code
843
+ st.markdown("""
844
+ <div style='padding: 1rem; text-align: center; background: #f8fafc; border-radius: 8px; margin: 1rem 0;'>
845
+ <div style='display: flex; align-items: center; justify-content: center; gap: 0.5rem; color: #475569;'>
846
+ <div style='font-weight: 500;'>🤖 Processing with """ + str(st.session_state.get('current_model', 'Unknown')) + """</div>
847
+ <div class='dots' style='display: inline-flex; gap: 2px;'>
848
+ <div class='dot' style='width: 4px; height: 4px; background: #3b82f6; border-radius: 50%; animation: bounce 1.4s infinite ease-in-out;'></div>
849
+ <div class='dot' style='width: 4px; height: 4px; background: #3b82f6; border-radius: 50%; animation: bounce 1.4s infinite ease-in-out; animation-delay: 0.16s;'></div>
850
+ <div class='dot' style='width: 4px; height: 4px; background: #3b82f6; border-radius: 50%; animation: bounce 1.4s infinite ease-in-out; animation-delay: 0.32s;'></div>
851
+ </div>
852
+ </div>
853
+ <div style='font-size: 0.75rem; color: #6b7280; margin-top: 0.25rem;'>Analyzing data and generating response...</div>
854
+ </div>
855
+ <style>
856
+ @keyframes bounce {
857
+ 0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
858
+ 40% { transform: scale(1.2); opacity: 1; }
859
  }
860
+ </style>
861
+ """, unsafe_allow_html=True)
862
+
863
+ prompt = st.session_state.get("current_question")
864
+ model_name = st.session_state.get("current_model")
865
+
866
+ try:
867
+ response = ask_question(model_name=model_name, question=prompt)
868
+
869
+ if not isinstance(response, dict):
870
+ response = {
871
+ "role": "assistant",
872
+ "content": "Error: Invalid response format",
873
+ "gen_code": "",
874
+ "ex_code": "",
875
+ "last_prompt": prompt,
876
+ "error": "Invalid response format",
877
+ "timestamp": datetime.now().strftime("%H:%M")
878
+ }
879
+
880
+ response.setdefault("role", "assistant")
881
+ response.setdefault("content", "No content generated")
882
+ response.setdefault("gen_code", "")
883
+ response.setdefault("ex_code", "")
884
+ response.setdefault("last_prompt", prompt)
885
+ response.setdefault("error", None)
886
+ response.setdefault("timestamp", datetime.now().strftime("%H:%M"))
887
+
888
+ except Exception as e:
889
+ response = {
890
+ "role": "assistant",
891
+ "content": f"Sorry, I encountered an error: {str(e)}",
892
+ "gen_code": "",
893
+ "ex_code": "",
894
+ "last_prompt": prompt,
895
+ "error": str(e),
896
+ "timestamp": datetime.now().strftime("%H:%M")
897
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898
 
899
+ st.session_state.responses.append(response)
900
+ st.session_state["last_prompt"] = prompt
901
+ st.session_state["last_model_name"] = model_name
902
+ st.session_state.processing = False
903
+
904
+ # Clear processing state
905
+ if "current_model" in st.session_state:
906
+ del st.session_state.current_model
907
+ if "current_question" in st.session_state:
908
+ del st.session_state.current_question
909
+
910
+ st.rerun()
911
 
912
+ # Close chat container
913
+ st.markdown("</div>", unsafe_allow_html=True)
 
 
 
 
914
 
915
+ # Minimal auto-scroll - only scroll when processing
916
+ if st.session_state.get("processing"):
917
+ st.markdown("<script>scrollToBottom();</script>", unsafe_allow_html=True)
 
 
 
 
918
 
919
+ # Beautiful sidebar footer
920
+ # with st.sidebar:
921
+ # st.markdown("---")
922
+ # st.markdown("""
923
+ # <div class='contact-section'>
924
+ # <h4>📄 Paper on VayuChat</h4>
925
+ # <p>Learn more about VayuChat in our <a href='https://arxiv.org/abs/2411.12760' target='_blank'>Research Paper</a>.</p>
926
+ # </div>
927
+ # """, unsafe_allow_html=True)
928
+
929
+ # Dataset Info Section (matching mockup)
930
+ st.markdown("### Dataset Info")
931
+ st.markdown("""
932
+ <div style='background: #f1f5f9; border-radius: 8px; padding: 1rem; margin-bottom: 1rem;'>
933
+ <h4 style='margin: 0 0 0.5rem 0; color: #1e293b; font-size: 0.9rem;'>PM2.5 Air Quality Data</h4>
934
+ <p style='margin: 0; font-size: 0.75rem; color: #475569;'><strong>Time Range:</strong> 2022 - 2023</p>
935
+ <p style='margin: 0; font-size: 0.75rem; color: #475569;'><strong>Locations:</strong> 300+ cities across India</p>
936
+ <p style='margin: 0; font-size: 0.75rem; color: #475569;'><strong>Records:</strong> 100,000+ measurements</p>
937
+ </div>
938
+ """, unsafe_allow_html=True)
939
 
 
 
 
 
940
 
 
 
 
 
 
 
 
ncap_funding_data.csv DELETED
@@ -1,118 +0,0 @@
1
- S. No.,state,city,Amount released during FY 2019-20,Amount released during FY 2020-21,Amount released during FY 2021-22,Total fund released,Utilisation as on June 2022
2
- 1,Andhra Pradesh,Vijaywada,6.0,,,6.0,22.91
3
- 2,Andhra Pradesh,Guntur,0.12,0.76,1.96,2.84,22.91
4
- 3,Andhra Pradesh,Kurnool,0.06,0.76,1.36,2.18,22.91
5
- 4,Andhra Pradesh,Nellore,0.06,0.76,1.92,2.74,22.91
6
- 5,Andhra Pradesh,Visakhapatnam,0.12,,,0.12,22.91
7
- 6,Andhra Pradesh,Srikakulam,,2.0,0.49,2.49,22.91
8
- 7,Andhra Pradesh,Chitoor,,2.0,0.46,2.46,22.91
9
- 8,Andhra Pradesh,Ongole,,2.0,0.64,2.64,22.91
10
- 9,Andhra Pradesh,vizianagaram,,2.0,0.83,2.83,22.91
11
- 10,Andhra Pradesh,Eluru,,2.0,0.82,2.82,22.91
12
- 11,Andhra Pradesh,Rajahmundry,,2.0,1.13,3.13,22.91
13
- 12,Andhra Pradesh,Anantapur,,2.0,1.04,3.04,22.91
14
- 13,Andhra Pradesh,Kadapa,,1.0,0.83,1.83,22.91
15
- 14,Assam,Guwahati,0.12,5.0,,5.12,1.45
16
- 15,Assam,Nagaon,0.06,2.0,,2.06,1.45
17
- 16,Assam,Nalbari,0.06,1.0,,1.06,1.45
18
- 17,Assam,Sibsagar,0.06,2.0,,2.06,1.45
19
- 18,Assam,Silchar,0.06,2.0,,2.06,1.45
20
- 19,Bihar,Patna,10.0,,,10.0,15.2
21
- 20,Bihar,Gaya,0.1,2.0,1.9,4.0,15.2
22
- 21,Bihar,Muzaffarpur,0.1,5.0,2.5,7.6,15.2
23
- 22,Chandigarh,Chandigarh,8.28,5.0,4.61,17.89,10.83
24
- 23,Chhattisgarh,Raipur,6.0,,,6.0,2.76
25
- 24,Chhattisgarh,Durg Bhilainagar,6.0,,,6.0,2.76
26
- 25,Chhattisgarh,Korba,0.06,1.0,,1.06,2.76
27
- 26,Delhi,Delhi,,,11.25,11.25,
28
- 27,Gujarat,Surat,6.0,,,6.0,12.0
29
- 28,Gujarat,Ahmedabad,6.0,,,6.0,12.0
30
- 29,Himachal Pradesh,Baddi (Baddi&nalagarh considered twin during FY 20-21),0.06,3.0,0.2,3.26,6.35
31
- 30,Himachal Pradesh,Nalagarh,0.06,,0.06,0.12,6.35
32
- 31,Himachal Pradesh,Paonta Sahib,0.06,1.0,0.1,1.16,6.35
33
- 32,Himachal Pradesh,Sunder Nagar,0.06,1.0,0.08,1.14,6.35
34
- 33,Himachal Pradesh,Kala Amb,,3.0,0.0,3.0,6.35
35
- 34,Himachal Pradesh,Damtal,,1.0,0.01,1.01,6.35
36
- 35,Himachal Pradesh,Parwanoo,,1.0,0.03,1.03,6.35
37
- 36,Jammu & Kashmir,Jammu,0.12,3.0,4.89,8.01,0.12
38
- 37,Jammu & Kashmir,Srinagar,,5.0,7.95,12.95,0.12
39
- 38,Jharkhand,Dhanbad,6.0,,,6.0,3.0
40
- 39,Karnataka,Bangalore,6.0,,,6.0,7.39
41
- 40,Karnataka,Gulburga,0.12,0.38,2.24,2.74,7.39
42
- 41,Karnataka,Hubli-Dharwad,0.12,0.38,3.68,4.18,7.39
43
- 42,Karnataka,Devangere,0.06,0.76,1.4,2.22,7.39
44
- 43,Madhya Pradesh,Bhopal,10.0,,,10.0,20.96
45
- 44,Madhya Pradesh,Gwalior,10.0,,,10.0,20.96
46
- 45,Madhya Pradesh,Indore,0.2,,,0.2,20.96
47
- 46,Madhya Pradesh,Ujjain,0.2,0.38,2.33,2.91,20.96
48
- 47,Madhya Pradesh,Sagar,0.1,0.76,1.36,2.22,20.96
49
- 48,Madhya Pradesh,Dewas,0.1,0.38,1.33,1.81,20.96
50
- 49,Maharashtra,Mumbai,9.5,,,9.5,29.92
51
- 50,Maharashtra,Nagpur,9.45,,,9.45,29.92
52
- 51,Maharashtra,Navi Mumbai,9.45,,,9.45,29.92
53
- 52,Maharashtra,Pune,9.45,,,9.45,29.92
54
- 53,Maharashtra,Amravati,0.2,1.14,2.91,4.25,29.92
55
- 54,Maharashtra,Aurangabad,0.2,,,0.2,29.92
56
- 55,Maharashtra,Nashik,0.2,,,0.2,29.92
57
- 56,Maharashtra,Kolhapur,0.2,0.76,,0.96,29.92
58
- 57,Maharashtra,Sangli,0.2,0.76,1.72,2.68,29.92
59
- 58,Maharashtra,Solapur,0.2,0.38,4.2,4.78,29.92
60
- 59,Maharashtra,Ulhasnagar,0.2,1.9,,2.1,29.92
61
- 60,Maharashtra,Akola,0.1,1.14,1.47,2.71,29.92
62
- 61,Maharashtra,Badlapur,0.1,1.9,,2.0,29.92
63
- 62,Maharashtra,Chandrapur,0.1,1.14,,1.24,29.92
64
- 63,Maharashtra,Jalgaon,0.1,0.76,,0.86,29.92
65
- 64,Maharashtra,Jalna,0.1,1.14,,1.24,29.92
66
- 65,Maharashtra,Latur,0.1,0.38,1.6,2.08,29.92
67
- 66,Meghalaya,Byrnihat,,3.0,0.0,3.0,1.97
68
- 67,Nagaland,Dimapur,0.06,3.0,0.53,3.59,6.12
69
- 68,Nagaland,Kohima,0.06,3.0,0.4,3.46,6.12
70
- 69,Odisha,Twin City Bhubaneshwar & Cuttack,6.0,,,6.0,3.62
71
- 70,Odisha,Balasore,0.06,0.76,,0.82,3.62
72
- 71,Odisha,Rourkela,0.06,1.14,,1.2,3.62
73
- 72,Odisha,Angul,0.06,1.14,,1.2,3.62
74
- 73,Odisha,Kalinga Nagar,,3.0,,3.0,3.62
75
- 74,Odisha,Talcher,,,0.22,0.22,3.62
76
- 75,Odisha,Cuttack,,,3.42,3.42,3.62
77
- 76,Punjab,Ludhiana,6.0,,,6.0,3.02
78
- 77,Punjab,Amritsar,6.0,,,6.0,3.02
79
- 78,Punjab,Jalandhar,0.12,4.0,,4.12,3.02
80
- 79,Punjab,Khanna,0.06,1.9,,1.96,3.02
81
- 80,Punjab,Gobindgarh,0.06,3.0,,3.06,3.02
82
- 81,Punjab,NayaNangal,0.06,1.0,,1.06,3.02
83
- 82,Punjab,Dera Baba Nanak,0.06,0.76,,0.82,3.02
84
- 83,Punjab,Patiala,0.06,4.0,,4.06,3.02
85
- 84,Punjab,DeraBassi,0.06,0.38,,0.44,3.02
86
- 85,Rajasthan,Jaipur,6.0,,,6.0,12.55
87
- 86,Rajasthan,Jodhpur,6.0,,,6.0,12.55
88
- 87,Rajasthan,Kota,6.0,,,6.0,12.55
89
- 88,Rajasthan,Alwar,0.06,1.9,,1.96,12.55
90
- 89,Rajasthan,Udaipur,0.06,1.9,,1.96,12.55
91
- 90,Tamil Nadu,Tuticorin,0.06,3.0,,3.06,
92
- 91,Telangana,Hyderabad,10.8,,,10.8,9.72
93
- 92,Telangana,Nalgonda,0.1,0.38,0.47,0.95,9.72
94
- 93,Telangana,Patencheru,0.1,0.38,,0.48,9.72
95
- 94,Telangana,Sangareddy,,2.0,0.32,2.32,9.72
96
- 95,Uttar Pradesh,Agra,9.45,,,9.45,30.57
97
- 96,Uttar Pradesh,Allahabad,9.45,,,9.45,30.57
98
- 97,Uttar Pradesh,Kanpur,9.45,,,9.45,30.57
99
- 98,Uttar Pradesh,Lucknow,9.45,,,9.45,30.57
100
- 99,Uttar Pradesh,Varanasi,9.47,,,9.47,30.57
101
- 100,Uttar Pradesh,Moradabad,0.2,1.9,,2.1,30.57
102
- 101,Uttar Pradesh,Bareily,0.2,1.9,,2.1,30.57
103
- 102,Uttar Pradesh,Firozabad,0.2,1.9,,2.1,30.57
104
- 103,Uttar Pradesh,Jhansi,0.2,1.14,,1.34,30.57
105
- 104,Uttar Pradesh,Khurja,0.1,1.9,,2.0,30.57
106
- 105,Uttar Pradesh,Anpara,0.1,1.14,,1.24,30.57
107
- 106,Uttar Pradesh,Gajraula,0.1,1.14,,1.24,30.57
108
- 107,Uttar Pradesh,Raebareli,0.1,1.14,,1.24,30.57
109
- 108,Uttar Pradesh,Gorakhpur,,,9.64,9.64,30.57
110
- 109,Uttar Pradesh,Noida,,,6.67,6.67,30.57
111
- 110,Uttarakhand,Kashipur,0.06,3.0,0.79,3.85,8.15
112
- 111,Uttarakhand,Rishikesh,0.06,5.0,,5.06,8.15
113
- 112,Uttarakhand,Dehradun,,3.0,4.88,7.88,8.15
114
- 113,West Bengal,Kolkata,6.0,,,6.0,19.0
115
- 114,West Bengal,Howrah,,5.0,,5.0,19.0
116
- 115,West Bengal,Haldia,,3.0,,3.0,19.0
117
- 116,West Bengal,Durgapur,,3.0,,3.0,19.0
118
- 117,West Bengal,Barrackpore,,2.0,,2.0,19.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new_system_prompt.txt DELETED
@@ -1,65 +0,0 @@
1
- Generate Python code to answer the user's question about air quality data.
2
-
3
- SCOPE VALIDATION (MANDATORY FIRST STEP):
4
- - ONLY answer questions about: air quality, pollution (PM2.5, PM10, NO2, ozone, etc.), meteorology (wind, temperature, humidity), NCAP funding, Indian cities/states environmental data
5
- - If question is NOT about air quality/pollution/environmental data, generate ONLY this code:
6
- answer = "I can only help with air quality and pollution data analysis. Please ask about PM2.5, pollution trends, city comparisons, meteorological factors, or NCAP funding."
7
- - Examples of REJECTED topics: general Python coding, politics, personal questions, unrelated data analysis
8
- - For rejected questions: write only the answer assignment - no other code needed
9
-
10
- CRITICAL: Only generate Python code - no explanations, no thinking, just clean executable code.
11
-
12
- OUTPUT TYPES (store result in 'answer' variable):
13
- 1. PLOTS: For visualization questions → save plot and store filename: answer = filename
14
- 2. TEXT: For simple questions → store direct string: answer = "The highest PM2.5 city is Delhi"
15
- 3. DATAFRAMES: For rankings/lists → store DataFrame: answer = result_df
16
-
17
- AVAILABLE LIBRARIES:
18
- - pandas, numpy (data manipulation)
19
- - matplotlib, seaborn, plotly (visualization)
20
- - statsmodels, scikit-learn (analysis)
21
- - geopandas (geospatial analysis)
22
-
23
- IMPORT REQUIREMENTS:
24
- - Always import what you use: import seaborn as sns, import numpy as np
25
- - Standard imports are already available: pandas as pd, matplotlib.pyplot as plt
26
-
27
- ESSENTIAL RULES:
28
-
29
- DATA SAFETY:
30
- - Always check if data exists: if df.empty: answer = "No data available"
31
- - For city-specific questions: filter first: df_city = df[df['City'].str.contains('CityName', case=False)]
32
- - Check sufficient data: if len(df_filtered) < 10: answer = "Insufficient data"
33
- - Use .dropna() to remove missing values before analysis
34
-
35
- PLOTTING REQUIREMENTS:
36
- - Create plots for visualization requests: fig, ax = plt.subplots(figsize=(9, 6))
37
- - Save plots with ULTRA high resolution: filename = f"plot_{uuid.uuid4().hex[:8]}.png"; plt.savefig(filename, dpi=1200, bbox_inches='tight', facecolor='white', edgecolor='none')
38
- - Close plots: plt.close()
39
- - Store filename: answer = filename
40
- - For non-plots: answer = "text result"
41
-
42
- BASIC ERROR PREVENTION:
43
- - Use try/except for complex operations
44
- - Validate results: if pd.isna(result): answer = "Analysis inconclusive"
45
- - For correlations: check len(data) > 20 before calculating
46
- - Use simple matplotlib plotting - avoid complex visualizations
47
-
48
- PLOTTING BEST PRACTICES:
49
- - Check data exists in each category before plotting
50
- - For comparisons (>, <): ensure both categories have data
51
- - Example: high_wind = df[df['WS'] > 3]; low_wind = df[df['WS'] <= 3]
52
- - If category is empty: create simple bar chart instead of box plots
53
- - Add data count labels: plt.text() to show sample sizes
54
-
55
- TECHNICAL REQUIREMENTS:
56
- - Save final result in variable called 'answer'
57
- - Use exact column names: 'PM2.5 (µg/m³)', 'WS (m/s)', etc.
58
- - Handle dates with pd.to_datetime() if needed
59
- - Round numerical results: round(value, 2)
60
-
61
- MANDATORY: ALWAYS END CODE WITH ANSWER ASSIGNMENT
62
- - Every code block MUST end with: answer = [result]
63
- - If analysis fails: answer = "Unable to complete analysis with available data"
64
- - If plotting fails: answer = "Unable to generate visualization"
65
- - NEVER leave answer variable unset - this will cause system failure
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
questions.txt CHANGED
@@ -1,30 +1,28 @@
1
- Which city has the highest average PM2.5 levels in 2023?
2
- Show monthly PM2.5 trends for Delhi in 2023
3
- Compare PM2.5 levels between winter and summer months
4
- Which month had the highest pollution levels in Mumbai?
5
- Calculate the average PM2.5 for all cities in November 2023
6
- Rank top 10 cities by the highest PM2.5 pollution levels
7
- Show seasonal pollution patterns across all cities
8
- Compare all pollutant levels between weekdays and weekends
9
- Which cities exceed the WHO PM2.5 guidelines of 15 µg/m³?
10
- Plot yearly PM2.5 trends from 2020 to 2023 for major cities
11
- How much NCAP funding did Delhi receive vs Mumbai?
12
- Which NCAP cities achieved the best PM2.5 reduction?
13
- Does wind speed above 3 m/s reduce PM2.5 levels in Delhi?
14
- Show the correlation between temperature and PM2.5 in the summer months
15
- Which states in India with populations above 1 million have average PM2.5 concentrations exceeding 60 µg/m³?
16
- Compare PM2.5 levels in high-funded vs low-funded NCAP cities
17
- Does rainfall help reduce pollution levels during the monsoon?
18
- Which meteorological factor correlates most with PM2.5 reduction?
19
- Show monthly PM2.5 trends for the top 5 Indian states by population
20
- Does humidity above 80% help reduce pollution in coastal cities?
21
- Compare NO2 vs PM2.5 levels in traffic-heavy areas
22
- Which NCAP-funded cities still exceed WHO guidelines?
23
- Show relationship between city population and average PM2.5
24
- Compare PM2.5 improvement rates: Delhi vs Mumbai vs Kolkata
25
- Create simple scatter plot of PM2.5 vs PM10 correlation
26
- Show state-wise average PM2.5 levels for policy planning
27
- Which cities need immediate intervention with PM2.5 above 60 µg/m³?
28
- Compare pollution trends between North vs South Indian cities
29
- Show seasonal variation in PM2.5 across different climate zones
30
- Identify cities with consistent pollution improvement over time
 
1
+ Which month in 2023 had the highest average PM2.5 pollution levels?
2
+ Which city has the worst air quality based on average PM2.5 levels?
3
+ Calculate the overall annual average PM2.5 concentration across all cities in 2023
4
+ Plot monthly average PM2.5 trends for 2023 across all cities
5
+ Rank all cities by average PM2.5 levels from highest to lowest pollution
6
+ Compare average PM2.5 levels: winter months (Dec-Feb) vs summer months (Apr-Jun)
7
+ Show seasonal PM2.5 patterns: which season has highest pollution levels?
8
+ Which cities have annual average PM2.5 exceeding WHO guideline of 15 μg/m³?
9
+ Identify the top 10 most polluted cities based on PM2.5 in 2023
10
+ Compare PM10 vs PM2.5 correlation strength across different cities
11
+ Which station in Ahmedabad shows highest PM2.5 variability in winter 2023?
12
+ Calculate PM2.5 improvement rate from 2022 to 2023 for Mumbai vs Delhi
13
+ Identify Gujarat cities with PM2.5 >50 μg/m³ for 6+ consecutive months in 2023
14
+ Compare PM2.5 vs PM10 correlation: winter vs summer across top 5 polluted cities
15
+ Compare average pollution levels: weekdays (Mon-Fri) vs weekends (Sat-Sun)
16
+ Plot year-over-year PM2.5 trend from 2022 to 2023 for major cities
17
+ Show PM2.5 distribution histogram across all cities and time periods
18
+ Create scatter plot showing PM2.5 vs PM10 correlation with trend line
19
+ Which cities need immediate emergency intervention with PM2.5 >100 μg/m³?
20
+ Identify cities showing consistent pollution improvement for policy replication
21
+ Which months require targeted interventions based on highest pollution spikes?
22
+ Compare pollution levels between industrial vs non-industrial cities for policy focus
23
+ Rank states by average air quality to prioritize national-level resource allocation
24
+ Which cities have PM2.5 levels in 'hazardous' category (>250 μg/m³) requiring urgent action?
25
+ Calculate population-weighted pollution exposure to identify areas affecting most people
26
+ Show cities with worsening pollution trends that need immediate policy intervention
27
+ Compare pollution reduction success: which cities improved most and how?
28
+ Identify seasonal emergency periods when public health advisories should be issued
 
 
src.py CHANGED
@@ -20,24 +20,19 @@ hf_token = os.getenv("HF_TOKEN")
20
  gemini_token = os.getenv("GEMINI_TOKEN")
21
 
22
  # Debug print (remove in production)
23
- # print(f"Debug - Groq Token: {'Present' if Groq_Token else 'Missing'}")
24
- # print(f"Debug - Groq Token Value: {Groq_Token[:10] + '...' if Groq_Token else 'None'}")
25
- # print(f"Debug - Gemini Token: {'Present' if gemini_token else 'Missing'}")
26
 
27
  models = {
28
- "gpt-oss-120b": "openai/gpt-oss-120b",
29
- "qwen3-32b": "qwen/qwen3-32b",
30
  "gpt-oss-20b": "openai/gpt-oss-20b",
31
- "llama4 maverik":"meta-llama/llama-4-maverick-17b-128e-instruct",
 
32
  "llama3.3": "llama-3.3-70b-versatile",
33
  "deepseek-R1": "deepseek-r1-distill-llama-70b",
34
- "gemini-2.5-flash": "gemini-2.5-flash",
35
- "gemini-2.5-pro": "gemini-2.5-pro",
36
- "gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
37
- "gemini-2.0-flash": "gemini-2.0-flash",
38
- "gemini-2.0-flash-lite": "gemini-2.0-flash-lite",
39
- # "llama4 scout":"meta-llama/llama-4-scout-17b-16e-instruct"
40
- # "llama3.1": "llama-3.1-8b-instant"
41
  }
42
 
43
  def log_interaction(user_query, model_name, response_content, generated_code, execution_time, error_message=None, is_image=False):
@@ -101,284 +96,461 @@ def preprocess_and_load_df(path: str) -> pd.DataFrame:
101
  raise Exception(f"Error loading dataframe: {e}")
102
 
103
 
 
104
  def get_from_user(prompt):
105
  """Format user prompt"""
106
  return {"role": "user", "content": prompt}
107
 
108
 
 
 
109
  def ask_question(model_name, question):
110
  """Ask question with comprehensive error handling and logging"""
111
  start_time = datetime.now()
112
- # ------------------------
113
- # Helper functions
114
- # ------------------------
115
- def make_error_response(msg, log_msg, content=None):
116
- """Build error response + log it"""
117
- execution_time = (datetime.now() - start_time).total_seconds()
118
- log_interaction(
119
- user_query=question,
120
- model_name=model_name,
121
- response_content=content or msg,
122
- generated_code="",
123
- execution_time=execution_time,
124
- error_message=log_msg,
125
- is_image=False
126
- )
127
- return {
128
- "role": "assistant",
129
- "content": content or msg,
130
- "gen_code": "",
131
- "ex_code": "",
132
- "last_prompt": question,
133
- "error": log_msg
134
- }
135
- def validate_api_token(token, token_name, msg_if_missing):
136
- """Check for missing/empty API tokens"""
137
- if not token or token.strip() == "":
138
- return make_error_response(
139
- msg="Missing or empty API token",
140
- log_msg="Missing or empty API token",
141
- content=msg_if_missing
142
- )
143
- return None # OK
144
- def run_safe_exec(full_code, df=None, extra_globals=None):
145
- """Safely execute generated code and handle errors"""
146
- local_vars = {}
147
 
148
- # Force matplotlib to use ULTRA high resolution settings in exec environment
149
- # Skip style file and set everything manually to ensure it works
150
- plt.rcParams['figure.dpi'] = 1200
151
- plt.rcParams['savefig.dpi'] = 1200
152
- plt.rcParams['figure.figsize'] = [9, 6]
153
- plt.rcParams['figure.facecolor'] = 'white'
154
- plt.rcParams['savefig.facecolor'] = 'white'
155
- plt.rcParams['savefig.bbox'] = 'tight'
156
- plt.rcParams['font.size'] = 11
157
- plt.rcParams['axes.titlesize'] = 14
158
- plt.rcParams['axes.labelsize'] = 12
159
- plt.rcParams['xtick.labelsize'] = 10
160
- plt.rcParams['ytick.labelsize'] = 10
161
- plt.rcParams['legend.fontsize'] = 10
162
 
163
- global_vars = {
164
- 'pd': pd, 'plt': plt, 'os': os,
165
- 'sns': __import__('seaborn'),
166
- 'uuid': __import__('uuid'),
167
- 'calendar': __import__('calendar'),
168
- 'np': __import__('numpy'),
169
- 'df': df, # <-- pass your DataFrame here
170
- 'st': __import__('streamlit') # Add streamlit for st.pyplot()
171
- }
172
-
173
- # allow user to inject more globals (optional)
174
- if extra_globals:
175
- global_vars.update(extra_globals)
176
-
177
- try:
178
- exec(full_code, global_vars, local_vars)
179
- return (
180
- local_vars.get('answer', "Code executed but no result was saved in 'answer' variable"),
181
- None
182
- )
183
- except Exception as code_error:
184
- return None, str(code_error)
185
-
186
- # ------------------------
187
- # Step 1: Reload env vars
188
- # ------------------------
189
- load_dotenv(override=True)
190
- fresh_groq_token = os.getenv("GROQ_API_KEY")
191
- fresh_gemini_token = os.getenv("GEMINI_TOKEN")
192
- # ------------------------
193
- # Step 2: Init LLM
194
- # ------------------------
195
- try:
196
- if "gemini" in model_name:
197
- token_error = validate_api_token(
198
- fresh_gemini_token,
199
- "GEMINI_TOKEN",
200
- "Gemini API token not available or empty. Please set GEMINI_TOKEN in your environment variable."
201
  )
202
- if token_error:
203
- return token_error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
 
205
  try:
206
- llm = ChatGoogleGenerativeAI(
207
- model=models[model_name],
208
- google_api_key=fresh_gemini_token,
209
- temperature=0
210
  )
211
- # Gemini requires async call
212
- llm.invoke("Test")
213
- # print("Gemini API key test successful")
214
  except Exception as api_error:
215
- return make_error_response(
216
- msg="API Connection Error",
217
- log_msg=str(api_error),
218
- content="API Key Error: Your Gemini API key appears to be invalid, expired, or restricted. Please check your GEMINI_TOKEN in the .env file."
219
- if "organization_restricted"in str(api_error).lower() or "unauthorized" in str(api_error).lower()
220
- else f"API Connection Error: {api_error}"
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  )
 
 
 
 
 
 
 
 
 
222
 
223
- else:
224
- token_error = validate_api_token(
225
- fresh_groq_token,
226
- "GROQ_API_KEY",
227
- "Groq API token not available or empty. Please set GROQ_API_KEY in your environment variables and restart the application."
 
 
 
 
 
 
 
 
 
228
  )
229
- if token_error:
230
- return token_error
 
 
 
 
 
 
 
231
 
232
- try:
233
- llm = ChatGroq(
234
- model=models[model_name],
235
- api_key=fresh_groq_token,
236
- temperature=0
237
- )
238
- llm.invoke("Test") # test API key
239
- # print("Groq API key test successful")
240
- except Exception as api_error:
241
- return make_error_response(
242
- msg="API Connection Error",
243
- log_msg=str(api_error),
244
- content="API Key Error: Your Groq API key appears to be invalid, expired, or restricted. Please check your GROQ_API_KEY in the .env file."
245
- if "organization_restricted"in str(api_error).lower() or "unauthorized" in str(api_error).lower()
246
- else f"API Connection Error: {api_error}"
247
- )
248
- except Exception as e:
249
- return make_error_response(str(e), str(e))
250
- # ------------------------
251
- # Step 3: Check AQ_met_data.csv
252
- # ------------------------
253
- if not os.path.exists("AQ_met_data.csv"):
254
- return make_error_response(
255
- msg="Data file not found",
256
- log_msg="Data file not found",
257
- content="AQ_met_data.csv file not found. Please ensure the data file is in the correct location."
258
- )
259
 
260
- df = pd.read_csv("AQ_met_data.csv")
261
- df["Timestamp"] = pd.to_datetime(df["Timestamp"])
262
- new_line = "\n"
263
- states_df = pd.read_csv("states_data.csv")
264
- ncap_df = pd.read_csv("ncap_funding_data.csv")
265
-
266
- # Template for user query
267
- template = f"""```python
268
  import pandas as pd
269
  import matplotlib.pyplot as plt
270
- import seaborn as sns
271
- import streamlit as st
272
  import uuid
273
  import calendar
274
  import numpy as np
275
- # Set professional matplotlib styling with high resolution
276
- plt.style.use('vayuchat.mplstyle')
277
- df = pd.read_csv("AQ_met_data.csv")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  df["Timestamp"] = pd.to_datetime(df["Timestamp"])
279
- states_df = pd.read_csv("states_data.csv")
280
- ncap_df = pd.read_csv("ncap_funding_data.csv")
281
- # df is pandas DataFrame with air quality data from India. Data frequency is daily from 2017 to 2024. The data has the following columns and data types:
282
- {new_line.join(map(lambda x: '# '+x, str(df.dtypes).split(new_line)))}
283
- # states_df is a pandas DataFrame of state-wise population, area and whether state is union territory or not of India.
284
- {new_line.join(map(lambda x: '# '+x, str(states_df.dtypes).split(new_line)))}
285
- # ncap_df is a pandas DataFrame of funding given to the cities of India from 2019-2022, under The National Clean Air Program (NCAP).
286
- {new_line.join(map(lambda x: '# '+x, str(ncap_df.dtypes).split(new_line)))}
287
  # Question: {question.strip()}
288
  # Generate code to answer the question and save result in 'answer' variable
289
  # If creating a plot, save it with a unique filename and store the filename in 'answer'
290
  # If returning text/numbers, store the result directly in 'answer'
291
  ```"""
292
 
293
- # Read system prompt from txt file
294
- with open("new_system_prompt.txt", "r", encoding="utf-8") as f:
295
- system_prompt = f.read().strip()
296
-
297
- messages = [
298
- {
299
- "role": "system",
300
- "content": system_prompt
301
- },
302
- {
303
- "role": "user",
304
- "content": f"""Complete the following code to answer the user's question:
305
- {template}"""
306
- }
307
- ]
308
-
309
- # ------------------------
310
- # Step 4: Call model
311
- # ------------------------
312
- try:
313
- response = llm.invoke(messages)
314
- answer = response.content
315
- except Exception as e:
316
- return make_error_response(f"Error: {e}", str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
- # ------------------------
319
- # Step 5: Extract code
320
- # ------------------------
321
- code_part = answer.split("```python")[1].split("```")[0] if "```python" in answer else answer
322
- full_code = f"""
323
  {template.split("```python")[1].split("```")[0]}
324
  {code_part}
325
  """
326
- answer_result, code_error = run_safe_exec(full_code, df, extra_globals={'states_df': states_df, 'ncap_df': ncap_df})
327
-
328
- execution_time = (datetime.now() - start_time).total_seconds()
329
- if code_error:
330
- # Friendly error messages
331
- msg = "I encountered an error while analyzing your data. "
332
- if "syntax" in code_error.lower():
333
- msg += "There was a syntax error in the generated code. Please try rephrasing your question."
334
- elif "not defined" in code_error.lower():
335
- msg += "Variable naming error occurred. Please try asking the question again."
336
- elif "division by zero" in code_error.lower():
337
- msg += "Calculation involved division by zero, possibly due to missing data."
338
- elif "no data" in code_error.lower() or "empty" in code_error.lower():
339
- msg += "No relevant data was found for your query."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  else:
341
- msg += f"Technical error: {code_error}"
342
-
343
- msg += "\n\n💡 **Suggestions:**\n- Try rephrasing your question\n- Use simpler terms\n- Check if the data exists for your specified criteria"
344
-
345
  log_interaction(
346
  user_query=question,
347
  model_name=model_name,
348
- response_content=msg,
349
- generated_code=full_code,
350
  execution_time=execution_time,
351
- error_message=code_error,
352
  is_image=False
353
  )
 
354
  return {
355
- "role": "assistant",
356
- "content": msg,
357
- "gen_code": full_code,
358
- "ex_code": full_code,
359
  "last_prompt": question,
360
- "error": code_error
361
- }
362
-
363
- # ------------------------
364
- # Step 7: Success logging
365
- # ------------------------
366
- is_image = isinstance(answer_result, str) and answer_result.endswith(('.png', '.jpg', '.jpeg'))
367
- log_interaction(
368
- user_query=question,
369
- model_name=model_name,
370
- response_content=str(answer_result),
371
- generated_code=full_code,
372
- execution_time=execution_time,
373
- error_message=None,
374
- is_image=is_image
375
- )
376
-
377
- return {
378
- "role": "assistant",
379
- "content": answer_result,
380
- "gen_code": full_code,
381
- "ex_code": full_code,
382
- "last_prompt": question,
383
- "error": None
384
- }
 
20
  gemini_token = os.getenv("GEMINI_TOKEN")
21
 
22
  # Debug print (remove in production)
23
+ print(f"Debug - Groq Token: {'Present' if Groq_Token else 'Missing'}")
24
+ print(f"Debug - Groq Token Value: {Groq_Token[:10] + '...' if Groq_Token else 'None'}")
25
+ print(f"Debug - Gemini Token: {'Present' if gemini_token else 'Missing'}")
26
 
27
  models = {
 
 
28
  "gpt-oss-20b": "openai/gpt-oss-20b",
29
+ "gpt-oss-120b": "openai/gpt-oss-120b",
30
+ "llama3.1": "llama-3.1-8b-instant",
31
  "llama3.3": "llama-3.3-70b-versatile",
32
  "deepseek-R1": "deepseek-r1-distill-llama-70b",
33
+ "llama4 maverik":"meta-llama/llama-4-maverick-17b-128e-instruct",
34
+ "llama4 scout":"meta-llama/llama-4-scout-17b-16e-instruct",
35
+ "gemini-pro": "gemini-1.5-pro"
 
 
 
 
36
  }
37
 
38
  def log_interaction(user_query, model_name, response_content, generated_code, execution_time, error_message=None, is_image=False):
 
96
  raise Exception(f"Error loading dataframe: {e}")
97
 
98
 
99
+
100
  def get_from_user(prompt):
101
  """Format user prompt"""
102
  return {"role": "user", "content": prompt}
103
 
104
 
105
+
106
+
107
  def ask_question(model_name, question):
108
  """Ask question with comprehensive error handling and logging"""
109
  start_time = datetime.now()
110
+ try:
111
+ # Reload environment variables to get fresh values
112
+ load_dotenv(override=True)
113
+ fresh_groq_token = os.getenv("GROQ_API_KEY")
114
+ fresh_gemini_token = os.getenv("GEMINI_TOKEN")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
+ print(f"ask_question - Fresh Groq Token: {'Present' if fresh_groq_token else 'Missing'}")
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
+ # Check API availability with fresh tokens
119
+ if model_name == "gemini-pro":
120
+ if not fresh_gemini_token or fresh_gemini_token.strip() == "":
121
+ execution_time = (datetime.now() - start_time).total_seconds()
122
+ error_msg = "Missing or empty API token"
123
+
124
+ # Log the failed interaction
125
+ log_interaction(
126
+ user_query=question,
127
+ model_name=model_name,
128
+ response_content="Gemini API token not available or empty",
129
+ generated_code="",
130
+ execution_time=execution_time,
131
+ error_message=error_msg,
132
+ is_image=False
133
+ )
134
+
135
+ return {
136
+ "role": "assistant",
137
+ "content": "Gemini API token not available or empty. Please set GEMINI_TOKEN in your environment variables.",
138
+ "gen_code": "",
139
+ "ex_code": "",
140
+ "last_prompt": question,
141
+ "error": error_msg
142
+ }
143
+ llm = ChatGoogleGenerativeAI(
144
+ model=models[model_name],
145
+ google_api_key=fresh_gemini_token,
146
+ temperature=0
 
 
 
 
 
 
 
 
 
147
  )
148
+ else:
149
+ if not fresh_groq_token or fresh_groq_token.strip() == "":
150
+ execution_time = (datetime.now() - start_time).total_seconds()
151
+ error_msg = "Missing or empty API token"
152
+
153
+ # Log the failed interaction
154
+ log_interaction(
155
+ user_query=question,
156
+ model_name=model_name,
157
+ response_content="Groq API token not available or empty",
158
+ generated_code="",
159
+ execution_time=execution_time,
160
+ error_message=error_msg,
161
+ is_image=False
162
+ )
163
+
164
+ return {
165
+ "role": "assistant",
166
+ "content": "Groq API token not available or empty. Please set GROQ_API_KEY in your environment variables and restart the application.",
167
+ "gen_code": "",
168
+ "ex_code": "",
169
+ "last_prompt": question,
170
+ "error": error_msg
171
+ }
172
 
173
+ # Test the API key by trying to create the client
174
  try:
175
+ llm = ChatGroq(
176
+ model=models[model_name],
177
+ api_key=fresh_groq_token,
178
+ temperature=0.1
179
  )
180
+ # Test with a simple call to verify the API key works
181
+ test_response = llm.invoke("Test")
182
+ print("API key test successful")
183
  except Exception as api_error:
184
+ execution_time = (datetime.now() - start_time).total_seconds()
185
+ error_msg = str(api_error)
186
+
187
+ if "organization_restricted" in error_msg.lower() or "unauthorized" in error_msg.lower():
188
+ response_content = "API Key Error: Your Groq API key appears to be invalid, expired, or restricted. Please check your API key in the .env file."
189
+ log_error_msg = f"API key validation failed: {error_msg}"
190
+ else:
191
+ response_content = f"API Connection Error: {error_msg}"
192
+ log_error_msg = error_msg
193
+
194
+ # Log the failed interaction
195
+ log_interaction(
196
+ user_query=question,
197
+ model_name=model_name,
198
+ response_content=response_content,
199
+ generated_code="",
200
+ execution_time=execution_time,
201
+ error_message=log_error_msg,
202
+ is_image=False
203
  )
204
+
205
+ return {
206
+ "role": "assistant",
207
+ "content": response_content,
208
+ "gen_code": "",
209
+ "ex_code": "",
210
+ "last_prompt": question,
211
+ "error": log_error_msg
212
+ }
213
 
214
+ # Check if data file exists
215
+ if not os.path.exists("Data.csv"):
216
+ execution_time = (datetime.now() - start_time).total_seconds()
217
+ error_msg = "Data file not found"
218
+
219
+ # Log the failed interaction
220
+ log_interaction(
221
+ user_query=question,
222
+ model_name=model_name,
223
+ response_content="Data.csv file not found",
224
+ generated_code="",
225
+ execution_time=execution_time,
226
+ error_message=error_msg,
227
+ is_image=False
228
  )
229
+
230
+ return {
231
+ "role": "assistant",
232
+ "content": "Data.csv file not found. Please ensure the data file is in the correct location.",
233
+ "gen_code": "",
234
+ "ex_code": "",
235
+ "last_prompt": question,
236
+ "error": error_msg
237
+ }
238
 
239
+ df_check = pd.read_csv("Data.csv")
240
+ df_check["Timestamp"] = pd.to_datetime(df_check["Timestamp"])
241
+ df_check = df_check.head(5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
+ new_line = "\n"
244
+
245
+ template = f"""```python
 
 
 
 
 
246
  import pandas as pd
247
  import matplotlib.pyplot as plt
 
 
248
  import uuid
249
  import calendar
250
  import numpy as np
251
+
252
+ # Set professional matplotlib styling
253
+ plt.rcParams.update({{
254
+ 'font.size': 12,
255
+ 'figure.dpi': 400,
256
+ 'figure.facecolor': 'white',
257
+ 'axes.facecolor': 'white',
258
+ 'axes.edgecolor': '#e2e8f0',
259
+ 'axes.linewidth': 1.2,
260
+ 'axes.labelcolor': '#374151',
261
+ 'axes.spines.top': False,
262
+ 'axes.spines.right': False,
263
+ 'axes.spines.left': True,
264
+ 'axes.spines.bottom': True,
265
+ 'axes.grid': True,
266
+ 'grid.color': '#f1f5f9',
267
+ 'grid.linewidth': 0.8,
268
+ 'grid.alpha': 0.7,
269
+ 'xtick.color': '#6b7280',
270
+ 'ytick.color': '#6b7280',
271
+ 'text.color': '#374151',
272
+ 'figure.figsize': [12, 6],
273
+ 'axes.prop_cycle': plt.cycler('color', ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#06b6d4'])
274
+ }})
275
+
276
+ df = pd.read_csv("Data.csv")
277
  df["Timestamp"] = pd.to_datetime(df["Timestamp"])
278
+
279
+ # Available columns and data types:
280
+ {new_line.join(map(lambda x: '# '+x, str(df_check.dtypes).split(new_line)))}
281
+
 
 
 
 
282
  # Question: {question.strip()}
283
  # Generate code to answer the question and save result in 'answer' variable
284
  # If creating a plot, save it with a unique filename and store the filename in 'answer'
285
  # If returning text/numbers, store the result directly in 'answer'
286
  ```"""
287
 
288
+ system_prompt = """Generate Python code to answer the user's question about air quality data.
289
+
290
+ CRITICAL: Only generate Python code - no explanations, no thinking, just clean executable code.
291
+
292
+ AVAILABLE LIBRARIES:
293
+ You can use these pre-installed libraries:
294
+ - pandas, numpy (data manipulation)
295
+ - matplotlib, seaborn, plotly (visualization)
296
+ - statsmodels (statistical modeling, trend analysis)
297
+ - scikit-learn (machine learning, regression)
298
+ - geopandas (geospatial analysis)
299
+
300
+ LIBRARY USAGE RULES:
301
+ - For trend analysis: Use numpy.polyfit(x, y, 1) for simple linear trends
302
+ - For regression: Use sklearn.linear_model.LinearRegression() for robust regression
303
+ - For statistical modeling: Use statsmodels only if needed, otherwise use numpy/sklearn
304
+ - Always import libraries at the top: import numpy as np, from sklearn.linear_model import LinearRegression
305
+ - Handle missing libraries gracefully with try-except around imports
306
+
307
+ OUTPUT TYPE REQUIREMENTS:
308
+ 1. PLOT GENERATION (for "plot", "chart", "visualize", "show trend", "graph"):
309
+ - MUST create matplotlib figure with proper labels, title, legend
310
+ - MUST save plot: filename = f"plot_{uuid.uuid4().hex[:8]}.png"
311
+ - MUST call plt.savefig(filename, dpi=300, bbox_inches='tight')
312
+ - MUST call plt.close() to prevent memory leaks
313
+ - MUST store filename in 'answer' variable: answer = filename
314
+ - Handle empty data gracefully before plotting
315
+
316
+ 2. TEXT ANSWERS (for simple "Which", "What", single values):
317
+ - Store direct string answer in 'answer' variable
318
+ - Example: answer = "December had the highest pollution"
319
+
320
+ 3. DATAFRAMES (for lists, rankings, comparisons, multiple results):
321
+ - Create clean DataFrame with descriptive column names
322
+ - Sort appropriately for readability
323
+ - Store DataFrame in 'answer' variable: answer = result_df
324
+
325
+ MANDATORY SAFETY & ROBUSTNESS RULES:
326
+
327
+ DATA VALIDATION (ALWAYS CHECK):
328
+ - Check if DataFrame exists and not empty: if df.empty: answer = "No data available"
329
+ - Validate required columns exist: if 'PM2.5' not in df.columns: answer = "Required data not available"
330
+ - Check for sufficient data: if len(df) < 10: answer = "Insufficient data for analysis"
331
+ - Remove invalid/missing values: df = df.dropna(subset=['PM2.5', 'city', 'Timestamp'])
332
+ - Use early exit pattern: if condition: answer = "error message"; else: continue with analysis
333
+
334
+ OPERATION SAFETY (PREVENT CRASHES):
335
+ - Wrap risky operations in try-except blocks
336
+ - Check denominators before division: if denominator == 0: continue
337
+ - Validate indexing bounds: if idx >= len(array): continue
338
+ - Check for empty results after filtering: if result_df.empty: answer = "No data found"
339
+ - Convert data types explicitly: pd.to_numeric(), .astype(int), .astype(str)
340
+ - Handle timezone issues with datetime operations
341
+ - NO return statements - this is script context, use if/else logic flow
342
+
343
+ PLOT GENERATION (MANDATORY FOR PLOTS):
344
+ - Check data exists before plotting: if plot_data.empty: answer = "No data to plot"
345
+ - Always create new figure: plt.figure(figsize=(12, 8))
346
+ - Add comprehensive labels: plt.title(), plt.xlabel(), plt.ylabel()
347
+ - Handle long city names: plt.xticks(rotation=45, ha='right')
348
+ - Use tight layout: plt.tight_layout()
349
+ - CRITICAL PLOT SAVING SEQUENCE (no return statements):
350
+ 1. filename = f"plot_{uuid.uuid4().hex[:8]}.png"
351
+ 2. plt.savefig(filename, dpi=300, bbox_inches='tight')
352
+ 3. plt.close()
353
+ 4. answer = filename
354
+ - Use if/else logic: if data_valid: create_plot(); answer = filename else: answer = "error"
355
+
356
+ CRITICAL CODING PRACTICES:
357
+
358
+ DATA VALIDATION & SAFETY:
359
+ - Always check if DataFrames/Series are empty before operations: if df.empty: return
360
+ - Use .dropna() to handle missing values or .fillna() with appropriate defaults
361
+ - Validate column names exist before accessing: if 'column' in df.columns
362
+ - Check data types before operations: df['col'].dtype, isinstance() checks
363
+ - Handle edge cases: empty results, single row/column DataFrames, all NaN columns
364
+ - Use .copy() when modifying DataFrames to avoid SettingWithCopyWarning
365
+
366
+ VARIABLE & TYPE HANDLING:
367
+ - Use descriptive variable names (avoid single letters in complex operations)
368
+ - Ensure all variables are defined before use - initialize with defaults
369
+ - Convert pandas/numpy objects to proper Python types before operations
370
+ - Convert datetime/period objects appropriately: .astype(str), .dt.strftime(), int()
371
+ - Always cast to appropriate types for indexing: int(), str(), list()
372
+ - CRITICAL: Convert pandas/numpy values to int before list indexing: int(value) for calendar.month_name[int(month_value)]
373
+ - Use explicit type conversions rather than relying on implicit casting
374
+
375
+ PANDAS OPERATIONS:
376
+ - Reference DataFrame properly: df['column'] not 'column' in operations
377
+ - Use .loc/.iloc correctly for indexing - avoid chained indexing
378
+ - Use .reset_index() after groupby operations when needed for clean DataFrames
379
+ - Sort results for consistent output: .sort_values(), .sort_index()
380
+ - Use .round() for numerical results to avoid excessive decimals
381
+ - Chain operations carefully - split complex chains for readability
382
+
383
+ MATPLOTLIB & PLOTTING:
384
+ - Always call plt.close() after saving plots to prevent memory leaks
385
+ - Use descriptive titles, axis labels, and legends
386
+ - Handle cases where no data exists for plotting
387
+ - Use proper figure sizing: plt.figure(figsize=(width, height))
388
+ - Convert datetime indices to strings for plotting if needed
389
+ - Use color palettes consistently
390
+
391
+ ERROR PREVENTION:
392
+ - Use try-except blocks for operations that might fail
393
+ - Check denominators before division operations
394
+ - Validate array/list lengths before indexing
395
+ - Use .get() method for dictionary access with defaults
396
+ - Handle timezone-aware vs naive datetime objects consistently
397
+ - Use proper string formatting and encoding for text output
398
+
399
+ TECHNICAL REQUIREMENTS:
400
+ - Save final result in variable called 'answer'
401
+ - For TEXT: Store the direct answer as a string in 'answer'
402
+ - For PLOTS: Save with unique filename f"plot_{{uuid.uuid4().hex[:8]}}.png" and store filename in 'answer'
403
+ - For DATAFRAMES: Store the pandas DataFrame directly in 'answer' (e.g., answer = result_df)
404
+ - Always use .iloc or .loc properly for pandas indexing
405
+ - Close matplotlib figures with plt.close() to prevent memory leaks
406
+ - Use proper column name checks before accessing columns
407
+ - For dataframes, ensure proper column names and sorting for readability
408
+ """
409
+
410
+ query = f"""{system_prompt}
411
+
412
+ Complete the following code to answer the user's question:
413
+
414
+ {template}
415
+ """
416
+
417
+ # Make API call
418
+ if model_name == "gemini-pro":
419
+ response = llm.invoke(query)
420
+ answer = response.content
421
+ else:
422
+ response = llm.invoke(query)
423
+ answer = response.content
424
+
425
+ # Extract and execute code with enhanced error handling
426
+ try:
427
+ if "```python" in answer:
428
+ code_part = answer.split("```python")[1].split("```")[0]
429
+ else:
430
+ code_part = answer
431
 
432
+ full_code = f"""
 
 
 
 
433
  {template.split("```python")[1].split("```")[0]}
434
  {code_part}
435
  """
436
+
437
+ # Execute code in a controlled environment with better error handling
438
+ local_vars = {}
439
+ global_vars = {
440
+ 'pd': pd,
441
+ 'plt': plt,
442
+ 'os': os,
443
+ 'uuid': __import__('uuid'),
444
+ 'calendar': __import__('calendar'),
445
+ 'np': __import__('numpy')
446
+ }
447
+
448
+ exec(full_code, global_vars, local_vars)
449
+
450
+ # Get the answer
451
+ if 'answer' in local_vars:
452
+ answer_result = local_vars['answer']
453
+ else:
454
+ answer_result = "Code executed but no result was saved in 'answer' variable"
455
+
456
+ execution_time = (datetime.now() - start_time).total_seconds()
457
+
458
+ # Determine if output is an image
459
+ is_image = isinstance(answer_result, str) and any(answer_result.endswith(ext) for ext in ['.png', '.jpg', '.jpeg'])
460
+
461
+ # Log successful interaction
462
+ log_interaction(
463
+ user_query=question,
464
+ model_name=model_name,
465
+ response_content=str(answer_result),
466
+ generated_code=full_code,
467
+ execution_time=execution_time,
468
+ error_message=None,
469
+ is_image=is_image
470
+ )
471
+
472
+ return {
473
+ "role": "assistant",
474
+ "content": answer_result,
475
+ "gen_code": full_code,
476
+ "ex_code": full_code,
477
+ "last_prompt": question,
478
+ "error": None
479
+ }
480
+
481
+ except Exception as code_error:
482
+ execution_time = (datetime.now() - start_time).total_seconds()
483
+ error_msg = str(code_error)
484
+
485
+ # Classify and provide user-friendly error messages
486
+ user_friendly_msg = "I encountered an error while analyzing your data. "
487
+
488
+ if "unmatched" in error_msg.lower() or "invalid syntax" in error_msg.lower():
489
+ user_friendly_msg += "There was a syntax error in the generated code (missing brackets or quotes). Please try rephrasing your question or try again."
490
+ elif "not defined" in error_msg.lower():
491
+ user_friendly_msg += "There was a variable naming error in the generated code. Please try asking the question again."
492
+ elif "has no attribute" in error_msg.lower():
493
+ user_friendly_msg += "There was an issue accessing data properties. Please try a simpler version of your question."
494
+ elif "division by zero" in error_msg.lower():
495
+ user_friendly_msg += "The calculation involved division by zero, possibly due to missing data. Please try a different time period or location."
496
+ elif "empty" in error_msg.lower() or "no data" in error_msg.lower():
497
+ user_friendly_msg += "No relevant data was found for your query. Please try adjusting the time period, location, or criteria."
498
+ else:
499
+ user_friendly_msg += f"Technical error: {error_msg}"
500
+
501
+ user_friendly_msg += "\n\n💡 **Suggestions:**\n- Try rephrasing your question\n- Use simpler terms\n- Check if the data exists for your specified criteria"
502
+
503
+ # Log the failed code execution
504
+ log_interaction(
505
+ user_query=question,
506
+ model_name=model_name,
507
+ response_content=user_friendly_msg,
508
+ generated_code=full_code if 'full_code' in locals() else "",
509
+ execution_time=execution_time,
510
+ error_message=error_msg,
511
+ is_image=False
512
+ )
513
+
514
+ return {
515
+ "role": "assistant",
516
+ "content": user_friendly_msg,
517
+ "gen_code": full_code if 'full_code' in locals() else "",
518
+ "ex_code": full_code if 'full_code' in locals() else "",
519
+ "last_prompt": question,
520
+ "error": error_msg
521
+ }
522
+
523
+ except Exception as e:
524
+ execution_time = (datetime.now() - start_time).total_seconds()
525
+ error_msg = str(e)
526
+
527
+ # Handle specific API errors
528
+ if "organization_restricted" in error_msg:
529
+ response_content = "API Organization Restricted: Your API key access has been restricted. Please check your Groq API key or try generating a new one."
530
+ log_error_msg = "API access restricted"
531
+ elif "rate_limit" in error_msg.lower():
532
+ response_content = "Rate limit exceeded. Please wait a moment and try again."
533
+ log_error_msg = "Rate limit exceeded"
534
  else:
535
+ response_content = f"Error: {error_msg}"
536
+ log_error_msg = error_msg
537
+
538
+ # Log the failed interaction
539
  log_interaction(
540
  user_query=question,
541
  model_name=model_name,
542
+ response_content=response_content,
543
+ generated_code="",
544
  execution_time=execution_time,
545
+ error_message=log_error_msg,
546
  is_image=False
547
  )
548
+
549
  return {
550
+ "role": "assistant",
551
+ "content": response_content,
552
+ "gen_code": "",
553
+ "ex_code": "",
554
  "last_prompt": question,
555
+ "error": log_error_msg
556
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
states_data.csv DELETED
@@ -1,32 +0,0 @@
1
- state,population,area (km2),isUnionTerritory
2
- Uttar Pradesh,199812341,240928,False
3
- Maharashtra,112374333,307713,False
4
- Bihar,104099452,94163,False
5
- West Bengal,91276115,88752,False
6
- Madhya Pradesh,72626809,308252,False
7
- Tamil Nadu,72147030,130058,False
8
- Rajasthan,68548437,342239,False
9
- Karnataka,61095297,191791,False
10
- Gujarat,60439692,196024,False
11
- Andhra Pradesh,49577103,162975,False
12
- Odisha,41974219,155707,False
13
- Telangana,35003674,112077,False
14
- Kerala,33406061,38863,False
15
- Jharkhand,32988134,79716,False
16
- Assam,31205576,78438,False
17
- Punjab,27743338,50362,False
18
- Chhattisgarh,25545198,135192,False
19
- Delhi,16787941,1484,True
20
- Haryana,25351462,44212,False
21
- Jammu and Kashmir,12267032,42241,True
22
- Uttarakhand,10086292,53483,False
23
- Himachal Pradesh,6864602,55673,False
24
- Tripura,3673917,10491,False
25
- Manipur,2570390,22327,False
26
- Meghalaya,2966889,22429,False
27
- Nagaland,1978502,16579,False
28
- Arunachal Pradesh,1383727,83743,False
29
- Puducherry,1247953,479,True
30
- Mizoram,1097206,21081,False
31
- Chandigarh,1055450,114,True
32
- Sikkim,610577,7096,False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
system_prompt.txt CHANGED
@@ -3,7 +3,6 @@ I have a pandas dataframe data of PM2.5 and PM10.
3
  * Frequency of data is daily.
4
  * `pollution` generally means `PM2.5`.
5
  * You already have df, so don't read the csv file
6
- * Available libraries: pandas, matplotlib, numpy, seaborn, plotly, geopandas, statsmodels, scikit-learn
7
  * Don't print anything, but save result in a variable `answer` and make it global.
8
  * Unless explicitly mentioned, don't consider the result as a plot.
9
  * PM2.5 guidelines: India: 60, WHO: 15.
 
3
  * Frequency of data is daily.
4
  * `pollution` generally means `PM2.5`.
5
  * You already have df, so don't read the csv file
 
6
  * Don't print anything, but save result in a variable `answer` and make it global.
7
  * Unless explicitly mentioned, don't consider the result as a plot.
8
  * PM2.5 guidelines: India: 60, WHO: 15.
test_image.py DELETED
@@ -1,129 +0,0 @@
1
- import pandas as pd
2
- import matplotlib.pyplot as plt
3
- import seaborn as sns
4
- import uuid
5
- import calendar
6
- import numpy as np
7
- # Set professional matplotlib styling with high resolution
8
- #plt.style.use('vayuchat.mplstyle')
9
- df = pd.read_csv("AQ_met_data.csv")
10
- df["Timestamp"] = pd.to_datetime(df["Timestamp"])
11
- states_df = pd.read_csv("states_data.csv")
12
- ncap_df = pd.read_csv("ncap_funding_data.csv")
13
- # df is pandas DataFrame with air quality data from India. Data frequency is daily from 2017 to 2024. The data has the following columns and data types:
14
- # Unnamed: 0 int64
15
- # Timestamp datetime64[ns]
16
- # State object
17
- # City object
18
- # Station object
19
- # site_id object
20
- # Year int64
21
- # PM2.5 (µg/m³) float64
22
- # PM10 (µg/m³) float64
23
- # NO (µg/m³) float64
24
- # NO2 (µg/m³) float64
25
- # NOx (ppb) float64
26
- # NH3 (µg/m³) float64
27
- # SO2 (µg/m³) float64
28
- # CO (mg/m³) float64
29
- # Ozone (µg/m³) float64
30
- # AT (°C) float64
31
- # RH (%) float64
32
- # WS (m/s) float64
33
- # WD (deg) float64
34
- # RF (mm) float64
35
- # TOT-RF (mm) float64
36
- # SR (W/mt2) float64
37
- # BP (mmHg) float64
38
- # VWS (m/s) float64
39
- # dtype: object
40
- # states_df is a pandas DataFrame of state-wise population, area and whether state is union territory or not of India.
41
- # state object
42
- # population int64
43
- # area (km2) int64
44
- # isUnionTerritory bool
45
- # dtype: object
46
- # ncap_df is a pandas DataFrame of funding given to the cities of India from 2019-2022, under The National Clean Air Program (NCAP).
47
- # S. No. int64
48
- # state object
49
- # city object
50
- # Amount released during FY 2019-20 float64
51
- # Amount released during FY 2020-21 float64
52
- # Amount released during FY 2021-22 float64
53
- # Total fund released float64
54
- # Utilisation as on June 2022 float64
55
- # dtype: object
56
- # Question: Compare the wind speed and PM2.5 levels during Delhi’s most polluted week (highest PM2.5) in December 2024 with the previous 15 days and the following 15 days on a time series plot.
57
- # Generate code to answer the question and save result in 'answer' variable
58
- # If creating a plot, save it with a unique filename and store the filename in 'answer'
59
- # If returning text/numbers, store the result directly in 'answer'
60
-
61
-
62
- import pandas as pd
63
- import matplotlib.pyplot as plt
64
- import seaborn as sns
65
- import uuid
66
- import numpy as np
67
-
68
- # Ensure data is loaded
69
- if df.empty:
70
- answer = "No data available"
71
- else:
72
- try:
73
- # Filter for Delhi in December 2024
74
- df_delhi = df[
75
- (df['City'].str.contains('Delhi', case=False, na=False)) &
76
- (df['Timestamp'].dt.year == 2024) &
77
- (df['Timestamp'].dt.month == 12)
78
- ].copy()
79
- df_delhi = df_delhi.dropna(subset=['PM2.5 (µg/m³)', 'WS (m/s)'])
80
- df_delhi = df_delhi.sort_values('Timestamp')
81
-
82
- # Need at least a full week to compute rolling mean
83
- if len(df_delhi) < 7:
84
- answer = "Insufficient data"
85
- else:
86
- # Compute 7‑day rolling mean of PM2.5
87
- df_delhi['PM2.5_roll7'] = df_delhi['PM2.5 (µg/m³)'].rolling(window=7, min_periods=7).mean()
88
- # Identify the window with the highest mean PM2.5
89
- max_idx = df_delhi['PM2.5_roll7'].idxmax()
90
- max_end_date = df_delhi.loc[max_idx, 'Timestamp']
91
- max_start_date = max_end_date - pd.Timedelta(days=6)
92
-
93
- # Define extended window: 15 days before start and 15 days after end
94
- ext_start = max_start_date - pd.Timedelta(days=15)
95
- ext_end = max_end_date + pd.Timedelta(days=15)
96
-
97
- # Filter data for the extended period
98
- mask = (df_delhi['Timestamp'] >= ext_start) & (df_delhi['Timestamp'] <= ext_end)
99
- df_plot = df_delhi.loc[mask].copy()
100
-
101
- if df_plot.empty or len(df_plot) < 30:
102
- answer = "Insufficient data"
103
- else:
104
- # Plot time series
105
- plt.figure(figsize=(9, 6))
106
- ax1 = plt.gca()
107
- sns.lineplot(data=df_plot, x='Timestamp', y='PM2.5 (µg/m³)', ax=ax1,
108
- label='PM2.5 (µg/m³)', color='tab:red')
109
- ax1.set_ylabel('PM2.5 (µg/m³)', color='tab:red')
110
- ax1.tick_params(axis='y', labelcolor='tab:red')
111
-
112
- ax2 = ax1.twinx()
113
- sns.lineplot(data=df_plot, x='Timestamp', y='WS (m/s)', ax=ax2,
114
- label='Wind Speed (m/s)', color='tab:blue')
115
- ax2.set_ylabel('Wind Speed (m/s)', color='tab:blue')
116
- ax2.tick_params(axis='y', labelcolor='tab:blue')
117
-
118
- plt.title('Delhi – PM2.5 and Wind Speed around Most Polluted Week (Dec 2024)')
119
- plt.xlabel('Date')
120
- plt.tight_layout()
121
-
122
- # Save plot
123
- filename = f"plot.png"
124
- plt.savefig(filename, dpi=1200, bbox_inches='tight', facecolor='white')
125
- plt.close()
126
-
127
- answer = filename
128
- except Exception as e:
129
- answer = "Unable to complete analysis with available data"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vayuchat.mplstyle DELETED
@@ -1,93 +0,0 @@
1
- # VayuChat - Modern Professional Style
2
- # Inspired by modern data visualization best practices
3
-
4
- # Typography & Layout
5
- font.size: 11
6
- font.family: sans-serif
7
- font.sans-serif: Inter, SF Pro Display, Segoe UI, system-ui, Arial
8
- figure.titlesize: 14
9
- axes.titlesize: 12
10
- axes.labelsize: 10
11
- xtick.labelsize: 9
12
- ytick.labelsize: 9
13
- legend.fontsize: 9
14
-
15
- # Figure & DPI - Ultra High Resolution
16
- figure.dpi: 1200
17
- figure.facecolor: white
18
- figure.edgecolor: none
19
- figure.figsize: 9, 6
20
- figure.autolayout: True
21
-
22
- # Modern Color Palette (inspired by Tailwind/GitHub)
23
- axes.prop_cycle: cycler('color', ['2563eb', 'dc2626', '059669', 'ea580c', '7c3aed', '0891b2', 'be123c', '16a34a', 'c2410c', '9333ea'])
24
-
25
- # Axes Styling
26
- axes.facecolor: white
27
- axes.edgecolor: e5e7eb
28
- axes.linewidth: 1
29
- axes.labelcolor: 374151
30
- axes.axisbelow: True
31
- axes.spines.left: True
32
- axes.spines.bottom: True
33
- axes.spines.top: False
34
- axes.spines.right: False
35
-
36
- # Grid (subtle and clean)
37
- axes.grid: True
38
- grid.color: f3f4f6
39
- grid.linewidth: 0.8
40
- grid.alpha: 0.7
41
- axes.grid.axis: both
42
-
43
- # Ticks
44
- xtick.direction: out
45
- ytick.direction: out
46
- xtick.major.size: 4
47
- ytick.major.size: 4
48
- xtick.minor.size: 2
49
- ytick.minor.size: 2
50
- xtick.color: 6b7280
51
- ytick.color: 6b7280
52
- xtick.major.pad: 7
53
- ytick.major.pad: 7
54
-
55
- # Legend
56
- legend.frameon: True
57
- legend.fancybox: True
58
- legend.shadow: False
59
- legend.framealpha: 0.95
60
- legend.facecolor: white
61
- legend.edgecolor: e5e7eb
62
- legend.borderpad: 0.8
63
- legend.columnspacing: 2
64
- legend.handlelength: 1.5
65
- legend.handletextpad: 0.8
66
-
67
- # Lines & Markers
68
- lines.linewidth: 2.5
69
- lines.markersize: 7
70
- lines.solid_capstyle: round
71
- patch.linewidth: 0.5
72
- patch.facecolor: 3b82f6
73
- patch.edgecolor: none
74
- patch.antialiased: True
75
-
76
- # Scatter plots
77
- scatter.marker: o
78
- scatter.edgecolors: white
79
-
80
- # Bars
81
- patch.force_edgecolor: False
82
-
83
- # Text & Annotations
84
- text.color: 1f2937
85
- text.antialiased: True
86
-
87
- # Savefig - Ultra High Resolution
88
- savefig.dpi: 1200
89
- savefig.facecolor: white
90
- savefig.edgecolor: none
91
- savefig.bbox: tight
92
- savefig.pad_inches: 0.2
93
- savefig.format: png