File size: 16,653 Bytes
c05de71
8274ba3
c05de71
8274ba3
 
 
 
 
e3be075
8274ba3
 
 
c05de71
8274ba3
 
 
 
 
c05de71
8274ba3
 
 
 
 
 
e3be075
 
 
8274ba3
 
 
 
e3be075
 
8274ba3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3be075
 
 
8274ba3
 
 
 
 
 
 
 
 
 
 
 
 
 
c05de71
8274ba3
 
 
 
 
 
c05de71
8274ba3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c05de71
e3be075
775d896
 
8274ba3
 
e3be075
8274ba3
e3be075
 
8274ba3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3be075
8274ba3
e3be075
 
8274ba3
 
 
 
e3be075
 
8274ba3
e3be075
8274ba3
e3be075
 
 
 
 
8274ba3
 
 
 
 
 
 
 
 
c05de71
8274ba3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c05de71
8274ba3
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
"""
Frontend Streamlit application for the chatbot.
"""
import os
os.environ["XDG_CACHE_HOME"] = "/tmp" # Fix the cloud bug
os.environ["HF_HOME"] = "/tmp" # Fix the cloud bug
os.environ["TRANSFORMERS_CACHE"] = "/tmp" # Fix the cloud bug
os.environ["LLAMA_INDEX_CACHE_DIR"] = "/tmp" # Fix the cloud bug
import streamlit as st
import time
import logging
import backend
import config
from config import APP_NAME, PRIMARY_COLOR, SECONDARY_COLOR
from utils.logging_config import setup_logging
from utils.feedback import feedback_manager  # Import the feedback manager
import hashlib
from datetime import datetime

# Set up logging
setup_logging()
logger = logging.getLogger(__name__)

# Constants for memory optimization
MAX_MESSAGES = 50

# Set page config
st.set_page_config(
    page_title=APP_NAME,
    page_icon="πŸ’¬",
    layout="centered",
    initial_sidebar_state="collapsed"
)

# Hide Streamlit's default elements and style sidebar
st.markdown("""
<style>
/* Hide default elements */
button[kind="deploy"], 
[data-testid="stToolbar"],
.stDeployButton,
#MainMenu,
footer {
    display: none !important;
}

/* Style the sidebar */
[data-testid="stSidebar"] {
    width: 120px !important;
    background-color: #0e1117 !important;
    border-right: 1px solid #1e1e1e !important;
}

/* Style the collapse button to prevent movement and maintain size */
button[kind="menuButton"] {
    left: 120px !important;
    margin-left: 0 !important;
    position: fixed !important;
    transform: translateX(0) !important;
    transition: none !important;
    background-color: #0e1117 !important;
    color: #fff !important;
    z-index: 999 !important;
}

/* Button styling */
[data-testid="stSidebar"] [data-testid="stButton"] button {
    background-color: #262730 !important;
    color: white !important;
    padding: 8px 10px !important;
    width: 100px !important;
    font-size: 0.9rem !important;
    margin: 1rem auto !important;
    display: block !important;
    border-radius: 4px !important;
    white-space: nowrap !important;
}

[data-testid="stSidebar"] [data-testid="stButton"] button p {
    text-align: center !important;
    white-space: nowrap !important;
    overflow: visible !important;
}

[data-testid="stSidebar"] [data-testid="stButton"] button:hover {
    background-color: #1E2130 !important;
}

/* Feedback button styles */
.feedback-buttons {
    display: flex;
    gap: 10px;
    margin-top: 5px;
}
.feedback-button {
    border: none;
    background: none;
    cursor: pointer;
    padding: 5px;
    border-radius: 4px;
    transition: background-color 0.3s;
}
.feedback-button:hover {
    background-color: #f0f0f0;
}
.feedback-form {
    margin-top: 10px;
    padding: 10px;
    border-radius: 4px;
    background-color: #f7f7f7;
}

/* Custom radio buttons */
.stRadio > div {
    display: flex;
    gap: 10px;
}
.stRadio > div > div {
    flex: 1;
}
</style>
""", unsafe_allow_html=True)

