|
|
import json |
|
|
import time |
|
|
import requests |
|
|
import streamlit as st |
|
|
from contextlib import contextmanager |
|
|
from typing import Optional, List, Literal |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="Chat with your data | Knowledge Assistant", |
|
|
page_icon="π¦", |
|
|
layout='wide', |
|
|
initial_sidebar_state='expanded', |
|
|
menu_items={ |
|
|
'Get Help': 'https://github.com/sanchit-shaleen/chat-with-your-data', |
|
|
'Report a bug': 'https://github.com/sanchit-shaleen/chat-with-your-data/issues', |
|
|
'About': '# Chat with your data\nA production-grade document intelligence system' |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
/* Modern, soothing color scheme */ |
|
|
:root { |
|
|
--primary: #4F46E5; |
|
|
--primary-dark: #4F46E5; |
|
|
--primary-light: #818CF8; |
|
|
--background-dark: #0F172A; |
|
|
--background-light: #1E293B; |
|
|
--surface: #334155; |
|
|
--text-primary: #F8FAFC; |
|
|
--text-secondary: #CBD5E1; |
|
|
--accent: #06B6D4; |
|
|
--success: #10B981; |
|
|
--warning: #F59E0B; |
|
|
} |
|
|
|
|
|
/* Page background */ |
|
|
body { |
|
|
background: linear-gradient(135deg, #0F172A 0%, #1E293B 100%); |
|
|
} |
|
|
|
|
|
/* Main container */ |
|
|
.main { |
|
|
background-color: transparent; |
|
|
} |
|
|
|
|
|
/* Header styling */ |
|
|
.header-container { |
|
|
background: linear-gradient(135deg, #4F46E5 0%, #06B6D4 100%); |
|
|
padding: 2rem 1.5rem; |
|
|
border-radius: 12px; |
|
|
margin-bottom: 2rem; |
|
|
box-shadow: 0 4px 20px rgba(79, 70, 229, 0.25); |
|
|
} |
|
|
|
|
|
.header-title { |
|
|
font-size: 2.5em; |
|
|
font-weight: 700; |
|
|
color: #FFFFFF; |
|
|
margin: 0; |
|
|
letter-spacing: -0.5px; |
|
|
} |
|
|
|
|
|
.header-subtitle { |
|
|
font-size: 1.1em; |
|
|
color: rgba(255, 255, 255, 0.9); |
|
|
margin: 0.5rem 0 0 0; |
|
|
font-weight: 400; |
|
|
} |
|
|
|
|
|
/* Auth sections */ |
|
|
.stTabs [data-baseweb="tab-list"] { |
|
|
gap: 1rem; |
|
|
} |
|
|
|
|
|
.stTabs [data-baseweb="tab"] { |
|
|
background: transparent; |
|
|
border-radius: 8px 8px 0 0; |
|
|
color: rgba(203, 213, 225, 0.7); |
|
|
font-weight: 600; |
|
|
padding: 1rem 2rem; |
|
|
border-bottom: 2px solid transparent; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.stTabs [data-baseweb="tab"][aria-selected="true"] { |
|
|
background: linear-gradient(135deg, #4F46E5 0%, #06B6D4 100%); |
|
|
color: white; |
|
|
border: none; |
|
|
} |
|
|
|
|
|
/* Input fields */ |
|
|
.stTextInput > div > div > input, |
|
|
.stTextArea > div > div > textarea { |
|
|
background-color: rgba(30, 41, 59, 0.7) !important; |
|
|
border: 1px solid rgba(79, 70, 229, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
color: #F8FAFC !important; |
|
|
padding: 0.75rem !important; |
|
|
transition: all 0.3s ease !important; |
|
|
} |
|
|
|
|
|
.stTextInput > div > div > input::placeholder { |
|
|
color: rgba(203, 213, 225, 0.5) !important; |
|
|
} |
|
|
|
|
|
.stTextInput > div > div > input:focus, |
|
|
.stTextArea > div > div > textarea:focus { |
|
|
border: 1px solid #4F46E5 !important; |
|
|
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2) !important; |
|
|
} |
|
|
|
|
|
/* Buttons */ |
|
|
.stButton > button { |
|
|
background: linear-gradient(135deg, #4F46E5 0%, #06B6D4 100%) !important; |
|
|
color: white !important; |
|
|
font-weight: 600 !important; |
|
|
border: none !important; |
|
|
border-radius: 8px !important; |
|
|
padding: 0.75rem 2rem !important; |
|
|
transition: all 0.3s ease !important; |
|
|
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3) !important; |
|
|
} |
|
|
|
|
|
.stButton > button:hover { |
|
|
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.5) !important; |
|
|
transform: translateY(-2px) !important; |
|
|
} |
|
|
|
|
|
.stButton > button[kind="secondary"] { |
|
|
background: rgba(79, 70, 229, 0.1) !important; |
|
|
color: #4F46E5 !important; |
|
|
border: 1px solid #4F46E5 !important; |
|
|
} |
|
|
|
|
|
/* Chat messages */ |
|
|
.stChatMessage { |
|
|
background: rgba(30, 41, 59, 0.5); |
|
|
border-radius: 8px; |
|
|
border: 1px solid rgba(79, 70, 229, 0.2); |
|
|
} |
|
|
|
|
|
/* Containers with borders */ |
|
|
.stContainer { |
|
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.5) 0%, rgba(15, 23, 42, 0.5) 100%); |
|
|
border: 1px solid rgba(79, 70, 229, 0.2); |
|
|
border-radius: 8px; |
|
|
} |
|
|
|
|
|
/* Checkbox and selections */ |
|
|
.stCheckbox { |
|
|
color: #F8FAFC; |
|
|
} |
|
|
|
|
|
.stCheckbox input[type="checkbox"] { |
|
|
accent-color: #4F46E5 !important; |
|
|
} |
|
|
|
|
|
/* Loading spinners and progress */ |
|
|
.stSpinner { |
|
|
color: #4F46E5 !important; |
|
|
} |
|
|
|
|
|
/* Metric styling */ |
|
|
.stMetric { |
|
|
background: rgba(30, 41, 59, 0.5); |
|
|
border: 1px solid rgba(79, 70, 229, 0.2); |
|
|
border-radius: 8px; |
|
|
padding: 1rem; |
|
|
} |
|
|
|
|
|
/* Sidebar */ |
|
|
[data-testid="stSidebar"] { |
|
|
background: linear-gradient(180deg, #1E293B 0%, #0F172A 100%); |
|
|
border-right: 1px solid rgba(79, 70, 229, 0.2); |
|
|
} |
|
|
|
|
|
/* Success/Error/Info messages */ |
|
|
.stSuccess { |
|
|
background: rgba(16, 185, 129, 0.1) !important; |
|
|
border: 1px solid rgba(16, 185, 129, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
.stError { |
|
|
background: rgba(239, 68, 68, 0.1) !important; |
|
|
border: 1px solid rgba(239, 68, 68, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
.stInfo { |
|
|
background: rgba(59, 130, 246, 0.1) !important; |
|
|
border: 1px solid rgba(59, 130, 246, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
.stWarning { |
|
|
background: rgba(245, 158, 11, 0.1) !important; |
|
|
border: 1px solid rgba(245, 158, 11, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
/* Dividers */ |
|
|
hr { |
|
|
border: none; |
|
|
border-top: 1px solid rgba(79, 70, 229, 0.2); |
|
|
margin: 1.5rem 0; |
|
|
} |
|
|
|
|
|
/* File uploader */ |
|
|
.stFileUploader { |
|
|
border: 2px dashed rgba(79, 70, 229, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
background: rgba(79, 70, 229, 0.05) !important; |
|
|
} |
|
|
|
|
|
/* Selectbox and multiselect */ |
|
|
.stSelectbox [data-baseweb="select"], |
|
|
.stMultiSelect [data-baseweb="multi-select"] { |
|
|
background-color: rgba(30, 41, 59, 0.7) !important; |
|
|
border-radius: 8px !important; |
|
|
border: 1px solid rgba(79, 70, 229, 0.3) !important; |
|
|
} |
|
|
|
|
|
/* Loading spinners and progress */ |
|
|
.stSpinner { |
|
|
color: #4F46E5 !important; |
|
|
} |
|
|
/* Metric styling */ |
|
|
.stMetric { |
|
|
background: rgba(30, 41, 59, 0.5); |
|
|
border: 1px solid rgba(79, 70, 229, 0.2); |
|
|
border-radius: 8px; |
|
|
padding: 1rem; |
|
|
} |
|
|
|
|
|
/* Sidebar */ |
|
|
[data-testid="stSidebar"] { |
|
|
background: linear-gradient(180deg, #1E293B 0%, #0F172A 100%); |
|
|
border-right: 1px solid rgba(79, 70, 229, 0.2); |
|
|
} |
|
|
|
|
|
/* Success/Error/Info messages */ |
|
|
.stSuccess { |
|
|
background: rgba(16, 185, 129, 0.1) !important; |
|
|
border: 1px solid rgba(16, 185, 129, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
.stError { |
|
|
background: rgba(239, 68, 68, 0.1) !important; |
|
|
border: 1px solid rgba(239, 68, 68, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
.stInfo { |
|
|
background: rgba(59, 130, 246, 0.1) !important; |
|
|
border: 1px solid rgba(59, 130, 246, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
.stWarning { |
|
|
background: rgba(245, 158, 11, 0.1) !important; |
|
|
border: 1px solid rgba(245, 158, 11, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
/* Multi-select styling */ |
|
|
.stMultiSelect [data-baseweb="multi-select"] { |
|
|
background-color: rgba(30, 41, 59, 0.7) !important; |
|
|
border-radius: 8px !important; |
|
|
border: 1px solid rgba(79, 70, 229, 0.3) !important; |
|
|
} |
|
|
|
|
|
/* Dividers */ |
|
|
hr { |
|
|
border: none; |
|
|
border-top: 1px solid rgba(79, 70, 229, 0.2); |
|
|
margin: 1.5rem 0; |
|
|
} |
|
|
|
|
|
/* File uploader */ |
|
|
.stFileUploader { |
|
|
border: 2px dashed rgba(79, 70, 229, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
background: rgba(79, 70, 229, 0.05) !important; |
|
|
} |
|
|
|
|
|
/* Selectbox and multiselect */ |
|
|
.stSelectbox [data-baseweb="select"], |
|
|
.stMultiSelect [data-baseweb="multi-select"] { |
|
|
background-color: rgba(30, 41, 59, 0.7) !important; |
|
|
border-radius: 8px !important; |
|
|
border: 1px solid rgba(79, 70, 229, 0.3) !important; |
|
|
} |
|
|
|
|
|
/* Text and number inputs */ |
|
|
.stNumberInput > div > div > input { |
|
|
color: #4F46E5; |
|
|
} |
|
|
|
|
|
/* Sliders */ |
|
|
.stSlider > div > div > div > div { |
|
|
color: #4F46E5; |
|
|
} |
|
|
|
|
|
/* Active tab underline */ |
|
|
.stTabs [data-baseweb="tab"][aria-selected="true"] { |
|
|
color: #4F46E5; |
|
|
border-bottom: 2px solid #4F46E5; |
|
|
} |
|
|
|
|
|
/* Features sidebar */ |
|
|
.features-sidebar { |
|
|
border-right: 1px solid rgba(79, 70, 229, 0.2); |
|
|
} |
|
|
|
|
|
/* Chat sidebar */ |
|
|
.chat-sidebar { |
|
|
border-top: 1px solid rgba(79, 70, 229, 0.2); |
|
|
} |
|
|
|
|
|
/* Upload area */ |
|
|
.upload-area { |
|
|
border: 2px dashed rgba(79, 70, 229, 0.3) !important; |
|
|
border-radius: 12px; |
|
|
background: rgba(79, 70, 229, 0.05) !important; |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Message: |
|
|
type: Literal['assistant', 'human'] |
|
|
content: str |
|
|
filenames: Optional[List[str]] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__( |
|
|
self, type: Literal['assistant', 'human'], |
|
|
content: str, filenames: Optional[List[str]] = None |
|
|
): |
|
|
self.type = type |
|
|
self.content = content |
|
|
self.filenames = filenames |
|
|
|
|
|
|
|
|
|
|
|
if "session_id" not in st.session_state: |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
/* Landing page styling */ |
|
|
.landing-container { |
|
|
display: flex; |
|
|
height: 100vh; |
|
|
background: linear-gradient(135deg, #0F172A 0%, #1E293B 100%); |
|
|
margin: -2rem -2rem -2rem -2rem; |
|
|
} |
|
|
|
|
|
.landing-left { |
|
|
flex: 1; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
justify-content: center; |
|
|
padding: 4rem; |
|
|
background: linear-gradient(135deg, rgba(79, 70, 229, 0.1) 0%, rgba(6, 182, 212, 0.05) 100%); |
|
|
border-right: 1px solid rgba(79, 70, 229, 0.2); |
|
|
} |
|
|
|
|
|
.landing-right { |
|
|
flex: 1; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
padding: 4rem 3rem; |
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
.landing-logo { |
|
|
font-size: 3.5em; |
|
|
margin-bottom: 1.5rem; |
|
|
animation: float 3s ease-in-out infinite; |
|
|
} |
|
|
|
|
|
@keyframes float { |
|
|
0%, 100% { transform: translateY(0px); } |
|
|
50% { transform: translateY(-10px); } |
|
|
} |
|
|
|
|
|
.landing-brand { |
|
|
font-size: 2.2em; |
|
|
font-weight: 700; |
|
|
color: #F1F5F9; |
|
|
margin-bottom: 0.5rem; |
|
|
letter-spacing: -0.5px; |
|
|
} |
|
|
|
|
|
.landing-tagline { |
|
|
font-size: 1.4em; |
|
|
color: #4F46E5; |
|
|
font-weight: 600; |
|
|
margin-bottom: 2rem; |
|
|
} |
|
|
|
|
|
.landing-description { |
|
|
font-size: 1.1em; |
|
|
color: rgba(241, 245, 249, 0.8); |
|
|
line-height: 1.6; |
|
|
margin-bottom: 3rem; |
|
|
max-width: 500px; |
|
|
} |
|
|
|
|
|
.feature-list { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 2rem; |
|
|
max-width: 500px; |
|
|
} |
|
|
|
|
|
.feature-item { |
|
|
display: flex; |
|
|
gap: 1.5rem; |
|
|
align-items: flex-start; |
|
|
} |
|
|
|
|
|
.feature-icon { |
|
|
font-size: 2em; |
|
|
min-width: 3rem; |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
padding: 0.5rem; |
|
|
background: rgba(79, 70, 229, 0.2); |
|
|
border-radius: 8px; |
|
|
height: fit-content; |
|
|
} |
|
|
|
|
|
.feature-content h4 { |
|
|
margin: 0 0 0.5rem 0; |
|
|
color: #F1F5F9; |
|
|
font-size: 1.1em; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.feature-content p { |
|
|
margin: 0; |
|
|
color: rgba(241, 245, 249, 0.7); |
|
|
font-size: 0.95em; |
|
|
} |
|
|
|
|
|
.auth-container { |
|
|
width: 100%; |
|
|
max-width: 420px; |
|
|
background: rgba(30, 41, 59, 0.9); |
|
|
backdrop-filter: blur(10px); |
|
|
border: 1px solid rgba(79, 70, 229, 0.2); |
|
|
border-radius: 12px; |
|
|
padding: 3rem; |
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
|
|
|
.auth-header { |
|
|
text-align: center; |
|
|
margin-bottom: 2.5rem; |
|
|
} |
|
|
|
|
|
.auth-header h2 { |
|
|
color: #F1F5F9; |
|
|
margin: 0 0 0.5rem 0; |
|
|
font-size: 1.8em; |
|
|
font-weight: 700; |
|
|
} |
|
|
|
|
|
.auth-header p { |
|
|
color: rgba(241, 245, 249, 0.6); |
|
|
margin: 0; |
|
|
font-size: 0.95em; |
|
|
} |
|
|
|
|
|
.auth-form-group { |
|
|
margin-bottom: 1.5rem; |
|
|
} |
|
|
|
|
|
.auth-form-group label { |
|
|
display: block; |
|
|
color: #F1F5F9; |
|
|
font-weight: 600; |
|
|
margin-bottom: 0.75rem; |
|
|
font-size: 0.95em; |
|
|
} |
|
|
|
|
|
.auth-form-group input { |
|
|
width: 100%; |
|
|
padding: 0.875rem 1rem; |
|
|
background-color: rgba(15, 23, 42, 0.8) !important; |
|
|
border: 1px solid rgba(79, 70, 229, 0.3) !important; |
|
|
border-radius: 8px !important; |
|
|
color: #F1F5F9 !important; |
|
|
font-size: 0.95em; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.auth-form-group input::placeholder { |
|
|
color: rgba(241, 245, 249, 0.4) !important; |
|
|
} |
|
|
|
|
|
.auth-form-group input:focus { |
|
|
border: 1px solid #4F46E5 !important; |
|
|
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1) !important; |
|
|
outline: none !important; |
|
|
} |
|
|
|
|
|
.auth-checkbox { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
margin-bottom: 1.5rem; |
|
|
} |
|
|
|
|
|
.auth-checkbox input[type="checkbox"] { |
|
|
accent-color: #4F46E5 !important; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.auth-checkbox label { |
|
|
color: rgba(241, 245, 249, 0.8); |
|
|
margin: 0; |
|
|
font-weight: 500; |
|
|
cursor: pointer; |
|
|
font-size: 0.9em; |
|
|
} |
|
|
|
|
|
.auth-forgot-link { |
|
|
text-align: right; |
|
|
margin-bottom: 1.5rem; |
|
|
} |
|
|
|
|
|
.auth-forgot-link a { |
|
|
color: #4F46E5; |
|
|
text-decoration: none; |
|
|
font-size: 0.9em; |
|
|
font-weight: 500; |
|
|
transition: color 0.3s ease; |
|
|
} |
|
|
|
|
|
.auth-forgot-link a:hover { |
|
|
color: #4F46E5; |
|
|
text-decoration: underline; |
|
|
} |
|
|
|
|
|
.auth-button { |
|
|
width: 100%; |
|
|
padding: 0.9rem; |
|
|
background: linear-gradient(135deg, #4F46E5 0%, #06B6D4 100%); |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 8px; |
|
|
font-size: 1em; |
|
|
font-weight: 600; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s ease; |
|
|
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.4); |
|
|
margin-bottom: 1rem; |
|
|
} |
|
|
|
|
|
.auth-button:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.6); |
|
|
} |
|
|
|
|
|
.auth-button:active { |
|
|
transform: translateY(0); |
|
|
} |
|
|
|
|
|
.auth-signup-link { |
|
|
text-align: center; |
|
|
color: rgba(241, 245, 249, 0.7); |
|
|
font-size: 0.9em; |
|
|
} |
|
|
|
|
|
.auth-signup-link a { |
|
|
color: #4F46E5; |
|
|
text-decoration: none; |
|
|
font-weight: 600; |
|
|
transition: color 0.3s ease; |
|
|
} |
|
|
|
|
|
.auth-signup-link a:hover { |
|
|
color: #4F46E5; |
|
|
text-decoration: underline; |
|
|
} |
|
|
|
|
|
/* Tab styling */ |
|
|
.auth-tabs { |
|
|
display: flex; |
|
|
gap: 0.5rem; |
|
|
margin-bottom: 2rem; |
|
|
border-bottom: 1px solid rgba(79, 70, 229, 0.2); |
|
|
} |
|
|
|
|
|
.auth-tab { |
|
|
flex: 1; |
|
|
padding: 0.75rem 1rem; |
|
|
background: transparent; |
|
|
border: none; |
|
|
color: rgba(241, 245, 249, 0.6); |
|
|
font-weight: 600; |
|
|
cursor: pointer; |
|
|
border-bottom: 2px solid transparent; |
|
|
transition: all 0.3s ease; |
|
|
font-size: 0.95em; |
|
|
} |
|
|
|
|
|
.auth-tab.active { |
|
|
color: #4F46E5; |
|
|
border-bottom: 2px solid #4F46E5; |
|
|
} |
|
|
|
|
|
.auth-tab:hover { |
|
|
color: #F1F5F9; |
|
|
} |
|
|
|
|
|
/* Responsive */ |
|
|
@media (max-width: 1024px) { |
|
|
.landing-container { |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.landing-left { |
|
|
padding: 2rem; |
|
|
border-right: none; |
|
|
border-bottom: 1px solid rgba(79, 70, 229, 0.2); |
|
|
} |
|
|
|
|
|
.landing-right { |
|
|
padding: 2rem; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<div style="background: linear-gradient(135deg, #4F46E5 0%, #06B6D4 100%); |
|
|
padding: 3rem 2rem; border-radius: 12px; margin-bottom: 2rem; |
|
|
box-shadow: 0 4px 20px rgba(79, 70, 229, 0.25);"> |
|
|
<div style="display: flex; align-items: center; gap: 1rem;"> |
|
|
<span style="font-size: 3em;">π¬</span> |
|
|
<div> |
|
|
<h1 style="margin: 0; color: white; font-size: 2em;">Chat with your data</h1> |
|
|
<p style="margin: 0.5rem 0 0 0; color: rgba(255,255,255,0.9);">Your Personal Document Intelligence Assistant</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
max_retries = 5 |
|
|
retry_delay = 2 |
|
|
server_ready = False |
|
|
|
|
|
for attempt in range(max_retries): |
|
|
try: |
|
|
if requests.get(f"{st.secrets.server.ip_address}/", timeout=5).status_code == 200: |
|
|
server_ready = True |
|
|
break |
|
|
except: |
|
|
if attempt < max_retries - 1: |
|
|
st.info(f"π Initializing services... ({attempt + 1}/{max_retries})") |
|
|
import time |
|
|
time.sleep(retry_delay) |
|
|
else: |
|
|
st.error("π« Server is not reachable. Please check your connection or server status.", icon="π") |
|
|
st.stop() |
|
|
|
|
|
|
|
|
col_left, col_right = st.columns([1, 1.2], gap="large") |
|
|
|
|
|
with col_left: |
|
|
st.markdown("### π¬ Why Chat with Your Data?", help="Key features") |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<div style="background: rgba(79, 70, 229, 0.1); padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem; border-left: 4px solid #4F46E5;"> |
|
|
<h4 style="margin: 0 0 0.5rem 0; color: #F8FAFC;">π€ Upload Any Data</h4> |
|
|
<p style="margin: 0; color: rgba(248, 250, 252, 0.8); font-size: 0.9em;">Support for CSV, Excel, JSON, and more</p> |
|
|
</div> |
|
|
|
|
|
<div style="background: rgba(6, 182, 212, 0.1); padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem; border-left: 4px solid #06B6D4;"> |
|
|
<h4 style="margin: 0 0 0.5rem 0; color: #F8FAFC;">π¬ Natural Conversations</h4> |
|
|
<p style="margin: 0; color: rgba(248, 250, 252, 0.8); font-size: 0.9em;">Ask questions in plain English, get instant answers</p> |
|
|
</div> |
|
|
|
|
|
<div style="background: rgba(79, 70, 229, 0.1); padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem; border-left: 4px solid #4F46E5;"> |
|
|
<h4 style="margin: 0 0 0.5rem 0; color: #F8FAFC;">β AI-Powered Insights</h4> |
|
|
<p style="margin: 0; color: rgba(248, 250, 252, 0.8); font-size: 0.9em;">Discover patterns and trends automatically</p> |
|
|
</div> |
|
|
|
|
|
<div style="background: rgba(6, 182, 212, 0.1); padding: 1.5rem; border-radius: 8px; border-left: 4px solid #06B6D4;"> |
|
|
<h4 style="margin: 0 0 0.5rem 0; color: #F8FAFC;">π Lightning Fast</h4> |
|
|
<p style="margin: 0; color: rgba(248, 250, 252, 0.8); font-size: 0.9em;">Optimized caching for instant responses</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
with col_right: |
|
|
|
|
|
if "auth_tab" not in st.session_state: |
|
|
st.session_state.auth_tab = "login" |
|
|
|
|
|
st.markdown(""" |
|
|
### π Sign In / Create Account |
|
|
""") |
|
|
|
|
|
|
|
|
tab1, tab2 = st.tabs(["π Login", "π Register"]) |
|
|
|
|
|
with tab1: |
|
|
st.write("Welcome back! Sign in to your account to continue.") |
|
|
|
|
|
ip_user_id = st.text_input( |
|
|
"User ID", |
|
|
placeholder="Enter your user ID", |
|
|
key="login_user_id" |
|
|
) |
|
|
|
|
|
ip_user_pw = st.text_input( |
|
|
"Password", |
|
|
placeholder="Enter your password", |
|
|
type="password", |
|
|
key="login_user_pw" |
|
|
) |
|
|
|
|
|
remember = st.checkbox("Remember me", key="login_remember", value=False) |
|
|
|
|
|
if st.button("π Login", type="primary", use_container_width=True, key="login_btn"): |
|
|
ip_user_id = "_".join(ip_user_id.strip().lower().split(" ")) |
|
|
ip_user_pw = ip_user_pw.strip() |
|
|
|
|
|
if not ip_user_id or not ip_user_pw: |
|
|
st.error("β Please fill all the fields.") |
|
|
else: |
|
|
try: |
|
|
resp = requests.post( |
|
|
f"{st.secrets.server.ip_address}/login", |
|
|
json={"login_id": ip_user_id, "password": ip_user_pw} |
|
|
) |
|
|
|
|
|
if resp.status_code == 200: |
|
|
session_id = resp.json().get("user_id") |
|
|
st.session_state.session_id = session_id |
|
|
name_of_user = resp.json().get("name", session_id) |
|
|
st.session_state.name_of_user = name_of_user |
|
|
|
|
|
st.success("β
Login successful! Redirecting...", icon="β¨") |
|
|
time.sleep(1) |
|
|
st.rerun() |
|
|
else: |
|
|
st.error("β " + resp.json().get("error", "Login failed.")) |
|
|
|
|
|
except requests.RequestException as e: |
|
|
st.error(f"β Error connecting to server: {e}") |
|
|
|
|
|
with tab2: |
|
|
st.write("Join us and start chatting with your data today!") |
|
|
|
|
|
ip_user_name = st.text_input( |
|
|
"Full Name", |
|
|
placeholder="John Doe", |
|
|
key="register_user_name" |
|
|
) |
|
|
|
|
|
ip_user_id = st.text_input( |
|
|
"User ID", |
|
|
placeholder="john_doe", |
|
|
key="register_user_id" |
|
|
) |
|
|
st.caption("Use: lowercase, numbers, `-`, `_` only") |
|
|
|
|
|
ip_user_pw = st.text_input( |
|
|
"Password", |
|
|
placeholder="Create a strong password", |
|
|
type="password", |
|
|
key="register_user_pw" |
|
|
) |
|
|
|
|
|
if st.button("β¨ Create Account", type="primary", use_container_width=True, key="register_btn"): |
|
|
ip_user_id = "_".join(ip_user_id.strip().lower().split(" ")) |
|
|
ip_user_pw = ip_user_pw.strip() |
|
|
|
|
|
if not ip_user_name or not ip_user_id or not ip_user_pw: |
|
|
st.error("β Please fill all the fields.") |
|
|
else: |
|
|
try: |
|
|
resp = requests.post( |
|
|
f"{st.secrets.server.ip_address}/register", |
|
|
json={ |
|
|
"name": ip_user_name, |
|
|
"user_id": ip_user_id, |
|
|
"password": ip_user_pw |
|
|
} |
|
|
) |
|
|
|
|
|
if resp.status_code == 201: |
|
|
st.success("β
Registration successful! You can now login.", icon="π") |
|
|
st.info("π Switch to the Login tab to sign in.", icon="βΉοΈ") |
|
|
else: |
|
|
st.error("β " + resp.json().get("error", "Registration failed.")) |
|
|
|
|
|
except requests.RequestException as e: |
|
|
st.error(f"β Error connecting to server: {e}") |
|
|
|
|
|
st.stop() |
|
|
|
|
|
|
|
|
if "initialized" not in st.session_state: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.session_state.server_ip = st.secrets.server.ip_address |
|
|
try: |
|
|
resp = requests.post( |
|
|
f"{st.session_state.server_ip}/chat_history", |
|
|
data={"user_id": st.session_state.session_id} |
|
|
) |
|
|
if resp.status_code == 200: |
|
|
|
|
|
st.session_state.chat_history = [Message('assistant', "π, How may I help you today?")] |
|
|
|
|
|
|
|
|
chat_hist = resp.json().get("chat_history", []) |
|
|
for msg in chat_hist: |
|
|
st.session_state.chat_history.append(Message(msg['role'], msg['content'])) |
|
|
else: |
|
|
|
|
|
st.error( |
|
|
"Failed to initialize chat history. Please try again later.", |
|
|
icon="π«" |
|
|
) |
|
|
st.stop() |
|
|
|
|
|
except requests.RequestException as e: |
|
|
|
|
|
st.error( |
|
|
"Failed to connect to the server. Please check your connection or server status.", |
|
|
icon="π«" |
|
|
) |
|
|
st.stop() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.session_state.user_uploads = requests.get( |
|
|
f"{st.session_state.server_ip}/uploads", |
|
|
params={"user_id": st.session_state.session_id} |
|
|
).json().get("files", []) |
|
|
|
|
|
|
|
|
st.session_state.last_retrieved_docs = [] |
|
|
|
|
|
|
|
|
st.session_state.initialized = True |
|
|
|
|
|
|
|
|
|
|
|
user_id = st.session_state.session_id |
|
|
chat_history = st.session_state.chat_history |
|
|
server_ip = st.session_state.server_ip |
|
|
|
|
|
|
|
|
with st.sidebar: |
|
|
st.caption(f"π€ Logged in as: `{user_id}`") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_as_ai(text): |
|
|
with st.chat_message(name='assistant', avatar='assistant'): |
|
|
st.markdown(text) |
|
|
|
|
|
|
|
|
def write_as_human(text: str, filenames: Optional[List[str]] = None): |
|
|
with st.chat_message(name='user', avatar='user'): |
|
|
st.markdown(text) |
|
|
if filenames: |
|
|
files = ", ".join([f"`'{file}'`" for file in filenames]) |
|
|
st.caption(f"π Attached file(s): {files}.") |
|
|
|
|
|
|
|
|
def upload_file(uploaded_file) -> tuple[bool, str]: |
|
|
"""Upload the st attachment/uploaded file to the server and save it. |
|
|
Args: |
|
|
uploaded_file: The file object uploaded by the user. |
|
|
Returns: |
|
|
tuple: A tuple containing: |
|
|
- bool: True if the file was uploaded successfully, False otherwise. |
|
|
- str: The server file name or error message. |
|
|
""" |
|
|
|
|
|
try: |
|
|
|
|
|
files = {"file": (uploaded_file.name, uploaded_file.getvalue())} |
|
|
data = {"user_id": user_id} |
|
|
response = requests.post(f"{server_ip}/upload", files=files, data=data) |
|
|
|
|
|
if response.status_code == 200: |
|
|
message = response.json().get("message", "") |
|
|
|
|
|
return True, message |
|
|
else: |
|
|
message = response.json().get("error", "Unknown error") |
|
|
|
|
|
|
|
|
return False, message |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
return False, str(e) |
|
|
|
|
|
|
|
|
def embed_file(file_name: str) -> tuple[bool, str, dict]: |
|
|
"""Embed the content of the file into the RAG system with multimodal support. |
|
|
Args: |
|
|
file_name: The name of the file to embed. |
|
|
Returns: |
|
|
tuple: A tuple containing: |
|
|
- bool: True if the file was embedded successfully, False otherwise. |
|
|
- str: Success message or error message. |
|
|
- dict: Additional metadata (items_embedded, text_chunks, images_extracted, image_paths) |
|
|
""" |
|
|
try: |
|
|
response = requests.post( |
|
|
f"{server_ip}/embed", |
|
|
json={ |
|
|
"user_id": user_id, |
|
|
"file_name": file_name |
|
|
} |
|
|
) |
|
|
|
|
|
if response.status_code == 200: |
|
|
resp_data = response.json() |
|
|
message = resp_data.get("message", "File embedded successfully.") |
|
|
|
|
|
|
|
|
detailed_message = message |
|
|
items = resp_data.get("items_embedded", 0) |
|
|
text_chunks = resp_data.get("text_chunks", 0) |
|
|
images = resp_data.get("images_extracted", 0) |
|
|
|
|
|
if items > 0: |
|
|
detailed_message = f"β
Ingested {items} items ({text_chunks} text chunks + {images} πΌοΈ images)" |
|
|
|
|
|
|
|
|
metadata = { |
|
|
"items_embedded": items, |
|
|
"text_chunks": text_chunks, |
|
|
"images_extracted": images, |
|
|
"image_paths": resp_data.get("image_paths", []) |
|
|
} |
|
|
|
|
|
return True, detailed_message, metadata |
|
|
else: |
|
|
error_message = response.json().get("error", "Unknown error") |
|
|
return False, error_message, {} |
|
|
|
|
|
except Exception as e: |
|
|
return False, str(e), {} |
|
|
|
|
|
|
|
|
def handle_uploaded_files(uploaded_files) -> bool: |
|
|
"""Handle the uploaded files by uploading them to the server and embedding their content.""" |
|
|
progress_status = "" |
|
|
|
|
|
with st.chat_message(name='assistant', avatar='./assets/settings_3.png'): |
|
|
with st.spinner("Processing files..."): |
|
|
container = st.empty() |
|
|
|
|
|
|
|
|
|
|
|
@contextmanager |
|
|
def write_progress(msg: str): |
|
|
|
|
|
nonlocal progress_status |
|
|
|
|
|
curr = progress_status + f"- β³ {msg}\n" |
|
|
container.container(border=True).markdown(curr) |
|
|
|
|
|
try: |
|
|
|
|
|
yield |
|
|
|
|
|
progress_status += f"\n- β
{msg}\n" |
|
|
curr = progress_status |
|
|
except Exception as e: |
|
|
progress_status += f"\n- β {msg}: {e}\n" |
|
|
raise e |
|
|
finally: |
|
|
container.container(border=True).markdown(curr) |
|
|
|
|
|
try: |
|
|
for i, file in enumerate(uploaded_files): |
|
|
progress_status += f"\nπ Processing file {i+1} of {len(uploaded_files)}...\n" |
|
|
|
|
|
|
|
|
|
|
|
with write_progress("Uploading file..."): |
|
|
status, message = upload_file(file) |
|
|
if not status: |
|
|
raise RuntimeError(f"Upload failed for file: {file.name}") |
|
|
server_file_name = message |
|
|
time.sleep(st.secrets.llm.per_step_delay) |
|
|
|
|
|
|
|
|
with write_progress("Embedding content..."): |
|
|
status, message, embed_metadata = embed_file(server_file_name) |
|
|
if not status: |
|
|
raise RuntimeError(f"Embedding failed for file: {file.name}") |
|
|
|
|
|
|
|
|
if embed_metadata.get("images_extracted", 0) > 0: |
|
|
st.success(f"π {message}", icon="β
") |
|
|
|
|
|
time.sleep(st.secrets.llm.per_step_delay) |
|
|
|
|
|
|
|
|
with write_progress("Finalizing the process..."): |
|
|
|
|
|
st.session_state.user_uploads = requests.get( |
|
|
f"{st.session_state.server_ip}/uploads", |
|
|
params={"user_id": user_id} |
|
|
).json().get("files", []) |
|
|
|
|
|
|
|
|
time.sleep(st.secrets.llm.end_delay) |
|
|
|
|
|
return True |
|
|
|
|
|
except Exception as e: |
|
|
st.exception(exception=e) |
|
|
st.stop() |
|
|
return False |
|
|
|
|
|
|
|
|
@st.cache_data(ttl=60 * 10, show_spinner=False) |
|
|
def get_iframe(file_name: str, num_pages: int = 5) -> tuple[bool, str]: |
|
|
"""Get the iframe HTML for the PDF file.""" |
|
|
try: |
|
|
response = requests.post( |
|
|
f"{st.session_state.server_ip}/iframe", |
|
|
json={ |
|
|
"user_id": user_id, |
|
|
"file_name": file_name, |
|
|
"num_pages": num_pages |
|
|
}, |
|
|
) |
|
|
if response.status_code == 200: |
|
|
return True, response.json().get("iframe", "") |
|
|
else: |
|
|
return False, response.json().get("error", "Unknown error") |
|
|
except requests.RequestException as e: |
|
|
|
|
|
return False, str(e) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with st.sidebar.container(border=True): |
|
|
col1, col2, col3 = st.columns([1, 5, 1]) |
|
|
col1.write("π€") |
|
|
col2.markdown(f"**{st.session_state.get('name_of_user', 'User')}**") |
|
|
col3.write(f"β¨") |
|
|
|
|
|
st.sidebar.divider() |
|
|
|
|
|
|
|
|
st.sidebar.subheader("π Document Management") |
|
|
|
|
|
selected_file = st.sidebar.selectbox( |
|
|
label="Choose Document", |
|
|
index=0, |
|
|
options=st.session_state.user_uploads, |
|
|
help="Select a document to preview" |
|
|
) |
|
|
|
|
|
|
|
|
if not st.session_state.user_uploads: |
|
|
st.sidebar.info("π No documents uploaded yet.\n\nStart by uploading your first document!", icon="βΉοΈ") |
|
|
else: |
|
|
col1, col2 = st.sidebar.columns([1, 1]) |
|
|
with col1: |
|
|
preview_button = st.sidebar.button("ποΈ Preview", use_container_width=True) |
|
|
with col2: |
|
|
delete_button = st.sidebar.button("ποΈ Delete", use_container_width=True) |
|
|
|
|
|
if selected_file and preview_button: |
|
|
status, content = get_iframe(selected_file) |
|
|
if status: |
|
|
st.sidebar.markdown(content, unsafe_allow_html=True) |
|
|
else: |
|
|
st.sidebar.error(f"β Error: {content}", icon="π«") |
|
|
|
|
|
if selected_file and delete_button: |
|
|
try: |
|
|
resp = requests.post( |
|
|
f"{st.session_state.server_ip}/delete_file", |
|
|
json={"user_id": user_id, "file_name": selected_file} |
|
|
) |
|
|
if resp.status_code == 200: |
|
|
st.sidebar.success("β
Document deleted successfully!", icon="ποΈ") |
|
|
st.cache_data.clear() |
|
|
st.rerun() |
|
|
else: |
|
|
st.sidebar.error(resp.json().get("error", "Failed to delete document."), icon="π«") |
|
|
except requests.RequestException as e: |
|
|
st.sidebar.error(f"β Error deleting document: {e}", icon="π") |
|
|
|
|
|
st.sidebar.divider() |
|
|
|
|
|
|
|
|
st.sidebar.subheader("βοΈ Advanced Options") |
|
|
|
|
|
col1, col2 = st.sidebar.columns([1, 1]) |
|
|
with col1: |
|
|
dummy_mode = st.sidebar.toggle( |
|
|
label="π Dummy Mode", |
|
|
value=False, |
|
|
key="dummy_mode", |
|
|
help="Use placeholder responses instead of LLM" |
|
|
) |
|
|
|
|
|
with col2: |
|
|
cache_mode = st.sidebar.toggle( |
|
|
label="πΎ Cache Info", |
|
|
value=False, |
|
|
key="show_cache_info", |
|
|
help="Show cache performance metrics" |
|
|
) |
|
|
|
|
|
st.sidebar.divider() |
|
|
|
|
|
|
|
|
st.sidebar.subheader("π΄ Danger Zone") |
|
|
|
|
|
col1, col2 = st.sidebar.columns([1, 1]) |
|
|
|
|
|
with col1: |
|
|
if st.sidebar.button("ποΈ Clear Uploads", type="secondary", use_container_width=True): |
|
|
try: |
|
|
resp = requests.post( |
|
|
f"{st.session_state.server_ip}/clear_my_files", |
|
|
data={"user_id": user_id} |
|
|
) |
|
|
if resp.status_code == 200: |
|
|
st.sidebar.success("β
All documents cleared!", icon="ποΈ") |
|
|
st.cache_data.clear() |
|
|
st.rerun() |
|
|
else: |
|
|
st.sidebar.error(resp.json().get("error", "Failed to clear documents."), icon="π«") |
|
|
except requests.RequestException as e: |
|
|
st.sidebar.error(f"β Error: {e}", icon="π") |
|
|
|
|
|
with col2: |
|
|
if st.sidebar.button("π¬ Clear Chat", type="secondary", use_container_width=True): |
|
|
resp = requests.post( |
|
|
f"{server_ip}/clear_chat_history", |
|
|
data={"user_id": user_id} |
|
|
) |
|
|
|
|
|
if resp.status_code == 200: |
|
|
st.session_state.chat_history = [ |
|
|
Message('assistant', "π Hello! How can I help you today?") |
|
|
] |
|
|
st.session_state.last_retrieved_docs = [] |
|
|
st.sidebar.success("β
Chat cleared!", icon="π¬") |
|
|
st.rerun() |
|
|
else: |
|
|
st.sidebar.error(resp.json().get("error", "Failed to clear chat."), icon="π«") |
|
|
|
|
|
st.sidebar.divider() |
|
|
|
|
|
|
|
|
st.sidebar.markdown(""" |
|
|
--- |
|
|
<div style='text-align: center; color: rgba(241, 245, 249, 0.6); font-size: 0.85em;'> |
|
|
<p>π¬ Chat with your data</p> |
|
|
<p style='font-size: 0.8em;'>Production-grade Document Intelligence</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
col1, col2 = st.columns([0.8, 9.2], vertical_alignment='bottom', gap='small') |
|
|
|
|
|
with col1: |
|
|
col1.markdown("π¦") |
|
|
|
|
|
with col2: |
|
|
st.markdown(""" |
|
|
<div style="margin-bottom: -1rem;"> |
|
|
<h1 style="margin: 0; background: linear-gradient(135deg, #4F46E5 0%, #06B6D4 100%); |
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent; |
|
|
background-clip: text; font-weight: 700;"> |
|
|
Chat with your data |
|
|
</h1> |
|
|
<p style="color: rgba(241, 245, 249, 0.7); margin: 0.5rem 0 0 0; font-size: 0.95em;"> |
|
|
Your intelligent document assistant powered by local AI inference |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
if st.session_state.get('show_cache_info', False): |
|
|
with st.expander("β‘ Cache Performance & Statistics", expanded=True): |
|
|
try: |
|
|
cache_stats = requests.get(f"{server_ip}/cache-debug").json() |
|
|
col1, col2, col3 = st.columns(3) |
|
|
with col1: |
|
|
st.metric("π¦ Cache Size", f"{cache_stats.get('cache_size', 0)} entries", |
|
|
help="Number of queries currently cached") |
|
|
with col2: |
|
|
st.metric("β‘ Status", "π’ Active", |
|
|
help="Cache is operational and responding") |
|
|
with col3: |
|
|
st.metric("πΎ Memory", "Optimized", |
|
|
help="Using LRU eviction and TTL expiration") |
|
|
|
|
|
if cache_stats.get('cache_keys'): |
|
|
st.caption(f"π Recent queries in cache: {', '.join([f'`{k[:15]}...`' for k in cache_stats.get('cache_keys', [])[:5]])}") |
|
|
except Exception as e: |
|
|
st.warning("β οΈ Cache stats unavailable", icon="βΉοΈ") |
|
|
|
|
|
|
|
|
for ind, message in enumerate(st.session_state.chat_history): |
|
|
if ind < len(st.session_state.chat_history) - 1: |
|
|
if message.type == 'human': |
|
|
write_as_human(message.content, message.filenames) |
|
|
|
|
|
elif message.type == 'assistant': |
|
|
answer = message.content |
|
|
if "<think>" in answer: |
|
|
answer = answer[answer.find("</think>") + len("</think>"):] |
|
|
write_as_ai(answer) |
|
|
|
|
|
else: |
|
|
if message.type == 'human': |
|
|
write_as_human(message.content) |
|
|
|
|
|
elif message.type == 'assistant': |
|
|
|
|
|
full = message.content |
|
|
thoughts = full[ |
|
|
full.find("<think>")+8:full.find("</think>") |
|
|
] if "<think>" in full else None |
|
|
answer = full[full.find("</think>") + len("</think>"):] if thoughts else full |
|
|
documents = st.session_state.last_retrieved_docs if st.session_state.last_retrieved_docs else None |
|
|
|
|
|
with st.chat_message(name='assistant', avatar='assistant'): |
|
|
with st.container(border=True): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if thoughts: |
|
|
|
|
|
cont_thoughts = st.popover( |
|
|
"π Thoughts", use_container_width=False).markdown(thoughts) |
|
|
|
|
|
st.markdown(answer) |
|
|
|
|
|
if documents: |
|
|
tabs = st.expander("ποΈ Sources", expanded=False).tabs( |
|
|
tabs=[f"Document {i+1}" for i in range(len(documents))] |
|
|
) |
|
|
for i, doc in enumerate(documents): |
|
|
with tabs[i]: |
|
|
|
|
|
is_image = doc.get('metadata', {}).get('type') == 'image' |
|
|
|
|
|
if is_image and 'image_path' in doc.get('metadata', {}): |
|
|
|
|
|
st.subheader("πΌοΈ Image Source") |
|
|
image_path = doc['metadata']['image_path'] |
|
|
try: |
|
|
from PIL import Image |
|
|
img = Image.open(image_path) |
|
|
st.image(img, caption=f"Image ID: {doc['metadata'].get('image_id', 'unknown')}", use_column_width=True) |
|
|
st.divider() |
|
|
st.subheader(":blue[Image Details:]") |
|
|
img_details = { |
|
|
"page_number": doc['metadata'].get('page_number'), |
|
|
"position": doc['metadata'].get('position'), |
|
|
"extractor": doc['metadata'].get('extractor'), |
|
|
"size": img.size if hasattr(img, 'size') else "unknown" |
|
|
} |
|
|
st.json(img_details, expanded=False) |
|
|
except FileNotFoundError: |
|
|
st.warning(f"β οΈ Image not found at: {image_path}") |
|
|
except Exception as e: |
|
|
st.error(f"β Error loading image: {e}") |
|
|
else: |
|
|
|
|
|
st.subheader(":blue[Content:]") |
|
|
st.markdown(doc['page_content']) |
|
|
st.divider() |
|
|
st.subheader(":blue[Source Details:]") |
|
|
st.json(doc['metadata'], expanded=False) |
|
|
|
|
|
|
|
|
last_metrics = st.session_state.get('last_metrics', {}) |
|
|
if last_metrics and "error" not in last_metrics: |
|
|
with st.expander("π Response Quality Metrics", expanded=True): |
|
|
st.markdown("**LLM-based evaluation using DeepEval + Ollama (Reference-Free)**") |
|
|
cols = st.columns(2) |
|
|
|
|
|
|
|
|
with cols[0]: |
|
|
relevancy_score = last_metrics.get("answer_relevancy", 0.0) |
|
|
st.metric( |
|
|
label="π― Answer Relevancy", |
|
|
value=f"{relevancy_score:.2%}", |
|
|
help="Measures how relevant the answer is to your question (0-100%)" |
|
|
) |
|
|
if relevancy_score >= 0.7: |
|
|
st.success("β Highly relevant", icon="β
") |
|
|
elif relevancy_score >= 0.5: |
|
|
st.warning("β Moderate", icon="β οΈ") |
|
|
else: |
|
|
st.error("β Low relevance", icon="β") |
|
|
|
|
|
|
|
|
with cols[1]: |
|
|
faithfulness_score = last_metrics.get("faithfulness", 0.0) |
|
|
st.metric( |
|
|
label="π Faithfulness", |
|
|
value=f"{faithfulness_score:.2%}", |
|
|
help="Measures how well the answer is grounded in the retrieved documents (0-100%)" |
|
|
) |
|
|
if faithfulness_score >= 0.7: |
|
|
st.success("β Well-grounded", icon="β
") |
|
|
elif faithfulness_score >= 0.5: |
|
|
st.warning("β Partial support", icon="β οΈ") |
|
|
else: |
|
|
st.error("β Weak grounding", icon="β") |
|
|
|
|
|
st.caption("π‘ Metrics use reference-free LLM-as-Judge approach - no ground truth needed") |
|
|
|
|
|
|
|
|
if user_message := st.chat_input( |
|
|
placeholder="π¬ Ask anything about your documents... Attach [pdf, txt, md] files with π", |
|
|
max_chars=2000, |
|
|
accept_file='multiple', |
|
|
file_type=['pdf', 'txt', 'md'], |
|
|
): |
|
|
|
|
|
new_message = Message( |
|
|
type="human", |
|
|
content=user_message.text, |
|
|
filenames=[file.name for file in user_message.files] if user_message.files else None |
|
|
) |
|
|
|
|
|
|
|
|
st.session_state.chat_history.append(new_message) |
|
|
|
|
|
write_as_human(new_message.content, new_message.filenames) |
|
|
|
|
|
st.session_state.last_retrieved_docs = [] |
|
|
|
|
|
|
|
|
if user_message.files: |
|
|
if handle_uploaded_files(user_message.files): |
|
|
st.toast("Files processed successfully!", icon="β
") |
|
|
else: |
|
|
st.error("Error processing files. Please try again.", icon="π«") |
|
|
|
|
|
|
|
|
with st.chat_message(name='assistant', avatar='assistant'): |
|
|
with st.spinner("Generating response..."): |
|
|
full = "" |
|
|
|
|
|
|
|
|
if st.session_state.get("dummy_mode", False): |
|
|
resp_holder = st.empty() |
|
|
response = requests.post( |
|
|
f"{server_ip}/rag", |
|
|
json={ |
|
|
"query": new_message.content, |
|
|
"session_id": user_id, |
|
|
"dummy": True |
|
|
}, |
|
|
stream=True |
|
|
) |
|
|
|
|
|
for chunk in response.iter_content(chunk_size=None): |
|
|
if chunk: |
|
|
decoded = chunk.decode("utf-8") |
|
|
decoded = json.loads(decoded) |
|
|
|
|
|
if decoded["type"] == "content": |
|
|
full += decoded["data"] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
resp_holder.markdown(full + "β") |
|
|
|
|
|
else: |
|
|
response = requests.post( |
|
|
f"{server_ip}/rag", |
|
|
json={ |
|
|
"query": new_message.content, |
|
|
"session_id": user_id, |
|
|
"dummy": False |
|
|
}, |
|
|
stream=True |
|
|
) |
|
|
|
|
|
documents = [] |
|
|
metrics = {} |
|
|
resp_holder = st.container(border=True) |
|
|
|
|
|
|
|
|
reply_container = resp_holder.container(border=True) |
|
|
reply_holder = reply_container.empty() |
|
|
|
|
|
document_container = resp_holder.container() |
|
|
document_holder = document_container.empty() |
|
|
|
|
|
for chunk in response.iter_content(chunk_size=None): |
|
|
print(" Received chunk... :", chunk, flush=True) |
|
|
if chunk: |
|
|
decoded = chunk.decode("utf-8") |
|
|
decoded = json.loads(decoded) |
|
|
print(" Decoded chunk: ", decoded, flush=True) |
|
|
if decoded["type"] == "metadata": |
|
|
|
|
|
continue |
|
|
|
|
|
|
|
|
elif decoded["type"] == "context": |
|
|
documents.append(decoded['data']) |
|
|
|
|
|
elif decoded["type"] == "content": |
|
|
full += decoded["data"] |
|
|
|
|
|
elif decoded["type"] == "metrics": |
|
|
metrics = decoded["data"] |
|
|
st.session_state.last_metrics = metrics |
|
|
print("π―π―π― METRICS RECEIVED:", metrics, flush=True) |
|
|
st.toast("π Metrics received!", icon="β
") |
|
|
|
|
|
else: |
|
|
st.error(decoded['data']) |
|
|
continue |
|
|
|
|
|
if documents: |
|
|
docs = document_holder.expander("ποΈ Sources", expanded=True) |
|
|
tabs = docs.tabs( |
|
|
tabs=[f"Document {i+1}" for i in range(len(documents))]) |
|
|
for i, doc in enumerate(documents): |
|
|
with tabs[i]: |
|
|
st.subheader(":blue[Content:]") |
|
|
st.markdown(doc['page_content']) |
|
|
st.divider() |
|
|
st.subheader(":blue[Source Details:]") |
|
|
st.json(doc['metadata'], expanded=False) |
|
|
|
|
|
print(" Updating reply_holder:", full) |
|
|
reply_holder.markdown(full + "β") |
|
|
|
|
|
|
|
|
reply_holder.markdown(full) |
|
|
|
|
|
|
|
|
print(f"π DEBUG: After streaming - metrics type={type(metrics)}, value={metrics}", flush=True) |
|
|
print(f"π DEBUG: metrics bool={bool(metrics)}, has error={'error' in metrics if metrics else 'N/A'}", flush=True) |
|
|
|
|
|
print(" Final response received: ", full) |
|
|
st.session_state.last_retrieved_docs = documents |
|
|
st.session_state.chat_history.append(Message("assistant", full)) |
|
|
st.rerun() |
|
|
|