shoaib4045 commited on
Commit
b7e1e0c
·
1 Parent(s): b6b1148

Add Conversational Analytics (Chat with Data) feature

Browse files
app/main.py CHANGED
@@ -657,3 +657,72 @@ async def analysis_history(
657
  for row in rows
658
  ]
659
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
  for row in rows
658
  ]
659
  }
660
+
661
+
662
+ class ChatRequest(BaseModel):
663
+ query: str
664
+
665
+ @app.post("/chat/{job_id}")
666
+ async def chat_with_data(job_id: str, req: ChatRequest, _: None = Depends(verify_api_key)):
667
+ """
668
+ Conversational Analytics feature: Allows users to ask follow-up questions
669
+ and executes safe Pandas queries using LLM.
670
+ """
671
+ from app.agent.nodes import LLM_MODEL
672
+ from langchain_groq import ChatGroq
673
+ from langchain_core.messages import HumanMessage
674
+ import pandas as pd
675
+ import json
676
+
677
+ with get_db_session() as db:
678
+ job = db.query(AnalysisJob).filter(AnalysisJob.id == job_id).first()
679
+ if not job:
680
+ raise HTTPException(status_code=404, detail="Job not found.")
681
+
682
+ # Load dataset securely
683
+ df = None
684
+ if DISABLE_DATA_PERSISTENCE:
685
+ from app.utils.data_store import get_dataset
686
+ df = get_dataset(job_id)
687
+ elif os.path.exists(job.file_path):
688
+ try:
689
+ df = pd.read_csv(job.file_path)
690
+ except UnicodeDecodeError:
691
+ df = pd.read_csv(job.file_path, encoding="latin-1")
692
+
693
+ if df is None:
694
+ raise HTTPException(
695
+ status_code=410,
696
+ detail="Raw dataset has been securely discarded from memory (Zero-Retention Mode). Chat feature unavailable."
697
+ )
698
+
699
+ llm = ChatGroq(model=LLM_MODEL, temperature=0.1, max_tokens=1024)
700
+ # Ask LLM to generate a safe pandas expression
701
+ schema_info = str(df.dtypes.to_dict())
702
+ prompt = f"""You are an expert Data Analyst Agent. You have a pandas DataFrame 'df' loaded with these columns and types: {schema_info}.
703
+ The user asks: "{req.query}".
704
+ Write a SINGLE line of Python code using pandas that evaluates to a string or number answering this question.
705
+ It must be purely an expression (e.g. df['Sales'].mean() or len(df)). No imports, no assignments. Just the expression.
706
+ If the question is just greeting/conversational, reply with 'CONVERSATIONAL: ' followed by your response.
707
+ Output ONLY the expression or conversational answer."""
708
+
709
+ try:
710
+ ai_expr = llm.invoke([HumanMessage(content=prompt)]).content.strip(" \n`'\"")
711
+ if ai_expr.startswith("Python") or ai_expr.startswith("python"):
712
+ ai_expr = ai_expr[6:].strip(" \n`")
713
+
714
+ if ai_expr.startswith("CONVERSATIONAL:"):
715
+ return {"response": ai_expr.replace("CONVERSATIONAL:", "").strip()}
716
+
717
+ # Safely evaluate
718
+ allowed_globals = {"__builtins__": {}, "pd": pd}
719
+ allowed_locals = {"df": df}
720
+ raw_result = eval(ai_expr, allowed_globals, allowed_locals)
721
+
722
+ # Format the result back into english
723
+ explanation_prompt = f"The user asked: '{req.query}'. The python result is: {raw_result}. Provide a short, direct human-friendly answer based on this."
724
+ final_answer = llm.invoke([HumanMessage(content=explanation_prompt)]).content
725
+ return {"response": final_answer}
726
+ except Exception as e:
727
+ logger.error(f"Chat error: {e}")
728
+ return {"response": "I couldn't calculate that from the data right now. Could you rephrase the question?"}
frontend/static/app.js CHANGED
@@ -528,4 +528,66 @@ document.addEventListener('DOMContentLoaded', () => {
528
  logTerminal('[Error]: Download failed - Network Error', true);
529
  }
530
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  });
 
528
  logTerminal('[Error]: Download failed - Network Error', true);
529
  }
530
  }