# Helper function to add messages and maintain history length
def add_message(role, content):
    """Add message to session state and trim if needed."""
    # Generate a unique ID for this message based on content
    message_id = f"{role}_{hashlib.md5(content.encode()).hexdigest()[:8]}"
    
    st.session_state.messages.append({
        "role": role, 
        "content": content,
        "id": message_id  # Store the ID with the message
    })
    
    # Trim message history if it gets too large
    if len(st.session_state.messages) > MAX_MESSAGES:
        # Keep the most recent messages
        st.session_state.messages = st.session_state.messages[-MAX_MESSAGES:]
    logger.info(f"Added {role} message with ID {message_id}. History length: {len(st.session_state.messages)}")

# Initialize session state for chat history and UI control
if "messages" not in st.session_state:
    st.session_state.messages = []

if "clear_clicked" not in st.session_state:
    st.session_state.clear_clicked = False

if "processing" not in st.session_state:
    st.session_state.processing = False

if "feedback" not in st.session_state:
    st.session_state.feedback = {}  # Store feedback data by message index

# Create a sidebar with button options
with st.sidebar:
    def clear_chat():
        # Clear messages in session state
        st.session_state.messages = []
        
        # Also clear the chatbot's memory if it exists
        if "chatbot" in st.session_state and hasattr(st.session_state.chatbot, "reset_chat_history"):
            st.session_state.chatbot.reset_chat_history()
            
        # Set flag to true to trigger page refresh
        st.session_state.clear_clicked = True
        
        logger.info("Chat history cleared via sidebar button")
    
    # Define stop processing function
    def stop_processing():
        st.session_state.processing = False
        logger.info("User requested to stop processing")
        st.rerun()
        
    st.button("Clear Chat", on_click=clear_chat)
    
    # Only show Stop button when processing
    if st.session_state.processing:
        st.button("Stop", on_click=stop_processing, type="primary")

# Handle the clear button click by refreshing the page
if st.session_state.clear_clicked:
    st.session_state.clear_clicked = False
    st.rerun()

# Initialize cached resources for all sessions
@st.cache_resource
def initialize_resources():
    """Initialize shared resources once for all sessions."""
    logger.info("Initializing shared resources...")
    
    # Get configuration from config module
    chatbot_config = config.get_chatbot_config()
    api_key = os.getenv("ANTHROPIC_API_KEY")
    
    # Load models using cache - using the backend module directly
    llm = backend.load_llm_model(
        api_key,
        chatbot_config.get("model", "claude-3-7-sonnet-20250219"),
        chatbot_config.get("temperature", 0.1),
        chatbot_config.get("max_tokens", 2048)
    )
    
    embed_model = backend.load_embedding_model(
        chatbot_config.get("embedding_model", "sentence-transformers/all-MiniLM-L6-v2"),
        chatbot_config.get("device", "cpu"),
        chatbot_config.get("embed_batch_size", 8)
    )
    
    # Load or create index (shared across sessions)
    index = backend.load_or_create_index()
    
    return {
        "config": chatbot_config,
        "llm": llm,
        "embed_model": embed_model,
        "index": index
    }

# Get shared resources
resources = initialize_resources()

# Initialize chatbot only in session state if it doesn't exist
if "chatbot" not in st.session_state:
    with st.spinner("Initializing chatbot..."):
        logger.info("Initializing chatbot with shared resources...")
        st.session_state.chatbot = backend.Chatbot(
            resources["config"],
            resources["llm"],
            resources["embed_model"],
            resources["index"]
        )
        # Initialize chat engine instead of query engine
        st.session_state.chatbot.initialize_chat_engine()
        logger.info("Chatbot initialized successfully")

