shareefmx's picture
Deploy PDF Knowledge Assistant to Hugging Face Spaces
a58c437 verified
Raw History Blame Contribute Delete
6.47 kB
"""Application Shared State and Service Dependency Provider.
Manages singletons for the vector store, embedding service, and RAG pipeline.
"""
from functools import lru_cache
import logging
from pathlib import Path
from typing import Optional
from app.config import Settings, get_settings
from app.embeddings.embedding_service import EmbeddingService, get_embedding_service
from app.generation.llm import LLMServiceFactory
from app.ingestion.chunker import RecursiveChunker
from app.ingestion.pdf_loader import PDFLoader
from app.rag.pipeline import RAGPipeline
from app.retrieval.query_rewriter import QueryRewriter
from app.retrieval.reranker import CrossEncoderReranker
from app.retrieval.retriever import DocumentRetriever
from app.vectorstore.base import BaseVectorStore
from app.vectorstore.chroma_store import ChromaVectorStore
from app.vectorstore.faiss_store import FAISSVectorStore
logger = logging.getLogger(__name__)
class AppState:
"""Encapsulates active services and indices across the application lifetime."""
def __init__(self, settings: Optional[Settings] = None):
self.settings = settings or get_settings()
self.pdf_loader = PDFLoader()
self.chunker = RecursiveChunker(
chunk_size=self.settings.chunk_size,
chunk_overlap=self.settings.chunk_overlap,
)
# 1. Embedding Service
self.embedding_service = EmbeddingService(
model_name=self.settings.embedding_model
)
# 2. Vector Database
self.vector_store: BaseVectorStore = self._init_vector_store()
# 3. Retriever
self.retriever = DocumentRetriever(
vector_store=self.vector_store,
embedding_service=self.embedding_service,
similarity_threshold=self.settings.similarity_threshold,
default_top_k=self.settings.top_k,
)
# 4. LLM Service
self.llm_service = LLMServiceFactory.create(
provider=self.settings.llm_provider,
gemini_api_key=self.settings.gemini_api_key,
nvidia_api_key=self.settings.nvidia_api_key,
openrouter_api_key=self.settings.openrouter_api_key,
model_name=self.settings.llm_model,
nvidia_model=self.settings.nvidia_model,
)
# 5. Optional Reranker
self.reranker = None
if self.settings.use_reranker:
try:
self.reranker = CrossEncoderReranker(model_name=self.settings.reranker_model)
except Exception as e:
logger.warning("Could not initialize reranker: %s", e)
# 6. Optional Query Rewriter
self.query_rewriter = None
if self.settings.enable_query_rewriting:
self.query_rewriter = QueryRewriter(llm_service=self.llm_service)
# 7. RAG Pipeline
self.pipeline = RAGPipeline(
retriever=self.retriever,
llm_service=self.llm_service,
reranker=self.reranker,
query_rewriter=self.query_rewriter,
)
def _init_vector_store(self) -> BaseVectorStore:
store_type = self.settings.vector_store.lower()
store_dir = self.settings.get_vectorstore_path()
if store_type == "chroma":
logger.info("Initializing Chroma vector store at '%s'", store_dir)
return ChromaVectorStore(persist_directory=store_dir)
else:
logger.info("Initializing FAISS vector store with dimension %d", self.embedding_service.dimension)
faiss_store = FAISSVectorStore(dimension=self.embedding_service.dimension)
# Attempt to restore if persisted files exist
if (store_dir / "index.faiss").exists() and (store_dir / "metadata.json").exists():
try:
faiss_store.load(store_dir)
logger.info("Restored existing FAISS index with %d chunks", faiss_store.total_chunks())
except Exception as e:
logger.warning("Could not restore FAISS index: %s", e)
return faiss_store
def index_pdf_file(self, file_path: Path) -> dict:
"""Processes a PDF file through extraction, cleaning, chunking, and embedding into the vector store."""
# Clean up existing index for the same filename if previously indexed
for doc in self.vector_store.list_documents():
if doc["filename"] == file_path.name:
logger.info("Replacing existing index for '%s' (ID: %s)", file_path.name, doc["document_id"])
self.vector_store.delete_document(doc["document_id"])
pages = self.pdf_loader.load(file_path)
chunks = self.chunker.chunk_documents(pages)
if not chunks:
raise ValueError(f"No valid text chunks generated from '{file_path.name}'.")
# Generate embeddings in batch
texts = [c.text for c in chunks]
embeddings = self.embedding_service.embed_documents(texts)
# Store in vector database
self.vector_store.add_chunks(chunks, embeddings)
# Persist index
try:
self.vector_store.save(self.settings.get_vectorstore_path())
except Exception as e:
logger.warning("Could not auto-save vector store: %s", e)
return {
"document_id": pages[0].document_id,
"filename": file_path.name,
"pages_extracted": len(pages),
"chunks_created": len(chunks),
"total_indexed_chunks": self.vector_store.total_chunks(),
}
def reload_vector_store_if_needed(self) -> None:
"""Reloads FAISS vector store from disk if persisted files exist and in-memory is empty."""
store_dir = self.settings.get_vectorstore_path()
index_file = store_dir / "index.faiss"
meta_file = store_dir / "metadata.json"
if index_file.exists() and meta_file.exists():
try:
self.vector_store.load(store_dir)
logger.info("Synchronized FAISS index from disk (%d chunks)", self.vector_store.total_chunks())
except Exception as e:
logger.warning("Could not sync FAISS index from disk: %s", e)
# Global app state singleton
_APP_STATE: Optional[AppState] = None
def get_app_state() -> AppState:
"""Returns application state singleton."""
global _APP_STATE
if _APP_STATE is None:
_APP_STATE = AppState()
return _APP_STATE