SaritMeshesha commited on
Commit
f01d7e2
Β·
verified Β·
1 Parent(s): 77f7771

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +43 -20
  2. app.py +845 -0
  3. requirements.txt +7 -3
README.md CHANGED
@@ -1,20 +1,43 @@
1
- ---
2
- title: Llm Data Analyst
3
- emoji: πŸš€
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: LLM-powered Data Analyst Agent
12
- license: mit
13
- ---
14
-
15
- # Welcome to Streamlit!
16
-
17
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
18
-
19
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LLM-powered Data Analyst Agent
2
+
3
+ This Streamlit application uses an LLM-powered agent to analyze the Bitext Customer Support LLM Chatbot Training Dataset. The agent can answer user questions about the dataset, performing both structured (quantitative) and unstructured (qualitative) analysis.
4
+
5
+ ## Features
6
+
7
+ - Ask questions about the customer support dataset
8
+ - Support for different types of analysis:
9
+ - Structured (Quantitative): Category frequencies, examples, intent distributions
10
+ - Unstructured (Qualitative): Summarize categories, analyze intents
11
+ - Scope detection to identify if questions are answerable from the dataset
12
+ - Support for follow-up questions
13
+ - Toggle between planning modes:
14
+ - Pre-planning + Execution: First classify the question, then execute the response
15
+ - ReActive Dynamic Planning: Let the LLM dynamically plan and execute the response
16
+
17
+ ## Setup
18
+
19
+ 1. Clone this repository
20
+ 2. Install the required dependencies:
21
+ ```
22
+ pip install -r requirements.txt
23
+ ```
24
+ 3. Run the Streamlit app:
25
+ ```
26
+ streamlit run app.py
27
+ ```
28
+ 4. Enter your OpenAI API key when prompted
29
+
30
+ ## Example Questions
31
+
32
+ - "What are the most frequent categories?"
33
+ - "Show examples of billing category"
34
+ - "What categories exist in the dataset?"
35
+ - "Summarize the technical support category"
36
+ - "What are the common intents in the billing category?"
37
+ - "How do agents typically respond to refund requests?"
38
+
39
+ ## Requirements
40
+
41
+ - Python 3.8+
42
+ - OpenAI API key (gpt-4o model access)
43
+ - Internet connection (to download the dataset)
app.py ADDED
@@ -0,0 +1,845 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from enum import Enum
4
+ from typing import List, Optional
5
+
6
+ import pandas as pd
7
+ import requests
8
+ import streamlit as st
9
+ from datasets import load_dataset
10
+ from dotenv import load_dotenv
11
+ from pydantic import BaseModel, Field
12
+
13
+ # Load environment variables from .env file (for local development)
14
+ load_dotenv()
15
+
16
+ # Set up page config with custom styling
17
+ st.set_page_config(
18
+ page_title="πŸ€– LLM Data Analyst Agent",
19
+ layout="wide",
20
+ page_icon="πŸ€–",
21
+ initial_sidebar_state="expanded",
22
+ )
23
+
24
+ # Custom CSS for styling
25
+ st.markdown(
26
+ """
27
+ <style>
28
+ /* Main theme colors */
29
+ :root {
30
+ --primary-color: #1f77b4;
31
+ --secondary-color: #ff7f0e;
32
+ --success-color: #2ca02c;
33
+ --error-color: #d62728;
34
+ --warning-color: #ff9800;
35
+ --background-color: #0e1117;
36
+ --card-background: #262730;
37
+ }
38
+
39
+ /* Custom styling for the main container */
40
+ .main-header {
41
+ background: linear-gradient(90deg, #1f77b4 0%, #ff7f0e 100%);
42
+ padding: 2rem 1rem;
43
+ border-radius: 10px;
44
+ margin-bottom: 2rem;
45
+ text-align: center;
46
+ color: white;
47
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
48
+ }
49
+
50
+ .main-header h1 {
51
+ margin: 0;
52
+ font-size: 2.5rem;
53
+ font-weight: 700;
54
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
55
+ }
56
+
57
+ .main-header p {
58
+ margin: 0.5rem 0 0 0;
59
+ font-size: 1.2rem;
60
+ opacity: 0.9;
61
+ }
62
+
63
+ /* Card styling */
64
+ .info-card {
65
+ background: var(--card-background);
66
+ padding: 1.5rem;
67
+ border-radius: 10px;
68
+ border-left: 4px solid var(--primary-color);
69
+ margin: 1rem 0;
70
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
71
+ }
72
+
73
+ .success-card {
74
+ background: linear-gradient(90deg, rgba(44, 160, 44, 0.1) 0%, rgba(44, 160, 44, 0.05) 100%);
75
+ border-left: 4px solid var(--success-color);
76
+ padding: 1rem;
77
+ border-radius: 8px;
78
+ margin: 1rem 0;
79
+ }
80
+
81
+ .error-card {
82
+ background: linear-gradient(90deg, rgba(214, 39, 40, 0.1) 0%, rgba(214, 39, 40, 0.05) 100%);
83
+ border-left: 4px solid var(--error-color);
84
+ padding: 1rem;
85
+ border-radius: 8px;
86
+ margin: 1rem 0;
87
+ }
88
+
89
+ .quick-actions-card {
90
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
91
+ padding: 1.5rem;
92
+ border-radius: 10px;
93
+ border-left: 4px solid var(--primary-color);
94
+ margin: 1rem 0;
95
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
96
+ color: #2c3e50;
97
+ }
98
+
99
+ .quick-actions-card h3 {
100
+ color: var(--primary-color);
101
+ margin-top: 0;
102
+ }
103
+
104
+ .quick-actions-card ul {
105
+ margin-bottom: 0;
106
+ }
107
+
108
+ .quick-actions-card li {
109
+ margin-bottom: 0.5rem;
110
+ color: #495057;
111
+ }
112
+
113
+ /* Button styling */
114
+ .stButton > button {
115
+ background: linear-gradient(90deg, #1f77b4 0%, #ff7f0e 100%);
116
+ color: white;
117
+ border: none;
118
+ border-radius: 25px;
119
+ padding: 0.5rem 2rem;
120
+ font-weight: 600;
121
+ transition: all 0.3s ease;
122
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
123
+ }
124
+
125
+ .stButton > button:hover {
126
+ transform: translateY(-2px);
127
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
128
+ }
129
+
130
+ /* Sidebar styling */
131
+ .css-1d391kg {
132
+ background: linear-gradient(180deg, #1f77b4 0%, #0e4b7a 100%);
133
+ }
134
+
135
+ /* Metrics styling */
136
+ .metric-container {
137
+ background: var(--card-background);
138
+ padding: 1rem;
139
+ border-radius: 8px;
140
+ text-align: center;
141
+ margin: 0.5rem 0;
142
+ border: 1px solid rgba(255, 255, 255, 0.1);
143
+ }
144
+
145
+ /* Chat message styling */
146
+ .user-message {
147
+ background: linear-gradient(90deg, rgba(31, 119, 180, 0.1) 0%, rgba(31, 119, 180, 0.05) 100%);
148
+ padding: 1rem;
149
+ border-radius: 10px;
150
+ margin: 0.5rem 0;
151
+ border-left: 4px solid var(--primary-color);
152
+ }
153
+
154
+ .assistant-message {
155
+ background: linear-gradient(90deg, rgba(255, 127, 14, 0.1) 0%, rgba(255, 127, 14, 0.05) 100%);
156
+ padding: 1rem;
157
+ border-radius: 10px;
158
+ margin: 0.5rem 0;
159
+ border-left: 4px solid var(--secondary-color);
160
+ }
161
+
162
+ /* Planning mode styling */
163
+ .planning-badge {
164
+ display: inline-block;
165
+ padding: 0.3rem 0.8rem;
166
+ border-radius: 15px;
167
+ font-size: 0.8rem;
168
+ font-weight: 600;
169
+ text-transform: uppercase;
170
+ letter-spacing: 0.5px;
171
+ }
172
+
173
+ .pre-planning {
174
+ background: rgba(31, 119, 180, 0.2);
175
+ color: var(--primary-color);
176
+ border: 1px solid var(--primary-color);
177
+ }
178
+
179
+ .reactive-planning {
180
+ background: rgba(255, 127, 14, 0.2);
181
+ color: var(--secondary-color);
182
+ border: 1px solid var(--secondary-color);
183
+ }
184
+
185
+ /* Animation for thinking indicator */
186
+ @keyframes pulse {
187
+ 0% { opacity: 1; }
188
+ 50% { opacity: 0.5; }
189
+ 100% { opacity: 1; }
190
+ }
191
+
192
+ .thinking-indicator {
193
+ animation: pulse 2s infinite;
194
+ }
195
+
196
+ /* Improved expander styling */
197
+ .streamlit-expanderHeader {
198
+ background: var(--card-background);
199
+ border-radius: 5px;
200
+ }
201
+ </style>
202
+ """,
203
+ unsafe_allow_html=True,
204
+ )
205
+
206
+ # API configuration - works for both local and Hugging Face deployment
207
+ api_key = os.environ.get("NEBIUS_API_KEY")
208
+
209
+ if not api_key:
210
+ st.markdown(
211
+ """
212
+ <div class="error-card">
213
+ <h3>πŸ”‘ API Key Configuration Required</h3>
214
+
215
+ <h4>For Local Development:</h4>
216
+ <ol>
217
+ <li>Open the <code>.env</code> file in your project directory</li>
218
+ <li>Replace <code>your_api_key_here</code> with your actual Nebius API key</li>
219
+ <li>Save the file and restart the application</li>
220
+ </ol>
221
+ <p><strong>Example .env file:</strong></p>
222
+ <pre>NEBIUS_API_KEY=your_actual_api_key_here</pre>
223
+
224
+ <h4>For Hugging Face Spaces Deployment:</h4>
225
+ <ol>
226
+ <li>Go to your Space settings</li>
227
+ <li>Navigate to the "Variables and secrets" section</li>
228
+ <li>Add a new secret: <code>NEBIUS_API_KEY</code> with your API key value</li>
229
+ <li>Restart your Space</li>
230
+ </ol>
231
+
232
+ <p><em>πŸ’‘ The app will automatically detect the environment and use the appropriate method.</em></p>
233
+ </div>
234
+ """,
235
+ unsafe_allow_html=True,
236
+ )
237
+ st.stop()
238
+
239
+ # Set the API key in environment for consistency
240
+ os.environ["OPENAI_API_KEY"] = api_key
241
+
242
+ # Nebius API settings
243
+ NEBIUS_API_URL = "https://api.studio.nebius.com/v1/chat/completions"
244
+ MODEL_NAME = "Qwen/Qwen3-30B-A3B"
245
+
246
+
247
+ # Function to call Nebius API
248
+ def call_nebius_api(messages, response_format=None, thinking_mode=False):
249
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
250
+
251
+ payload = {"model": MODEL_NAME, "messages": messages}
252
+
253
+ if response_format:
254
+ payload["response_format"] = response_format
255
+
256
+ # If in thinking mode, ask the model to show its reasoning
257
+ if thinking_mode:
258
+ # Add instruction to show thinking process
259
+ last_message = messages[-1]
260
+ enhanced_content = (
261
+ f"{last_message['content']}\n\n"
262
+ f"Important: First explain your thinking process step by step, "
263
+ f"then provide your final answer clearly labeled as 'FINAL ANSWER:'"
264
+ )
265
+ messages[-1]["content"] = enhanced_content
266
+ payload["messages"] = messages
267
+
268
+ try:
269
+ response = requests.post(NEBIUS_API_URL, headers=headers, json=payload)
270
+ response.raise_for_status()
271
+ return response.json()
272
+ except Exception as e:
273
+ st.error(f"API Error: {str(e)}")
274
+ if hasattr(e, "response") and hasattr(e.response, "text"):
275
+ st.error(f"Response: {e.response.text}")
276
+ return None
277
+
278
+
279
+ # Load Bitext dataset
280
+ @st.cache_data
281
+ def load_bitext_dataset():
282
+ try:
283
+ dataset = load_dataset(
284
+ "bitext/Bitext-customer-support-llm-chatbot-training-dataset"
285
+ )
286
+ df = pd.DataFrame(dataset["train"])
287
+ return df
288
+ except Exception as e:
289
+ st.error(f"Error loading dataset: {e}")
290
+ return None
291
+
292
+
293
+ # Define enums for request types
294
+ class AnalysisType(str, Enum):
295
+ QUANTITATIVE = "quantitative"
296
+ QUALITATIVE = "qualitative"
297
+ OUT_OF_SCOPE = "out_of_scope"
298
+
299
+
300
+ class ColumnType(str, Enum):
301
+ CATEGORY = "category"
302
+ INTENT = "intent"
303
+ CUSTOMER = "customer"
304
+ AGENT = "agent"
305
+
306
+
307
+ # Define schema for agent requests
308
+ class AgentRequest(BaseModel):
309
+ question: str = Field(..., description="The user's question")
310
+ analysis_type: AnalysisType = Field(..., description="Type of analysis to perform")
311
+ target_columns: Optional[List[ColumnType]] = Field(
312
+ None, description="Columns to analyze"
313
+ )
314
+ is_follow_up: bool = Field(
315
+ False, description="Whether this is a follow-up question"
316
+ )
317
+ previous_context: Optional[str] = Field(
318
+ None, description="Context from previous question"
319
+ )
320
+
321
+
322
+ # Function to classify the user question
323
+ def classify_question(
324
+ question: str, previous_context: Optional[str] = None
325
+ ) -> AgentRequest:
326
+ """
327
+ Use the LLM to classify the question and determine the analysis type and target columns.
328
+ """
329
+ system_prompt = """
330
+ You are a data analyst assistant that classifies user questions about a customer support dataset.
331
+ The dataset contains customer support conversations with these columns:
332
+ - category: The category of the customer query
333
+ - intent: The specific intent of the customer query
334
+ - customer: The customer's message
335
+ - agent: The agent's response
336
+
337
+ Classify the question into one of these types:
338
+ - quantitative: Questions about statistics, frequencies, distributions, or examples of categories/intents
339
+ - qualitative: Questions asking for summaries or insights about specific categories/intents
340
+ - out_of_scope: Questions that cannot be answered using the dataset
341
+
342
+ Also identify which columns are relevant to the question.
343
+
344
+ Return a JSON object with the following fields:
345
+ {
346
+ "analysis_type": "quantitative" | "qualitative" | "out_of_scope",
347
+ "target_columns": ["category", "intent", "customer", "agent"]
348
+ }
349
+ """
350
+
351
+ context_info = f"\nPrevious context: {previous_context}" if previous_context else ""
352
+
353
+ user_prompt = f"Classify this question: {question}{context_info}"
354
+
355
+ response = call_nebius_api(
356
+ [
357
+ {"role": "system", "content": system_prompt},
358
+ {"role": "user", "content": user_prompt},
359
+ ],
360
+ response_format={"type": "json_object"},
361
+ )
362
+
363
+ if not response:
364
+ # Fallback if API call fails
365
+ return AgentRequest(
366
+ question=question,
367
+ analysis_type=AnalysisType.OUT_OF_SCOPE,
368
+ target_columns=[],
369
+ is_follow_up=bool(previous_context),
370
+ previous_context=previous_context,
371
+ )
372
+
373
+ try:
374
+ content = (
375
+ response.get("choices", [{}])[0].get("message", {}).get("content", "{}")
376
+ )
377
+ result = json.loads(content)
378
+
379
+ # Convert string column names to ColumnType enum values
380
+ target_columns = []
381
+ for col in result.get("target_columns", []):
382
+ try:
383
+ target_columns.append(ColumnType(col))
384
+ except ValueError:
385
+ pass # Skip invalid column types
386
+
387
+ return AgentRequest(
388
+ question=question,
389
+ analysis_type=AnalysisType(result.get("analysis_type", "out_of_scope")),
390
+ target_columns=target_columns,
391
+ is_follow_up=bool(previous_context),
392
+ previous_context=previous_context,
393
+ )
394
+ except (json.JSONDecodeError, ValueError) as e:
395
+ st.warning(f"Error parsing API response: {str(e)}")
396
+ return AgentRequest(
397
+ question=question,
398
+ analysis_type=AnalysisType.OUT_OF_SCOPE,
399
+ target_columns=[],
400
+ is_follow_up=bool(previous_context),
401
+ previous_context=previous_context,
402
+ )
403
+
404
+
405
+ # Function to generate a response to the user's question
406
+ def generate_response(df: pd.DataFrame, request: AgentRequest) -> str:
407
+ """
408
+ Generate a response to the user's question based on the request classification.
409
+ """
410
+ # Get thinking mode setting from session state
411
+ show_thinking = st.session_state.get("show_thinking", True)
412
+
413
+ if request.analysis_type == AnalysisType.OUT_OF_SCOPE:
414
+ return "I'm sorry, but I can't answer that question based on the available customer support data."
415
+
416
+ # Prepare context with dataset information
417
+ data_description = f"Dataset contains {len(df)} customer support conversations."
418
+
419
+ if request.analysis_type == AnalysisType.QUANTITATIVE:
420
+ # For quantitative questions, prepare relevant statistics
421
+ stats_context = ""
422
+ if ColumnType.CATEGORY in request.target_columns:
423
+ category_counts = df["category"].value_counts().to_dict()
424
+ stats_context += f"\nCategory distribution: {json.dumps(category_counts)}"
425
+
426
+ if ColumnType.INTENT in request.target_columns:
427
+ intent_counts = df["intent"].value_counts().to_dict()
428
+ stats_context += f"\nIntent distribution: {json.dumps(intent_counts)}"
429
+
430
+ # If specific examples are requested, include sample data
431
+ if "example" in request.question.lower() or "show" in request.question.lower():
432
+ for col in request.target_columns:
433
+ if col.value in df.columns:
434
+ # Try to extract a specific value the user might be looking for
435
+ search_terms = [term.lower() for term in df[col.value].unique()]
436
+ for term in search_terms:
437
+ if term in request.question.lower():
438
+ examples = (
439
+ df[df[col.value].str.lower() == term]
440
+ .head(5)
441
+ .to_dict("records")
442
+ )
443
+ stats_context += f"\nExamples of {col.value}='{term}': {json.dumps(examples)}"
444
+ break
445
+ else: # QUALITATIVE
446
+ stats_context = ""
447
+ # For qualitative questions, prepare relevant data for summarization
448
+ for col in request.target_columns:
449
+ if col.value in df.columns:
450
+ unique_values = df[col.value].unique().tolist()
451
+ stats_context += (
452
+ f"\nUnique values for {col.value}: {json.dumps(unique_values)}"
453
+ )
454
+
455
+ # If there's a specific category/intent mentioned in the question
456
+ for value in unique_values:
457
+ if value.lower() in request.question.lower():
458
+ filtered_data = (
459
+ df[df[col.value] == value].head(10).to_dict("records")
460
+ )
461
+ stats_context += f"\nSample data for {col.value}='{value}': {json.dumps(filtered_data)}"
462
+ break
463
+
464
+ # Generate the response using LLM
465
+ system_prompt = f"""
466
+ You are a data analyst assistant that answers questions about a customer support dataset.
467
+ {data_description}
468
+
469
+ Use the following context to answer the question:
470
+ {stats_context}
471
+
472
+ Be concise and data-driven in your response. Mention specific numbers and patterns when appropriate.
473
+ If there isn't enough information to fully answer the question, acknowledge that limitation.
474
+ """
475
+
476
+ previous_context = ""
477
+ if request.is_follow_up:
478
+ previous_context = (
479
+ f"\nThis is a follow-up to previous context: {request.previous_context}"
480
+ )
481
+
482
+ response = call_nebius_api(
483
+ [
484
+ {"role": "system", "content": system_prompt},
485
+ {
486
+ "role": "user",
487
+ "content": f"Question: {request.question}{previous_context}",
488
+ },
489
+ ],
490
+ thinking_mode=show_thinking,
491
+ )
492
+
493
+ if not response:
494
+ return "I'm sorry, I encountered an error while processing your question. Please try again."
495
+
496
+ return (
497
+ response.get("choices", [{}])[0]
498
+ .get("message", {})
499
+ .get("content", "I couldn't generate a response. Please try again.")
500
+ )
501
+
502
+
503
+ # Function to plan and execute approach based on mode
504
+ def process_question(
505
+ df: pd.DataFrame, question: str, mode: str, previous_context: Optional[str] = None
506
+ ) -> str:
507
+ """
508
+ Process the user question using the specified planning mode.
509
+ """
510
+ # Add thinking indicator to the UI with custom styling
511
+ thinking_placeholder = st.empty()
512
+ thinking_placeholder.markdown(
513
+ """
514
+ <div class="thinking-indicator">
515
+ <div class="info-card">
516
+ βš™οΈ <strong>Agent is thinking...</strong> Analyzing your question and preparing response.
517
+ </div>
518
+ </div>
519
+ """,
520
+ unsafe_allow_html=True,
521
+ )
522
+
523
+ # Get thinking mode setting from session state
524
+ show_thinking = st.session_state.get("show_thinking", True)
525
+
526
+ if mode == "pre_planning":
527
+ # Pre-planning: First classify, then execute
528
+ request = classify_question(question, previous_context)
529
+ st.session_state.last_request = request
530
+
531
+ # Show classification if thinking is enabled
532
+ if show_thinking:
533
+ thinking_placeholder.markdown(
534
+ f"""
535
+ <div class="info-card">
536
+ βš™οΈ <strong>Agent classified this as a
537
+ <span style="color: var(--primary-color);">{request.analysis_type}</span> question</strong>
538
+ <br>πŸ“Š Target columns: {[col.value for col in request.target_columns]}
539
+ </div>
540
+ """,
541
+ unsafe_allow_html=True,
542
+ )
543
+
544
+ result = generate_response(df, request)
545
+ else: # reactive_planning
546
+ # Reactive planning: Let the LLM decide approach dynamically
547
+ system_prompt = """
548
+ You are a data analyst assistant that answers questions about a customer support dataset.
549
+ The dataset contains customer support conversations with categories, intents, customer messages, and agent responses.
550
+
551
+ Analyze the question and determine how to approach it:
552
+ 1. Identify if it's asking for statistics, examples, summaries, or insights
553
+ 2. Determine which aspects of the data are relevant
554
+ 3. Generate a direct and concise response based on the data
555
+
556
+ If the question cannot be answered with the customer support dataset, politely explain that it's outside your scope.
557
+ """
558
+
559
+ # Prepare dataset information
560
+ data_description = f"Dataset with {len(df)} records. "
561
+ data_description += f"Sample of 5 records: {df.sample(5).to_dict('records')}"
562
+ data_description += f"\nColumns: {df.columns.tolist()}"
563
+
564
+ # Include full distributions for categories and intents
565
+ # Check if the question is about distributions or frequencies
566
+ question_lower = question.lower()
567
+ include_distributions = any(
568
+ term in question_lower
569
+ for term in [
570
+ "distribution",
571
+ "frequency",
572
+ "count",
573
+ "how many",
574
+ "most frequent",
575
+ "most common",
576
+ "statistics",
577
+ ]
578
+ )
579
+
580
+ # Always include category values
581
+ data_description += f"\nCategory values: {df['category'].unique().tolist()}"
582
+
583
+ # Include full distribution data if the question appears to need it
584
+ if include_distributions:
585
+ if "category" in question_lower or "categories" in question_lower:
586
+ category_counts = df["category"].value_counts().to_dict()
587
+ data_description += (
588
+ f"\nCategory distribution: {json.dumps(category_counts)}"
589
+ )
590
+
591
+ if "intent" in question_lower or "intents" in question_lower:
592
+ intent_counts = df["intent"].value_counts().to_dict()
593
+ data_description += (
594
+ f"\nIntent distribution: {json.dumps(intent_counts)}"
595
+ )
596
+ else:
597
+ # Just provide a sample of intents if not specifically asking about them
598
+ data_description += f"\nIntent values sample: {df['intent'].sample(10).unique().tolist()}"
599
+ else:
600
+ # Just provide a sample of intents
601
+ data_description += (
602
+ f"\nIntent values sample: {df['intent'].sample(10).unique().tolist()}"
603
+ )
604
+
605
+ context_info = ""
606
+ if previous_context:
607
+ context_info = f"\nThis is a follow-up to: {previous_context}"
608
+
609
+ response = call_nebius_api(
610
+ [
611
+ {"role": "system", "content": system_prompt},
612
+ {
613
+ "role": "user",
614
+ "content": f"Question: {question}\n\nDataset information: {data_description}{context_info}",
615
+ },
616
+ ],
617
+ thinking_mode=show_thinking,
618
+ )
619
+
620
+ if not response:
621
+ thinking_placeholder.empty()
622
+ return "I'm sorry, I encountered an error while processing your question. Please try again."
623
+
624
+ result = (
625
+ response.get("choices", [{}])[0]
626
+ .get("message", {})
627
+ .get("content", "I couldn't generate a response. Please try again.")
628
+ )
629
+
630
+ # Clear the thinking indicator
631
+ thinking_placeholder.empty()
632
+
633
+ # Process the result to separate thinking from final answer if needed
634
+ if show_thinking and "FINAL ANSWER:" in result:
635
+ parts = result.split("FINAL ANSWER:")
636
+ thinking = parts[0].strip()
637
+ final_answer = parts[1].strip()
638
+
639
+ # Display thinking and final answer with clear separation
640
+ with st.expander("🧠 Agent's Thinking Process", expanded=True):
641
+ st.markdown(thinking)
642
+
643
+ return final_answer
644
+ else:
645
+ return result
646
+
647
+
648
+ # Main app interface
649
+ def main():
650
+ # Custom header
651
+ st.markdown(
652
+ """
653
+ <div class="main-header">
654
+ <h1>πŸ€– LLM-powered Data Analyst Agent</h1>
655
+ <p>Intelligent Analysis of Bitext Customer Support Dataset</p>
656
+ </div>
657
+ """,
658
+ unsafe_allow_html=True,
659
+ )
660
+
661
+ # Load dataset
662
+ with st.spinner("πŸ”„ Loading dataset..."):
663
+ df = load_bitext_dataset()
664
+
665
+ if df is None:
666
+ st.markdown(
667
+ """
668
+ <div class="error-card">
669
+ <h3>❌ Dataset Loading Failed</h3>
670
+ <p>Failed to load dataset. Please check your internet connection and try again.</p>
671
+ </div>
672
+ """,
673
+ unsafe_allow_html=True,
674
+ )
675
+ return
676
+
677
+ # Success message with dataset info
678
+ st.markdown(
679
+ f"""
680
+ <div class="success-card">
681
+ <h3>βœ… Dataset Loaded Successfully</h3>
682
+ <p>Loaded <strong>{len(df):,}</strong> customer support records ready for analysis</p>
683
+ </div>
684
+ """,
685
+ unsafe_allow_html=True,
686
+ )
687
+
688
+ # Sidebar configuration
689
+ with st.sidebar:
690
+ st.markdown("## βš™οΈ Configuration")
691
+
692
+ # Planning mode selection with styling
693
+ st.markdown("### 🧠 Planning Mode")
694
+ planning_mode = st.radio(
695
+ "Select how the agent should approach questions:",
696
+ ["pre_planning", "reactive_planning"],
697
+ format_func=lambda x: (
698
+ "🎯 Pre-planning + Execution"
699
+ if x == "pre_planning"
700
+ else "⚑ Reactive Dynamic Planning"
701
+ ),
702
+ help="Choose between structured pre-analysis or dynamic reactive planning",
703
+ )
704
+
705
+ # Display current mode with badge
706
+ mode_class = (
707
+ "pre-planning" if planning_mode == "pre_planning" else "reactive-planning"
708
+ )
709
+ mode_name = (
710
+ "Pre-Planning" if planning_mode == "pre_planning" else "Reactive Planning"
711
+ )
712
+ st.markdown(
713
+ f"""
714
+ <div class="planning-badge {mode_class}">
715
+ {mode_name} Mode Active
716
+ </div>
717
+ """,
718
+ unsafe_allow_html=True,
719
+ )
720
+
721
+ st.markdown("---")
722
+
723
+ # Thinking process toggle
724
+ st.markdown("### 🧠 Agent Behavior")
725
+ if "show_thinking" not in st.session_state:
726
+ st.session_state.show_thinking = True
727
+
728
+ show_thinking = st.checkbox(
729
+ "πŸ” Show Agent's Thinking Process",
730
+ value=st.session_state.show_thinking,
731
+ help="Display the agent's reasoning and analysis steps",
732
+ )
733
+ st.session_state.show_thinking = show_thinking
734
+
735
+ st.markdown("---")
736
+
737
+ # Dataset stats in sidebar
738
+ st.markdown("### πŸ“Š Dataset Overview")
739
+ col1, col2 = st.columns(2)
740
+ with col1:
741
+ st.metric("πŸ“ Total Records", f"{len(df):,}")
742
+ with col2:
743
+ st.metric("πŸ“‚ Categories", len(df["category"].unique()))
744
+
745
+ st.metric("🎯 Unique Intents", len(df["intent"].unique()))
746
+
747
+ # Main content area
748
+ # Dataset information in an expandable section
749
+ with st.expander("πŸ“Š Dataset Information", expanded=False):
750
+ st.markdown("### Dataset Details")
751
+
752
+ # Create metrics row
753
+ metrics_col1, metrics_col2, metrics_col3, metrics_col4 = st.columns(4)
754
+ with metrics_col1:
755
+ st.metric("Total Records", f"{len(df):,}")
756
+ with metrics_col2:
757
+ st.metric("Columns", len(df.columns))
758
+ with metrics_col3:
759
+ st.metric("Categories", len(df["category"].unique()))
760
+ with metrics_col4:
761
+ st.metric("Intents", len(df["intent"].unique()))
762
+
763
+ st.markdown("### Sample Data")
764
+ st.dataframe(df.head(), use_container_width=True)
765
+
766
+ st.markdown("### Category Distribution")
767
+ st.bar_chart(df["category"].value_counts())
768
+
769
+ # Initialize session state for conversation history
770
+ if "conversation" not in st.session_state:
771
+ st.session_state.conversation = []
772
+
773
+ if "last_request" not in st.session_state:
774
+ st.session_state.last_request = None
775
+
776
+ # User input section
777
+ st.markdown("## πŸ’¬ Ask Your Question")
778
+
779
+ # Create a more prominent input area
780
+ user_question = st.text_input(
781
+ "What would you like to know about the customer support data?",
782
+ placeholder="e.g., What are the most common customer issues?",
783
+ key="user_input",
784
+ help="Ask questions about statistics, examples, or insights from the dataset",
785
+ )
786
+
787
+ # Submit button with custom styling
788
+ col1, col2, col3 = st.columns([1, 2, 1])
789
+ with col2:
790
+ submit_clicked = st.button("πŸš€ Analyze Question", use_container_width=True)
791
+
792
+ if submit_clicked and user_question:
793
+ # Add user question to conversation
794
+ st.session_state.conversation.append({"role": "user", "content": user_question})
795
+
796
+ # Get previous context if this might be a follow-up
797
+ previous_context = None
798
+ if len(st.session_state.conversation) > 2:
799
+ # Get the previous assistant response
800
+ previous_context = st.session_state.conversation[-3]["content"]
801
+
802
+ # Process the question with enhanced loading indicator
803
+ with st.spinner("πŸ€– Agent is analyzing your question..."):
804
+ response = process_question(
805
+ df, user_question, planning_mode, previous_context
806
+ )
807
+
808
+ # Add response to conversation
809
+ st.session_state.conversation.append({"role": "assistant", "content": response})
810
+
811
+ # Display conversation with styled messages
812
+ if st.session_state.conversation:
813
+ st.markdown("## πŸ’­ Conversation History")
814
+
815
+ for i, message in enumerate(st.session_state.conversation):
816
+ if message["role"] == "user":
817
+ st.markdown(
818
+ f"""
819
+ <div class="user-message">
820
+ <strong>πŸ‘€ You:</strong> {message['content']}
821
+ </div>
822
+ """,
823
+ unsafe_allow_html=True,
824
+ )
825
+ else:
826
+ st.markdown(
827
+ f"""
828
+ <div class="assistant-message">
829
+ <strong>πŸ€– Agent:</strong> {message['content']}
830
+ </div>
831
+ """,
832
+ unsafe_allow_html=True,
833
+ )
834
+
835
+ if i < len(st.session_state.conversation) - 1: # Not the last message
836
+ st.markdown("---")
837
+
838
+ # Clear conversation button
839
+ if st.button("πŸ—‘οΈ Clear Conversation"):
840
+ st.session_state.conversation = []
841
+ st.rerun()
842
+
843
+
844
+ if __name__ == "__main__":
845
+ main()
requirements.txt CHANGED
@@ -1,3 +1,7 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
1
+ streamlit==1.32.0
2
+ pandas==2.1.3
3
+ datasets==2.17.0
4
+ openai==1.12.0
5
+ pydantic==2.5.2
6
+ python-dotenv==1.0.0
7
+ requests==2.31.0