# Only show header/description if there are no user messages yet
if not any(msg["role"] == "user" for msg in st.session_state.messages):
    st.title("Paul's Chatbot v0.5")
    st.markdown("""
This chatbot can answer questions about your documents. 
Ask any question about the content in your documents! This demo is focused on a psychology book. Ask it questions about how the human brain works, how to stick with a habit, or how we are easily fooled.
""")

# Debug information - only show if DEBUG is enabled
if config.DEBUG:
    st.sidebar.write("Debug Info:")
    st.sidebar.write(f"Number of messages: {len(st.session_state.messages)}")
    
    # Show message IDs
    message_ids = [msg.get("id", "no-id") for msg in st.session_state.messages if msg["role"] == "assistant"]
    st.sidebar.write(f"Assistant message IDs: {message_ids}")
    
    # Show feedback state in a more readable format
    if st.session_state.feedback:
        st.sidebar.write("Feedback state:")
        for msg_id, feedback in st.session_state.feedback.items():
            st.sidebar.write(f"- {msg_id}: {feedback}")
    else:
        st.sidebar.write("No feedback recorded yet")
        
    st.sidebar.write(f"Processing: {st.session_state.processing}")

# Function to handle feedback with the feedback manager
def handle_feedback(message_id, feedback_type, query, response):
    logger.info(f"Handling {feedback_type} feedback for message ID: {message_id}")
    if feedback_type == "positive":
        feedback_data = {
            "user_id": "",  # Add user tracking if available
            "query": query,
            "normalized_query": query.lower().strip() if query else "",
            "question_hash": hashlib.md5(query.lower().strip().encode()).hexdigest()[:8] if query else "",
            "response": response,
            "rating": "positive",
            "category": "",
            "comment": "",
            "tags": "",
            "status": "open",
            "admin_note": "",
            "assigned_to": "",
            "document_id": "",
            "source": "user",
            "priority": "low",
            "reviewed": False,
            "timestamp": datetime.now().isoformat()
        }
        success = feedback_manager.save_feedback_supabase(feedback_data)
        if not success:
            logger.error(f"Failed to save positive feedback for message {message_id}")
        st.session_state.feedback[message_id] = {
            "rating": "positive",
            "submitted": True
        }
        logger.info(f"Positive feedback saved for message {message_id}")
    else:
        st.session_state.feedback[message_id] = {
            "rating": "negative",
            "show_form": True,
            "submitted": False
        }
        logger.info(f"Negative feedback started for message {message_id}, showing form")
    logger.info(f"Current feedback state: {st.session_state.feedback}")

# Function to submit detailed negative feedback
def submit_negative_feedback(message_id, category, comment, query, response):
    feedback_data = {
        "user_id": "",  # Add user tracking if available
        "query": query,
        "normalized_query": query.lower().strip() if query else "",
        "question_hash": hashlib.md5(query.lower().strip().encode()).hexdigest()[:8] if query else "",
        "response": response,
        "rating": "negative",
        "category": category,
        "comment": comment,
        "tags": category.lower().replace(" ", "_") if category else "",
        "status": "open",
        "admin_note": "",
        "assigned_to": "",
        "document_id": "",
        "source": "user",
        "priority": "medium",
        "reviewed": False,
        "timestamp": datetime.now().isoformat()
    }
    success = feedback_manager.save_feedback_supabase(feedback_data)
    if not success:
        logger.error(f"Failed to save negative feedback for message {message_id}")
    st.session_state.feedback[message_id] = {
        "rating": "negative",
        "submitted": True,
        "show_form": False
    }
    logger.info(f"Negative feedback with category '{category}' submitted for message {message_id}")

