File size: 24,064 Bytes
77348b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
"""
Vector Database Management for Legal RAG Pipeline
================================================

This module provides comprehensive vector database functionality for the Legal
RAG Pipeline, including document loading, text extraction, OCR processing,
and FAISS-based vector storage with GPU acceleration support.

Features:
- Multi-format document loading (PDF, web pages, text files)
- OCR fallback for scanned documents and images
- FAISS vector store with GPU acceleration
- Chunking and embedding of documents
- Web document scraping and processing
- Local file caching and management


"""

from langchain_community.document_loaders import WebBaseLoader, PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
import faiss
try:
    from faiss import StandardGpuResources, index_cpu_to_gpu
except ImportError:
    StandardGpuResources = None
    index_cpu_to_gpu = None
from langchain_community.vectorstores import FAISS
from langchain_community.docstore.in_memory import InMemoryDocstore
from langchain.text_splitter import RecursiveCharacterTextSplitter
from src.generation.model_provider import get_embedding_model
from langchain_core.documents import Document
import os
import pytesseract
from pdf2image import convert_from_path
import webbrowser
from PIL import Image
import pytesseract
import requests
import time
from src.generation.model_provider import get_embedding_model
import numpy as np
from uuid import uuid4
from pathlib import Path
from tqdm import tqdm
import logging

# Set up logger
logger = logging.getLogger(__name__)

# Ensure local directory exists for storing downloaded PDFs
local_file_dir = "app_data/local_pdfs"
os.makedirs(local_file_dir, exist_ok=True)


def extract_text_with_ocr(file_path):
    """
    Extract text from PDF or image files using OCR technology.
    
    This function uses Tesseract OCR to extract text from scanned documents
    and images when standard text extraction methods fail.
    
    Args:
        file_path (str): Path to the PDF or image file
        
    Returns:
        str: Extracted text content
    """
    ext = os.path.splitext(file_path)[1].lower()
    text = ""
    
    try:
        if ext == ".pdf":
            # Convert PDF pages to images and apply OCR
            images = convert_from_path(file_path)
            for img in images:
                text += pytesseract.image_to_string(img)
        elif ext in [".png", ".jpg", ".jpeg", ".bmp", ".tiff"]:
            # Apply OCR directly to image files
            img = Image.open(file_path)
            text = pytesseract.image_to_string(img)
    except Exception as e:
        logger.info(f"OCR extraction failed for {file_path}: {e}")
    
    return text


