Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Jatin-Mehra119/PDF-Insight-Beta/llms.txt

Use this file to discover all available pages before exploring further.

Every interaction with PDF Insight Pro is scoped to a session — a server-side record identified by a UUID that ties together the SentenceTransformer embedding model, the faiss.IndexHNSWFlat vector index, the ChatGroq LLM instance, the list of document chunks with their metadata, and the full conversation history. The SessionManager class in services/session_service.py owns the entire lifecycle: it creates sessions on upload, retrieves and reconstructs them on each chat turn, appends to chat history after every response, and deletes all associated files on removal. A global singleton instance (session_manager) is shared across all route handlers.

Session Lifecycle

1

Session created — SessionManager.create_session()

Called by upload_pdf_handler after the PDF has been loaded and chunked. The method:
  1. Generates a fresh uuid.uuid4() session ID.
  2. Instantiates a SentenceTransformer(Config.EMBEDDING_MODEL) (BAAI/bge-large-en-v1.5).
  3. Calls create_embeddings(chunks_with_metadata, embedding_model) to produce the embedding matrix.
  4. Calls build_faiss_index(embeddings) to produce the in-memory IndexHNSWFlat.
  5. Calls create_llm_model(model_name) to instantiate a ChatGroq object.
  6. Assembles the full session dict and writes it to both the in-memory cache (self.active_sessions) and to disk via save_session_to_file().
def create_session(self, file_path, file_name, chunks_with_metadata, model_name) -> str:
    session_id = str(uuid.uuid4())
    embedding_model = SentenceTransformer(Config.EMBEDDING_MODEL)
    embeddings, _ = create_embeddings(chunks_with_metadata, embedding_model)
    index = build_faiss_index(embeddings)
    llm = create_llm_model(model_name)

    session_data = {
        "file_path": file_path,
        "file_name": file_name,
        "chunks": chunks_with_metadata,
        "model": embedding_model,
        "index": index,
        "llm": llm,
        "chat_history": []
    }

    self.active_sessions[session_id] = session_data
    save_session_to_file(session_id, session_data)
    return session_id
2

Session retrieved — SessionManager.get_session()

Called on every POST /chat request. The lookup follows a two-stage strategy:Stage 1 — In-memory cache hit. If session_id is already in self.active_sessions, the cached dict is returned immediately. Two consistency checks are performed before returning:
  • If the cached llm is None or its model_name differs from the requested model, a new ChatGroq instance is created in-place.
  • If the model (embedding model) is None, a fresh SentenceTransformer is instantiated.
  • If index is None but chunks is present, the FAISS index is rebuilt from scratch via create_embeddings() + build_faiss_index().
Stage 2 — File-based reload. If the session is absent from memory, load_session_from_file(session_id) reads the pickle from uploads/{session_id}_session.pkl. The loaded data contains only file_path, file_name, chunks, and chat_history — the non-serialisable objects are absent. reconstruct_session_objects() re-creates the SentenceTransformer, rebuilds the FAISS index from the stored chunks, and instantiates a new ChatGroq. The fully-reconstructed dict is cached back into self.active_sessions before being returned.
def get_session(self, session_id, model_name=None):
    if model_name is None:
        model_name = Config.DEFAULT_MODEL

    if session_id in self.active_sessions:
        cached = self.active_sessions[session_id]
        # Refresh LLM if model changed
        if cached.get("llm") is None or (
            hasattr(cached["llm"], "model_name") and
            cached["llm"].model_name != model_name
        ):
            cached["llm"] = create_llm_model(model_name)
        return cached, True

    data, success = load_session_from_file(session_id)
    if not success:
        return None, False

    embedding_model = SentenceTransformer(Config.EMBEDDING_MODEL)
    full_session = reconstruct_session_objects(data, model_name, embedding_model)
    self.active_sessions[session_id] = full_session
    return full_session, True
3

Chat entry appended — add_chat_entry()

After rag_service.generate_response() returns, the chat handler calls session_manager.add_chat_entry(session_id, user_message, assistant_message). The method appends a {"user": ..., "assistant": ...} dict to the session’s chat_history list and immediately re-serialises the session to disk via save_session()save_session_to_file().
def add_chat_entry(self, session_id, user_message, assistant_message) -> bool:
    session_data, found = self.get_session(session_id)
    if not found:
        return False
    session_data["chat_history"].append({
        "user": user_message,
        "assistant": assistant_message
    })
    return self.save_session(session_id, session_data)
