""" Streamlit Frontend для RAG вопросно-ответной системы Чат-интерфейс с поддержкой нескольких диалогов """ import streamlit as st from datetime import datetime, timedelta from typing import List, Dict, Optional import uuid from src import RAG from src.db_utils.history_utils import ( init_history_table, log_query, get_all_history, get_history_by_dialogue, search_history, get_history_stats, delete_history, get_recent_dialogues ) # --- Инициализация RAG и БД --- @st.cache_resource(show_spinner=False) def get_rag(): """Initialize RAG once and cache it""" return RAG( embed_model_name = "Qwen/Qwen3-Embedding-0.6B", embed_index_name = "recursive_Qwen3-Embedding-0.6B" ) @st.cache_resource(show_spinner=False) def init_db(): """Initialize database once and cache it""" try: init_history_table() return True except Exception as e: st.error(f"⚠️ Не удалось инициализировать таблицу истории: {e}") return False # --- Session State Management --- def init_session_state(): """Initialize session state with caching""" if "current_dialogue_id" not in st.session_state: st.session_state.current_dialogue_id = None if "chat_list" not in st.session_state: st.session_state.chat_list = [] if "current_chat_messages" not in st.session_state: st.session_state.current_chat_messages = [] if "chat_list_loaded" not in st.session_state: st.session_state.chat_list_loaded = False def generate_dialogue_id() -> str: """Generate unique dialogue ID""" return f"chat_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}" def get_chat_display_name(dialogue_id: str, first_query: str = None) -> str: """Get display name for chat - always from DB, no caching""" if first_query: # Use first 40 chars of first query as name name = first_query[:40] + "..." if len(first_query) > 40 else first_query return name return "Новый диалог" # --- Chat Management Functions --- def load_chats_list(): """Load and cache chats list from DB""" try: st.session_state.chat_list = get_recent_dialogues(limit=50) st.session_state.chat_list_loaded = True except Exception as e: st.error(f"❌ Ошибка при загрузке чатов: {e}") st.session_state.chat_list = [] def create_new_chat(): """Create a new chat""" new_id = generate_dialogue_id() st.session_state.current_dialogue_id = new_id st.session_state.current_chat_messages = [] st.session_state.needs_rerun = True return new_id def switch_to_chat(dialogue_id: str): """Switch to an existing chat and load its messages""" st.session_state.current_dialogue_id = dialogue_id load_current_chat_messages() st.session_state.needs_rerun = True def load_current_chat_messages(): """Load messages for current chat from DB and cache""" if not st.session_state.current_dialogue_id: st.session_state.current_chat_messages = [] return try: st.session_state.current_chat_messages = get_history_by_dialogue( st.session_state.current_dialogue_id ) except Exception as e: st.error(f"❌ Ошибка при загрузке сообщений: {e}") st.session_state.current_chat_messages = [] def get_current_chat_messages() -> List[Dict]: """Get cached messages for current chat""" return st.session_state.current_chat_messages def send_message(query: str) -> Optional[Dict]: """Send a message in current chat and update cache""" try: if not st.session_state.current_dialogue_id: create_new_chat() # Get RAG and invoke with cached history rag = get_rag() # Use cached messages current_history = get_current_chat_messages() # Pass history to RAG (it will use last N messages internally for enrichment) result = rag.invoke(query, history=current_history) # Log to history DB query_id = log_query( query=query, answer=result.get("answer", ""), reason=result.get("reason", ""), dialogue_id=st.session_state.current_dialogue_id ) result["query_id"] = query_id # Update only current messages, not all chats load_current_chat_messages() # Mark that we need to refresh chat list (but don't do it immediately) st.session_state.chat_list_loaded = False st.session_state.needs_rerun = True return result except Exception as e: st.error(f"❌ Ошибка при отправке сообщения: {e}") return None def delete_chat(dialogue_id: str) -> bool: """Delete a chat from DB and update cache""" try: delete_history(dialogue_id=dialogue_id) # If deleted current chat, clear selection if st.session_state.current_dialogue_id == dialogue_id: st.session_state.current_dialogue_id = None st.session_state.current_chat_messages = [] # Mark that we need to reload chat list st.session_state.chat_list_loaded = False st.session_state.needs_rerun = True return True except Exception as e: st.error(f"❌ Ошибка при удалении чата: {e}") return False # --- Page: Chat Interface --- def page_chat(): """Main chat interface page""" # Custom CSS to fix chat input at the bottom + keyboard shortcuts st.markdown(""" """, unsafe_allow_html=True) # Check if we have a current chat if not st.session_state.current_dialogue_id: # Show welcome screen st.title("💬 Чат с RAG системой") st.markdown("---") col1, col2, col3 = st.columns([1, 2, 1]) with col2: st.info("👋 Добро пожаловать! Создайте новый чат или выберите существующий из списка слева.") if st.button("🆕 Начать новый чат", type="primary", use_container_width=True): create_new_chat() return # Get cached messages current_messages = get_current_chat_messages() # Display chat header if current_messages: chat_name = get_chat_display_name( st.session_state.current_dialogue_id, current_messages[0]["query"] ) else: chat_name = "Новый диалог" col1, col2 = st.columns([4, 1]) with col1: st.title(f"💬 {chat_name}") with col2: if st.button("🗑️ Удалить чат", use_container_width=True): if delete_chat(st.session_state.current_dialogue_id): st.success("✅ Чат удален") st.markdown("---") # Chat messages container - load from DB if not current_messages: st.info("📝 Начните диалог, задав первый вопрос ниже") else: # Display all messages for msg in current_messages: # User message with st.chat_message("user"): st.markdown(msg["query"]) timestamp_str = msg.get("timestamp", "") try: dt = datetime.fromisoformat(timestamp_str) st.caption(f"🕐 {dt.strftime('%H:%M:%S')}") except: pass # Assistant message with st.chat_message("assistant"): st.markdown(msg["answer"]) # Show reasoning in expander if msg.get("reason"): with st.expander("📝 Обоснование"): st.markdown(msg["reason"]) # Input area - fixed at the bottom via CSS query = st.chat_input( "Введите ваш вопрос...", key="chat_input" ) if query: # Send message and get response with st.spinner("🤔 Думаю..."): result = send_message(query) # --- Main App --- def main(): st.set_page_config( page_title="RAG Chat System", page_icon="💬", layout="wide", initial_sidebar_state="expanded" ) # Initialize session state FIRST (before any other operations) init_session_state() # Initialize needs_rerun flag if not exists if "needs_rerun" not in st.session_state: st.session_state.needs_rerun = False # Initialize history table once using cache init_db() # Load chats list if not loaded yet if not st.session_state.chat_list_loaded: load_chats_list() # Sidebar with st.sidebar: st.title("💬 RAG Chat") # New chat button if st.button("➕ Новый чат", use_container_width=True, type="primary"): create_new_chat() st.markdown("---") # Chats list - use cached col1, col2 = st.columns([3, 1]) with col1: st.subheader("📝 Ваши чаты") with col2: if st.button("🔄", help="Обновить список чатов"): st.session_state.chat_list_loaded = False load_chats_list() if not st.session_state.chat_list: st.info("Нет чатов. Создайте новый!") else: # Display chats from cache for chat in st.session_state.chat_list: dialogue_id = chat["dialogue_id"] message_count = chat.get("message_count", 0) started_at = chat.get("started_at", "") # Get chat name (only load history if chat has messages) if message_count > 0: history = get_history_by_dialogue(dialogue_id) first_query = history[0]["query"] if history else None else: first_query = None chat_name = get_chat_display_name(dialogue_id, first_query) # Format time try: dt = datetime.fromisoformat(started_at) time_str = dt.strftime('%d.%m %H:%M') except: time_str = "" # Check if this is current chat is_current = dialogue_id == st.session_state.current_dialogue_id # Format button text with chat name and metadata button_text = f"{'📌' if is_current else '💬'} {chat_name}\n💬 {message_count} • {time_str}" if st.button( button_text, key=f"chat_{dialogue_id}", use_container_width=True, type="primary" if is_current else "secondary" ): switch_to_chat(dialogue_id) # Handle rerun at the end if needed if st.session_state.needs_rerun: st.session_state.needs_rerun = False st.rerun() # Main content area page_chat() if __name__ == "__main__": main()