def load_source_with_fallback(source):
    """
    Load document text with automatic fallback to OCR if needed.
    
    This function attempts to load documents using standard methods
    (PyPDFLoader for PDFs, web scraping for URLs) and falls back to
    OCR processing if standard extraction fails or produces poor results.
    
    Supports multiple document formats:
    - PDF files (local and web URLs)
    - Image files (PNG, JPG, JPEG, BMP, TIFF)
    - Text files (UTF-8 encoded)
    
    Args:
        source (str): Path to the document file or URL
        
    Returns:
        Document: LangChain Document object containing extracted text and metadata
    """
    doc = None
    local_file_path = os.path.join(local_file_dir, source.split("/")[-1])
    ext = os.path.splitext(source)[1].lower()
    previous_cache_from_web = f"{local_file_path}.txt"

    if os.path.exists(previous_cache_from_web):
        source = previous_cache_from_web
        ext = ".txt"

    if ext == ".pdf":
        # Handle web PDF downloads
        if source.startswith("https"):
            logger.info(f"\tLoading PDF from URL: {source}")
            # Download the PDF if it doesn't exist locally
            if not os.path.exists(local_file_path):
                headers = {
                            "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0",
                            }

                response = requests.get(source, headers=headers, stream=True)
                if response.status_code == 200:
                    os.makedirs(local_file_dir, exist_ok=True)
                    with open(local_file_path, "wb") as pdf_file:
                        for chunk in response.iter_content(chunk_size=1024):
                            pdf_file.write(chunk)
                    logger.info(f"PDF downloaded successfully to {local_file_path}")
                    ocr_text = extract_text_with_ocr(local_file_path)
                    if ocr_text.strip():
                        doc = Document(page_content=ocr_text, metadata={'source': source, "ocr_applied": True})
    
                elif response.status_code == 404:
                    source = source.replace(".pdf", ".html")
                    web_doc = WebBaseLoader(source, continue_on_failure=True).load()[0]
                    web_doc.page_content = web_doc.page_content.strip()
                    with open(previous_cache_from_web, "w", encoding="utf-8") as f:
                        f.write(web_doc.page_content)
                    return web_doc

                elif response.status_code == 429:
                    time.sleep(10)  # Wait for the browser to close
                    return load_source_with_fallback(source)

            else:
                logger.info(f"PDF already exists locally at {local_file_path}")
                ocr_text = extract_text_with_ocr(local_file_path)
                if ocr_text.strip():
                    doc = Document(page_content=ocr_text, metadata={'source': source, "ocr_applied": True})
    
        else:
            logger.info(f"\tUsing PyPDFLoader for local PDF")
            loader = PyPDFLoader(source)
            doc = loader.load()[0]

        # If no text extracted, fallback to OCR
        if not doc or not doc.page_content.strip():
            logger.info(f"\tNo text extracted from PDF, applying OCR")
            ocr_text = extract_text_with_ocr(source)
            if ocr_text.strip():
                doc = Document(page_content=ocr_text, metadata={'source': source, "ocr_applied": True})
    elif ext in [".png", ".jpg", ".jpeg", ".bmp", ".tiff"]:
        ocr_text = extract_text_with_ocr(source)
        if ocr_text.strip():
            doc = Document(page_content=ocr_text, metadata={'source': source, "ocr_applied": True})
    else:
        # Fallback: treat as text file
        logger.info(f"\tLoading text file: {source}")
        with open(source, 'r', encoding='utf-8', errors='ignore') as f:
            content = f.read()
        doc = Document(page_content=content, metadata={'source': source})
    return doc

# Index documents from URLs and create a retriever for RAG

