akryldigital commited on
Commit
04f2072
Β·
verified Β·
1 Parent(s): 21eb407

add extra columns for feedback functionality

Browse files
Files changed (1) hide show
  1. app.py +264 -239
app.py CHANGED
@@ -10,18 +10,19 @@ import uuid
10
  import logging
11
  import traceback
12
  from pathlib import Path
13
- from typing import List, Dict, Any
14
  from collections import Counter
 
 
15
 
16
- import streamlit as st
17
- from langchain_core.messages import HumanMessage, AIMessage
18
  import pandas as pd
 
19
  import plotly.express as px
 
20
 
21
  from multi_agent_chatbot import get_multi_agent_chatbot
22
  from smart_chatbot import get_chatbot as get_smart_chatbot
23
- from src.reporting.feedback_schema import create_feedback_from_dict
24
  from src.reporting.snowflake_connector import save_to_snowflake
 
25
  from src.config.paths import (
26
  IS_DEPLOYED,
27
  PROJECT_DIR,
@@ -98,6 +99,8 @@ st.markdown("""
98
  color: #1f77b4;
99
  text-align: center;
100
  margin-bottom: 1rem;
 
 
101
  }
102
 
103
  .subtitle {
@@ -105,54 +108,8 @@ st.markdown("""
105
  color: #666;
106
  text-align: center;
107
  margin-bottom: 2rem;
108
- }
109
-
110
- .example-questions-header {
111
- text-align: center;
112
- margin-bottom: 1rem;
113
- }
114
-
115
- .example-questions-description {
116
- text-align: center;
117
- color: #666;
118
- margin-bottom: 2rem;
119
- }
120
-
121
- /* Hide ALL default Streamlit text input help messages about Enter key */
122
- /* This is the key one - hides "Press Enter to apply" message inside input field */
123
- div[data-testid="InputInstructions"],
124
- span[data-testid="InputInstructions"],
125
- *[data-testid="InputInstructions"] {
126
- display: none !important;
127
- visibility: hidden !important;
128
- opacity: 0 !important;
129
- height: 0 !important;
130
- width: 0 !important;
131
- overflow: hidden !important;
132
- position: absolute !important;
133
- left: -9999px !important;
134
- }
135
-
136
- /* Also hide other potential locations */
137
- div[data-testid="stTextInput"] + div > small,
138
- div[data-testid="stTextInput"] ~ div > small,
139
- div[data-testid="stTextInputContainer"] + div > small,
140
- div[data-testid="stTextInputContainer"] ~ div > small,
141
- div[data-baseweb="input"] + div > small,
142
- div[data-baseweb="input"] ~ div > small {
143
- display: none !important;
144
- visibility: hidden !important;
145
- opacity: 0 !important;
146
- height: 0 !important;
147
- overflow: hidden !important;
148
- }
149
-
150
- /* Custom help text for input */
151
- .input-help-text {
152
- font-size: 0.85rem;
153
- color: #666;
154
- margin-top: 0.25rem;
155
- text-align: left;
156
  }
157
 
158
  .session-info {
@@ -304,6 +261,114 @@ def serialize_documents(sources):
304
 
305
  return serialized
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  def extract_chunk_statistics(sources: List[Any]) -> Dict[str, Any]:
308
  """Extract statistics from retrieved chunks."""
309
  if not sources:
@@ -483,7 +548,7 @@ def display_chunk_statistics_table(stats: Dict[str, Any], title: str = "Retrieva
483
  return
484
 
485
  # Wrap in styled container
486
- # st.markdown('<div class="retrieval-distribution-container">', unsafe_allow_html=True)
487
 
488
  st.subheader(f"πŸ“Š {title}")
489
 
@@ -600,7 +665,7 @@ def main():
600
  st.session_state.reset_conversation = False
601
  st.rerun()
602
 
603
- # Header - centered
604
  st.markdown('<h1 class="main-header">πŸ€– Intelligent Audit Report Chatbot</h1>', unsafe_allow_html=True)
605
  st.markdown('<p class="subtitle">Ask questions about audit reports. Use the sidebar filters to narrow down your search!</p>', unsafe_allow_html=True)
606
 
@@ -627,21 +692,15 @@ def main():
627
 
628
  2. **Leave filters empty** to search across all data
629
 
630
- 3. **Type your question** in the chat and click "Send"
631
 
632
- 4. **Choose sample questions from the bottom of the page**
633
 
634
  #### πŸ’‘ Tips
635
 
636
  - Use specific questions for better results
637
  - Combine multiple filters for precise searches
638
- - Check the "Retrieved Documents" tab to get various insights
639
-
640
- #### πŸ’¬ Feedback Section
641
-
642
- - Rate your experience (1-5 stars)
643
- - Provide optional text feedback
644
- - Located at the bottom of the page
645
 
646
  #### ⚠️ Important
647
 
@@ -666,13 +725,11 @@ def main():
666
  help="Choose specific reports to search. When enabled, all other filters are ignored."
667
  )
668
  st.markdown('</div>', unsafe_allow_html=True)
669
-
670
- st.markdown('---')
671
 
672
  # Determine if filename filter is active
673
  filename_mode = len(selected_filenames) > 0
674
  # Sources filter
675
- # st.markdown('<div class="filter-section">', unsafe_allow_html=True)
676
  st.markdown('<div class="filter-title">πŸ“Š Sources</div>', unsafe_allow_html=True)
677
  selected_sources = st.multiselect(
678
  "Select sources:",
@@ -767,67 +824,6 @@ def main():
767
  label_visibility="collapsed",
768
  value=default_value if default_value else None
769
  )
770
-
771
- # Use JavaScript to specifically target and hide "Press Enter to apply" message
772
- st.markdown("""
773
- <script>
774
- (function() {
775
- // Hide InputInstructions element (contains "Press Enter to apply")
776
- function hideInputInstructions() {
777
- // Target the specific Streamlit element
778
- const instructions = document.querySelector('[data-testid="InputInstructions"]');
779
- if (instructions) {
780
- instructions.style.display = 'none';
781
- instructions.style.visibility = 'hidden';
782
- instructions.style.opacity = '0';
783
- instructions.style.height = '0';
784
- instructions.style.width = '0';
785
- instructions.style.overflow = 'hidden';
786
- instructions.style.position = 'absolute';
787
- instructions.style.left = '-9999px';
788
- }
789
-
790
- // Also search for any text containing "Press Enter" or "apply" inside input containers
791
- const allElements = document.querySelectorAll('*');
792
- allElements.forEach(el => {
793
- const text = el.textContent || el.innerText || '';
794
- if ((text.toLowerCase().includes('press enter') ||
795
- text.toLowerCase().includes('enter to') ||
796
- text.toLowerCase().includes('to apply')) &&
797
- (el.tagName === 'SPAN' || el.tagName === 'DIV' || el.tagName === 'SMALL')) {
798
- const style = window.getComputedStyle(el);
799
- const fontSize = parseFloat(style.fontSize);
800
- // Hide if it's small text (likely help text)
801
- if (fontSize < 14 || el.hasAttribute('data-testid')) {
802
- el.style.display = 'none';
803
- el.style.visibility = 'hidden';
804
- el.style.height = '0';
805
- el.style.overflow = 'hidden';
806
- }
807
- }
808
- });
809
- }
810
-
811
- // Run immediately and after delays to catch dynamic elements
812
- hideInputInstructions();
813
- setTimeout(hideInputInstructions, 50);
814
- setTimeout(hideInputInstructions, 100);
815
- setTimeout(hideInputInstructions, 500);
816
-
817
- // Observe for new elements added by Streamlit
818
- const observer = new MutationObserver(function(mutations) {
819
- hideInputInstructions();
820
- });
821
- observer.observe(document.body, { childList: true, subtree: true, attributes: true });
822
- })();
823
- </script>
824
- """, unsafe_allow_html=True)
825
-
826
- # # Show custom help text below input - this replaces the default "Press Enter" message
827
- # st.markdown(
828
- # "<div class='input-help-text'>πŸ’‘ Press the <strong>Send</strong> button to submit your question</div>",
829
- # unsafe_allow_html=True
830
- # )
831
 
832
  with col2:
833
  send_button = st.button("Send", key="send_button", use_container_width=True)
@@ -930,8 +926,7 @@ def main():
930
  # Count unique filenames
931
  unique_filenames = set()
932
  for doc in sources:
933
- metadata = getattr(doc, 'metadata', {})
934
- filename = metadata.get('filename', 'Unknown')
935
  unique_filenames.add(filename)
936
 
937
  st.markdown(f"**Found {len(sources)} document chunks from {len(unique_filenames)} unique documents (showing top 20):**")
@@ -986,44 +981,6 @@ def main():
986
  st.info("No documents were retrieved for the last query.")
987
  else:
988
  st.info("No documents have been retrieved yet. Start a conversation to see retrieved documents here.")
989
-
990
- # Display retrieval history stats
991
- st.markdown("---")
992
- if st.session_state.rag_retrieval_history:
993
- st.markdown("#### πŸ“Š Retrieval History")
994
- st.markdown(f"This conversation has **{len(st.session_state.rag_retrieval_history)}** retrieval entries.")
995
-
996
- with st.expander(f"View {len(st.session_state.rag_retrieval_history)} retrieval entries", expanded=False):
997
- for idx, entry in enumerate(st.session_state.rag_retrieval_history, 1):
998
- with st.expander(f"Entry {idx}: {entry.get('rag_query_expansion', 'N/A')[:50]}...", expanded=False):
999
- st.markdown(f"**Query:** {entry.get('rag_query_expansion', 'N/A')}")
1000
- st.markdown(f"**Documents Retrieved:** {len(entry.get('docs_retrieved', []))}")
1001
-
1002
- # Show conversation up to this point
1003
- conversation = entry.get('conversation_up_to', [])
1004
- if conversation:
1005
- st.markdown("**Conversation Context:**")
1006
- for msg in conversation[-3:]: # Show last 3 messages
1007
- role = msg.get('type', 'unknown')
1008
- content = msg.get('content', '')[:200] + "..." if len(msg.get('content', '')) > 200 else msg.get('content', '')
1009
- if role == 'human':
1010
- st.markdown(f"- **You:** {content}")
1011
- elif role == 'ai':
1012
- st.markdown(f"- **Bot:** {content}")
1013
-
1014
- # Show retrieved documents summary
1015
- docs = entry.get('docs_retrieved', [])
1016
- if docs:
1017
- st.markdown("**Retrieved Documents:**")
1018
- for doc_idx, doc in enumerate(docs[:5], 1): # Show first 5
1019
- doc_meta = doc.get('metadata', {})
1020
- filename = doc_meta.get('filename', 'Unknown')[:50]
1021
- st.markdown(f"{doc_idx}. {filename}")
1022
- if len(docs) > 5:
1023
- st.markdown(f"... and {len(docs) - 5} more documents")
1024
- else:
1025
- st.markdown("---")
1026
- st.info("πŸ“Š Retrieval history will appear here after you start asking questions.")
1027
 
1028
  # Feedback Dashboard Section
1029
  st.markdown("---")
@@ -1085,17 +1042,39 @@ def main():
1085
  print("=" * 80)
1086
  st.write("πŸ” **Debug: Feedback Data Being Submitted:**")
1087
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1088
  # Create feedback data dictionary
1089
  feedback_dict = {
1090
  "open_ended_feedback": open_ended_feedback,
1091
  "score": feedback_score,
1092
  "is_feedback_about_last_retrieval": is_feedback_about_last_retrieval,
1093
- "retrieved_data": st.session_state.rag_retrieval_history.copy() if st.session_state.rag_retrieval_history else [],
1094
  "conversation_id": st.session_state.conversation_id,
1095
  "timestamp": time.time(),
1096
  "message_count": len(st.session_state.messages),
1097
  "has_retrievals": has_retrievals,
1098
- "retrieval_count": len(st.session_state.rag_retrieval_history)
 
 
 
 
1099
  }
1100
 
1101
  print(f"πŸ“ FEEDBACK SUBMISSION: Score={feedback_score}, Retrievals={len(st.session_state.rag_retrieval_history) if st.session_state.rag_retrieval_history else 0}")
@@ -1137,19 +1116,18 @@ def main():
1137
  # Ensure parent directory exists before writing
1138
  feedback_file.parent.mkdir(parents=True, mode=0o777, exist_ok=True)
1139
 
1140
- # Save to local file
1141
  print(f"πŸ’Ύ FEEDBACK SAVE: Saving to local file: {feedback_file}")
1142
  with open(feedback_file, 'w') as f:
1143
  json.dump(feedback_data, f, indent=2, default=str)
1144
 
1145
  print(f"βœ… FEEDBACK SAVE: Local file saved successfully")
1146
- st.success("βœ… Thank you for your feedback! It has been saved locally.")
1147
- st.balloons()
1148
 
1149
  # Save to Snowflake if enabled and credentials available
1150
  logger.info("πŸ”„ FEEDBACK SAVE: Starting Snowflake save process...")
1151
  logger.info(f"πŸ“Š FEEDBACK SAVE: feedback_obj={'exists' if feedback_obj else 'None'}")
1152
 
 
1153
  try:
1154
  snowflake_enabled = os.getenv("SNOWFLAKE_ENABLED", "false").lower() == "true"
1155
  logger.info(f"πŸ” SNOWFLAKE CHECK: enabled={snowflake_enabled}")
@@ -1160,36 +1138,39 @@ def main():
1160
  logger.info("πŸ“€ SNOWFLAKE UI: Attempting to save feedback to Snowflake...")
1161
  print("πŸ“€ SNOWFLAKE UI: Attempting to save feedback to Snowflake...")
1162
 
1163
- if save_to_snowflake(feedback_obj):
 
1164
  logger.info("βœ… SNOWFLAKE UI: Successfully saved to Snowflake")
1165
  print("βœ… SNOWFLAKE UI: Successfully saved to Snowflake")
1166
- st.success("βœ… Feedback also saved to Snowflake!")
1167
  else:
1168
  logger.warning("⚠️ SNOWFLAKE UI: Save failed")
1169
  print("⚠️ SNOWFLAKE UI: Save failed")
1170
- st.warning("⚠️ Snowflake save failed, but local save succeeded")
1171
  except Exception as e:
1172
  logger.error(f"❌ SNOWFLAKE UI ERROR: {e}")
1173
  print(f"❌ SNOWFLAKE UI ERROR: {e}")
1174
  traceback.print_exc()
1175
- st.warning(f"⚠️ Could not save to Snowflake: {e}")
1176
  else:
1177
  logger.warning("⚠️ SNOWFLAKE UI: Skipping (feedback object not created)")
1178
  print("⚠️ SNOWFLAKE UI: Skipping (feedback object not created)")
1179
- st.warning("⚠️ Skipping Snowflake save (feedback object not created)")
1180
  else:
1181
  logger.info("πŸ’‘ SNOWFLAKE UI: Integration disabled")
1182
  print("πŸ’‘ SNOWFLAKE UI: Integration disabled")
1183
- st.info("πŸ’‘ Snowflake integration disabled (set SNOWFLAKE_ENABLED=true to enable)")
1184
- except NameError as e:
1185
- traceback.print_exc()
1186
- logger.error(f"❌ NameError in Snowflake save: {e}")
1187
- print(f"❌ NameError in Snowflake save: {e}")
1188
- st.warning(f"⚠️ Snowflake save error: {e}")
1189
  except Exception as e:
1190
  logger.error(f"❌ Exception in Snowflake save: {type(e).__name__}: {e}")
1191
  print(f"❌ Exception in Snowflake save: {type(e).__name__}: {e}")
1192
- st.warning(f"⚠️ Snowflake save error: {e}")
 
 
 
 
 
 
 
1193
 
1194
  # Mark feedback as submitted to prevent resubmission
1195
  st.session_state.feedback_submitted = True
@@ -1225,16 +1206,30 @@ def main():
1225
  # Scroll to conversation - this is handled by the auto-scroll at bottom
1226
  pass
1227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1228
  # Example Questions Section
1229
  st.markdown("---")
1230
- st.markdown(
1231
- "<h3 class='example-questions-header'>πŸ’‘ Example Questions</h3>",
1232
- unsafe_allow_html=True
1233
- )
1234
- st.markdown(
1235
- "<p class='example-questions-description'>Click on any question below to use it, or modify the editable examples:</p>",
1236
- unsafe_allow_html=True
1237
- )
1238
 
1239
  # Initialize example question state
1240
  if 'custom_question_1' not in st.session_state:
@@ -1257,53 +1252,56 @@ def main():
1257
 
1258
  st.markdown("---")
1259
 
1260
- # Questions 2 & 3: Editable examples (collapsible, side by side)
1261
- with st.expander("#### ✏️ Customizable Questions (Edit and use)", expanded=False):
1262
- # Place questions side by side
1263
- col1, col2 = st.columns(2)
1264
-
1265
- # Question 2
1266
- with col1:
1267
- st.markdown("**Question 2:**")
1268
- custom_q1 = st.text_area(
1269
- "Edit question 2:",
1270
- value=st.session_state.custom_question_1,
1271
- height=100,
1272
- key="edit_question_2",
1273
- help="Modify this question to fit your needs, then click 'Use This Question'",
1274
- label_visibility="collapsed"
1275
- )
1276
- if st.button("πŸ“‹ Use Question 2", key="use_custom_1", use_container_width=True):
1277
- if custom_q1.strip():
1278
- st.session_state.pending_question = custom_q1.strip()
1279
- st.session_state.custom_question_1 = custom_q1.strip()
1280
- st.session_state.input_counter = (st.session_state.get('input_counter', 0) + 1) % 1000
1281
- st.rerun()
1282
- else:
1283
- st.warning("Please enter a question first!")
1284
- st.caption("πŸ’‘ Tip: Add specific details like dates, names, or amounts to get more precise answers")
1285
- st.info("πŸ’‘ **Filter to apply:** Select District(s) and Year(s) from sidebar panel")
1286
-
1287
- # Question 3
1288
- with col2:
1289
- st.markdown("**Question 3:**")
1290
- custom_q2 = st.text_area(
1291
- "Edit question 3:",
1292
- value=st.session_state.custom_question_2,
1293
- height=100,
1294
- key="edit_question_3",
1295
- help="Modify this question to fit your needs, then click 'Use This Question'",
1296
- label_visibility="collapsed"
1297
- )
1298
- if st.button("πŸ“‹ Use Question 3", key="use_custom_2", use_container_width=True):
1299
- if custom_q2.strip():
1300
- st.session_state.pending_question = custom_q2.strip()
1301
- st.session_state.custom_question_2 = custom_q2.strip()
1302
- st.session_state.input_counter = (st.session_state.get('input_counter', 0) + 1) % 1000
1303
- st.rerun()
1304
- else:
1305
- st.warning("Please enter a question first!")
1306
- st.caption("πŸ’‘ Tip: Use specific terms from the documents (e.g., 'PDM', 'SACCOs', 'FY 2022/23')")
 
 
 
1307
 
1308
 
1309
  # Store selected question for next render (handled in input section above)
@@ -1316,5 +1314,32 @@ def main():
1316
  </script>
1317
  """, unsafe_allow_html=True)
1318
 
 
1319
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1320
  main()
 
10
  import logging
11
  import traceback
12
  from pathlib import Path
 
13
  from collections import Counter
14
+ from typing import List, Dict, Any, Optional
15
+
16
 
 
 
17
  import pandas as pd
18
+ import streamlit as st
19
  import plotly.express as px
20
+ from langchain_core.messages import HumanMessage, AIMessage
21
 
22
  from multi_agent_chatbot import get_multi_agent_chatbot
23
  from smart_chatbot import get_chatbot as get_smart_chatbot
 
24
  from src.reporting.snowflake_connector import save_to_snowflake
25
+ from src.reporting.feedback_schema import create_feedback_from_dict
26
  from src.config.paths import (
27
  IS_DEPLOYED,
28
  PROJECT_DIR,
 
99
  color: #1f77b4;
100
  text-align: center;
101
  margin-bottom: 1rem;
102
+ width: 100%;
103
+ display: block;
104
  }
105
 
106
  .subtitle {
 
108
  color: #666;
109
  text-align: center;
110
  margin-bottom: 2rem;
111
+ width: 100%;
112
+ display: block;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
 
115
  .session-info {
 
261
 
262
  return serialized
263
 
264
+
265
+ def extract_transcript(messages: List[Any]) -> List[Dict[str, str]]:
266
+ """Extract transcript from messages - only user and bot messages, no extra metadata"""
267
+ transcript = []
268
+ for msg in messages:
269
+ if isinstance(msg, HumanMessage):
270
+ transcript.append({
271
+ "role": "user",
272
+ "content": str(msg.content) if hasattr(msg, 'content') else str(msg)
273
+ })
274
+ elif isinstance(msg, AIMessage):
275
+ transcript.append({
276
+ "role": "assistant",
277
+ "content": str(msg.content) if hasattr(msg, 'content') else str(msg)
278
+ })
279
+ return transcript
280
+
281
+
282
+ def build_retrievals_structure(rag_retrieval_history: List[Dict[str, Any]], messages: List[Any]) -> List[Dict[str, Any]]:
283
+ """Build retrievals structure from retrieval history"""
284
+ retrievals = []
285
+
286
+ for entry in rag_retrieval_history:
287
+ # Get the user message that triggered this retrieval
288
+ # The entry has conversation_up_to which includes messages up to that point
289
+ conversation_up_to = entry.get("conversation_up_to", [])
290
+
291
+ # Find the last user message in conversation_up_to (this is the trigger)
292
+ user_message_trigger = ""
293
+ for msg_dict in reversed(conversation_up_to):
294
+ if msg_dict.get("type") == "HumanMessage":
295
+ user_message_trigger = msg_dict.get("content", "")
296
+ break
297
+
298
+ # Fallback: if not found in conversation_up_to, get from actual messages
299
+ # This handles edge cases where conversation_up_to might be incomplete
300
+ if not user_message_trigger:
301
+ # Find which retrieval this is (0-indexed)
302
+ retrieval_idx = rag_retrieval_history.index(entry)
303
+ # The user message that triggered this retrieval is at position (retrieval_idx * 2)
304
+ # because each retrieval is preceded by: user message, bot response, user message, ...
305
+ # But we need to account for the fact that the first retrieval happens after the first user message
306
+ user_msgs = [msg for msg in messages if isinstance(msg, HumanMessage)]
307
+ if retrieval_idx < len(user_msgs):
308
+ user_message_trigger = str(user_msgs[retrieval_idx].content)
309
+ elif user_msgs:
310
+ # Fallback to last user message
311
+ user_message_trigger = str(user_msgs[-1].content)
312
+
313
+ # Get retrieved documents and truncate content to 100 chars
314
+ docs_retrieved = entry.get("docs_retrieved", [])
315
+ retrieved_docs = []
316
+ for doc in docs_retrieved:
317
+ doc_copy = doc.copy()
318
+ # Truncate content to 100 characters (keep all other fields)
319
+ if "content" in doc_copy:
320
+ doc_copy["content"] = doc_copy["content"][:100]
321
+ retrieved_docs.append(doc_copy)
322
+
323
+ retrievals.append({
324
+ "retrieved_docs": retrieved_docs,
325
+ "user_message_trigger": user_message_trigger
326
+ })
327
+
328
+ return retrievals
329
+
330
+
331
+ def build_feedback_score_related_retrieval_docs(
332
+ is_feedback_about_last_retrieval: bool,
333
+ messages: List[Any],
334
+ rag_retrieval_history: List[Dict[str, Any]]
335
+ ) -> Optional[Dict[str, Any]]:
336
+ """Build feedback_score_related_retrieval_docs structure"""
337
+ if not rag_retrieval_history:
338
+ return None
339
+
340
+ # Get the relevant retrieval entry
341
+ if is_feedback_about_last_retrieval:
342
+ relevant_entry = rag_retrieval_history[-1]
343
+ else:
344
+ # If feedback is about all retrievals, use the last one as default
345
+ relevant_entry = rag_retrieval_history[-1]
346
+
347
+ # Get conversation up to that point
348
+ conversation_up_to = relevant_entry.get("conversation_up_to", [])
349
+
350
+ # Convert to transcript format (role/content)
351
+ conversation_up_to_point = []
352
+ for msg_dict in conversation_up_to:
353
+ if msg_dict.get("type") == "HumanMessage":
354
+ conversation_up_to_point.append({
355
+ "role": "user",
356
+ "content": msg_dict.get("content", "")
357
+ })
358
+ elif msg_dict.get("type") == "AIMessage":
359
+ conversation_up_to_point.append({
360
+ "role": "assistant",
361
+ "content": msg_dict.get("content", "")
362
+ })
363
+
364
+ # Get retrieved docs with full content (not truncated)
365
+ retrieved_docs = relevant_entry.get("docs_retrieved", [])
366
+
367
+ return {
368
+ "conversation_up_to_point": conversation_up_to_point,
369
+ "retrieved_docs": retrieved_docs
370
+ }
371
+
372
  def extract_chunk_statistics(sources: List[Any]) -> Dict[str, Any]:
373
  """Extract statistics from retrieved chunks."""
374
  if not sources:
 
548
  return
549
 
550
  # Wrap in styled container
551
+ st.markdown('<div class="retrieval-distribution-container">', unsafe_allow_html=True)
552
 
553
  st.subheader(f"πŸ“Š {title}")
554
 
 
665
  st.session_state.reset_conversation = False
666
  st.rerun()
667
 
668
+ # Header - fully center aligned
669
  st.markdown('<h1 class="main-header">πŸ€– Intelligent Audit Report Chatbot</h1>', unsafe_allow_html=True)
670
  st.markdown('<p class="subtitle">Ask questions about audit reports. Use the sidebar filters to narrow down your search!</p>', unsafe_allow_html=True)
671
 
 
692
 
693
  2. **Leave filters empty** to search across all data
694
 
695
+ 3. **Type your question** in the chat input at the bottom
696
 
697
+ 4. **Click "Send"** to submit your question
698
 
699
  #### πŸ’‘ Tips
700
 
701
  - Use specific questions for better results
702
  - Combine multiple filters for precise searches
703
+ - Check the "Retrieved Documents" tab to see source material
 
 
 
 
 
 
704
 
705
  #### ⚠️ Important
706
 
 
725
  help="Choose specific reports to search. When enabled, all other filters are ignored."
726
  )
727
  st.markdown('</div>', unsafe_allow_html=True)
 
 
728
 
729
  # Determine if filename filter is active
730
  filename_mode = len(selected_filenames) > 0
731
  # Sources filter
732
+ st.markdown('<div class="filter-section">', unsafe_allow_html=True)
733
  st.markdown('<div class="filter-title">πŸ“Š Sources</div>', unsafe_allow_html=True)
734
  selected_sources = st.multiselect(
735
  "Select sources:",
 
824
  label_visibility="collapsed",
825
  value=default_value if default_value else None
826
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827
 
828
  with col2:
829
  send_button = st.button("Send", key="send_button", use_container_width=True)
 
926
  # Count unique filenames
927
  unique_filenames = set()
928
  for doc in sources:
929
+ filename = getattr(doc, 'metadata', {}).get('filename', 'Unknown')
 
930
  unique_filenames.add(filename)
931
 
932
  st.markdown(f"**Found {len(sources)} document chunks from {len(unique_filenames)} unique documents (showing top 20):**")
 
981
  st.info("No documents were retrieved for the last query.")
982
  else:
983
  st.info("No documents have been retrieved yet. Start a conversation to see retrieved documents here.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
 
985
  # Feedback Dashboard Section
986
  st.markdown("---")
 
1042
  print("=" * 80)
1043
  st.write("πŸ” **Debug: Feedback Data Being Submitted:**")
1044
 
1045
+ # Extract transcript from messages
1046
+ transcript = extract_transcript(st.session_state.messages)
1047
+
1048
+ # Build retrievals structure
1049
+ retrievals = build_retrievals_structure(
1050
+ st.session_state.rag_retrieval_history.copy() if st.session_state.rag_retrieval_history else [],
1051
+ st.session_state.messages
1052
+ )
1053
+
1054
+ # Build feedback_score_related_retrieval_docs
1055
+ feedback_score_related_retrieval_docs = build_feedback_score_related_retrieval_docs(
1056
+ is_feedback_about_last_retrieval,
1057
+ st.session_state.messages,
1058
+ st.session_state.rag_retrieval_history.copy() if st.session_state.rag_retrieval_history else []
1059
+ )
1060
+
1061
+ # Preserve old retrieved_data format for backward compatibility
1062
+ retrieved_data_old_format = st.session_state.rag_retrieval_history.copy() if st.session_state.rag_retrieval_history else []
1063
+
1064
  # Create feedback data dictionary
1065
  feedback_dict = {
1066
  "open_ended_feedback": open_ended_feedback,
1067
  "score": feedback_score,
1068
  "is_feedback_about_last_retrieval": is_feedback_about_last_retrieval,
 
1069
  "conversation_id": st.session_state.conversation_id,
1070
  "timestamp": time.time(),
1071
  "message_count": len(st.session_state.messages),
1072
  "has_retrievals": has_retrievals,
1073
+ "retrieval_count": len(st.session_state.rag_retrieval_history) if st.session_state.rag_retrieval_history else 0,
1074
+ "transcript": transcript,
1075
+ "retrievals": retrievals,
1076
+ "feedback_score_related_retrieval_docs": feedback_score_related_retrieval_docs,
1077
+ "retrieved_data": retrieved_data_old_format # Preserved old column
1078
  }
1079
 
1080
  print(f"πŸ“ FEEDBACK SUBMISSION: Score={feedback_score}, Retrievals={len(st.session_state.rag_retrieval_history) if st.session_state.rag_retrieval_history else 0}")
 
1116
  # Ensure parent directory exists before writing
1117
  feedback_file.parent.mkdir(parents=True, mode=0o777, exist_ok=True)
1118
 
1119
+ # Save to local file first
1120
  print(f"πŸ’Ύ FEEDBACK SAVE: Saving to local file: {feedback_file}")
1121
  with open(feedback_file, 'w') as f:
1122
  json.dump(feedback_data, f, indent=2, default=str)
1123
 
1124
  print(f"βœ… FEEDBACK SAVE: Local file saved successfully")
 
 
1125
 
1126
  # Save to Snowflake if enabled and credentials available
1127
  logger.info("πŸ”„ FEEDBACK SAVE: Starting Snowflake save process...")
1128
  logger.info(f"πŸ“Š FEEDBACK SAVE: feedback_obj={'exists' if feedback_obj else 'None'}")
1129
 
1130
+ snowflake_success = False
1131
  try:
1132
  snowflake_enabled = os.getenv("SNOWFLAKE_ENABLED", "false").lower() == "true"
1133
  logger.info(f"πŸ” SNOWFLAKE CHECK: enabled={snowflake_enabled}")
 
1138
  logger.info("πŸ“€ SNOWFLAKE UI: Attempting to save feedback to Snowflake...")
1139
  print("πŸ“€ SNOWFLAKE UI: Attempting to save feedback to Snowflake...")
1140
 
1141
+ snowflake_success = save_to_snowflake(feedback_obj)
1142
+ if snowflake_success:
1143
  logger.info("βœ… SNOWFLAKE UI: Successfully saved to Snowflake")
1144
  print("βœ… SNOWFLAKE UI: Successfully saved to Snowflake")
 
1145
  else:
1146
  logger.warning("⚠️ SNOWFLAKE UI: Save failed")
1147
  print("⚠️ SNOWFLAKE UI: Save failed")
 
1148
  except Exception as e:
1149
  logger.error(f"❌ SNOWFLAKE UI ERROR: {e}")
1150
  print(f"❌ SNOWFLAKE UI ERROR: {e}")
1151
  traceback.print_exc()
1152
+ snowflake_success = False
1153
  else:
1154
  logger.warning("⚠️ SNOWFLAKE UI: Skipping (feedback object not created)")
1155
  print("⚠️ SNOWFLAKE UI: Skipping (feedback object not created)")
1156
+ snowflake_success = False
1157
  else:
1158
  logger.info("πŸ’‘ SNOWFLAKE UI: Integration disabled")
1159
  print("πŸ’‘ SNOWFLAKE UI: Integration disabled")
1160
+ # If Snowflake is disabled, consider it successful (local save only)
1161
+ snowflake_success = True
1162
+
 
 
 
1163
  except Exception as e:
1164
  logger.error(f"❌ Exception in Snowflake save: {type(e).__name__}: {e}")
1165
  print(f"❌ Exception in Snowflake save: {type(e).__name__}: {e}")
1166
+ snowflake_success = False
1167
+
1168
+ # Only show success if Snowflake save succeeded (or if Snowflake is disabled)
1169
+ if snowflake_success:
1170
+ st.success("βœ… Thank you for your feedback! It has been saved successfully.")
1171
+ st.balloons()
1172
+ else:
1173
+ st.warning("⚠️ Feedback saved locally, but Snowflake save failed. Please check logs.")
1174
 
1175
  # Mark feedback as submitted to prevent resubmission
1176
  st.session_state.feedback_submitted = True
 
1206
  # Scroll to conversation - this is handled by the auto-scroll at bottom
1207
  pass
1208
 
1209
+ # Display retrieval history stats
1210
+ if st.session_state.rag_retrieval_history:
1211
+ st.markdown("---")
1212
+ st.markdown("#### πŸ“Š Retrieval History")
1213
+
1214
+ with st.expander(f"View {len(st.session_state.rag_retrieval_history)} retrieval entries", expanded=False):
1215
+ for idx, entry in enumerate(st.session_state.rag_retrieval_history, 1):
1216
+ st.markdown(f"**Retrieval #{idx}**")
1217
+
1218
+ # Display the actual RAG query
1219
+ rag_query_expansion = entry.get("rag_query_expansion", "No query available")
1220
+ st.code(rag_query_expansion, language="text")
1221
+
1222
+ # Display summary stats
1223
+ st.json({
1224
+ "conversation_length": len(entry.get("conversation_up_to", [])),
1225
+ "documents_retrieved": len(entry.get("docs_retrieved", []))
1226
+ })
1227
+ st.markdown("---")
1228
+
1229
  # Example Questions Section
1230
  st.markdown("---")
1231
+ st.markdown("### πŸ’‘ Example Questions")
1232
+ st.markdown("Click on any question below to use it, or modify the editable examples:")
 
 
 
 
 
 
1233
 
1234
  # Initialize example question state
1235
  if 'custom_question_1' not in st.session_state:
 
1252
 
1253
  st.markdown("---")
1254
 
1255
+ # Questions 2 & 3: Editable examples
1256
+ st.markdown("#### ✏️ Customizable Questions (Edit and use)")
1257
+
1258
+ # Question 2
1259
+ # st.markdown("**Question 2:**")
1260
+ custom_q1 = st.text_area(
1261
+ "Edit question 2:",
1262
+ value=st.session_state.custom_question_1,
1263
+ height=80,
1264
+ key="edit_question_2",
1265
+ help="Modify this question to fit your needs, then click 'Use This Question'"
1266
+ )
1267
+ col1, col2 = st.columns([1, 4])
1268
+ with col1:
1269
+ if st.button("πŸ“‹ Use Question 2", key="use_custom_1", use_container_width=True):
1270
+ if custom_q1.strip():
1271
+ st.session_state.pending_question = custom_q1.strip()
1272
+ st.session_state.custom_question_1 = custom_q1.strip()
1273
+ st.session_state.input_counter = (st.session_state.get('input_counter', 0) + 1) % 1000
1274
+ st.rerun()
1275
+ else:
1276
+ st.warning("Please enter a question first!")
1277
+ with col2:
1278
+ st.caption("πŸ’‘ Tip: Add specific details like dates, names, or amounts to get more precise answers")
1279
+
1280
+ st.info("πŸ’‘ **Filter to apply:** Select District(s) and Year(s) sidebar panel before asking this question.")
1281
+
1282
+ st.markdown("---")
1283
+
1284
+ # Question 3
1285
+ # st.markdown("**Question 3:**")
1286
+ custom_q2 = st.text_area(
1287
+ "Edit question 3:",
1288
+ value=st.session_state.custom_question_2,
1289
+ height=80,
1290
+ key="edit_question_3",
1291
+ help="Modify this question to fit your needs, then click 'Use This Question'"
1292
+ )
1293
+ col1, col2 = st.columns([1, 4])
1294
+ with col1:
1295
+ if st.button("πŸ“‹ Use Question 3", key="use_custom_2", use_container_width=True):
1296
+ if custom_q2.strip():
1297
+ st.session_state.pending_question = custom_q2.strip()
1298
+ st.session_state.custom_question_2 = custom_q2.strip()
1299
+ st.session_state.input_counter = (st.session_state.get('input_counter', 0) + 1) % 1000
1300
+ st.rerun()
1301
+ else:
1302
+ st.warning("Please enter a question first!")
1303
+ with col2:
1304
+ st.caption("πŸ’‘ Tip: Use specific terms from the documents (e.g., 'PDM', 'SACCOs', 'FY 2022/23')")
1305
 
1306
 
1307
  # Store selected question for next render (handled in input section above)
 
1314
  </script>
1315
  """, unsafe_allow_html=True)
1316
 
1317
+
1318
  if __name__ == "__main__":
1319
+ # Check if running in Streamlit context
1320
+ try:
1321
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
1322
+ if get_script_run_ctx() is None:
1323
+ # Not in Streamlit runtime - show helpful message
1324
+ print("=" * 80)
1325
+ print("⚠️ WARNING: This is a Streamlit app!")
1326
+ print("=" * 80)
1327
+ print("\nPlease run this app using:")
1328
+ print(" streamlit run app.py")
1329
+ print("\nNot: python app.py")
1330
+ print("\nThe app will not function correctly when run with 'python app.py'")
1331
+ print("=" * 80)
1332
+ import sys
1333
+ sys.exit(1)
1334
+ except ImportError:
1335
+ # Streamlit not installed or not in Streamlit context
1336
+ print("=" * 80)
1337
+ print("⚠️ WARNING: This is a Streamlit app!")
1338
+ print("=" * 80)
1339
+ print("\nPlease run this app using:")
1340
+ print(" streamlit run app.py")
1341
+ print("\nNot: python app.py")
1342
+ print("=" * 80)
1343
+ import sys
1344
+ sys.exit(1)
1345
  main()