# Function to generate visible feedback UI for a message
def display_feedback_ui(message, index):
    """Generate and display feedback UI for an assistant message"""
    message_id = message.get("id", f"msg_{index}")  # Fallback for old messages
    
    # Check if feedback exists for THIS specific message
    has_feedback = message_id in st.session_state.feedback
    feedback_data = st.session_state.feedback.get(message_id, {})
    
    # Only show thank you message if feedback was explicitly submitted for THIS message
    if has_feedback and feedback_data.get("submitted") == True:
        # Show thank you message if feedback was submitted
        st.success("Thank you for your feedback!")
    else:
        # Add a little vertical space
        st.write("")
        
        # Create more spaced columns for the feedback buttons
        feedback_cols = st.columns([0.7, 0.7, 8.6])
        
        with feedback_cols[0]:
            # Only disable button if positive feedback was given for THIS message
            thumbs_up_disabled = has_feedback and feedback_data.get("rating") == "positive"
            if st.button("πŸ‘", key=f"up_{message_id}", disabled=thumbs_up_disabled):
                handle_feedback(message_id, "positive", 
                              st.session_state.messages[index-1]["content"] if index > 0 else "", 
                              message["content"])
                # Force a rerun to show the thank you message
                st.rerun()
                
        with feedback_cols[1]:
            # Only disable button if negative feedback was given for THIS message
            thumbs_down_disabled = has_feedback and feedback_data.get("rating") == "negative"
            if st.button("πŸ‘Ž", key=f"down_{message_id}", disabled=thumbs_down_disabled):
                handle_feedback(message_id, "negative", 
                              st.session_state.messages[index-1]["content"] if index > 0 else "", 
                              message["content"])
                # Force a rerun to show the feedback form
                st.rerun()
        
        # Show feedback form if thumbs down was clicked for THIS message
        if has_feedback and feedback_data.get("show_form") == True:
            with st.expander("Please tell us why this response wasn't helpful", expanded=True):
                # Add form for detailed feedback
                category = st.radio(
                    "What was the issue with this response?",
                    ["Incorrect information", "Incomplete answer", "Irrelevant to question", "Other"],
                    key=f"category_{message_id}"
                )
                
                comment = st.text_area(
                    "Additional comments (optional):",
                    key=f"comment_{message_id}",
                    height=100
                )
                
                if st.button("Submit Feedback", key=f"submit_{message_id}"):
                    submit_negative_feedback(message_id, category, comment, 
                                          st.session_state.messages[index-1]["content"] if index > 0 else "", 
                                          message["content"])
                    # Force a rerun to show the thank you message
                    st.rerun()

# Display chat messages
for i, message in enumerate(st.session_state.messages):
    with st.chat_message(message["role"]):
        st.markdown(message["content"])
        
        # Add feedback buttons only for assistant messages
        if message["role"] == "assistant":
            display_feedback_ui(message, i)

# Chat input
if prompt := st.chat_input("What would you like to know?", disabled=st.session_state.processing):
    # Add user message to chat history
    add_message("user", prompt)
    with st.chat_message("user"):
        st.markdown(prompt)

    # Get chatbot response
    with st.chat_message("assistant"):
        # Set processing flag to true
        st.session_state.processing = True
        
        # Create containers for the response and status
        status_container = st.empty()
        response_container = st.empty()
        
        # Process the query with Streamlit's built-in spinner
        try:
            logger.info(f"User query: {prompt}")
            
            # Show spinner with "Processing..." text
            with status_container:
                with st.spinner("Processing your question..."):
                    # Execute the query
                    response = st.session_state.chatbot.query(prompt)
            
            # Clear the status container
            status_container.empty()
            
            # Display the response
            response_container.markdown(response)
            
            # Add assistant message with a unique ID
            add_message("assistant", response)
            
            logger.info("Response provided to user")
            
            # Reset processing flag
            st.session_state.processing = False
            
            # Force a rerun to properly display feedback buttons
            st.rerun()
            
        except Exception as e:
            logger.error(f"Error during query processing: {e}")
            status_container.empty()
            response_container.error(f"Sorry, I encountered an error while processing your request: {str(e)}")
            
            # Reset processing flag when done
            st.session_state.processing = False