531
+
532
+ // --- Chat with Data Logic ---
533
+ const chatBtn = document.getElementById('chat-btn');
534
+ const chatInput = document.getElementById('chat-input');
535
+ const chatHistory = document.getElementById('chat-history');
536
+
537
+ async function sendChatMessage() {
538
+ if (!currentJobId) return;
539
+ const query = chatInput.value.trim();
540
+ if (!query) return;
541
+
542
+ // Add user message
543
+ const userMsg = document.createElement('div');
544
+ userMsg.style = "align-self: flex-end; background: #3b82f6; color: white; padding: 8px 12px; border-radius: 8px; max-width: 80%; word-wrap: break-word;";
545
+ userMsg.innerText = query;
546
+ chatHistory.appendChild(userMsg);
547
+
548
+ chatInput.value = '';
549
+ chatInput.disabled = true;
550
+ chatBtn.disabled = true;
551
+ chatBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
552
+
553
+ chatHistory.scrollTop = chatHistory.scrollHeight;
554
+
555
+ try {
556
+ const res = await fetch(buildUrl(`/chat/${currentJobId}`), {
557
+ method: 'POST',
558
+ headers: buildHeaders({ 'Content-Type': 'application/json' }),
559
+ body: JSON.stringify({ query: query })
560
+ });
561
+
562
+ const data = await res.json();
563
+
564
+ const aiMsg = document.createElement('div');
565
+ aiMsg.style = "align-self: flex-start; background: #334155; color: #f8fafc; padding: 8px 12px; border-radius: 8px; max-width: 80%; word-wrap: break-word; border:1px solid #475569;";
566
+ if (res.ok) {
567
+ aiMsg.innerText = data.response;
568
+ } else {
569
+ aiMsg.innerText = `Error: ${data.detail || 'Could not process query.'}`;
570
+ }
571
+ chatHistory.appendChild(aiMsg);
572
+ } catch (error) {
573
+ const aiMsg = document.createElement('div');
574
+ aiMsg.style = "align-self: flex-start; background: #7f1d1d; color: #f8fafc; padding: 8px 12px; border-radius: 8px; border:1px solid #991b1b;";
575
+ aiMsg.innerText = "Network error. Please try again.";
576
+ chatHistory.appendChild(aiMsg);
577
+ } finally {
578
+ chatInput.disabled = false;
579
+ chatBtn.disabled = false;
580
+ chatBtn.innerHTML = 'Send <i class="fa-solid fa-paper-plane"></i>';
581
+ chatHistory.scrollTop = chatHistory.scrollHeight;
582
+ chatInput.focus();
583
+ }
584
+ }
585
+
586
+ if (chatBtn && chatInput) {
587
+ chatBtn.addEventListener('click', sendChatMessage);
588
+ chatInput.addEventListener('keypress', (e) => {
589
+ if (e.key === 'Enter') sendChatMessage();
590
+ });
591
+ }
592
+
593
  });
frontend/templates/index.html CHANGED
@@ -141,6 +141,19 @@
141
  </div>
142
  </div>
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  <div class="panel glass-panel">
145
  <h3>Export Reports</h3>
146
  <div class="export-actions">
 
141
  </div>
142
  </div>
143
 
144
+ <!-- Conversational AI Data Chat -->
145
+ <div class="panel glass-panel chat-panel" style="margin-top:2rem;">
146
+ <h3><i class="fa-solid fa-messages"></i> Chat with your Data</h3>
147
+ <p style="color:#94a3b8; font-size:0.9rem; margin-bottom:1rem;">Ask follow-up questions. The AI will write and execute safe Pandas code dynamically.</p>
148
+ <div id="chat-history" style="min-height:100px; max-height:300px; overflow-y:auto; padding:1rem; background:rgba(0,0,0,0.2); border-radius:8px; margin-bottom:1rem; display:flex; flex-direction:column; gap:10px;">
149
+ <!-- Chat messages injected here -->
150
+ </div>
151
+ <div style="display:flex; gap:10px;">
152
+ <input type="text" id="chat-input" placeholder="E.g., What was the average salary?" style="flex:1; padding:10px; border-radius:6px; border:1px solid #334155; background:#1e293b; color:white;">
153
+ <button id="chat-btn" class="upload-btn" style="width:100px; margin-top:0;">Send <i class="fa-solid fa-paper-plane"></i></button>
154
+ </div>
155
+ </div>
156
+
157
  <div class="panel glass-panel">
158
  <h3>Export Reports</h3>
159
  <div class="export-actions">