Kalpokoch commited on
Commit
ad44818
·
1 Parent(s): 1dd073c

revised setup2

Browse files
Files changed (3) hide show
  1. Dockerfile +8 -6
  2. app/app.py +42 -26
  3. app/policy_vector_db.py +72 -54
Dockerfile CHANGED
@@ -6,19 +6,19 @@ RUN apt-get update && apt-get install -y \
6
  build-essential \
7
  && rm -rf /var/lib/apt/lists/*
8
 
9
- # Set working directory
10
  WORKDIR /app
11
 
12
  # Set Hugging Face cache directory and grant permissions
 
13
  ENV TRANSFORMERS_CACHE=/app/.cache \
14
  HF_HOME=/app/.cache
15
  RUN mkdir -p /app/.cache && chmod -R 777 /app/.cache
16
 
17
- # --- NEW: Copy the pre-built vector database ---
18
- # Create the directory for the DB inside the container
 
19
  RUN mkdir -p /app/vector_database && chmod -R 777 /app/vector_database
20
- # Copy the contents of your local 'vector_database' into the container
21
- COPY vector_database/ /app/vector_database/
22
 
23
  # Copy only the requirements file to leverage Docker cache
24
  COPY requirements.txt .
@@ -26,11 +26,13 @@ COPY requirements.txt .
26
  # Install Python dependencies
27
  RUN pip install --no-cache-dir -r requirements.txt
28
 
29
- # Copy the rest of your application code (app/ processed_chunks.json, README.md etc.)
 
30
  COPY . .
31
 
32
  # Expose the port the app runs on
33
  EXPOSE 7860
34
 
35
  # Command to run the FastAPI application
 
36
  CMD ["uvicorn", "app.app:app", "--host", "0.0.0.0", "--port", "7860"]
 
6
  build-essential \
7
  && rm -rf /var/lib/apt/lists/*
8
 
9
+ # Set working directory inside the container
10
  WORKDIR /app
11
 
12
  # Set Hugging Face cache directory and grant permissions
13
+ # This helps with model downloads and caching within the Space
14
  ENV TRANSFORMERS_CACHE=/app/.cache \
15
  HF_HOME=/app/.cache
16
  RUN mkdir -p /app/.cache && chmod -R 777 /app/.cache
17
 
18
+ # Ensure ChromaDB can write its persistent database
19
+ # This directory will hold the DB built at runtime.
20
+ # It MUST be a consistent, writable location for persistence.
21
  RUN mkdir -p /app/vector_database && chmod -R 777 /app/vector_database
 
 
22
 
23
  # Copy only the requirements file to leverage Docker cache
24
  COPY requirements.txt .
 
26
  # Install Python dependencies
27
  RUN pip install --no-cache-dir -r requirements.txt
28
 
29
+ # Copy the rest of your application code, including 'app/' directory and 'processed_chunks.json'
30
+ # Assuming 'app' and 'processed_chunks.json' are at the root level of your project
31
  COPY . .
32
 
33
  # Expose the port the app runs on
34
  EXPOSE 7860
35
 
36
  # Command to run the FastAPI application
37
+ # 'app.app' refers to the 'app' FastAPI instance within 'app.py' inside the 'app' package
38
  CMD ["uvicorn", "app.app:app", "--host", "0.0.0.0", "--port", "7860"]
app/app.py CHANGED
@@ -2,30 +2,40 @@ from fastapi import FastAPI, Request
2
  from pydantic import BaseModel
3
  from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
4
  import torch
5
- from app.policy_vector_db import PolicyVectorDB # Make sure this is your local DB logic
6
- import chromadb
7
- from app.policy_vector_db import PolicyVectorDB
8
- import chromadb # Make sure chromadb is imported if you use it directly later, though PolicyVectorDB handles it.
9
 
10
- # Create FastAPI app
11
  app = FastAPI()
12
 
13
- # --- REVISED: Load the vector database from the path inside the Docker container ---
14
- print("Loading Vector Database...")
15
- db = PolicyVectorDB(persist_directory="/app/policy_vector_db")
16
- # The path must match where you copied the DB in the Dockerfile
17
- DB_PERSIST_DIRECTORY = "/app/vector_database"
 
 
18
  db = PolicyVectorDB(persist_directory=DB_PERSIST_DIRECTORY)
19
- print("Vector Database loaded successfully!")
20
 
21
- # Load your quantized model from Hugging Face Hub
 
 
 
 
 
 
 
 
 
 
22
  model_id = "Kalpokoch/QuantizedTinyLama"
23
- print(f"Loading model: {model_id}...")
24
 
25
  # Load tokenizer
26
  tokenizer = AutoTokenizer.from_pretrained(model_id)
27
 
28
- # Quantization config for bitsandbytes
29
  bnb_config = BitsAndBytesConfig(
30
  load_in_4bit=True,
31
  bnb_4bit_use_double_quant=True,
@@ -33,39 +43,45 @@ bnb_config = BitsAndBytesConfig(
33
  bnb_4bit_compute_dtype=torch.bfloat16
34
  )
35
 
36
- # Load quantized model
37
  model = AutoModelForCausalLM.from_pretrained(
38
  model_id,
39
  device_map="auto",
40
  quantization_config=bnb_config
41
  )
42
 
43
- print("Model and tokenizer loaded successfully!")
44
 
45
 
46
- # Input schema
47
  class Query(BaseModel):
48
  question: str
49
 
50
 
51
- # Define endpoint
52
  @app.post("/chat/")
53
  async def chat(query: Query):
54
  question = query.question
55
 
56
- # Step 1: Vector DB search
57
  search_results = db.search(question)
58
- # --- FIX: Use 'text' key as per policy_vector_db.py's search return ---
59
  context = "\n".join([res["text"] for res in search_results])
60
 
61
- # Step 2: Build prompt
62
  prompt = f"Context:\n{context}\n\nQuestion: {question}\nAnswer:"
63
 
64
- # Step 3: Tokenize and generate
65
  inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
66
- outputs = model.generate(**inputs, max_new_tokens=200, do_sample=True, temperature=0.7)
67
-
68
- # --- REVISED: Decode only the new tokens to avoid re-including prompt ---
 
 
 
 
 
69
  answer = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True).strip()
70
 
71
- return {"answer": answer} # Return the directly decoded answer
 
 
2
  from pydantic import BaseModel
3
  from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
4
  import torch
5
+ import os # Imported for path joining and checking file existence
6
+ from app.policy_vector_db import PolicyVectorDB, ensure_db_populated # Import the class and the new helper function
7
+ import chromadb # Make sure chromadb is imported if you use it directly, though PolicyVectorDB handles it.
 
8
 
9
+ # Create FastAPI app instance
10
  app = FastAPI()
11
 
12
+ # --- REVISED: Dynamic Vector Database Initialization ---
13
+ # This is the consistent, persistent location for the DB inside the Docker container
14
+ DB_PERSIST_DIRECTORY = "/app/vector_database"
15
+ # This is the path to your source data for DB building, assumed to be at /app/ (WORKDIR)
16
+ CHUNKS_FILE_PATH = "/app/processed_chunks.json"
17
+
18
+ print("Starting Vector Database initialization process...")
19
  db = PolicyVectorDB(persist_directory=DB_PERSIST_DIRECTORY)
 
20
 
21
+ # Ensure the database is populated on application startup.
22
+ # This function handles checking if the DB is already built and builds it if not.
23
+ if not ensure_db_populated(db, CHUNKS_FILE_PATH):
24
+ print("WARNING: Database population failed or chunks file not found. RAG functionality may be impaired.")
25
+ # You might consider raising an exception here if the DB is absolutely critical for app function
26
+ else:
27
+ print("Vector Database initialization complete.")
28
+
29
+
30
+ # --- LLM Model Loading ---
31
+ # Model ID for the quantized TinyLama model on Hugging Face Hub
32
  model_id = "Kalpokoch/QuantizedTinyLama"
33
+ print(f"Loading LLM model: {model_id}...")
34
 
35
  # Load tokenizer
36
  tokenizer = AutoTokenizer.from_pretrained(model_id)
37
 
38
+ # Quantization configuration for bitsandbytes 4-bit loading
39
  bnb_config = BitsAndBytesConfig(
40
  load_in_4bit=True,
41
  bnb_4bit_use_double_quant=True,
 
43
  bnb_4bit_compute_dtype=torch.bfloat16
44
  )
45
 
46
+ # Load the quantized model, distributing layers automatically across available devices (GPU/CPU)
47
  model = AutoModelForCausalLM.from_pretrained(
48
  model_id,
49
  device_map="auto",
50
  quantization_config=bnb_config
51
  )
52
 
53
+ print("LLM Model and tokenizer loaded successfully!")
54
 
55
 
56
+ # Input schema for the FastAPI endpoint
57
  class Query(BaseModel):
58
  question: str
59
 
60
 
61
+ # Define the chat endpoint
62
  @app.post("/chat/")
63
  async def chat(query: Query):
64
  question = query.question
65
 
66
+ # Step 1: Vector Database search to retrieve relevant context
67
  search_results = db.search(question)
68
+ # Correctly extract text from search results using the 'text' key
69
  context = "\n".join([res["text"] for res in search_results])
70
 
71
+ # Step 2: Build the prompt for the LLM using the retrieved context
72
  prompt = f"Context:\n{context}\n\nQuestion: {question}\nAnswer:"
73
 
74
+ # Step 3: Tokenize the prompt and generate response using the LLM
75
  inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
76
+ outputs = model.generate(
77
+ **inputs,
78
+ max_new_tokens=200, # Max number of new tokens to generate
79
+ do_sample=True, # Enable sampling for more creative responses
80
+ temperature=0.7 # Control randomness of generation
81
+ )
82
+
83
+ # Decode only the newly generated tokens (excluding the input prompt)
84
  answer = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True).strip()
85
 
86
+ # Return the generated answer
87
+ return {"answer": answer}
app/policy_vector_db.py CHANGED
@@ -1,41 +1,35 @@
1
  import json
2
  import os
3
- import shutil # Keep for potential cleanup during local testing, but not for deployment init
4
  from typing import List, Dict
5
 
6
  import chromadb
7
  from sentence_transformers import SentenceTransformer
 
8
 
9
  class PolicyVectorDB:
10
  """Manages the creation and searching of a persistent vector database."""
11
- def __init__(self, persist_directory: str = "/app/policy_vector_db"):
 
12
  self.client = chromadb.PersistentClient(path=persist_directory)
13
  self.collection_name = "neepco_dop_policies"
14
- # Using 'cuda' if available, otherwise 'cpu' for the embedding model
15
- # You can keep 'cpu' if you are sure about resource allocation.
16
  self.embedding_model = SentenceTransformer('BAAI/bge-large-en-v1.5', device='cuda' if torch.cuda.is_available() else 'cpu')
17
 
18
- # When loading a pre-existing DB, use get_or_create_collection cautiously.
19
- # If the collection doesn't exist at the path, it will create an empty one.
20
- # If you are always pre-building, get_collection is safer as it will fail if not found.
21
- # However, get_or_create_collection is more robust against initial empty state.
22
- try:
23
- self.collection = self.client.get_collection(name=self.collection_name)
24
- print(f"Successfully loaded existing collection '{self.collection_name}' from '{persist_directory}'")
25
- except Exception as e:
26
- # If get_collection fails, it means the collection doesn't exist yet,
27
- # which shouldn't happen if pre-built correctly.
28
- # For robustness, you could add creation here if desired, but for pre-built,
29
- # this indicates an issue with the pre-built DB or path.
30
- print(f"Error loading collection '{self.collection_name}': {e}")
31
- print("Attempting to create a new (likely empty) collection. Ensure your pre-built DB is copied correctly.")
32
- self.collection = self.client.create_collection(
33
  name=self.collection_name,
34
  metadata={"description": "NEEPCO Delegation of Powers Policy"}
35
  )
36
-
37
- print(f"ChromaDB client initialized for collection '{self.collection_name}' at '{persist_directory}'")
38
-
39
 
40
  def _flatten_metadata(self, metadata: Dict) -> Dict:
41
  """Ensures all metadata values are strings for ChromaDB compatibility."""
@@ -43,21 +37,21 @@ class PolicyVectorDB:
43
 
44
  def add_chunks(self, chunks: List[Dict]):
45
  """Encodes and adds a list of chunk dictionaries to the database."""
46
- # This method is primarily for initial DB building, less for runtime in a deployed RAG.
47
- # However, keeping it makes the class reusable.
48
  if not chunks:
49
  print("No chunks provided to add.")
50
  return
51
 
52
- existing_ids = set(self.collection.get(include=[])['ids'])
 
53
  new_chunks = [chunk for chunk in chunks if chunk.get('id') not in existing_ids]
54
 
55
  if not new_chunks:
56
  print("No new chunks to add. All provided chunks already exist in the database.")
57
  return
58
 
59
- print(f"Found {len(new_chunks)} new chunks to add.")
60
- batch_size = 128
61
 
62
  for i in range(0, len(new_chunks), batch_size):
63
  batch = new_chunks[i:i + batch_size]
@@ -68,25 +62,27 @@ class PolicyVectorDB:
68
  metadatas = [self._flatten_metadata(chunk['metadata']) for chunk in batch]
69
 
70
  embeddings = self.embedding_model.encode(texts, show_progress_bar=False).tolist()
71
- self.collection.add(ids=ids, embeddings=embeddings, documents=texts, metadatas=metadatas)
72
 
73
- print(f"Successfully added {len(new_chunks)} new chunks to the database!")
74
 
75
  def search(self, query_text: str, top_k: int = 3) -> List[Dict]:
76
  """Searches the collection for a given query text."""
 
77
  query_embedding = self.embedding_model.encode([query_text]).tolist()
78
- results = self.collection.query(
79
  query_embeddings=query_embedding,
80
  n_results=top_k,
81
- include=['documents', 'metadatas', 'distances']
82
  )
83
 
84
  search_results = []
85
  if not results.get('documents'):
 
86
  return []
87
 
88
  for i, doc in enumerate(results['documents'][0]):
89
- relevance_score = 1 - results['distances'][0][i]
90
  search_results.append({
91
  'text': doc,
92
  'metadata': results['metadatas'][0][i],
@@ -94,28 +90,54 @@ class PolicyVectorDB:
94
  })
95
  return search_results
96
 
97
- # --- REVISED: Remove database building logic from main for deployment ---
98
- # This main function is typically used for initial local building.
99
- # For deployment, the DB is now pre-built and copied.
100
- def main():
101
- """Main function to build and verify the vector database (for local pre-building)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
103
  INPUT_CHUNKS_PATH = os.path.join(BASE_DIR, "../processed_chunks.json")
104
- PERSIST_DIRECTORY = "/app/policy_vector_db"
105
-
106
- if not os.path.exists(INPUT_CHUNKS_PATH):
107
- print(f"FATAL ERROR: The input chunk file was not found at '{INPUT_CHUNKS_PATH}'")
108
- print("Please ensure 'processed_chunks.json' is in the root directory.")
109
- return
110
 
111
- # Remove existing local build directory to ensure clean start
112
  if os.path.exists(PERSIST_DIRECTORY):
113
  print(f"Removing existing local build database at '{PERSIST_DIRECTORY}' to ensure a clean build.")
114
  shutil.rmtree(PERSIST_DIRECTORY)
115
 
116
  print(f"Creating database directory: '{PERSIST_DIRECTORY}'")
117
  os.makedirs(PERSIST_DIRECTORY, exist_ok=True)
118
- os.chmod(PERSIST_DIRECTORY, 0o777) # Ensure write permissions
119
 
120
  print("\nStep 1: Loading processed chunks...")
121
  with open(INPUT_CHUNKS_PATH, 'r', encoding='utf-8') as f:
@@ -123,17 +145,16 @@ def main():
123
  print(f"Loaded {len(chunks_to_add)} chunks.")
124
 
125
  print("\nStep 2: Setting up persistent vector database (local build)...")
126
- db = PolicyVectorDB(persist_directory=PERSIST_DIRECTORY) # Pass the local build path
127
 
128
  print("\nStep 3: Adding chunks to the database...")
129
  db.add_chunks(chunks_to_add)
130
 
131
- print(f"\n✅ Vector database setup complete. Total chunks in DB: {db.collection.count()}")
132
  print(f"Database is saved in: {os.path.abspath(PERSIST_DIRECTORY)}")
133
- print("\n--- Important: Copy the contents of this directory (NOT the directory itself) to your 'vector_database' folder in the project root for deployment. ---")
134
-
135
 
136
- print("\n--- Running Verification Tests ---")
137
  test_questions = [
138
  "Who can approve changes to the pay structure?",
139
  "What is the financial limit for a DGM for works on a limited tender basis?",
@@ -150,7 +171,4 @@ def main():
150
  print(f" Text: {result['text'][:300]}...")
151
  print(f" Metadata: {result['metadata']}")
152
  else:
153
- print(" No results found.")
154
-
155
- if __name__ == "__main__":
156
- main()
 
1
  import json
2
  import os
3
+ import shutil # Keep for potential cleanup during local testing
4
  from typing import List, Dict
5
 
6
  import chromadb
7
  from sentence_transformers import SentenceTransformer
8
+ import torch # Imported for device detection (e.g., cuda vs cpu)
9
 
10
  class PolicyVectorDB:
11
  """Manages the creation and searching of a persistent vector database."""
12
+ def __init__(self, persist_directory: str):
13
+ self.persist_directory = persist_directory # Store the path for later use
14
  self.client = chromadb.PersistentClient(path=persist_directory)
15
  self.collection_name = "neepco_dop_policies"
16
+ # Use 'cuda' if available, otherwise fallback to 'cpu' for the embedding model
 
17
  self.embedding_model = SentenceTransformer('BAAI/bge-large-en-v1.5', device='cuda' if torch.cuda.is_available() else 'cpu')
18
 
19
+ # Collection is not retrieved/created immediately here.
20
+ # This is handled by _get_collection() which is called on demand.
21
+ self.collection = None # Initialize as None
22
+
23
+ def _get_collection(self):
24
+ """Lazy loads or creates the collection to ensure it exists before operations."""
25
+ if self.collection is None:
26
+ print(f"Attempting to get or create collection '{self.collection_name}' at '{self.persist_directory}'...")
27
+ self.collection = self.client.get_or_create_collection(
 
 
 
 
 
 
28
  name=self.collection_name,
29
  metadata={"description": "NEEPCO Delegation of Powers Policy"}
30
  )
31
+ print(f"Collection '{self.collection_name}' is ready. Current count: {self.collection.count()} documents.")
32
+ return self.collection
 
33
 
34
  def _flatten_metadata(self, metadata: Dict) -> Dict:
35
  """Ensures all metadata values are strings for ChromaDB compatibility."""
 
37
 
38
  def add_chunks(self, chunks: List[Dict]):
39
  """Encodes and adds a list of chunk dictionaries to the database."""
40
+ collection = self._get_collection() # Ensure collection is active
 
41
  if not chunks:
42
  print("No chunks provided to add.")
43
  return
44
 
45
+ # Fetch existing IDs to avoid re-adding the same chunks on subsequent runs
46
+ existing_ids = set(collection.get(include=['ids'])['ids'])
47
  new_chunks = [chunk for chunk in chunks if chunk.get('id') not in existing_ids]
48
 
49
  if not new_chunks:
50
  print("No new chunks to add. All provided chunks already exist in the database.")
51
  return
52
 
53
+ print(f"Found {len(new_chunks)} new chunks to add to the DB.")
54
+ batch_size = 128 # Process in batches to manage memory and network efficiently
55
 
56
  for i in range(0, len(new_chunks), batch_size):
57
  batch = new_chunks[i:i + batch_size]
 
62
  metadatas = [self._flatten_metadata(chunk['metadata']) for chunk in batch]
63
 
64
  embeddings = self.embedding_model.encode(texts, show_progress_bar=False).tolist()
65
+ collection.add(ids=ids, embeddings=embeddings, documents=texts, metadatas=metadatas)
66
 
67
+ print(f"Successfully added {len(new_chunks)} new chunks to the database! Total documents: {collection.count()}")
68
 
69
  def search(self, query_text: str, top_k: int = 3) -> List[Dict]:
70
  """Searches the collection for a given query text."""
71
+ collection = self._get_collection() # Ensure collection is active
72
  query_embedding = self.embedding_model.encode([query_text]).tolist()
73
+ results = collection.query(
74
  query_embeddings=query_embedding,
75
  n_results=top_k,
76
+ include=['documents', 'metadatas', 'distances'] # Request necessary info
77
  )
78
 
79
  search_results = []
80
  if not results.get('documents'):
81
+ print("No search results found.")
82
  return []
83
 
84
  for i, doc in enumerate(results['documents'][0]):
85
+ relevance_score = 1 - results['distances'][0][i] # Higher score = more relevant
86
  search_results.append({
87
  'text': doc,
88
  'metadata': results['metadatas'][0][i],
 
90
  })
91
  return search_results
92
 
93
+ # --- NEW FUNCTION: To be called by app.py to ensure DB is populated ---
94
+ def ensure_db_populated(db_instance: PolicyVectorDB, chunks_file_path: str):
95
+ """
96
+ Checks if the database is populated. If not, loads chunks from JSON and adds them.
97
+ This function is intended to run at application startup.
98
+ """
99
+ print(f"Checking if database at '{db_instance.persist_directory}' needs population...")
100
+ try:
101
+ # Check count of the collection to see if it's already populated
102
+ if db_instance._get_collection().count() == 0:
103
+ print("Database is empty or collection not found. Populating from chunks...")
104
+ if not os.path.exists(chunks_file_path):
105
+ print(f"ERROR: Chunks file not found at '{chunks_file_path}'. Cannot populate DB.")
106
+ return False
107
+
108
+ with open(chunks_file_path, 'r', encoding='utf-8') as f:
109
+ chunks_to_add = json.load(f)
110
+
111
+ print(f"Loaded {len(chunks_to_add)} chunks from '{chunks_file_path}'.")
112
+ db_instance.add_chunks(chunks_to_add)
113
+ print(f"Database population complete. Total documents: {db_instance._get_collection().count()}")
114
+ return True
115
+ else:
116
+ print(f"Database already populated with {db_instance._get_collection().count()} documents.")
117
+ return True
118
+ except Exception as e:
119
+ print(f"An error occurred during database population check: {e}")
120
+ # Log more details for debugging if needed
121
+ return False
122
+
123
+
124
+ # The 'main' function is kept for local testing/manual initial setup,
125
+ # but it WILL NOT be called by the Dockerized application on Hugging Face Spaces.
126
+ if __name__ == "__main__":
127
+ print("\n--- Running PolicyVectorDB main for LOCAL TESTING/BUILD ONLY ---")
128
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
129
  INPUT_CHUNKS_PATH = os.path.join(BASE_DIR, "../processed_chunks.json")
130
+ # Use a temporary local path for building so it doesn't interfere with your repo structure
131
+ PERSIST_DIRECTORY = "./.temp_local_vector_db_build"
 
 
 
 
132
 
133
+ # Clean up old local build directory if it exists for a fresh build
134
  if os.path.exists(PERSIST_DIRECTORY):
135
  print(f"Removing existing local build database at '{PERSIST_DIRECTORY}' to ensure a clean build.")
136
  shutil.rmtree(PERSIST_DIRECTORY)
137
 
138
  print(f"Creating database directory: '{PERSIST_DIRECTORY}'")
139
  os.makedirs(PERSIST_DIRECTORY, exist_ok=True)
140
+ os.chmod(PERSIST_DIRECTORY, 0o777) # Ensure write permissions for local build
141
 
142
  print("\nStep 1: Loading processed chunks...")
143
  with open(INPUT_CHUNKS_PATH, 'r', encoding='utf-8') as f:
 
145
  print(f"Loaded {len(chunks_to_add)} chunks.")
146
 
147
  print("\nStep 2: Setting up persistent vector database (local build)...")
148
+ db = PolicyVectorDB(persist_directory=PERSIST_DIRECTORY)
149
 
150
  print("\nStep 3: Adding chunks to the database...")
151
  db.add_chunks(chunks_to_add)
152
 
153
+ print(f"\n✅ Local vector database setup complete. Total chunks in DB: {db._get_collection().count()}")
154
  print(f"Database is saved in: {os.path.abspath(PERSIST_DIRECTORY)}")
155
+ print("\n--- Remember: This local build is for testing. The deployed app will build its own DB. ---")
 
156
 
157
+ print("\n--- Running Local Verification Tests ---")
158
  test_questions = [
159
  "Who can approve changes to the pay structure?",
160
  "What is the financial limit for a DGM for works on a limited tender basis?",
 
171
  print(f" Text: {result['text'][:300]}...")
172
  print(f" Metadata: {result['metadata']}")
173
  else:
174
+ print(" No results found.")