AbhayVG commited on
Commit
62f5efd
Β·
verified Β·
1 Parent(s): 2ec6195

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +673 -0
  2. src.py +379 -0
app.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import json
4
+ import pandas as pd
5
+ import random
6
+ from os.path import join
7
+ from datetime import datetime
8
+ from src import (
9
+ preprocess_and_load_df,
10
+ load_agent,
11
+ ask_agent,
12
+ decorate_with_code,
13
+ show_response,
14
+ get_from_user,
15
+ load_smart_df,
16
+ ask_question,
17
+ )
18
+ from dotenv import load_dotenv
19
+ from langchain_groq import ChatGroq
20
+ from langchain_google_genai import ChatGoogleGenerativeAI
21
+ from streamlit_feedback import streamlit_feedback
22
+ from huggingface_hub import HfApi
23
+ from datasets import load_dataset, get_dataset_config_info, Dataset
24
+ from PIL import Image
25
+ import time
26
+
27
+ # Page config with beautiful theme
28
+ st.set_page_config(
29
+ page_title="VayuBuddy - AI Air Quality Assistant",
30
+ page_icon="🌬️",
31
+ layout="wide",
32
+ initial_sidebar_state="expanded"
33
+ )
34
+
35
+ # Custom CSS for beautiful styling
36
+ st.markdown("""
37
+ <style>
38
+ /* Clean app background */
39
+ .stApp {
40
+ background-color: #ffffff;
41
+ color: #212529;
42
+ font-family: 'Segoe UI', sans-serif;
43
+ }
44
+
45
+ /* Sidebar */
46
+ [data-testid="stSidebar"] {
47
+ background-color: #f8f9fa;
48
+ border-right: 1px solid #dee2e6;
49
+ padding: 1rem;
50
+ }
51
+
52
+ /* Main title */
53
+ .main-title {
54
+ text-align: center;
55
+ color: #343a40;
56
+ font-size: 2.5rem;
57
+ font-weight: 700;
58
+ margin-bottom: 0.5rem;
59
+ }
60
+
61
+ /* Subtitle */
62
+ .subtitle {
63
+ text-align: center;
64
+ color: #6c757d;
65
+ font-size: 1.1rem;
66
+ margin-bottom: 1.5rem;
67
+ }
68
+
69
+ /* Instructions */
70
+ .instructions {
71
+ background-color: #f1f3f5;
72
+ border-left: 4px solid #0d6efd;
73
+ padding: 1rem;
74
+ margin-bottom: 1.5rem;
75
+ border-radius: 6px;
76
+ color: #495057;
77
+ text-align: left;
78
+ }
79
+
80
+ /* Quick prompt buttons */
81
+ .quick-prompt-container {
82
+ display: flex;
83
+ flex-wrap: wrap;
84
+ gap: 8px;
85
+ margin-bottom: 1.5rem;
86
+ padding: 1rem;
87
+ background-color: #f8f9fa;
88
+ border-radius: 10px;
89
+ border: 1px solid #dee2e6;
90
+ }
91
+
92
+ .quick-prompt-btn {
93
+ background-color: #0d6efd;
94
+ color: white;
95
+ border: none;
96
+ padding: 8px 16px;
97
+ border-radius: 20px;
98
+ font-size: 0.9rem;
99
+ cursor: pointer;
100
+ transition: all 0.2s ease;
101
+ white-space: nowrap;
102
+ }
103
+
104
+ .quick-prompt-btn:hover {
105
+ background-color: #0b5ed7;
106
+ transform: translateY(-2px);
107
+ }
108
+
109
+ /* User message styling */
110
+ .user-message {
111
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
112
+ color: white;
113
+ padding: 15px 20px;
114
+ border-radius: 20px 20px 5px 20px;
115
+ margin: 10px 0;
116
+ margin-left: auto;
117
+ margin-right: 0;
118
+ max-width: 80%;
119
+ position: relative;
120
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
121
+ }
122
+
123
+ .user-info {
124
+ font-size: 0.8rem;
125
+ opacity: 0.8;
126
+ margin-bottom: 5px;
127
+ text-align: right;
128
+ }
129
+
130
+ /* Assistant message styling */
131
+ .assistant-message {
132
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
133
+ color: white;
134
+ padding: 15px 20px;
135
+ border-radius: 20px 20px 20px 5px;
136
+ margin: 10px 0;
137
+ margin-left: 0;
138
+ margin-right: auto;
139
+ max-width: 80%;
140
+ position: relative;
141
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
142
+ }
143
+
144
+ .assistant-info {
145
+ font-size: 0.8rem;
146
+ opacity: 0.8;
147
+ margin-bottom: 5px;
148
+ }
149
+
150
+ /* Processing indicator */
151
+ .processing-indicator {
152
+ background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
153
+ color: #333;
154
+ padding: 15px 20px;
155
+ border-radius: 20px 20px 20px 5px;
156
+ margin: 10px 0;
157
+ margin-left: 0;
158
+ margin-right: auto;
159
+ max-width: 80%;
160
+ position: relative;
161
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
162
+ animation: pulse 2s infinite;
163
+ }
164
+
165
+ @keyframes pulse {
166
+ 0% { opacity: 1; }
167
+ 50% { opacity: 0.7; }
168
+ 100% { opacity: 1; }
169
+ }
170
+
171
+ /* Feedback box */
172
+ .feedback-section {
173
+ background-color: #f8f9fa;
174
+ border: 1px solid #dee2e6;
175
+ padding: 1rem;
176
+ border-radius: 8px;
177
+ margin: 1rem 0;
178
+ }
179
+
180
+ /* Success and error messages */
181
+ .success-message {
182
+ background-color: #d1e7dd;
183
+ color: #0f5132;
184
+ padding: 1rem;
185
+ border-radius: 6px;
186
+ border: 1px solid #badbcc;
187
+ }
188
+
189
+ .error-message {
190
+ background-color: #f8d7da;
191
+ color: #842029;
192
+ padding: 1rem;
193
+ border-radius: 6px;
194
+ border: 1px solid #f5c2c7;
195
+ }
196
+
197
+ /* Chat input */
198
+ .stChatInput {
199
+ border-radius: 6px;
200
+ border: 1px solid #ced4da;
201
+ background: #ffffff;
202
+ }
203
+
204
+ /* Button */
205
+ .stButton > button {
206
+ background-color: #0d6efd;
207
+ color: white;
208
+ border-radius: 6px;
209
+ padding: 0.5rem 1.25rem;
210
+ border: none;
211
+ font-weight: 600;
212
+ transition: background-color 0.2s ease;
213
+ }
214
+
215
+ .stButton > button:hover {
216
+ background-color: #0b5ed7;
217
+ }
218
+
219
+ /* Code details styling */
220
+ .code-details {
221
+ background-color: #f8f9fa;
222
+ border: 1px solid #dee2e6;
223
+ border-radius: 8px;
224
+ padding: 10px;
225
+ margin-top: 10px;
226
+ }
227
+
228
+ /* Hide default menu and footer */
229
+ #MainMenu {visibility: hidden;}
230
+ footer {visibility: hidden;}
231
+ header {visibility: hidden;}
232
+
233
+ /* Auto scroll */
234
+ .main-container {
235
+ height: 70vh;
236
+ overflow-y: auto;
237
+ }
238
+ </style>
239
+ """, unsafe_allow_html=True)
240
+
241
+ # Auto-scroll JavaScript
242
+ st.markdown("""
243
+ <script>
244
+ function scrollToBottom() {
245
+ setTimeout(function() {
246
+ const mainContainer = document.querySelector('.main-container');
247
+ if (mainContainer) {
248
+ mainContainer.scrollTop = mainContainer.scrollHeight;
249
+ }
250
+ window.scrollTo(0, document.body.scrollHeight);
251
+ }, 100);
252
+ }
253
+ </script>
254
+ """, unsafe_allow_html=True)
255
+
256
+ # FORCE reload environment variables
257
+ load_dotenv(override=True)
258
+
259
+ # Get API keys
260
+ Groq_Token = os.getenv("GROQ_API_KEY")
261
+ hf_token = os.getenv("HF_TOKEN")
262
+ gemini_token = os.getenv("GEMINI_TOKEN")
263
+
264
+ models = {
265
+ "llama3.1": "llama-3.1-8b-instant",
266
+ "mistral": "mistral-saba-24b",
267
+ "llama3.3": "llama-3.3-70b-versatile",
268
+ "gemma": "gemma2-9b-it",
269
+ "gemini-pro": "gemini-1.5-pro",
270
+ }
271
+
272
+ self_path = os.path.dirname(os.path.abspath(__file__))
273
+
274
+ # Beautiful header
275
+ st.markdown("<h1 class='main-title'>🌬️ VayuBuddy</h1>", unsafe_allow_html=True)
276
+
277
+ st.markdown("""
278
+ <div class='subtitle'>
279
+ <strong>AI-Powered Air Quality Insights</strong><br>
280
+ Simplifying pollution analysis using conversational AI.
281
+ </div>
282
+ """, unsafe_allow_html=True)
283
+
284
+ st.markdown("""
285
+ <div class='instructions'>
286
+ <strong>How to Use:</strong><br>
287
+ Select a model from the sidebar and ask questions directly in the chat. Use quick prompts below for common queries.
288
+ </div>
289
+ """, unsafe_allow_html=True)
290
+
291
+ os.environ["PANDASAI_API_KEY"] = "$2a$10$gbmqKotzJOnqa7iYOun8eO50TxMD/6Zw1pLI2JEoqncwsNx4XeBS2"
292
+
293
+ # Load data with error handling
294
+ try:
295
+ df = preprocess_and_load_df(join(self_path, "Data.csv"))
296
+ st.success("βœ… Data loaded successfully!")
297
+ except Exception as e:
298
+ st.error(f"❌ Error loading data: {e}")
299
+ st.stop()
300
+
301
+ inference_server = "https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.2"
302
+ image_path = "IITGN_Logo.png"
303
+
304
+ # Beautiful sidebar
305
+ with st.sidebar:
306
+ # Logo and title
307
+ col1, col2, col3 = st.columns([1, 2, 1])
308
+ with col2:
309
+ if os.path.exists(image_path):
310
+ st.image(image_path, use_column_width=True)
311
+
312
+ # Model selection
313
+ st.markdown("### πŸ€– AI Model Selection")
314
+
315
+ # Filter available models
316
+ available_models = []
317
+ if Groq_Token and Groq_Token.strip():
318
+ available_models.extend(["llama3.1", "llama3.3", "mistral", "gemma"])
319
+ if gemini_token and gemini_token.strip():
320
+ available_models.append("gemini-pro")
321
+
322
+ if not available_models:
323
+ st.error("❌ No API keys available! Please set up your API keys in the .env file")
324
+ st.stop()
325
+
326
+ model_name = st.selectbox(
327
+ "Choose your AI assistant:",
328
+ available_models,
329
+ help="Different models have different strengths. Try them all!"
330
+ )
331
+
332
+ # Model descriptions
333
+ model_descriptions = {
334
+ "llama3.1": "πŸ¦™ Fast and efficient for general queries",
335
+ "llama3.3": "πŸ¦™ Most advanced Llama model",
336
+ "mistral": "⚑ Balanced performance and speed",
337
+ "gemma": "πŸ’Ž Google's lightweight model",
338
+ "gemini-pro": "🧠 Google's most powerful model"
339
+ }
340
+
341
+ if model_name in model_descriptions:
342
+ st.info(model_descriptions[model_name])
343
+
344
+ st.markdown("---")
345
+
346
+ # Clear Chat Button
347
+ if st.button("🧹 Clear Chat"):
348
+ st.session_state.responses = []
349
+ st.session_state.processing = False
350
+ try:
351
+ st.rerun()
352
+ except AttributeError:
353
+ st.experimental_rerun()
354
+
355
+ st.markdown("---")
356
+
357
+ # Chat History in Sidebar
358
+ with st.expander("πŸ“œ Chat History"):
359
+ for i, response in enumerate(st.session_state.get("responses", [])):
360
+ if response.get("role") == "user":
361
+ st.markdown(f"**You:** {response.get('content', '')[:50]}...")
362
+ elif response.get("role") == "assistant":
363
+ content = response.get('content', '')
364
+ if isinstance(content, str) and len(content) > 50:
365
+ st.markdown(f"**VayuBuddy:** {content[:50]}...")
366
+ else:
367
+ st.markdown(f"**VayuBuddy:** {str(content)[:50]}...")
368
+ st.markdown("---")
369
+
370
+ # Load quick prompts
371
+ questions = []
372
+ questions_file = join(self_path, "questions.txt")
373
+ if os.path.exists(questions_file):
374
+ try:
375
+ with open(questions_file, 'r', encoding='utf-8') as f:
376
+ content = f.read()
377
+ questions = [q.strip() for q in content.split("\n") if q.strip()]
378
+ print(f"Loaded {len(questions)} quick prompts") # Debug
379
+ except Exception as e:
380
+ st.error(f"Error loading questions: {e}")
381
+ questions = []
382
+
383
+ # Add some default prompts if file doesn't exist or is empty
384
+ if not questions:
385
+ questions = [
386
+ "What is the average PM2.5 level in the dataset?",
387
+ "Show me the air quality trend over time",
388
+ "Which pollutant has the highest concentration?",
389
+ "Create a correlation plot between different pollutants",
390
+ "What are the peak pollution hours?",
391
+ "Compare weekday vs weekend pollution levels"
392
+ ]
393
+
394
+ # Quick prompts section (horizontal)
395
+ st.markdown("### πŸ’­ Quick Prompts")
396
+
397
+ # Create columns for horizontal layout
398
+ cols_per_row = 2 # Reduced to 2 for better fit
399
+ rows = [questions[i:i + cols_per_row] for i in range(0, len(questions), cols_per_row)]
400
+
401
+ selected_prompt = None
402
+ for row_idx, row in enumerate(rows):
403
+ cols = st.columns(len(row))
404
+ for col_idx, question in enumerate(row):
405
+ with cols[col_idx]:
406
+ # Create unique key using row and column indices
407
+ unique_key = f"prompt_btn_{row_idx}_{col_idx}"
408
+ button_text = f"πŸ“ {question[:35]}{'...' if len(question) > 35 else ''}"
409
+
410
+ if st.button(button_text,
411
+ key=unique_key,
412
+ help=question,
413
+ use_container_width=True):
414
+ selected_prompt = question
415
+
416
+ st.markdown("---")
417
+
418
+ # Initialize chat history and processing state
419
+ if "responses" not in st.session_state:
420
+ st.session_state.responses = []
421
+ if "processing" not in st.session_state:
422
+ st.session_state.processing = False
423
+
424
+ def upload_feedback():
425
+ try:
426
+ data = {
427
+ "feedback": feedback.get("score", ""),
428
+ "comment": feedback.get("text", ""),
429
+ "error": error,
430
+ "output": output,
431
+ "prompt": last_prompt,
432
+ "code": code,
433
+ }
434
+
435
+ random_folder_name = str(datetime.now()).replace(" ", "_").replace(":", "-").replace(".", "-")
436
+ save_path = f"/tmp/vayubuddy_feedback.md"
437
+ path_in_repo = f"data/{random_folder_name}/feedback.md"
438
+
439
+ with open(save_path, "w") as f:
440
+ template = f"""Prompt: {last_prompt}
441
+
442
+ Output: {output}
443
+
444
+ Code:
445
+
446
+ ```py
447
+ {code}
448
+ ```
449
+
450
+ Error: {error}
451
+
452
+ Feedback: {feedback.get('score', '')}
453
+
454
+ Comments: {feedback.get('text', '')}
455
+ """
456
+ print(template, file=f)
457
+
458
+ if hf_token:
459
+ api = HfApi(token=hf_token)
460
+ api.upload_file(
461
+ path_or_fileobj=save_path,
462
+ path_in_repo=path_in_repo,
463
+ repo_id="SustainabilityLabIITGN/VayuBuddy_Feedback",
464
+ repo_type="dataset",
465
+ )
466
+ if status.get("is_image", False):
467
+ api.upload_file(
468
+ path_or_fileobj=output,
469
+ path_in_repo=f"data/{random_folder_name}/plot.png",
470
+ repo_id="SustainabilityLabIITGN/VayuBuddy_Feedback",
471
+ repo_type="dataset",
472
+ )
473
+ st.success("πŸŽ‰ Feedback uploaded successfully!")
474
+ else:
475
+ st.warning("⚠️ Cannot upload feedback - HF_TOKEN not available")
476
+ except Exception as e:
477
+ st.error(f"❌ Error uploading feedback: {e}")
478
+
479
+ def show_custom_response(response):
480
+ """Custom response display function"""
481
+ role = response.get("role", "assistant")
482
+ content = response.get("content", "")
483
+
484
+ if role == "user":
485
+ st.markdown(f"""
486
+ <div class='user-message'>
487
+ <div class='user-info'>You</div>
488
+ {content}
489
+ </div>
490
+ """, unsafe_allow_html=True)
491
+ elif role == "assistant":
492
+ st.markdown(f"""
493
+ <div class='assistant-message'>
494
+ <div class='assistant-info'>πŸ€– VayuBuddy</div>
495
+ {content if isinstance(content, str) else str(content)}
496
+ </div>
497
+ """, unsafe_allow_html=True)
498
+
499
+ # Show generated code if available
500
+ if response.get("gen_code"):
501
+ with st.expander("πŸ“‹ View Generated Code"):
502
+ st.code(response["gen_code"], language="python")
503
+
504
+ # Try to display image if content is a file path
505
+ try:
506
+ if isinstance(content, str) and (content.endswith('.png') or content.endswith('.jpg')):
507
+ if os.path.exists(content):
508
+ st.image(content)
509
+ return {"is_image": True}
510
+ except:
511
+ pass
512
+
513
+ return {"is_image": False}
514
+
515
+ def show_processing_indicator(model_name, question):
516
+ """Show processing indicator"""
517
+ st.markdown(f"""
518
+ <div class='processing-indicator'>
519
+ <div class='assistant-info'>πŸ€– VayuBuddy β€’ Processing with {model_name}</div>
520
+ <strong>Question:</strong> {question}<br>
521
+ <em>πŸ”„ Generating response...</em>
522
+ </div>
523
+ """, unsafe_allow_html=True)
524
+
525
+ # Main chat container
526
+ chat_container = st.container()
527
+
528
+ with chat_container:
529
+ # Display chat history
530
+ for response_id, response in enumerate(st.session_state.responses):
531
+ status = show_custom_response(response)
532
+
533
+ # Show feedback section for assistant responses
534
+ if response["role"] == "assistant":
535
+ feedback_key = f"feedback_{int(response_id/2)}"
536
+ error = response.get("error", "No error information")
537
+ output = response.get("content", "No output")
538
+ last_prompt = response.get("last_prompt", "No prompt")
539
+ code = response.get("gen_code", "No code generated")
540
+
541
+ if "feedback" in st.session_state.responses[response_id]:
542
+ st.markdown(f"""
543
+ <div class='feedback-section'>
544
+ <strong>πŸ“ Your Feedback:</strong> {st.session_state.responses[response_id]["feedback"]}
545
+ </div>
546
+ """, unsafe_allow_html=True)
547
+ else:
548
+ # Beautiful feedback section
549
+ col1, col2 = st.columns(2)
550
+ with col1:
551
+ thumbs_up = st.button("πŸ‘ Helpful", key=f"{feedback_key}_up", use_container_width=True)
552
+ with col2:
553
+ thumbs_down = st.button("πŸ‘Ž Not Helpful", key=f"{feedback_key}_down", use_container_width=True)
554
+
555
+ if thumbs_up or thumbs_down:
556
+ thumbs = "πŸ‘" if thumbs_up else "πŸ‘Ž"
557
+ comments = st.text_area(
558
+ "πŸ’¬ Tell us more (optional):",
559
+ key=f"{feedback_key}_comments",
560
+ placeholder="What could be improved?"
561
+ )
562
+ feedback = {"score": thumbs, "text": comments}
563
+ if st.button("πŸš€ Submit Feedback", key=f"{feedback_key}_submit"):
564
+ upload_feedback()
565
+ st.session_state.responses[response_id]["feedback"] = feedback
566
+ st.rerun()
567
+
568
+ # Show processing indicator if processing
569
+ if st.session_state.get("processing"):
570
+ show_processing_indicator(
571
+ st.session_state.get("current_model", "Unknown"),
572
+ st.session_state.get("current_question", "Processing...")
573
+ )
574
+
575
+ # Chat input (always visible at bottom)
576
+ prompt = st.chat_input("πŸ’¬ Ask me anything about air quality!", key="main_chat")
577
+
578
+ # Handle selected prompt from quick prompts
579
+ if selected_prompt:
580
+ prompt = selected_prompt
581
+
582
+ # Handle new queries
583
+ if prompt and not st.session_state.get("processing"):
584
+ # Prevent duplicate processing
585
+ if "last_prompt" in st.session_state:
586
+ last_prompt = st.session_state["last_prompt"]
587
+ last_model_name = st.session_state.get("last_model_name", "")
588
+ if (prompt == last_prompt) and (model_name == last_model_name):
589
+ prompt = None
590
+
591
+ if prompt:
592
+ # Add user input to chat history
593
+ user_response = get_from_user(prompt)
594
+ st.session_state.responses.append(user_response)
595
+
596
+ # Set processing state
597
+ st.session_state.processing = True
598
+ st.session_state.current_model = model_name
599
+ st.session_state.current_question = prompt
600
+
601
+ # Rerun to show processing indicator
602
+ st.rerun()
603
+
604
+ # Process the question if we're in processing state
605
+ if st.session_state.get("processing"):
606
+ prompt = st.session_state.get("current_question")
607
+ model_name = st.session_state.get("current_model")
608
+
609
+ try:
610
+ response = ask_question(model_name=model_name, question=prompt)
611
+
612
+ if not isinstance(response, dict):
613
+ response = {
614
+ "role": "assistant",
615
+ "content": "❌ Error: Invalid response format",
616
+ "gen_code": "",
617
+ "ex_code": "",
618
+ "last_prompt": prompt,
619
+ "error": "Invalid response format"
620
+ }
621
+
622
+ response.setdefault("role", "assistant")
623
+ response.setdefault("content", "No content generated")
624
+ response.setdefault("gen_code", "")
625
+ response.setdefault("ex_code", "")
626
+ response.setdefault("last_prompt", prompt)
627
+ response.setdefault("error", None)
628
+
629
+ except Exception as e:
630
+ response = {
631
+ "role": "assistant",
632
+ "content": f"Sorry, I encountered an error: {str(e)}",
633
+ "gen_code": "",
634
+ "ex_code": "",
635
+ "last_prompt": prompt,
636
+ "error": str(e)
637
+ }
638
+
639
+ st.session_state.responses.append(response)
640
+ st.session_state["last_prompt"] = prompt
641
+ st.session_state["last_model_name"] = model_name
642
+ st.session_state.processing = False
643
+
644
+ # Clear processing state
645
+ if "current_model" in st.session_state:
646
+ del st.session_state.current_model
647
+ if "current_question" in st.session_state:
648
+ del st.session_state.current_question
649
+
650
+ st.rerun()
651
+
652
+ # Auto-scroll to bottom
653
+ if st.session_state.responses:
654
+ st.markdown("<script>scrollToBottom();</script>", unsafe_allow_html=True)
655
+
656
+ # Beautiful sidebar footer
657
+ with st.sidebar:
658
+ st.markdown("---")
659
+ st.markdown("""
660
+ <div class='contact-section'>
661
+ <h4>πŸ“„ Paper on VayuBuddy</h4>
662
+ <p>Learn more about VayuBuddy in our <a href='https://arxiv.org/abs/2411.12760' target='_blank'>Research Paper</a>.</p>
663
+ </div>
664
+ """, unsafe_allow_html=True)
665
+
666
+ # Footer
667
+ st.markdown("""
668
+ <div style='text-align: center; margin-top: 3rem; padding: 2rem; background: rgba(255,255,255,0.1); border-radius: 15px;'>
669
+ <h3>🌍 Together for Cleaner Air</h3>
670
+ <p>VayuBuddy - Empowering environmental awareness through AI</p>
671
+ <small>Β© 2024 IIT Gandhinagar Sustainability Lab</small>
672
+ </div>
673
+ """, unsafe_allow_html=True)
src.py ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ from pandasai import Agent, SmartDataframe
4
+ from typing import Tuple
5
+ from PIL import Image
6
+ from pandasai.llm import HuggingFaceTextGen
7
+ from dotenv import load_dotenv
8
+ from langchain_groq import ChatGroq
9
+ from langchain_google_genai import ChatGoogleGenerativeAI
10
+ import matplotlib.pyplot as plt
11
+
12
+ # FORCE reload environment variables
13
+ load_dotenv(override=True)
14
+
15
+ # Get API keys with explicit None handling and debugging
16
+ Groq_Token = os.getenv("GROQ_API_KEY")
17
+ hf_token = os.getenv("HF_TOKEN")
18
+ gemini_token = os.getenv("GEMINI_TOKEN")
19
+
20
+ # Debug print (remove in production)
21
+ print(f"Debug - Groq Token: {'Present' if Groq_Token else 'Missing'}")
22
+ print(f"Debug - Groq Token Value: {Groq_Token[:10] + '...' if Groq_Token else 'None'}")
23
+ print(f"Debug - Gemini Token: {'Present' if gemini_token else 'Missing'}")
24
+
25
+ models = {
26
+ "mistral": "mistral-saba-24b",
27
+ "llama3.3": "llama-3.3-70b-versatile",
28
+ "llama3.1": "llama-3.1-8b-instant",
29
+ "gemma2": "gemma2-9b-it",
30
+ "gemini-pro": "gemini-1.5-pro"
31
+ }
32
+
33
+ def preprocess_and_load_df(path: str) -> pd.DataFrame:
34
+ """Load and preprocess the dataframe"""
35
+ try:
36
+ df = pd.read_csv(path)
37
+ df["Timestamp"] = pd.to_datetime(df["Timestamp"])
38
+ return df
39
+ except Exception as e:
40
+ raise Exception(f"Error loading dataframe: {e}")
41
+
42
+ def load_agent(df: pd.DataFrame, context: str, inference_server: str, name="mistral") -> Agent:
43
+ """Load pandas AI agent with error handling"""
44
+ try:
45
+ if name == "gemini-pro":
46
+ if not gemini_token or gemini_token.strip() == "":
47
+ raise ValueError("Gemini API token not available or empty")
48
+ llm = ChatGoogleGenerativeAI(
49
+ model=models[name],
50
+ google_api_key=gemini_token,
51
+ temperature=0.1
52
+ )
53
+ else:
54
+ if not Groq_Token or Groq_Token.strip() == "":
55
+ raise ValueError("Groq API token not available or empty")
56
+ llm = ChatGroq(
57
+ model=models[name],
58
+ api_key=Groq_Token,
59
+ temperature=0.1
60
+ )
61
+
62
+ agent = Agent(df, config={"llm": llm, "enable_cache": False, "options": {"wait_for_model": True}})
63
+ if context:
64
+ agent.add_message(context)
65
+ return agent
66
+ except Exception as e:
67
+ raise Exception(f"Error loading agent: {e}")
68
+
69
+ def load_smart_df(df: pd.DataFrame, inference_server: str, name="mistral") -> SmartDataframe:
70
+ """Load smart dataframe with error handling"""
71
+ try:
72
+ if name == "gemini-pro":
73
+ if not gemini_token or gemini_token.strip() == "":
74
+ raise ValueError("Gemini API token not available or empty")
75
+ llm = ChatGoogleGenerativeAI(
76
+ model=models[name],
77
+ google_api_key=gemini_token,
78
+ temperature=0.1
79
+ )
80
+ else:
81
+ if not Groq_Token or Groq_Token.strip() == "":
82
+ raise ValueError("Groq API token not available or empty")
83
+ llm = ChatGroq(
84
+ model=models[name],
85
+ api_key=Groq_Token,
86
+ temperature=0.1
87
+ )
88
+
89
+ df = SmartDataframe(df, config={"llm": llm, "max_retries": 5, "enable_cache": False})
90
+ return df
91
+ except Exception as e:
92
+ raise Exception(f"Error loading smart dataframe: {e}")
93
+
94
+ def get_from_user(prompt):
95
+ """Format user prompt"""
96
+ return {"role": "user", "content": prompt}
97
+
98
+ def ask_agent(agent: Agent, prompt: str) -> dict:
99
+ """Ask agent with comprehensive error handling"""
100
+ try:
101
+ response = agent.chat(prompt)
102
+ gen_code = getattr(agent, 'last_code_generated', '')
103
+ ex_code = getattr(agent, 'last_code_executed', '')
104
+ last_prompt = getattr(agent, 'last_prompt', prompt)
105
+
106
+ return {
107
+ "role": "assistant",
108
+ "content": response,
109
+ "gen_code": gen_code,
110
+ "ex_code": ex_code,
111
+ "last_prompt": last_prompt,
112
+ "error": None
113
+ }
114
+ except Exception as e:
115
+ return {
116
+ "role": "assistant",
117
+ "content": f"Error: {str(e)}",
118
+ "gen_code": "",
119
+ "ex_code": "",
120
+ "last_prompt": prompt,
121
+ "error": str(e)
122
+ }
123
+
124
+ def decorate_with_code(response: dict) -> str:
125
+ """Decorate response with code details"""
126
+ gen_code = response.get("gen_code", "No code generated")
127
+ last_prompt = response.get("last_prompt", "No prompt")
128
+
129
+ return f"""<details>
130
+ <summary>Generated Code</summary>
131
+
132
+ ```python
133
+ {gen_code}
134
+ ```
135
+ </details>
136
+
137
+ <details>
138
+ <summary>Prompt</summary>
139
+
140
+ {last_prompt}
141
+ """
142
+
143
+ def show_response(st, response):
144
+ """Display response with error handling"""
145
+ try:
146
+ with st.chat_message(response["role"]):
147
+ content = response.get("content", "No content")
148
+
149
+ try:
150
+ # Try to open as image
151
+ image = Image.open(content)
152
+ if response.get("gen_code"):
153
+ st.markdown(decorate_with_code(response), unsafe_allow_html=True)
154
+ st.image(image)
155
+ return {"is_image": True}
156
+ except:
157
+ # Not an image, display as text
158
+ if response.get("gen_code"):
159
+ display_content = decorate_with_code(response) + f"""</details>
160
+
161
+ {content}"""
162
+ else:
163
+ display_content = content
164
+ st.markdown(display_content, unsafe_allow_html=True)
165
+ return {"is_image": False}
166
+ except Exception as e:
167
+ st.error(f"Error displaying response: {e}")
168
+ return {"is_image": False}
169
+
170
+ def ask_question(model_name, question):
171
+ """Ask question with comprehensive error handling"""
172
+ try:
173
+ # Reload environment variables to get fresh values
174
+ load_dotenv(override=True)
175
+ fresh_groq_token = os.getenv("GROQ_API_KEY")
176
+ fresh_gemini_token = os.getenv("GEMINI_TOKEN")
177
+
178
+ print(f"ask_question - Fresh Groq Token: {'Present' if fresh_groq_token else 'Missing'}")
179
+
180
+ # Check API availability with fresh tokens
181
+ if model_name == "gemini-pro":
182
+ if not fresh_gemini_token or fresh_gemini_token.strip() == "":
183
+ return {
184
+ "role": "assistant",
185
+ "content": "❌ Gemini API token not available or empty. Please set GEMINI_TOKEN in your environment variables.",
186
+ "gen_code": "",
187
+ "ex_code": "",
188
+ "last_prompt": question,
189
+ "error": "Missing or empty API token"
190
+ }
191
+ llm = ChatGoogleGenerativeAI(
192
+ model=models[model_name],
193
+ google_api_key=fresh_gemini_token,
194
+ temperature=0
195
+ )
196
+ else:
197
+ if not fresh_groq_token or fresh_groq_token.strip() == "":
198
+ return {
199
+ "role": "assistant",
200
+ "content": "❌ Groq API token not available or empty. Please set GROQ_API_KEY in your environment variables and restart the application.",
201
+ "gen_code": "",
202
+ "ex_code": "",
203
+ "last_prompt": question,
204
+ "error": "Missing or empty API token"
205
+ }
206
+
207
+ # Test the API key by trying to create the client
208
+ try:
209
+ llm = ChatGroq(
210
+ model=models[model_name],
211
+ api_key=fresh_groq_token,
212
+ temperature=0.1
213
+ )
214
+ # Test with a simple call to verify the API key works
215
+ test_response = llm.invoke("Test")
216
+ print("API key test successful")
217
+ except Exception as api_error:
218
+ error_msg = str(api_error).lower()
219
+ if "organization_restricted" in error_msg or "unauthorized" in error_msg:
220
+ return {
221
+ "role": "assistant",
222
+ "content": "❌ API Key Error: Your Groq API key appears to be invalid, expired, or restricted. Please check your API key in the .env file.",
223
+ "gen_code": "",
224
+ "ex_code": "",
225
+ "last_prompt": question,
226
+ "error": f"API key validation failed: {str(api_error)}"
227
+ }
228
+ else:
229
+ return {
230
+ "role": "assistant",
231
+ "content": f"❌ API Connection Error: {str(api_error)}",
232
+ "gen_code": "",
233
+ "ex_code": "",
234
+ "last_prompt": question,
235
+ "error": str(api_error)
236
+ }
237
+
238
+ # Check if data file exists
239
+ if not os.path.exists("Data.csv"):
240
+ return {
241
+ "role": "assistant",
242
+ "content": "❌ Data.csv file not found. Please ensure the data file is in the correct location.",
243
+ "gen_code": "",
244
+ "ex_code": "",
245
+ "last_prompt": question,
246
+ "error": "Data file not found"
247
+ }
248
+
249
+ df_check = pd.read_csv("Data.csv")
250
+ df_check["Timestamp"] = pd.to_datetime(df_check["Timestamp"])
251
+ df_check = df_check.head(5)
252
+
253
+ new_line = "\n"
254
+ parameters = {"font.size": 12, "figure.dpi": 600}
255
+
256
+ template = f"""```python
257
+ import pandas as pd
258
+ import matplotlib.pyplot as plt
259
+ import uuid
260
+
261
+ plt.rcParams.update({parameters})
262
+
263
+ df = pd.read_csv("Data.csv")
264
+ df["Timestamp"] = pd.to_datetime(df["Timestamp"])
265
+
266
+ # Available columns and data types:
267
+ {new_line.join(map(lambda x: '# '+x, str(df_check.dtypes).split(new_line)))}
268
+
269
+ # Question: {question.strip()}
270
+ # Generate code to answer the question and save result in 'answer' variable
271
+ # If creating a plot, save it with a unique filename and store the filename in 'answer'
272
+ # If returning text/numbers, store the result directly in 'answer'
273
+ ```"""
274
+
275
+ system_prompt = """You are a helpful assistant that generates Python code for data analysis.
276
+
277
+ Rules:
278
+ 1. Always save your final result in a variable called 'answer'
279
+ 2. If creating a plot, save it with plt.savefig() and store the filename in 'answer'
280
+ 3. If returning text/numbers, store the result directly in 'answer'
281
+ 4. Use descriptive variable names and add comments
282
+ 5. Handle potential errors gracefully
283
+ 6. For plots, use unique filenames to avoid conflicts
284
+ """
285
+
286
+ query = f"""{system_prompt}
287
+
288
+ Complete the following code to answer the user's question:
289
+
290
+ {template}
291
+ """
292
+
293
+ # Make API call
294
+ if model_name == "gemini-pro":
295
+ response = llm.invoke(query)
296
+ answer = response.content
297
+ else:
298
+ response = llm.invoke(query)
299
+ answer = response.content
300
+
301
+ # Extract and execute code
302
+ try:
303
+ if "```python" in answer:
304
+ code_part = answer.split("```python")[1].split("```")[0]
305
+ else:
306
+ code_part = answer
307
+
308
+ full_code = f"""
309
+ {template.split("```python")[1].split("```")[0]}
310
+ {code_part}
311
+ """
312
+
313
+ # Execute code in a controlled environment
314
+ local_vars = {}
315
+ global_vars = {
316
+ 'pd': pd,
317
+ 'plt': plt,
318
+ 'os': os,
319
+ 'uuid': __import__('uuid')
320
+ }
321
+
322
+ exec(full_code, global_vars, local_vars)
323
+
324
+ # Get the answer
325
+ if 'answer' in local_vars:
326
+ answer_result = local_vars['answer']
327
+ else:
328
+ answer_result = "No answer variable found in generated code"
329
+
330
+ return {
331
+ "role": "assistant",
332
+ "content": answer_result,
333
+ "gen_code": full_code,
334
+ "ex_code": full_code,
335
+ "last_prompt": question,
336
+ "error": None
337
+ }
338
+
339
+ except Exception as code_error:
340
+ return {
341
+ "role": "assistant",
342
+ "content": f"❌ Error executing generated code: {str(code_error)}",
343
+ "gen_code": full_code if 'full_code' in locals() else "",
344
+ "ex_code": full_code if 'full_code' in locals() else "",
345
+ "last_prompt": question,
346
+ "error": str(code_error)
347
+ }
348
+
349
+ except Exception as e:
350
+ error_msg = str(e)
351
+
352
+ # Handle specific API errors
353
+ if "organization_restricted" in error_msg:
354
+ return {
355
+ "role": "assistant",
356
+ "content": "❌ API Organization Restricted: Your API key access has been restricted. Please check your Groq API key or try generating a new one.",
357
+ "gen_code": "",
358
+ "ex_code": "",
359
+ "last_prompt": question,
360
+ "error": "API access restricted"
361
+ }
362
+ elif "rate_limit" in error_msg.lower():
363
+ return {
364
+ "role": "assistant",
365
+ "content": "❌ Rate limit exceeded. Please wait a moment and try again.",
366
+ "gen_code": "",
367
+ "ex_code": "",
368
+ "last_prompt": question,
369
+ "error": "Rate limit exceeded"
370
+ }
371
+ else:
372
+ return {
373
+ "role": "assistant",
374
+ "content": f"❌ Error: {error_msg}",
375
+ "gen_code": "",
376
+ "ex_code": "",
377
+ "last_prompt": question,
378
+ "error": error_msg
379
+ }