|
|
""" |
|
|
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 |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
@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 |
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
name = first_query[:40] + "..." if len(first_query) > 40 else first_query |
|
|
return name |
|
|
|
|
|
return "Новый диалог" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
rag = get_rag() |
|
|
|
|
|
|
|
|
current_history = get_current_chat_messages() |
|
|
|
|
|
|
|
|
result = rag.invoke(query, history=current_history) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
load_current_chat_messages() |
|
|
|
|
|
|
|
|
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 st.session_state.current_dialogue_id == dialogue_id: |
|
|
st.session_state.current_dialogue_id = None |
|
|
st.session_state.current_chat_messages = [] |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def page_chat(): |
|
|
"""Main chat interface page""" |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
/* Fix chat input at the bottom of main content area */ |
|
|
section[data-testid="stSidebar"] ~ div .stChatInput { |
|
|
position: fixed; |
|
|
bottom: 0; |
|
|
background: white; |
|
|
padding: 1rem; |
|
|
z-index: 999; |
|
|
border-top: 1px solid #e6e6e6; |
|
|
margin-left: 0; |
|
|
} |
|
|
|
|
|
/* Add padding to main content to prevent overlap with fixed input */ |
|
|
.main .block-container { |
|
|
padding-bottom: 100px; |
|
|
} |
|
|
|
|
|
/* Dark mode support */ |
|
|
[data-testid="stAppViewContainer"][data-theme="dark"] section[data-testid="stSidebar"] ~ div .stChatInput { |
|
|
background: rgb(14, 17, 23); |
|
|
border-top: 1px solid #333; |
|
|
} |
|
|
|
|
|
/* Adjust width to account for sidebar */ |
|
|
@media (min-width: 768px) { |
|
|
section[data-testid="stSidebar"] ~ div .stChatInput { |
|
|
left: var(--sidebar-width, 21rem); |
|
|
right: 0; |
|
|
} |
|
|
} |
|
|
|
|
|
/* When sidebar is collapsed */ |
|
|
section[data-testid="stSidebar"][aria-expanded="false"] ~ div .stChatInput { |
|
|
left: 0; |
|
|
} |
|
|
</style> |
|
|
|
|
|
<script> |
|
|
// Add keyboard shortcuts support |
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
// Find chat input field |
|
|
const observer = new MutationObserver(function(mutations) { |
|
|
const chatInput = document.querySelector('textarea[data-testid="stChatInput"]'); |
|
|
if (chatInput && !chatInput.hasAttribute('data-shortcut-attached')) { |
|
|
chatInput.setAttribute('data-shortcut-attached', 'true'); |
|
|
|
|
|
// Add keyboard event listener |
|
|
chatInput.addEventListener('keydown', function(e) { |
|
|
// Enter (without Shift) - send message |
|
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
// Trigger the send button |
|
|
const sendButton = document.querySelector('button[kind="primary"]'); |
|
|
if (sendButton) { |
|
|
sendButton.click(); |
|
|
} |
|
|
} |
|
|
// Ctrl+Enter or Cmd+Enter - send message (alternative) |
|
|
else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { |
|
|
e.preventDefault(); |
|
|
const sendButton = document.querySelector('button[kind="primary"]'); |
|
|
if (sendButton) { |
|
|
sendButton.click(); |
|
|
} |
|
|
} |
|
|
// Shift+Enter - new line (default behavior) |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
observer.observe(document.body, { |
|
|
childList: true, |
|
|
subtree: true |
|
|
}); |
|
|
}); |
|
|
</script> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
if not st.session_state.current_dialogue_id: |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
current_messages = get_current_chat_messages() |
|
|
|
|
|
|
|
|
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("---") |
|
|
|
|
|
|
|
|
if not current_messages: |
|
|
st.info("📝 Начните диалог, задав первый вопрос ниже") |
|
|
else: |
|
|
|
|
|
for msg in current_messages: |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
with st.chat_message("assistant"): |
|
|
st.markdown(msg["answer"]) |
|
|
|
|
|
|
|
|
if msg.get("reason"): |
|
|
with st.expander("📝 Обоснование"): |
|
|
st.markdown(msg["reason"]) |
|
|
|
|
|
|
|
|
query = st.chat_input( |
|
|
"Введите ваш вопрос...", |
|
|
key="chat_input" |
|
|
) |
|
|
|
|
|
if query: |
|
|
|
|
|
with st.spinner("🤔 Думаю..."): |
|
|
result = send_message(query) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(): |
|
|
st.set_page_config( |
|
|
page_title="RAG Chat System", |
|
|
page_icon="💬", |
|
|
layout="wide", |
|
|
initial_sidebar_state="expanded" |
|
|
) |
|
|
|
|
|
|
|
|
init_session_state() |
|
|
|
|
|
|
|
|
if "needs_rerun" not in st.session_state: |
|
|
st.session_state.needs_rerun = False |
|
|
|
|
|
|
|
|
init_db() |
|
|
|
|
|
|
|
|
if not st.session_state.chat_list_loaded: |
|
|
load_chats_list() |
|
|
|
|
|
|
|
|
with st.sidebar: |
|
|
st.title("💬 RAG Chat") |
|
|
|
|
|
|
|
|
if st.button("➕ Новый чат", use_container_width=True, type="primary"): |
|
|
create_new_chat() |
|
|
|
|
|
st.markdown("---") |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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", "") |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
try: |
|
|
dt = datetime.fromisoformat(started_at) |
|
|
time_str = dt.strftime('%d.%m %H:%M') |
|
|
except: |
|
|
time_str = "" |
|
|
|
|
|
|
|
|
is_current = dialogue_id == st.session_state.current_dialogue_id |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
if st.session_state.needs_rerun: |
|
|
st.session_state.needs_rerun = False |
|
|
st.rerun() |
|
|
|
|
|
|
|
|
page_chat() |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |
|
|
|