Spaces:
Sleeping
Sleeping
Commit ·
b7e1e0c
1
Parent(s): b6b1148
Add Conversational Analytics (Chat with Data) feature
Browse files- app/main.py +69 -0
- frontend/static/app.js +62 -0
- frontend/templates/index.html +13 -0
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">
|