4

Session removed — remove_session()

Triggered by POST /remove-pdf. The method deletes the session from self.active_sessions and delegates disk cleanup to cleanup_session_files(session_id) in utils/session_utils.py. The cleanup function reads the pickle to retrieve file_path, deletes the PDF from uploads/, and then removes the pickle file itself.
def remove_session(self, session_id) -> bool:
    if session_id in self.active_sessions:
        del self.active_sessions[session_id]
    return cleanup_session_files(session_id)

Session Data Structure

A fully-populated session dict contains the following keys:
KeyTypeDescription
file_pathstrAbsolute path to the uploaded PDF in uploads/, e.g. uploads/{session_id}_{filename}.pdf
file_namestrOriginal filename as submitted by the client
chunksList[Dict]List of {"text": str, "metadata": dict} dicts produced by chunk_text()
modelSentenceTransformerEmbedding model instance (BAAI/bge-large-en-v1.5) — not serialised to disk
indexfaiss.IndexHNSWFlatIn-memory FAISS HNSW index — not serialised to disk
llmChatGroqLangChain-Groq chat model instance — not serialised to disk
chat_historyList[Dict]Ordered list of {"user": str, "assistant": str} exchange records
Only file_path, file_name, chunks, and chat_history are written to the pickle. The three non-serialisable objects (model, index, llm) are always reconstructed at load time — this is intentional and handled by reconstruct_session_objects() in utils/session_utils.py.

Persistence

PDF Insight Pro uses a hybrid in-memory + file strategy for session persistence. The in-memory cache (SessionManager.active_sessions) is a plain Python dict keyed by session UUID. It provides O(1) lookups and avoids repeated deserialisaton on every request within the same process lifetime. The on-disk representation is a Python pickle file written to Config.UPLOAD_DIR (uploads/) using the path template {session_id}_session.pkl.
# utils/session_utils.py

def prepare_pickle_safe_data(session_data: Dict[str, Any]) -> Dict[str, Any]:
    """Only serialisable fields are persisted."""
    return {
        "file_path": session_data.get("file_path"),
        "file_name": session_data.get("file_name"),
        "chunks": session_data.get("chunks"),   # list of {"text", "metadata"} dicts
        "chat_history": session_data.get("chat_history", [])
        # FAISS index, embedding model, and LLM are NOT pickled
    }

def save_session_to_file(session_id: str, session_data: Dict[str, Any]) -> bool:
    pickle_safe = prepare_pickle_safe_data(session_data)
    file_path = f"{Config.UPLOAD_DIR}/{session_id}_session.pkl"
    with open(file_path, "wb") as f:
        pickle.dump(pickle_safe, f)
    return True
Because the FAISS index is not pickled, reloading a session after a process restart requires re-encoding all stored chunks with the embedding model. This adds latency on the first request to a cold session but keeps the pickle files small and avoids FAISS serialisation complexity. Both artefacts for a given session share the same uploads/ directory:
uploads/
├── {session_id}_{original_filename}.pdf
└── {session_id}_session.pkl

Validation

Two functions guard against sessions with missing components before they reach the agent. validate_session_data(session_data) in utils/session_utils.py checks that all four required runtime keys are present:
def validate_session_data(session_data: Dict[str, Any]) -> Tuple[bool, List[str]]:
    required_keys = ["index", "chunks", "model", "llm"]
    missing_keys = [key for key in required_keys if key not in session_data]
    return len(missing_keys) == 0, missing_keys
SessionManager.validate_session(session_id) is the higher-level wrapper called by chat_handler after get_session() returns. It calls get_session() internally to ensure the in-memory copy is up-to-date, then delegates to validate_session_data() and returns the (is_valid, missing_keys) tuple. If is_valid is False, the chat handler raises an HTTP 500 with ErrorMessages.SESSION_INCOMPLETE.
Sessions are ephemeral in serverless or read-only-filesystem deployments (such as some Hugging Face Spaces tiers) where the uploads/ directory is not writable or is wiped between invocations. In those environments, save_session_to_file() will silently fail and the in-memory session will not survive a cold start or worker restart. Ensure the deployment target mounts a persistent writable volume at the path specified by Config.UPLOAD_DIR (uploads/) for sessions to survive across restarts.

Build docs developers (and LLMs) love