Spaces:
Sleeping
Sleeping
add extra columns for feedback functionality
Browse files
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 |
-
|
| 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 -
|
| 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
|
| 631 |
|
| 632 |
-
4. **
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 1176 |
else:
|
| 1177 |
logger.warning("β οΈ SNOWFLAKE UI: Skipping (feedback object not created)")
|
| 1178 |
print("β οΈ SNOWFLAKE UI: Skipping (feedback object not created)")
|
| 1179 |
-
|
| 1180 |
else:
|
| 1181 |
logger.info("π‘ SNOWFLAKE UI: Integration disabled")
|
| 1182 |
print("π‘ SNOWFLAKE UI: Integration disabled")
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 1261 |
-
|
| 1262 |
-
|
| 1263 |
-
|
| 1264 |
-
|
| 1265 |
-
|
| 1266 |
-
|
| 1267 |
-
|
| 1268 |
-
|
| 1269 |
-
|
| 1270 |
-
|
| 1271 |
-
|
| 1272 |
-
|
| 1273 |
-
|
| 1274 |
-
|
| 1275 |
-
)
|
| 1276 |
-
|
| 1277 |
-
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
|
| 1282 |
-
|
| 1283 |
-
|
| 1284 |
-
|
| 1285 |
-
|
| 1286 |
-
|
| 1287 |
-
|
| 1288 |
-
|
| 1289 |
-
|
| 1290 |
-
|
| 1291 |
-
|
| 1292 |
-
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
|
| 1297 |
-
|
| 1298 |
-
|
| 1299 |
-
|
| 1300 |
-
|
| 1301 |
-
|
| 1302 |
-
|
| 1303 |
-
|
| 1304 |
-
|
| 1305 |
-
|
| 1306 |
-
|
|
|
|
|
|
|
|
|
|
| 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()
|