Ara Yeroyan commited on
Commit
ce77124
Β·
1 Parent(s): fab49c5
Files changed (1) hide show
  1. app.py +694 -0
app.py ADDED
@@ -0,0 +1,694 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Intelligent Audit Report Chatbot UI
3
+ """
4
+
5
+
6
+ import os
7
+ import sys
8
+ import time
9
+ import json
10
+ import uuid
11
+ import logging
12
+ from pathlib import Path
13
+
14
+ import argparse
15
+ import streamlit as st
16
+ from langchain_core.messages import HumanMessage, AIMessage
17
+
18
+ from multi_agent_chatbot import get_multi_agent_chatbot
19
+ from smart_chatbot import get_chatbot as get_smart_chatbot
20
+ from src.reporting.feedback_schema import create_feedback_from_dict
21
+
22
+ # Configure logging
23
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Page config
27
+ st.set_page_config(
28
+ layout="wide",
29
+ page_icon="πŸ€–",
30
+ initial_sidebar_state="expanded",
31
+ page_title="Intelligent Audit Report Chatbot"
32
+ )
33
+
34
+ # Custom CSS
35
+ st.markdown("""
36
+ <style>
37
+ .main-header {
38
+ font-size: 2.5rem;
39
+ font-weight: bold;
40
+ color: #1f77b4;
41
+ text-align: center;
42
+ margin-bottom: 1rem;
43
+ }
44
+
45
+ .subtitle {
46
+ font-size: 1.2rem;
47
+ color: #666;
48
+ text-align: center;
49
+ margin-bottom: 2rem;
50
+ }
51
+
52
+ .session-info {
53
+ background-color: #f0f2f6;
54
+ padding: 10px;
55
+ border-radius: 5px;
56
+ margin-bottom: 20px;
57
+ font-size: 0.9rem;
58
+ }
59
+
60
+ .user-message {
61
+ background-color: #007bff;
62
+ color: white;
63
+ padding: 12px 16px;
64
+ border-radius: 18px 18px 4px 18px;
65
+ margin: 8px 0;
66
+ margin-left: 20%;
67
+ word-wrap: break-word;
68
+ }
69
+
70
+ .bot-message {
71
+ background-color: #f1f3f4;
72
+ color: #333;
73
+ padding: 12px 16px;
74
+ border-radius: 18px 18px 18px 4px;
75
+ margin: 8px 0;
76
+ margin-right: 20%;
77
+ word-wrap: break-word;
78
+ border: 1px solid #e0e0e0;
79
+ }
80
+
81
+ .filter-section {
82
+ margin-bottom: 20px;
83
+ padding: 15px;
84
+ background-color: #f8f9fa;
85
+ border-radius: 8px;
86
+ border: 1px solid #e9ecef;
87
+ }
88
+
89
+ .filter-title {
90
+ font-weight: bold;
91
+ margin-bottom: 10px;
92
+ color: #495057;
93
+ }
94
+
95
+ .feedback-section {
96
+ background-color: #f8f9fa;
97
+ padding: 20px;
98
+ border-radius: 10px;
99
+ margin-top: 30px;
100
+ border: 2px solid #dee2e6;
101
+ }
102
+
103
+ .retrieval-history {
104
+ background-color: #ffffff;
105
+ padding: 15px;
106
+ border-radius: 5px;
107
+ margin: 10px 0;
108
+ border-left: 4px solid #007bff;
109
+ }
110
+ </style>
111
+ """, unsafe_allow_html=True)
112
+
113
+ def get_system_type():
114
+ """Get the current system type"""
115
+ system = os.environ.get('CHATBOT_SYSTEM', 'multi-agent')
116
+ if system == 'smart':
117
+ return "Smart Chatbot System"
118
+ else:
119
+ return "Multi-Agent System"
120
+
121
+ def get_chatbot():
122
+ """Initialize and return the chatbot based on system type"""
123
+ # Check environment variable for system type
124
+ system = os.environ.get('CHATBOT_SYSTEM', 'multi-agent')
125
+ if system == 'smart':
126
+ return get_smart_chatbot()
127
+ else:
128
+ return get_multi_agent_chatbot()
129
+
130
+ def serialize_messages(messages):
131
+ """Serialize LangChain messages to dictionaries"""
132
+ serialized = []
133
+ for msg in messages:
134
+ if hasattr(msg, 'content'):
135
+ serialized.append({
136
+ "type": type(msg).__name__,
137
+ "content": str(msg.content)
138
+ })
139
+ return serialized
140
+
141
+ def serialize_documents(sources):
142
+ """Serialize document objects to dictionaries with deduplication"""
143
+ serialized = []
144
+ seen_content = set()
145
+
146
+ for doc in sources:
147
+ content = getattr(doc, 'page_content', getattr(doc, 'content', ''))
148
+
149
+ # Skip if we've seen this exact content before
150
+ if content in seen_content:
151
+ continue
152
+
153
+ seen_content.add(content)
154
+
155
+ doc_dict = {
156
+ "content": content,
157
+ "metadata": getattr(doc, 'metadata', {}),
158
+ "score": getattr(doc, 'metadata', {}).get('reranked_score', getattr(doc, 'metadata', {}).get('original_score', 0.0)),
159
+ "id": getattr(doc, 'metadata', {}).get('_id', 'unknown'),
160
+ "source": getattr(doc, 'metadata', {}).get('source', 'unknown'),
161
+ "year": getattr(doc, 'metadata', {}).get('year', 'unknown'),
162
+ "district": getattr(doc, 'metadata', {}).get('district', 'unknown'),
163
+ "page": getattr(doc, 'metadata', {}).get('page', 'unknown'),
164
+ "chunk_id": getattr(doc, 'metadata', {}).get('chunk_id', 'unknown'),
165
+ "page_label": getattr(doc, 'metadata', {}).get('page_label', 'unknown'),
166
+ "original_score": getattr(doc, 'metadata', {}).get('original_score', 0.0),
167
+ "reranked_score": getattr(doc, 'metadata', {}).get('reranked_score', None)
168
+ }
169
+ serialized.append(doc_dict)
170
+
171
+ return serialized
172
+
173
+ @st.cache_data
174
+ def load_filter_options():
175
+ try:
176
+ with open("filter_options.json", "r") as f:
177
+ return json.load(f)
178
+ except FileNotFoundError:
179
+ st.info([x for x in os.listdir() if x.endswith('.json')])
180
+ st.error("filter_options.json not found. Please run the metadata analysis script.")
181
+ return {"sources": [], "years": [], "districts": [], 'filenames': []}
182
+
183
+ def main():
184
+ # Initialize session state
185
+ if 'messages' not in st.session_state:
186
+ st.session_state.messages = []
187
+ if 'conversation_id' not in st.session_state:
188
+ st.session_state.conversation_id = f"session_{uuid.uuid4().hex[:8]}"
189
+ if 'session_start_time' not in st.session_state:
190
+ st.session_state.session_start_time = time.time()
191
+ if 'active_filters' not in st.session_state:
192
+ st.session_state.active_filters = {'sources': [], 'years': [], 'districts': [], 'filenames': []}
193
+ # Track RAG retrieval history for feedback
194
+ if 'rag_retrieval_history' not in st.session_state:
195
+ st.session_state.rag_retrieval_history = []
196
+ # Initialize chatbot only once per app session (cached)
197
+ if 'chatbot' not in st.session_state:
198
+ with st.spinner("πŸ”„ Loading AI models and connecting to database..."):
199
+ st.session_state.chatbot = get_chatbot()
200
+ st.success("βœ… AI system ready!")
201
+
202
+ # Reset conversation history if needed (but keep chatbot cached)
203
+ if 'reset_conversation' in st.session_state and st.session_state.reset_conversation:
204
+ st.session_state.messages = []
205
+ st.session_state.conversation_id = f"session_{uuid.uuid4().hex[:8]}"
206
+ st.session_state.session_start_time = time.time()
207
+ st.session_state.rag_retrieval_history = []
208
+ st.session_state.feedback_submitted = False
209
+ st.session_state.reset_conversation = False
210
+ st.rerun()
211
+
212
+ # Header with system indicator
213
+ col1, col2 = st.columns([3, 1])
214
+ with col1:
215
+ st.markdown('<h1 class="main-header">πŸ€– Intelligent Audit Report Chatbot</h1>', unsafe_allow_html=True)
216
+ with col2:
217
+ system_type = get_system_type()
218
+ if "Multi-Agent" in system_type:
219
+ st.success(f"πŸ”§ {system_type}")
220
+ else:
221
+ st.info(f"πŸ”§ {system_type}")
222
+ st.markdown('<p class="subtitle">Ask questions about audit reports. Use the sidebar filters to narrow down your search!</p>', unsafe_allow_html=True)
223
+
224
+ # Session info
225
+ duration = int(time.time() - st.session_state.session_start_time)
226
+ duration_str = f"{duration // 60}m {duration % 60}s"
227
+ st.markdown(f'''
228
+ <div class="session-info">
229
+ <strong>Session Info:</strong> Messages: {len(st.session_state.messages)} | Duration: {duration_str} | Status: Active | ID: {st.session_state.conversation_id}
230
+ </div>
231
+ ''', unsafe_allow_html=True)
232
+
233
+ # Load filter options
234
+ filter_options = load_filter_options()
235
+
236
+ # Sidebar for filters
237
+ with st.sidebar:
238
+ st.markdown("### πŸ” Search Filters")
239
+ st.markdown("Select filters to narrow down your search. Leave empty to search all data.")
240
+
241
+ st.markdown('<div class="filter-section">', unsafe_allow_html=True)
242
+ st.markdown('<div class="filter-title">πŸ“„ Specific Reports (Filename Filter)</div>', unsafe_allow_html=True)
243
+ st.markdown('<p style="font-size: 0.85em; color: #666;">⚠️ Selecting specific reports will ignore all other filters</p>', unsafe_allow_html=True)
244
+ selected_filenames = st.multiselect(
245
+ "Select specific reports:",
246
+ options=filter_options.get('filenames', []),
247
+ default=st.session_state.active_filters.get('filenames', []),
248
+ key="filenames_filter",
249
+ help="Choose specific reports to search. When enabled, all other filters are ignored."
250
+ )
251
+ st.markdown('</div>', unsafe_allow_html=True)
252
+
253
+ # Determine if filename filter is active
254
+ filename_mode = len(selected_filenames) > 0
255
+ # Sources filter
256
+ st.markdown('<div class="filter-section">', unsafe_allow_html=True)
257
+ st.markdown('<div class="filter-title">πŸ“Š Sources</div>', unsafe_allow_html=True)
258
+ selected_sources = st.multiselect(
259
+ "Select sources:",
260
+ options=filter_options['sources'],
261
+ default=st.session_state.active_filters['sources'],
262
+ disabled = filename_mode,
263
+ key="sources_filter",
264
+ help="Choose which types of reports to search"
265
+ )
266
+ st.markdown('</div>', unsafe_allow_html=True)
267
+
268
+ # Years filter
269
+ st.markdown('<div class="filter-section">', unsafe_allow_html=True)
270
+ st.markdown('<div class="filter-title">πŸ“… Years</div>', unsafe_allow_html=True)
271
+ selected_years = st.multiselect(
272
+ "Select years:",
273
+ options=filter_options['years'],
274
+ default=st.session_state.active_filters['years'],
275
+ disabled = filename_mode,
276
+ key="years_filter",
277
+ help="Choose which years to search"
278
+ )
279
+ st.markdown('</div>', unsafe_allow_html=True)
280
+
281
+ # Districts filter
282
+ st.markdown('<div class="filter-section">', unsafe_allow_html=True)
283
+ st.markdown('<div class="filter-title">🏘️ Districts</div>', unsafe_allow_html=True)
284
+ selected_districts = st.multiselect(
285
+ "Select districts:",
286
+ options=filter_options['districts'],
287
+ default=st.session_state.active_filters['districts'],
288
+ disabled = filename_mode,
289
+ key="districts_filter",
290
+ help="Choose which districts to search"
291
+ )
292
+ st.markdown('</div>', unsafe_allow_html=True)
293
+
294
+ # Update active filters
295
+ st.session_state.active_filters = {
296
+ 'sources': selected_sources if not filename_mode else [],
297
+ 'years': selected_years if not filename_mode else [],
298
+ 'districts': selected_districts if not filename_mode else [],
299
+ 'filenames': selected_filenames
300
+ }
301
+
302
+ # Clear filters button
303
+ if st.button("πŸ—‘οΈ Clear All Filters", key="clear_filters_button"):
304
+ st.session_state.active_filters = {'sources': [], 'years': [], 'districts': [], 'filenames': []}
305
+ st.rerun()
306
+
307
+ # Main content area with tabs
308
+ tab1, tab2 = st.tabs(["πŸ’¬ Chat", "πŸ“„ Retrieved Documents"])
309
+
310
+ with tab1:
311
+ # Chat container
312
+ chat_container = st.container()
313
+
314
+ with chat_container:
315
+ # Display conversation history
316
+ for message in st.session_state.messages:
317
+ if isinstance(message, HumanMessage):
318
+ st.markdown(f'<div class="user-message">{message.content}</div>', unsafe_allow_html=True)
319
+ elif isinstance(message, AIMessage):
320
+ st.markdown(f'<div class="bot-message">{message.content}</div>', unsafe_allow_html=True)
321
+
322
+ # Input area
323
+ st.markdown("<br>", unsafe_allow_html=True)
324
+
325
+ # Create two columns for input and button
326
+ col1, col2 = st.columns([4, 1])
327
+
328
+ with col1:
329
+ # Use a counter to force input clearing
330
+ if 'input_counter' not in st.session_state:
331
+ st.session_state.input_counter = 0
332
+
333
+ user_input = st.text_input(
334
+ "Type your message here...",
335
+ placeholder="Ask about budget allocations, expenditures, or audit findings...",
336
+ key=f"user_input_{st.session_state.input_counter}",
337
+ label_visibility="collapsed"
338
+ )
339
+
340
+ with col2:
341
+ send_button = st.button("Send", key="send_button", use_container_width=True)
342
+
343
+ # Clear chat button
344
+ if st.button("πŸ—‘οΈ Clear Chat", key="clear_chat_button"):
345
+ st.session_state.reset_conversation = True
346
+ # Clear all conversation files
347
+ import os
348
+ conversations_dir = "conversations"
349
+ if os.path.exists(conversations_dir):
350
+ for file in os.listdir(conversations_dir):
351
+ if file.endswith('.json'):
352
+ os.remove(os.path.join(conversations_dir, file))
353
+ st.rerun()
354
+
355
+ # Handle user input
356
+ if send_button and user_input:
357
+ # Construct filter context string
358
+ filter_context_str = ""
359
+ if selected_filenames:
360
+ filter_context_str += "FILTER CONTEXT:\n"
361
+ filter_context_str += f"Filenames: {', '.join(selected_filenames)}\n"
362
+ filter_context_str += "USER QUERY:\n"
363
+ elif selected_sources or selected_years or selected_districts:
364
+ filter_context_str += "FILTER CONTEXT:\n"
365
+ if selected_sources:
366
+ filter_context_str += f"Sources: {', '.join(selected_sources)}\n"
367
+ if selected_years:
368
+ filter_context_str += f"Years: {', '.join(selected_years)}\n"
369
+ if selected_districts:
370
+ filter_context_str += f"Districts: {', '.join(selected_districts)}\n"
371
+ filter_context_str += "USER QUERY:\n"
372
+
373
+ full_query = filter_context_str + user_input
374
+
375
+ # Add user message to history
376
+ st.session_state.messages.append(HumanMessage(content=user_input))
377
+
378
+ # Get chatbot response
379
+ with st.spinner("πŸ€” Thinking..."):
380
+ try:
381
+ # Pass the full query with filter context
382
+ chat_result = st.session_state.chatbot.chat(full_query, st.session_state.conversation_id)
383
+
384
+ # Handle both old format (string) and new format (dict)
385
+ if isinstance(chat_result, dict):
386
+ response = chat_result['response']
387
+ rag_result = chat_result.get('rag_result')
388
+ st.session_state.last_rag_result = rag_result
389
+
390
+ # Track RAG retrieval for feedback
391
+ if rag_result:
392
+ sources = rag_result.get('sources', []) if isinstance(rag_result, dict) else (rag_result.sources if hasattr(rag_result, 'sources') else [])
393
+
394
+ # Get the actual RAG query
395
+ actual_rag_query = chat_result.get('actual_rag_query', '')
396
+ if actual_rag_query:
397
+ # Format it like the log message
398
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
399
+ formatted_query = f"{timestamp} - INFO - πŸ” ACTUAL RAG QUERY: '{actual_rag_query}'"
400
+ else:
401
+ formatted_query = "No RAG query available"
402
+
403
+ retrieval_entry = {
404
+ "conversation_up_to": serialize_messages(st.session_state.messages),
405
+ "rag_query_expansion": formatted_query,
406
+ "docs_retrieved": serialize_documents(sources)
407
+ }
408
+ st.session_state.rag_retrieval_history.append(retrieval_entry)
409
+ else:
410
+ response = chat_result
411
+ st.session_state.last_rag_result = None
412
+
413
+ # Add bot response to history
414
+ st.session_state.messages.append(AIMessage(content=response))
415
+
416
+ except Exception as e:
417
+ error_msg = f"Sorry, I encountered an error: {str(e)}"
418
+ st.session_state.messages.append(AIMessage(content=error_msg))
419
+
420
+ # Clear input and rerun
421
+ st.session_state.input_counter += 1 # This will clear the input
422
+ st.rerun()
423
+
424
+ with tab2:
425
+ # Document retrieval panel
426
+ if hasattr(st.session_state, 'last_rag_result') and st.session_state.last_rag_result:
427
+ rag_result = st.session_state.last_rag_result
428
+
429
+ # Handle both PipelineResult object and dictionary formats
430
+ sources = None
431
+ if hasattr(rag_result, 'sources'):
432
+ # PipelineResult object format
433
+ sources = rag_result.sources
434
+ elif isinstance(rag_result, dict) and 'sources' in rag_result:
435
+ # Dictionary format from multi-agent system
436
+ sources = rag_result['sources']
437
+
438
+ if sources and len(sources) > 0:
439
+ # Count unique filenames
440
+ unique_filenames = set()
441
+ for doc in sources:
442
+ filename = getattr(doc, 'metadata', {}).get('filename', 'Unknown')
443
+ unique_filenames.add(filename)
444
+
445
+ st.markdown(f"**Found {len(sources)} document chunks from {len(unique_filenames)} unique documents (showing top 10):**")
446
+ if len(unique_filenames) < len(sources):
447
+ st.info(f"πŸ’‘ **Note**: Each document is split into multiple chunks. You're seeing {len(sources)} chunks from {len(unique_filenames)} documents.")
448
+
449
+ for i, doc in enumerate(sources[:10]): # Show top 10
450
+ # Get relevance score and ID if available
451
+ metadata = getattr(doc, 'metadata', {})
452
+ score = metadata.get('reranked_score', metadata.get('original_score', None))
453
+ chunk_id = metadata.get('_id', 'Unknown')
454
+ score_text = f" (Score: {score:.3f}, ID: {chunk_id[:8]}...)" if score is not None else f" (ID: {chunk_id[:8]}...)"
455
+
456
+ with st.expander(f"πŸ“„ Document {i+1}: {getattr(doc, 'metadata', {}).get('filename', 'Unknown')[:50]}...{score_text}"):
457
+ # Display document metadata with emojis
458
+ metadata = getattr(doc, 'metadata', {})
459
+ col1, col2, col3, col4 = st.columns([2, 1.5, 1, 1])
460
+
461
+ with col1:
462
+ st.write(f"πŸ“„ **File:** {metadata.get('filename', 'Unknown')}")
463
+ with col2:
464
+ st.write(f"πŸ›οΈ **Source:** {metadata.get('source', 'Unknown')}")
465
+ with col3:
466
+ st.write(f"πŸ“… **Year:** {metadata.get('year', 'Unknown')}")
467
+ with col4:
468
+ # Display page number and chunk ID
469
+ page = metadata.get('page_label', metadata.get('page', 'Unknown'))
470
+ chunk_id = metadata.get('_id', 'Unknown')
471
+ st.write(f"πŸ“– **Page:** {page}")
472
+ st.write(f"πŸ†” **ID:** {chunk_id}")
473
+
474
+ # Display full content (no truncation)
475
+ content = getattr(doc, 'page_content', 'No content available')
476
+ st.write(f"**Full Content:**")
477
+ st.text_area("Full Content", value=content, height=300, disabled=True, label_visibility="collapsed", key=f"preview_{i}")
478
+ else:
479
+ st.info("No documents were retrieved for the last query.")
480
+ else:
481
+ st.info("No documents have been retrieved yet. Start a conversation to see retrieved documents here.")
482
+
483
+ # Feedback Dashboard Section
484
+ st.markdown("---")
485
+ st.markdown("### πŸ’¬ Feedback Dashboard")
486
+
487
+ # Check if there's any conversation to provide feedback on
488
+ has_conversation = len(st.session_state.messages) > 0
489
+ has_retrievals = len(st.session_state.rag_retrieval_history) > 0
490
+
491
+ if not has_conversation:
492
+ st.info("πŸ’‘ Start a conversation to provide feedback!")
493
+ st.markdown("The feedback dashboard will be enabled once you begin chatting.")
494
+ else:
495
+ st.markdown("Help us improve by providing feedback on this conversation.")
496
+
497
+ # Initialize feedback state if not exists
498
+ if 'feedback_submitted' not in st.session_state:
499
+ st.session_state.feedback_submitted = False
500
+
501
+ # Feedback form
502
+ with st.form("feedback_form", clear_on_submit=False):
503
+ col1, col2 = st.columns([1, 1])
504
+
505
+ with col1:
506
+ feedback_score = st.slider(
507
+ "Rate this conversation (1-5)",
508
+ min_value=1,
509
+ max_value=5,
510
+ help="How satisfied are you with the conversation?"
511
+ )
512
+
513
+ with col2:
514
+ is_feedback_about_last_retrieval = st.checkbox(
515
+ "Feedback about last retrieval only",
516
+ value=True,
517
+ help="If checked, feedback applies to the most recent document retrieval"
518
+ )
519
+
520
+ open_ended_feedback = st.text_area(
521
+ "Your feedback (optional)",
522
+ placeholder="Tell us what went well or what could be improved...",
523
+ height=100
524
+ )
525
+
526
+ # Disable submit if no score selected
527
+ submit_disabled = feedback_score is None
528
+
529
+ submitted = st.form_submit_button(
530
+ "πŸ“€ Submit Feedback",
531
+ use_container_width=True,
532
+ disabled=submit_disabled
533
+ )
534
+
535
+ if submitted and not st.session_state.feedback_submitted:
536
+ # Log the feedback data being submitted
537
+ print("=" * 80)
538
+ print("πŸ”„ FEEDBACK SUBMISSION: Starting...")
539
+ print("=" * 80)
540
+ st.write("πŸ” **Debug: Feedback Data Being Submitted:**")
541
+
542
+ # Create feedback data dictionary
543
+ feedback_dict = {
544
+ "open_ended_feedback": open_ended_feedback,
545
+ "score": feedback_score,
546
+ "is_feedback_about_last_retrieval": is_feedback_about_last_retrieval,
547
+ "retrieved_data": st.session_state.rag_retrieval_history.copy() if st.session_state.rag_retrieval_history else [],
548
+ "conversation_id": st.session_state.conversation_id,
549
+ "timestamp": time.time(),
550
+ "message_count": len(st.session_state.messages),
551
+ "has_retrievals": has_retrievals,
552
+ "retrieval_count": len(st.session_state.rag_retrieval_history)
553
+ }
554
+
555
+ print(f"πŸ“ FEEDBACK SUBMISSION: Score={feedback_score}, Retrievals={len(st.session_state.rag_retrieval_history) if st.session_state.rag_retrieval_history else 0}")
556
+
557
+ # Create UserFeedback dataclass instance
558
+ feedback_obj = None # Initialize outside try block
559
+ try:
560
+ feedback_obj = create_feedback_from_dict(feedback_dict)
561
+ print(f"βœ… FEEDBACK SUBMISSION: Feedback object created - ID={feedback_obj.feedback_id}")
562
+ st.write(f"βœ… **Feedback Object Created**")
563
+ st.write(f"- Feedback ID: {feedback_obj.feedback_id}")
564
+ st.write(f"- Score: {feedback_obj.score}/5")
565
+ st.write(f"- Has Retrievals: {feedback_obj.has_retrievals}")
566
+
567
+ # Convert back to dict for JSON serialization
568
+ feedback_data = feedback_obj.to_dict()
569
+ except Exception as e:
570
+ print(f"❌ FEEDBACK SUBMISSION: Failed to create feedback object: {e}")
571
+ st.error(f"Failed to create feedback object: {e}")
572
+ feedback_data = feedback_dict
573
+
574
+ # Display the data being submitted
575
+ st.json(feedback_data)
576
+
577
+ # Save feedback to file
578
+ feedback_dir = Path("feedback")
579
+ feedback_dir.mkdir(exist_ok=True)
580
+
581
+ feedback_file = feedback_dir / f"feedback_{st.session_state.conversation_id}_{int(time.time())}.json"
582
+
583
+ try:
584
+ # Save to local file
585
+ print(f"πŸ’Ύ FEEDBACK SAVE: Saving to local file: {feedback_file}")
586
+ with open(feedback_file, 'w') as f:
587
+ json.dump(feedback_data, f, indent=2, default=str)
588
+
589
+ print(f"βœ… FEEDBACK SAVE: Local file saved successfully")
590
+ st.success("βœ… Thank you for your feedback! It has been saved locally.")
591
+ st.balloons()
592
+
593
+ # Save to Snowflake if enabled and credentials available
594
+ logger.info("πŸ”„ FEEDBACK SAVE: Starting Snowflake save process...")
595
+ logger.info(f"πŸ“Š FEEDBACK SAVE: feedback_obj={'exists' if feedback_obj else 'None'}")
596
+
597
+ try:
598
+ import os
599
+ snowflake_enabled = os.getenv("SNOWFLAKE_ENABLED", "false").lower() == "true"
600
+ logger.info(f"πŸ” SNOWFLAKE CHECK: enabled={snowflake_enabled}")
601
+
602
+ if snowflake_enabled:
603
+ if feedback_obj:
604
+ try:
605
+ from auditqa.reporting.snowflake_connector import save_to_snowflake
606
+ logger.info("πŸ“€ SNOWFLAKE UI: Attempting to save feedback to Snowflake...")
607
+ print("πŸ“€ SNOWFLAKE UI: Attempting to save feedback to Snowflake...") # Also print to terminal
608
+
609
+ if save_to_snowflake(feedback_obj):
610
+ logger.info("βœ… SNOWFLAKE UI: Successfully saved to Snowflake")
611
+ print("βœ… SNOWFLAKE UI: Successfully saved to Snowflake") # Also print to terminal
612
+ st.success("βœ… Feedback also saved to Snowflake!")
613
+ else:
614
+ logger.warning("⚠️ SNOWFLAKE UI: Save failed")
615
+ print("⚠️ SNOWFLAKE UI: Save failed") # Also print to terminal
616
+ st.warning("⚠️ Snowflake save failed, but local save succeeded")
617
+ except Exception as e:
618
+ logger.error(f"❌ SNOWFLAKE UI ERROR: {e}")
619
+ print(f"❌ SNOWFLAKE UI ERROR: {e}") # Also print to terminal
620
+ import traceback
621
+ traceback.print_exc() # Print full traceback to terminal
622
+ st.warning(f"⚠️ Could not save to Snowflake: {e}")
623
+ else:
624
+ logger.warning("⚠️ SNOWFLAKE UI: Skipping (feedback object not created)")
625
+ print("⚠️ SNOWFLAKE UI: Skipping (feedback object not created)") # Also print to terminal
626
+ st.warning("⚠️ Skipping Snowflake save (feedback object not created)")
627
+ else:
628
+ logger.info("πŸ’‘ SNOWFLAKE UI: Integration disabled")
629
+ print("πŸ’‘ SNOWFLAKE UI: Integration disabled") # Also print to terminal
630
+ st.info("πŸ’‘ Snowflake integration disabled (set SNOWFLAKE_ENABLED=true to enable)")
631
+ except NameError as e:
632
+ import traceback
633
+ traceback.print_exc()
634
+ logger.error(f"❌ NameError in Snowflake save: {e}")
635
+ print(f"❌ NameError in Snowflake save: {e}") # Also print to terminal
636
+ st.warning(f"⚠️ Snowflake save error: {e}")
637
+ except Exception as e:
638
+ logger.error(f"❌ Exception in Snowflake save: {type(e).__name__}: {e}")
639
+ print(f"❌ Exception in Snowflake save: {type(e).__name__}: {e}") # Also print to terminal
640
+ st.warning(f"⚠️ Snowflake save error: {e}")
641
+
642
+ # Mark feedback as submitted to prevent resubmission
643
+ st.session_state.feedback_submitted = True
644
+
645
+ print("=" * 80)
646
+ print(f"βœ… FEEDBACK SUBMISSION: Completed successfully")
647
+ print("=" * 80)
648
+
649
+ # Log file location
650
+ st.info(f"πŸ“ Feedback saved to: {feedback_file}")
651
+
652
+ except Exception as e:
653
+ print(f"❌ FEEDBACK SUBMISSION: Error saving feedback: {e}")
654
+ print(f"❌ FEEDBACK SUBMISSION: Error type: {type(e).__name__}")
655
+ import traceback
656
+ traceback.print_exc()
657
+ st.error(f"❌ Error saving feedback: {e}")
658
+ st.write(f"Debug error: {str(e)}")
659
+
660
+ elif st.session_state.feedback_submitted:
661
+ st.success("βœ… Feedback already submitted for this conversation!")
662
+ if st.button("πŸ”„ Submit New Feedback", key="new_feedback_button"):
663
+ st.session_state.feedback_submitted = False
664
+ st.rerun()
665
+
666
+ # Display retrieval history stats
667
+ if st.session_state.rag_retrieval_history:
668
+ st.markdown("---")
669
+ st.markdown("#### πŸ“Š Retrieval History")
670
+
671
+ with st.expander(f"View {len(st.session_state.rag_retrieval_history)} retrieval entries", expanded=False):
672
+ for idx, entry in enumerate(st.session_state.rag_retrieval_history, 1):
673
+ st.markdown(f"**Retrieval #{idx}**")
674
+
675
+ # Display the actual RAG query
676
+ rag_query_expansion = entry.get("rag_query_expansion", "No query available")
677
+ st.code(rag_query_expansion, language="text")
678
+
679
+ # Display summary stats
680
+ st.json({
681
+ "conversation_length": len(entry.get("conversation_up_to", [])),
682
+ "documents_retrieved": len(entry.get("docs_retrieved", []))
683
+ })
684
+ st.markdown("---")
685
+
686
+ # Auto-scroll to bottom
687
+ st.markdown("""
688
+ <script>
689
+ window.scrollTo(0, document.body.scrollHeight);
690
+ </script>
691
+ """, unsafe_allow_html=True)
692
+
693
+ if __name__ == "__main__":
694
+ main()