class VectorStoreManager:
    """
    Vector Store Manager for Legal Document Processing.
    
    This class manages FAISS vector stores with GPU acceleration support,
    handling document indexing, embedding generation, and similarity search
    for legal document retrieval.
    
    Features:
    - GPU-accelerated FAISS indexing with configurable memory allocation
    - Multiple document format support (PDF, web, text)
    - Automatic embedding dimension detection
    - IVF (Inverted File) indexing for large-scale similarity search
    - Persistent storage and loading of vector indexes
    - Memory optimization for resource-constrained environments
    
    Attributes:
        file_path (str): Path to vector store file for persistence
        use_gpu (bool): Whether to use GPU acceleration
        nlist (int): Number of clusters for IVF index
        gpu_memory_mb (int): GPU memory allocation in MB
        gpu_res: FAISS GPU resources object
        embeddings: Embedding model instance
        dim (int): Embedding vector dimensionality
        vector_store: FAISS vector store instance
    """
    
    def __init__(self, file_path=None, nlist=100, gpu_memory_mb=128):
        """
        Initialize VectorStoreManager with configurable GPU and memory settings.
        
        Sets up embedding model, detects vector dimensions, and initializes
        GPU resources.
        
        Args:
            file_path (str, optional): Path to existing vector store file
            nlist (int): Number of clusters for IVF index
            gpu_memory_mb (int): GPU memory allocation in MB
        """
        self.file_path = file_path
        self.nlist = nlist
        self.gpu_memory_mb = gpu_memory_mb
        self.gpu_res = None
        self.batch_size = 128
        
        # Configure embedding model with optimized settings
        self.embeddings_config = {"wait_time": 60}
        self.embeddings = get_embedding_model("mistral-embed", **self.embeddings_config)

        # Detect embedding dimensionality
        test_embedding = self.embeddings.embed_query("Test query to determine embedding dimensionality.")
        self.dim = len(test_embedding)
        logger.info(f"Embedding dimension detected: {self.dim}")


        # Initialize GPU resources when available. Hugging Face Spaces and
        # most local installs use faiss-cpu, which does not expose GPU helpers.
        if StandardGpuResources is not None:
            try:
                self.gpu_res = StandardGpuResources()
                self.gpu_res.setTempMemory(self.gpu_memory_mb * 1024 * 1024)
                logger.info(f"GPU resources initialized for FAISS with {self.gpu_memory_mb} MB memory allocation.")
            except Exception as e:
                logger.info(f"FAISS GPU initialization failed; using CPU index: {e}")
                self.gpu_res = None
        else:
            logger.info("FAISS GPU helpers are unavailable; using CPU index.")
          
        if self.file_path: # Load existing vector store from disk
            self.vector_store = FAISS.load_local(
                self.file_path, self.embeddings, allow_dangerous_deserialization=True
            )
            self._move_to_gpu()
        else: # Create a new vector store
            # Start with 'Flat' and upgrade to IVF when dataset size exceeds threshold
            index = self._create_optimized_index()
            self.vector_store = FAISS(
                embedding_function=self.embeddings,
                index=index,
                docstore=InMemoryDocstore(),
                index_to_docstore_id={},
            )

        self.text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
            chunk_size=1500,
            chunk_overlap=150, # 10% overlap
            separators=["\n\n", "\n", ".", "!", "?", "。", "!", "?", " "]
        )

    def _train_index(self, embedding_vectors: list[np.ndarray]):
        """
        Train the FAISS index if it requires training (IVF indices).
        Also handles switching from Flat to IVF index when enough vectors are available.
        
        Args:
            embedding_vectors: List of embedding vectors for training
        """
        index = self.vector_store.index
        num_vectors = len(embedding_vectors)

        # Check if we have enough vectors to qualify for IVF
        ivf_index_required = num_vectors + index.ntotal >= self.nlist * 2 
        
        # Upgrade
        if ("Flat" in type(index).__name__ and 
            ivf_index_required):  # Check if we have enough vectors to qualify for IVF
            logger.info(f"Switching to IVF index with {num_vectors + index.ntotal} vectors (>= {self.nlist * 2})")
            self._upgrade_to_ivf_index()
            index = self.vector_store.index
        
            # Check if we need to train the index
            if hasattr(index, "is_trained") and not index.is_trained:
                # Check if we have enough vectors to train IVF index
                if ivf_index_required:
                    logger.info(f"Not enough vectors to train IVF index with {self.nlist} clusters. "
                        f"Need at least {self.nlist} vectors. Deferring training.")
                    return
                
            vectors_np = np.array(embedding_vectors, dtype='float32')
            logger.info(f"Training index with {len(vectors_np)} vectors...")
            
            # Training happens on the current device (GPU or CPU)
            index.train(vectors_np)
            logger.info("Index training completed.")
     
    def _move_to_gpu(self):
        """
        Move the loaded vector store index to GPU.
        Handles different index types that might be loaded from disk.
        """
        if self.gpu_res:
            current_index = self.vector_store.index
            
            # Check if index is already on GPU
            if "Gpu" in type(current_index).__name__:
                logger.info("Index is already on GPU.")
                return
                
            try:
                # Move index to GPU
                gpu_index = index_cpu_to_gpu(self.gpu_res, 0, current_index)
                self.vector_store.index = gpu_index
                logger.info(f"Vector store index moved to GPU after load: {type(gpu_index).__name__}")
            except Exception as e:
                logger.info(f"Failed to move index to GPU (will continue with CPU): {e}")

    def _upgrade_to_ivf_index(self):
        """
        Upgrade from a Flat index to an IVF index when we have enough vectors.
        """
        current_index = self.vector_store.index
        
        # Extract all vectors from current index
        if current_index.ntotal > 0:
            logger.info(f"Extracting {current_index.ntotal} vectors from current index...")
            vectors = np.zeros((current_index.ntotal, self.dim), dtype='float32')
            for i in range(current_index.ntotal):
                vectors[i] = current_index.reconstruct(i)
        else:
            vectors = np.empty((0, self.dim), dtype='float32')
        
        # Create new IVF index
        quantizer = faiss.IndexFlatL2(self.dim)
        new_index = faiss.IndexIVFFlat(quantizer, self.dim, self.nlist)
        
        # Move to GPU if needed
        if self.gpu_res:
            try:
                new_index = index_cpu_to_gpu(self.gpu_res, 0, new_index)
                logger.info("New IVF index moved to GPU.")
            except Exception as e:
                logger.info(f"Failed to move new IVF index to GPU: {e}")
                logger.info("Continuing with CPU-based IVF index.")
                # Don't disable GPU entirely, just use CPU for this index
        
        # Train the new index if we have enough vectors
        if len(vectors) >= self.nlist:
            new_index.train(vectors)
            logger.info(f"New IVF index trained with {len(vectors)} vectors.")
        
        # Add existing vectors to new index
        if len(vectors) > 0:
            new_index.add(vectors)
            logger.info(f"Added {len(vectors)} existing vectors to new IVF index.")
        
        # Replace the index in vector store
        self.vector_store.index = new_index
        logger.info("Successfully upgraded to IVF index.")

    def _index_document(self, content, doc_metadata={'type': "unknown"}):
        """
        Index a single document by splitting it into chunks and adding to vector store.
        
        Args:
            content: Document content to index
            doc_metadata: Metadata for the document
            
        Returns:
            List of chunk UUIDs
        """
        doc_chunks = self.text_splitter.split_text(content)
        chunk_uuids = [str(uuid4()) for _ in range(len(doc_chunks))]
        
        # Clean up chunks to remove any empty strings
        doc_chunks = [chunk for chunk in doc_chunks if chunk.strip()]

        
        # Generate embeddings for all chunks
        vectors = [self.embeddings.embed_query(chunk) for chunk in doc_chunks]

        try:
            # Train the index if necessary (only for IVF indices)
            self._train_index(vectors)
            
            # Add texts with appropriate batch size for GPU performance
            self.vector_store.add_texts(
                texts=doc_chunks,
                metadatas=[doc_metadata] * len(doc_chunks),
                ids=chunk_uuids,
                batch_size=self.batch_size
            )
            logger.info(f"Successfully indexed {len(doc_chunks)} chunks from {doc_metadata.get('type', 'unknown')} document.")
        except Exception as e:
            logger.info(f"Error adding document {doc_metadata.get('type', 'unknown')} to vector store: {e}")
        return chunk_uuids

    def save_vector_store(self, index_path=None):
        if not index_path:
            if not self.file_path:
                timestamp = int(time.time())
                index_path = os.path.join("data/vector_db", f"vector_store_{timestamp}")
                os.makedirs(index_path, exist_ok=True)
            else:
                index_path = self.file_path
        else:
            os.makedirs(index_path, exist_ok=True)

        # Move index to CPU before saving if on GPU
        index_on_gpu = False
        current_index = self.vector_store.index
        
        # Check if index is on GPU by examining its type name
        if "Gpu" in type(current_index).__name__:
            try:
                import faiss
                cpu_index = faiss.index_gpu_to_cpu(current_index)
                self.vector_store.index = cpu_index
                index_on_gpu = True
                logger.info("Moved index to CPU for saving.")
            except Exception as e:
                logger.info(f"Failed to move index to CPU for saving: {e}")

        self.vector_store.save_local(index_path)
        self.file_path = index_path
        logger.info(f"Saved vector store to: {index_path}")

        # Move back to GPU if needed
        if index_on_gpu and self.gpu_res:
            try:
                from faiss import index_cpu_to_gpu
                self.vector_store.index = index_cpu_to_gpu(self.gpu_res, 0, self.vector_store.index)
                logger.info("Restored index to GPU after saving.")
            except Exception as e:
                logger.info(f"Failed to restore index to GPU after saving: {e}")
        return index_path

    def index_file_documents(self, paths, source_type):
        meta_datas = []
        for path in tqdm(paths, desc="Indexing file documents"):
            doc = load_source_with_fallback(path)
            if not doc:
                logger.info(f"Failed to load document from {path}. Skipping.")
                continue
            doc.metadata["type"] = source_type
            doc.metadata["chunk_ids"] = self._index_document(doc.page_content, doc.metadata)
            meta_datas.append(doc.metadata)
        return meta_datas
    
    def index_web_documents(self, urls, source_type):
        web_documents = WebBaseLoader(urls, continue_on_failure=True).load()
        # Process each URL
        meta_datas = []
        for doc in web_documents:
            # Doc metadata set by WebBaseLoader: 
            #   "source": url
            #   "title": soup.find("title").get_text()
            #   "description": soup.find("meta", attrs={"name": "description"}).get("content", "No description found.")
            #   "language": soup.find("html").get("lang", "No language found.")
            doc.metadata["source_type"] = source_type
            doc.metadata["chunk_ids"] = self._index_document(doc.page_content, doc.metadata)
            meta_datas.append(doc.metadata)
        return meta_datas

    def add_texts(self, texts, metadatas=None, ids=None):
        """
        Add texts to the vector store with proper training if needed.
        Optimized for GPU performance when available.
        
        Args:
            texts: List of text strings to add
            metadatas: List of metadata dictionaries (optional)
            ids: List of document IDs (optional)
            
        Returns:
            List of document IDs
        """
        if not ids:
            ids = [str(uuid4()) for _ in range(len(texts))]
        
        # Generate embeddings for training if needed
        logger.info(f"Generating embeddings for {len(texts)} texts...")
        vectors = [self.embeddings.embed_query(text) for text in texts]
        
        # Train the index if necessary
        self._train_index(vectors)
        
        # Add the texts to the vector store
        result = self.vector_store.add_texts(
            texts=texts, metadatas=metadatas, ids=ids, batch_size=self.batch_size
        )
        
        logger.info(f"Successfully added {len(texts)} texts to vector store.")
        return result

    def query_vector_store(self, query, k=5, meta_filter=None):
        if not self.vector_store:
            logger.info("No vector store loaded.")
            return []
        return self.vector_store.similarity_search_with_score(query, k=k, filter=meta_filter)

    def _create_optimized_index(self):
        """
        Create an optimized FAISS index based on expected dataset size and GPU availability.
        Starts with a Flat index and upgrades to IVF when enough vectors are available.
        
        Returns:
            FAISS index optimized for the current configuration
        """
        # Always start with a Flat index for small datasets
        # We'll upgrade to IVF when we have enough vectors (>= nlist * 2)
        index = faiss.IndexFlatL2(self.dim)
        
        if self.gpu_res:
            try:
                # Move to GPU immediately for better performance
                index = index_cpu_to_gpu(self.gpu_res, 0, index)
                logger.info(f"Created Flat index on GPU (will upgrade to IVF when >= {self.nlist * 2} vectors)")
                return index
            except Exception as e:
                logger.info(f"Failed to create GPU index: {e}")
                logger.info("This might be due to GPU memory limitations. Falling back to CPU.")
                self.gpu_res = None
                
        # CPU index
        logger.info(f"Using CPU-based IndexFlatL2 (will upgrade to IVF when >= {self.nlist * 2} vectors)")
        return index

    def get_index_info(self):
        """
        Get information about the current FAISS index configuration.
        
        Returns:
            Dictionary with index information
        """
        if not self.vector_store or not self.vector_store.index:
            return {"status": "No index available"}
            
        index = self.vector_store.index
        info = {
            "index_type": type(index).__name__,
            "dimension": self.dim,
            "total_vectors": index.ntotal,
            "is_trained": getattr(index, "is_trained", True),
            "on_gpu": "Gpu" in type(index).__name__
        }
        
        if hasattr(index, "nlist"):
            info["nlist"] = index.nlist
            
        return info
    
    def print_index_status(self):
        """
        Print current index status and configuration.
        """
        info = self.get_index_info()
        logger.info("=== FAISS Index Status ===")
        for key, value in info.items():
            logger.info(f"{key}: {value}")
        logger.info("=" * 26)


    def get_case_vector_store_dict(self):
        """
        Get a dictionary representation of the vector store for case management.
        """
        return self.vector_store.docstore._dict

    def delete_documents(self, uuids_to_delete):
        """
        Delete documents from the vector store by their UUIDs.
        
        Args:
            uuids_to_delete (list): List of UUIDs to delete from the vector store
        """
        if not uuids_to_delete:
            logger.info("No UUIDs provided for deletion.")
            return
        
        logger.info(f"Deleting {len(uuids_to_delete)} documents from vector store...")
        self.vector_store.delete(uuids_to_delete)
        logger.info("Deletion completed.")