smart-doc-chat / app.py
muusto's picture
Update app.py
28b4135 verified
Raw
History Blame Contribute Delete
4.23 kB
import json
import os
import re
from pathlib import Path
import streamlit as st
from rag.ingestion_pipeline import process_doc, create_vector_store
from rag.retrieval_pipeline import retrieval_pipeline, load_vector_store, chat_history
import hashlib
DB_DIR = Path("db")
INDEX_PATH = DB_DIR / "index.json"
st.title("RAG Assistant")
def load_index():
""" Load the stored filename and his path. """
if INDEX_PATH.exists():
return json.loads(INDEX_PATH.read_text())
return {}
def save_index(data):
""" Save the filename mapping to disk """
# Persist the filename -> db_path mapping on disk.
DB_DIR.mkdir(parents=True, exist_ok=True)
INDEX_PATH.write_text(json.dumps(data, indent=2))
def file_hash(uploaded_file) -> str :
""" Return the content of the file uploaded in a short MD5 hash """
hash_md5 = hashlib.md5()
hash_md5.update(uploaded_file.getbuffer())
return hash_md5.hexdigest()[:10]
def safe_name(file_name: str, uploaded_file) -> str:
""" Convert The uploaded filename into a unique, safe and clean folder name"""
# Convert the uploaded filename into a safe folder name.
# This avoids spaces and special characters in the DB path.
clean_name = re.sub(r"[^a-zA-Z0-9_-]", "_", Path(file_name).stem)
file_name_hashed = file_hash(uploaded_file)
return f"{clean_name}_{file_name_hashed}"
def get_db_path(filename: str, uploaded_file) -> str:
""" Return the local Chroma database path for a specific uploaded name."""
# Each filename gets its own Chroma folder.
# This avoids deleting an open DB and causing readonly/locked errors.
return str(DB_DIR / f"chroma_db_{safe_name(filename, uploaded_file)}")
# Load the filename registry once at startup.
if "file_index" not in st.session_state:
st.session_state.file_index = load_index()
if "vector_store" not in st.session_state:
st.session_state.vector_store = None
st.session_state.current_document = None
uploaded = st.file_uploader("Upload a PDF or TXT file", type=["pdf", "txt"])
# If a different file is selected, clear the current in-memory store.
if uploaded and uploaded.name != st.session_state.current_document:
st.info("A new file was selected. Process it to load or create its document store.")
st.session_state.vector_store = None
# Process only when the button is clicked.
if uploaded and st.button("Process Document"):
file_name = uploaded.name
registry_key = safe_name(file_name, uploaded)
db_path = st.session_state.file_index.get(registry_key, get_db_path(file_name, uploaded))
# If this file was already processed before, reuse its existing vector store.
if os.path.exists(db_path):
st.session_state.vector_store = load_vector_store(db_path)
st.session_state.current_document = file_name
st.success("Existing vector store loaded for this file.")
else:
# New file: parse, chunk, and store it in a new Chroma folder.
chunked_text = process_doc(uploaded, file_hash(uploaded))
if not chunked_text:
st.warning("No text chunks were created from the uploaded file.")
else:
# Clear chat history because this is a different document.
chat_history.clear()
with st.spinner("Creating vector store..."):
st.session_state.vector_store = create_vector_store(
chunked_text,
path=db_path,
)
# Save the filename -> db_path mapping so we can reuse it later.
st.session_state.file_index[registry_key] = db_path
save_index(st.session_state.file_index)
st.session_state.current_document = file_name
st.success(f"Split into {len(chunked_text)} chunks")
st.success("Document processed and saved.")
if st.session_state.vector_store is None:
st.info("Upload and process a document before asking questions.")
question = st.text_input(
"Ask a question about your document",
disabled=st.session_state.vector_store is None,
)
if question and st.session_state.vector_store:
answer = retrieval_pipeline(question, st.session_state.vector_store)
st.write(answer)