Spaces:
Runtime error
Runtime error
Commit ·
4d2a44f
1
Parent(s): ac5fcf7
Added prompt system to compassia
Browse files- .gitignore +1 -0
- compassia.py +58 -98
.gitignore
CHANGED
|
@@ -6,3 +6,4 @@ all-libraries.txt
|
|
| 6 |
# Ignore ChromaDB persistent storage
|
| 7 |
chroma_db/
|
| 8 |
|
|
|
|
|
|
| 6 |
# Ignore ChromaDB persistent storage
|
| 7 |
chroma_db/
|
| 8 |
|
| 9 |
+
temp*
|
compassia.py
CHANGED
|
@@ -1,10 +1,3 @@
|
|
| 1 |
-
import requests
|
| 2 |
-
import os
|
| 3 |
-
import io
|
| 4 |
-
import re
|
| 5 |
-
import uuid # For generating unique IDs for ChromaDB
|
| 6 |
-
from PIL import Image
|
| 7 |
-
|
| 8 |
import sys
|
| 9 |
# IMPORTANT: These lines MUST be at the very top of compassia.py
|
| 10 |
# They ensure that any subsequent import of 'sqlite3' (even indirectly by chromadb)
|
|
@@ -15,6 +8,12 @@ try:
|
|
| 15 |
except ImportError:
|
| 16 |
pass # Fallback if pysqlite3 isn't available, but it should be in Docker
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# For text extraction from PDFs (non-OCR)
|
| 20 |
from pdfminer.high_level import extract_text_to_fp
|
|
@@ -25,14 +24,14 @@ from pdf2image import convert_from_path
|
|
| 25 |
import pytesseract
|
| 26 |
|
| 27 |
# For embeddings and vector search
|
| 28 |
-
from FlagEmbedding import BGEM3FlagModel
|
| 29 |
import chromadb # pip install chromadb
|
| 30 |
|
| 31 |
# --- IMPORTANT: Configure Paths for Tesseract and Poppler ---
|
| 32 |
-
# If Tesseract is not in your system's PATH, uncomment and set this:
|
| 33 |
# pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
|
| 34 |
|
| 35 |
-
# If pdf2image gives errors about poppler, uncomment and set this:
|
| 36 |
# poppler_path = r'C:\path\to\poppler\bin'
|
| 37 |
|
| 38 |
# --- OpenRouter DeepSeek API Configuration ---
|
|
@@ -50,21 +49,15 @@ HEADERS = {
|
|
| 50 |
}
|
| 51 |
|
| 52 |
# --- Embedding Model Configuration (Local BGE-M3) ---
|
| 53 |
-
# IMPORTANT: This assumes you've run 'pip install -U FlagEmbedding'
|
| 54 |
-
# BGE-M3 is multilingual, which is good for Turkish PDFs.
|
| 55 |
-
# You might need to download the model weights the first time it's initialized.
|
| 56 |
-
# Ensure you have enough RAM/VRAM for the model.
|
| 57 |
print("Loading FlagEmbedding (BGE-M3) model...")
|
| 58 |
try:
|
| 59 |
-
# Initialize BGEM3FlagModel. It will download weights to Hugging Face cache
|
| 60 |
-
# the first time, hence the need for disk space.
|
| 61 |
embedding_model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=True)
|
| 62 |
print("FlagEmbedding (BGE-M3) model loaded successfully.")
|
| 63 |
except Exception as e:
|
| 64 |
print(f"Error loading FlagEmbedding model: {e}")
|
| 65 |
print("Ensure you have resolved disk space issues for model download and have enough memory.")
|
| 66 |
print("You might need to adjust 'use_fp16' based on your hardware (e.g., False for CPU/older GPUs).")
|
| 67 |
-
exit(1)
|
| 68 |
|
| 69 |
# --- PDF Processing Functions ---
|
| 70 |
|
|
@@ -77,11 +70,9 @@ def extract_text_from_pdf(pdf_path: str) -> str:
|
|
| 77 |
output_string = io.StringIO()
|
| 78 |
with open(pdf_path, 'rb') as fp:
|
| 79 |
try:
|
| 80 |
-
# Use LAParams for better layout analysis
|
| 81 |
extract_text_to_fp(fp, output_string, laparams=LAParams())
|
| 82 |
text = output_string.getvalue()
|
| 83 |
-
|
| 84 |
-
if len(text.strip()) < 100 and os.path.getsize(pdf_path) > 10000: # Check file size as well
|
| 85 |
print("Direct extraction yielded sparse text. Attempting OCR...")
|
| 86 |
return ocr_pdf(pdf_path)
|
| 87 |
return text
|
|
@@ -96,17 +87,15 @@ def ocr_pdf(pdf_path: str) -> str:
|
|
| 96 |
"""
|
| 97 |
all_text = []
|
| 98 |
try:
|
| 99 |
-
|
| 100 |
-
# Pass poppler_path=poppler_path if it's not in your system's PATH
|
| 101 |
-
images = convert_from_path(pdf_path, dpi=300) # You can adjust dpi for quality vs. speed
|
| 102 |
|
| 103 |
print(f" Performing OCR on {len(images)} pages...")
|
| 104 |
for i, img in enumerate(images):
|
| 105 |
-
#
|
| 106 |
-
#
|
| 107 |
-
#
|
| 108 |
-
|
| 109 |
-
#
|
| 110 |
page_text = pytesseract.image_to_string(img, lang='eng+tur')
|
| 111 |
all_text.append(page_text)
|
| 112 |
print(f" Page {i+1} OCR complete.")
|
|
@@ -114,7 +103,7 @@ def ocr_pdf(pdf_path: str) -> str:
|
|
| 114 |
except Exception as e:
|
| 115 |
print(f"OCR process failed: {e}")
|
| 116 |
print("Please ensure Tesseract OCR and Poppler are correctly installed and their executables are in your system's PATH.")
|
| 117 |
-
return ""
|
| 118 |
|
| 119 |
return "\n".join(all_text)
|
| 120 |
|
|
@@ -122,12 +111,10 @@ def chunk_text(text: str, max_chunk_size: int = 700, overlap: int = 100) -> list
|
|
| 122 |
"""
|
| 123 |
Splits text into chunks of a maximum size with optional overlap.
|
| 124 |
Aims to split by paragraphs/sentences first, then by word.
|
| 125 |
-
Note: Increased max_chunk_size to 700 to match your previous code's `chunk_size` for RAG.
|
| 126 |
"""
|
| 127 |
if not text:
|
| 128 |
return []
|
| 129 |
|
| 130 |
-
# Simple paragraph-based chunking
|
| 131 |
paragraphs = re.split(r'\n\s*\n', text)
|
| 132 |
chunks = []
|
| 133 |
current_chunk = []
|
|
@@ -137,16 +124,12 @@ def chunk_text(text: str, max_chunk_size: int = 700, overlap: int = 100) -> list
|
|
| 137 |
if not para.strip():
|
| 138 |
continue
|
| 139 |
|
| 140 |
-
# If adding paragraph plus a separator exceeds max_chunk_size,
|
| 141 |
-
# or if the current_chunk is already substantial and adding this makes it too big,
|
| 142 |
-
# then finalize the current chunk.
|
| 143 |
if current_chunk_len + len(para) + len('\n\n') > max_chunk_size:
|
| 144 |
-
if current_chunk:
|
| 145 |
chunks.append("\n\n".join(current_chunk))
|
| 146 |
current_chunk = []
|
| 147 |
current_chunk_len = 0
|
| 148 |
|
| 149 |
-
# If a single paragraph is larger than max_chunk_size, split it by words
|
| 150 |
if len(para) > max_chunk_size:
|
| 151 |
words = para.split(' ')
|
| 152 |
sub_chunk = []
|
|
@@ -159,32 +142,28 @@ def chunk_text(text: str, max_chunk_size: int = 700, overlap: int = 100) -> list
|
|
| 159 |
else:
|
| 160 |
sub_chunk.append(word)
|
| 161 |
sub_chunk_len += len(word) + len(' ')
|
| 162 |
-
if sub_chunk:
|
| 163 |
chunks.append(" ".join(sub_chunk))
|
| 164 |
-
else:
|
| 165 |
current_chunk.append(para)
|
| 166 |
current_chunk_len += len(para) + len('\n\n')
|
| 167 |
-
else:
|
| 168 |
current_chunk.append(para)
|
| 169 |
current_chunk_len += len(para) + len('\n\n')
|
| 170 |
|
| 171 |
-
if current_chunk:
|
| 172 |
chunks.append("\n\n".join(current_chunk))
|
| 173 |
-
|
| 174 |
-
# Apply overlap: This is a simplistic overlap implementation.
|
| 175 |
-
# For more robust RAG, consider sentence-window retrieval or more advanced chunking libraries.
|
| 176 |
final_chunks_with_overlap = []
|
| 177 |
for i in range(len(chunks)):
|
| 178 |
chunk = chunks[i]
|
| 179 |
if i > 0 and overlap > 0:
|
| 180 |
-
# Take a portion of the previous chunk to overlap
|
| 181 |
prev_chunk_part = chunks[i-1][-overlap:]
|
| 182 |
chunk = prev_chunk_part + "\n" + chunk
|
| 183 |
final_chunks_with_overlap.append(chunk)
|
| 184 |
|
| 185 |
return final_chunks_with_overlap
|
| 186 |
|
| 187 |
-
|
| 188 |
# --- RAG Core Functions with ChromaDB ---
|
| 189 |
|
| 190 |
class DocumentRAG:
|
|
@@ -195,30 +174,21 @@ class DocumentRAG:
|
|
| 195 |
self.persist_directory = persist_directory
|
| 196 |
self.collection_name = collection_name
|
| 197 |
|
| 198 |
-
# Initialize ChromaDB client and collection
|
| 199 |
print(f"Initializing ChromaDB at: {self.persist_directory}")
|
| 200 |
self.client = chromadb.PersistentClient(path=self.persist_directory)
|
| 201 |
|
| 202 |
-
# Get or create the collection
|
| 203 |
self.collection = self.client.get_or_create_collection(
|
| 204 |
name=self.collection_name,
|
| 205 |
-
# Genkit uses 'cosine' by default. 'l2' (Euclidean) or 'ip' (Inner Product)
|
| 206 |
-
# are also common. BGE-M3 generally uses cosine.
|
| 207 |
metadata={"hnsw:space": "cosine"}
|
| 208 |
)
|
| 209 |
print(f"ChromaDB collection '{self.collection_name}' ready.")
|
| 210 |
|
| 211 |
def _generate_chunk_id(self, pdf_path: str, chunk_idx: int) -> str:
|
| 212 |
-
"""Generates a unique ID for each chunk based on file path and index."""
|
| 213 |
-
# Use UUID to ensure uniqueness even if paths are similar or contain problematic chars
|
| 214 |
return f"{os.path.basename(pdf_path)}_{chunk_idx}_{uuid.uuid4().hex}"
|
| 215 |
|
| 216 |
def add_document(self, pdf_path: str):
|
| 217 |
print(f"Adding document: {pdf_path}")
|
| 218 |
|
| 219 |
-
# Check if the document has already been indexed in ChromaDB
|
| 220 |
-
# We'll use the file path as a simple way to check if _any_ chunk from this PDF exists.
|
| 221 |
-
# A more robust check might involve hashing the file content or checking specific metadata.
|
| 222 |
results = self.collection.get(
|
| 223 |
where={"source": pdf_path},
|
| 224 |
limit=1
|
|
@@ -237,43 +207,31 @@ class DocumentRAG:
|
|
| 237 |
print(f"Warning: No chunks generated for {pdf_path}. Skipping.")
|
| 238 |
return
|
| 239 |
|
| 240 |
-
# Prepare data for ChromaDB
|
| 241 |
documents_to_add = []
|
| 242 |
metadatas_to_add = []
|
| 243 |
ids_to_add = []
|
| 244 |
|
| 245 |
print(f" Generating embeddings for {len(chunks)} chunks and preparing for ChromaDB...")
|
| 246 |
|
| 247 |
-
# BGE-M3's encode method returns a dictionary for dense, sparse, etc.
|
| 248 |
-
# We need the 'dense_vecs' for standard vector search.
|
| 249 |
encoded_results = self.embedding_model.encode(
|
| 250 |
chunks,
|
| 251 |
-
batch_size=32,
|
| 252 |
return_dense=True,
|
| 253 |
return_sparse=False,
|
| 254 |
return_colbert_vecs=False
|
| 255 |
)
|
| 256 |
|
| 257 |
-
# Extract only the dense vectors for ChromaDB
|
| 258 |
chunk_embeddings = encoded_results["dense_vecs"]
|
| 259 |
|
| 260 |
-
# Ensure embeddings are normalized if using cosine similarity with IP index,
|
| 261 |
-
# but ChromaDB's 'cosine' space handles this internally.
|
| 262 |
-
# If using FAISS with IP, you'd normalize here:
|
| 263 |
-
# from numpy.linalg import norm
|
| 264 |
-
# chunk_embeddings = chunk_embeddings / norm(chunk_embeddings, axis=1, keepdims=True)
|
| 265 |
-
|
| 266 |
-
|
| 267 |
for i, chunk in enumerate(chunks):
|
| 268 |
unique_id = self._generate_chunk_id(pdf_path, i)
|
| 269 |
documents_to_add.append(chunk)
|
| 270 |
metadatas_to_add.append({"source": pdf_path, "chunk_id": i})
|
| 271 |
ids_to_add.append(unique_id)
|
| 272 |
|
| 273 |
-
# Add to ChromaDB collection
|
| 274 |
self.collection.add(
|
| 275 |
documents=documents_to_add,
|
| 276 |
-
embeddings=chunk_embeddings.tolist(),
|
| 277 |
metadatas=metadatas_to_add,
|
| 278 |
ids=ids_to_add
|
| 279 |
)
|
|
@@ -291,7 +249,6 @@ class DocumentRAG:
|
|
| 291 |
|
| 292 |
print(f"Retrieving context for query: '{query}'")
|
| 293 |
|
| 294 |
-
# Encode the query using the embedding model
|
| 295 |
query_embedding_result = self.embedding_model.encode(
|
| 296 |
[query],
|
| 297 |
batch_size=1,
|
|
@@ -299,9 +256,8 @@ class DocumentRAG:
|
|
| 299 |
return_sparse=False,
|
| 300 |
return_colbert_vecs=False
|
| 301 |
)
|
| 302 |
-
query_embedding = query_embedding_result["dense_vecs"].tolist()
|
| 303 |
|
| 304 |
-
# Query ChromaDB
|
| 305 |
results = self.collection.query(
|
| 306 |
query_embeddings=query_embedding,
|
| 307 |
n_results=top_k,
|
|
@@ -313,7 +269,7 @@ class DocumentRAG:
|
|
| 313 |
for i, doc_text in enumerate(results['documents'][0]):
|
| 314 |
source_info = results['metadatas'][0][i].get('source', 'Unknown Source')
|
| 315 |
chunk_id_info = results['metadatas'][0][i].get('chunk_id', 'N/A')
|
| 316 |
-
distance_info = results['distances'][0][i]
|
| 317 |
|
| 318 |
retrieved_chunks_texts.append(doc_text)
|
| 319 |
print(f" Retrieved chunk {i+1} (distance: {distance_info:.4f}) from '{source_info}' (chunk {chunk_id_info}).")
|
|
@@ -327,12 +283,9 @@ class DocumentRAG:
|
|
| 327 |
Answers a question by ensuring PDFs are indexed, retrieving context,
|
| 328 |
and querying DeepSeek.
|
| 329 |
"""
|
| 330 |
-
# Ensure documents are added for specified paths.
|
| 331 |
-
# This will now intelligently skip already indexed documents.
|
| 332 |
for path in pdf_paths:
|
| 333 |
self.add_document(path)
|
| 334 |
|
| 335 |
-
# Get relevant context from ChromaDB
|
| 336 |
context_chunks = self.retrieve_context(question)
|
| 337 |
context = "\n\n".join(context_chunks)
|
| 338 |
|
|
@@ -342,19 +295,36 @@ class DocumentRAG:
|
|
| 342 |
else:
|
| 343 |
context_prompt = f"Using the following context:\n\n{context}\n\n"
|
| 344 |
|
| 345 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
messages = [
|
| 347 |
-
{"role": "system", "content":
|
| 348 |
{"role": "user", "content": f"{context_prompt}Question: {question}"}
|
| 349 |
]
|
| 350 |
|
| 351 |
-
# Call DeepSeek API via OpenRouter
|
| 352 |
print("\nSending request to DeepSeek API...")
|
| 353 |
data = {
|
| 354 |
-
"model": "deepseek/deepseek-chat:free",
|
| 355 |
"messages": messages,
|
| 356 |
-
"temperature": 0.5,
|
| 357 |
-
"max_tokens": 500,
|
| 358 |
}
|
| 359 |
|
| 360 |
response = requests.post(API_URL, json=data, headers=HEADERS)
|
|
@@ -372,30 +342,22 @@ class DocumentRAG:
|
|
| 372 |
|
| 373 |
# --- Main execution logic ---
|
| 374 |
if __name__ == "__main__":
|
| 375 |
-
# Initialize the RAG system with ChromaDB persistence
|
| 376 |
-
# The 'chroma_db' directory will be created in your project root.
|
| 377 |
rag_system = DocumentRAG(
|
| 378 |
embedding_model=embedding_model,
|
| 379 |
-
persist_directory="./chroma_db",
|
| 380 |
-
collection_name="pdf_documents_collection",
|
| 381 |
chunk_size=700,
|
| 382 |
overlap=100
|
| 383 |
)
|
| 384 |
|
| 385 |
-
# --- Define your PDF documents ---
|
| 386 |
-
# Replace with the actual paths to your PDF files.
|
| 387 |
-
# For testing, ensure 'documents' directory exists and contains your PDFs.
|
| 388 |
pdf_document_paths = [
|
| 389 |
-
"documents/heracles_tr.pdf",
|
| 390 |
-
"documents/heracles_en.pdf",
|
| 391 |
-
# Add more PDF paths here if you have them
|
| 392 |
"documents/ogrenci_katki_payi_ogrenim_ucretleri.pdf",
|
| 393 |
"documents/Ogrenci_Liderligi_Burs_Programi_Sozlesme_Metni_2024-2025.pdf",
|
| 394 |
"documents/tmv-bursluluk-yonergesi.pdf"
|
| 395 |
]
|
| 396 |
|
| 397 |
-
# --- Add PDFs to the RAG system for indexing ---
|
| 398 |
-
# This will now process only new or unindexed documents.
|
| 399 |
print("\n--- Indexing Documents ---")
|
| 400 |
for pdf_path in pdf_document_paths:
|
| 401 |
if os.path.exists(pdf_path):
|
|
@@ -403,13 +365,11 @@ if __name__ == "__main__":
|
|
| 403 |
else:
|
| 404 |
print(f"Error: PDF file not found at {pdf_path}. Please check the path.")
|
| 405 |
|
| 406 |
-
|
| 407 |
-
print("\n--- PDF Chat with DeepSeek (Type 'quit' to exit) ---")
|
| 408 |
while True:
|
| 409 |
-
user_question = input("\
|
| 410 |
if user_question.lower() == 'quit':
|
| 411 |
print("Exiting chat.")
|
| 412 |
break
|
| 413 |
|
| 414 |
-
|
| 415 |
-
rag_system.answer_question(user_question, []) # Pass an empty list, as documents are in DB
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import sys
|
| 2 |
# IMPORTANT: These lines MUST be at the very top of compassia.py
|
| 3 |
# They ensure that any subsequent import of 'sqlite3' (even indirectly by chromadb)
|
|
|
|
| 8 |
except ImportError:
|
| 9 |
pass # Fallback if pysqlite3 isn't available, but it should be in Docker
|
| 10 |
|
| 11 |
+
import requests
|
| 12 |
+
import os
|
| 13 |
+
import io
|
| 14 |
+
import re
|
| 15 |
+
import uuid # For generating unique IDs for ChromaDB
|
| 16 |
+
from PIL import Image
|
| 17 |
|
| 18 |
# For text extraction from PDFs (non-OCR)
|
| 19 |
from pdfminer.high_level import extract_text_to_fp
|
|
|
|
| 24 |
import pytesseract
|
| 25 |
|
| 26 |
# For embeddings and vector search
|
| 27 |
+
from FlagEmbedding import BGEM3FlagModel
|
| 28 |
import chromadb # pip install chromadb
|
| 29 |
|
| 30 |
# --- IMPORTANT: Configure Paths for Tesseract and Poppler ---
|
| 31 |
+
# If Tesseract is not in your system's PATH, uncomment and set this locally:
|
| 32 |
# pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
|
| 33 |
|
| 34 |
+
# If pdf2image gives errors about poppler, uncomment and set this locally:
|
| 35 |
# poppler_path = r'C:\path\to\poppler\bin'
|
| 36 |
|
| 37 |
# --- OpenRouter DeepSeek API Configuration ---
|
|
|
|
| 49 |
}
|
| 50 |
|
| 51 |
# --- Embedding Model Configuration (Local BGE-M3) ---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
print("Loading FlagEmbedding (BGE-M3) model...")
|
| 53 |
try:
|
|
|
|
|
|
|
| 54 |
embedding_model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=True)
|
| 55 |
print("FlagEmbedding (BGE-M3) model loaded successfully.")
|
| 56 |
except Exception as e:
|
| 57 |
print(f"Error loading FlagEmbedding model: {e}")
|
| 58 |
print("Ensure you have resolved disk space issues for model download and have enough memory.")
|
| 59 |
print("You might need to adjust 'use_fp16' based on your hardware (e.g., False for CPU/older GPUs).")
|
| 60 |
+
exit(1)
|
| 61 |
|
| 62 |
# --- PDF Processing Functions ---
|
| 63 |
|
|
|
|
| 70 |
output_string = io.StringIO()
|
| 71 |
with open(pdf_path, 'rb') as fp:
|
| 72 |
try:
|
|
|
|
| 73 |
extract_text_to_fp(fp, output_string, laparams=LAParams())
|
| 74 |
text = output_string.getvalue()
|
| 75 |
+
if len(text.strip()) < 100 and os.path.getsize(pdf_path) > 10000:
|
|
|
|
| 76 |
print("Direct extraction yielded sparse text. Attempting OCR...")
|
| 77 |
return ocr_pdf(pdf_path)
|
| 78 |
return text
|
|
|
|
| 87 |
"""
|
| 88 |
all_text = []
|
| 89 |
try:
|
| 90 |
+
images = convert_from_path(pdf_path, dpi=300)
|
|
|
|
|
|
|
| 91 |
|
| 92 |
print(f" Performing OCR on {len(images)} pages...")
|
| 93 |
for i, img in enumerate(images):
|
| 94 |
+
# Tesseract language packs:
|
| 95 |
+
# 'eng' for English, 'tur' for Turkish
|
| 96 |
+
# If you have scanned PDFs in Arabic or French, you MUST install
|
| 97 |
+
# 'tesseract-ocr-ara' and 'tesseract-ocr-fra' in your Dockerfile
|
| 98 |
+
# and change 'lang' to 'eng+tur+ara+fra'.
|
| 99 |
page_text = pytesseract.image_to_string(img, lang='eng+tur')
|
| 100 |
all_text.append(page_text)
|
| 101 |
print(f" Page {i+1} OCR complete.")
|
|
|
|
| 103 |
except Exception as e:
|
| 104 |
print(f"OCR process failed: {e}")
|
| 105 |
print("Please ensure Tesseract OCR and Poppler are correctly installed and their executables are in your system's PATH.")
|
| 106 |
+
return ""
|
| 107 |
|
| 108 |
return "\n".join(all_text)
|
| 109 |
|
|
|
|
| 111 |
"""
|
| 112 |
Splits text into chunks of a maximum size with optional overlap.
|
| 113 |
Aims to split by paragraphs/sentences first, then by word.
|
|
|
|
| 114 |
"""
|
| 115 |
if not text:
|
| 116 |
return []
|
| 117 |
|
|
|
|
| 118 |
paragraphs = re.split(r'\n\s*\n', text)
|
| 119 |
chunks = []
|
| 120 |
current_chunk = []
|
|
|
|
| 124 |
if not para.strip():
|
| 125 |
continue
|
| 126 |
|
|
|
|
|
|
|
|
|
|
| 127 |
if current_chunk_len + len(para) + len('\n\n') > max_chunk_size:
|
| 128 |
+
if current_chunk:
|
| 129 |
chunks.append("\n\n".join(current_chunk))
|
| 130 |
current_chunk = []
|
| 131 |
current_chunk_len = 0
|
| 132 |
|
|
|
|
| 133 |
if len(para) > max_chunk_size:
|
| 134 |
words = para.split(' ')
|
| 135 |
sub_chunk = []
|
|
|
|
| 142 |
else:
|
| 143 |
sub_chunk.append(word)
|
| 144 |
sub_chunk_len += len(word) + len(' ')
|
| 145 |
+
if sub_chunk:
|
| 146 |
chunks.append(" ".join(sub_chunk))
|
| 147 |
+
else:
|
| 148 |
current_chunk.append(para)
|
| 149 |
current_chunk_len += len(para) + len('\n\n')
|
| 150 |
+
else:
|
| 151 |
current_chunk.append(para)
|
| 152 |
current_chunk_len += len(para) + len('\n\n')
|
| 153 |
|
| 154 |
+
if current_chunk:
|
| 155 |
chunks.append("\n\n".join(current_chunk))
|
| 156 |
+
|
|
|
|
|
|
|
| 157 |
final_chunks_with_overlap = []
|
| 158 |
for i in range(len(chunks)):
|
| 159 |
chunk = chunks[i]
|
| 160 |
if i > 0 and overlap > 0:
|
|
|
|
| 161 |
prev_chunk_part = chunks[i-1][-overlap:]
|
| 162 |
chunk = prev_chunk_part + "\n" + chunk
|
| 163 |
final_chunks_with_overlap.append(chunk)
|
| 164 |
|
| 165 |
return final_chunks_with_overlap
|
| 166 |
|
|
|
|
| 167 |
# --- RAG Core Functions with ChromaDB ---
|
| 168 |
|
| 169 |
class DocumentRAG:
|
|
|
|
| 174 |
self.persist_directory = persist_directory
|
| 175 |
self.collection_name = collection_name
|
| 176 |
|
|
|
|
| 177 |
print(f"Initializing ChromaDB at: {self.persist_directory}")
|
| 178 |
self.client = chromadb.PersistentClient(path=self.persist_directory)
|
| 179 |
|
|
|
|
| 180 |
self.collection = self.client.get_or_create_collection(
|
| 181 |
name=self.collection_name,
|
|
|
|
|
|
|
| 182 |
metadata={"hnsw:space": "cosine"}
|
| 183 |
)
|
| 184 |
print(f"ChromaDB collection '{self.collection_name}' ready.")
|
| 185 |
|
| 186 |
def _generate_chunk_id(self, pdf_path: str, chunk_idx: int) -> str:
|
|
|
|
|
|
|
| 187 |
return f"{os.path.basename(pdf_path)}_{chunk_idx}_{uuid.uuid4().hex}"
|
| 188 |
|
| 189 |
def add_document(self, pdf_path: str):
|
| 190 |
print(f"Adding document: {pdf_path}")
|
| 191 |
|
|
|
|
|
|
|
|
|
|
| 192 |
results = self.collection.get(
|
| 193 |
where={"source": pdf_path},
|
| 194 |
limit=1
|
|
|
|
| 207 |
print(f"Warning: No chunks generated for {pdf_path}. Skipping.")
|
| 208 |
return
|
| 209 |
|
|
|
|
| 210 |
documents_to_add = []
|
| 211 |
metadatas_to_add = []
|
| 212 |
ids_to_add = []
|
| 213 |
|
| 214 |
print(f" Generating embeddings for {len(chunks)} chunks and preparing for ChromaDB...")
|
| 215 |
|
|
|
|
|
|
|
| 216 |
encoded_results = self.embedding_model.encode(
|
| 217 |
chunks,
|
| 218 |
+
batch_size=32,
|
| 219 |
return_dense=True,
|
| 220 |
return_sparse=False,
|
| 221 |
return_colbert_vecs=False
|
| 222 |
)
|
| 223 |
|
|
|
|
| 224 |
chunk_embeddings = encoded_results["dense_vecs"]
|
| 225 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
for i, chunk in enumerate(chunks):
|
| 227 |
unique_id = self._generate_chunk_id(pdf_path, i)
|
| 228 |
documents_to_add.append(chunk)
|
| 229 |
metadatas_to_add.append({"source": pdf_path, "chunk_id": i})
|
| 230 |
ids_to_add.append(unique_id)
|
| 231 |
|
|
|
|
| 232 |
self.collection.add(
|
| 233 |
documents=documents_to_add,
|
| 234 |
+
embeddings=chunk_embeddings.tolist(),
|
| 235 |
metadatas=metadatas_to_add,
|
| 236 |
ids=ids_to_add
|
| 237 |
)
|
|
|
|
| 249 |
|
| 250 |
print(f"Retrieving context for query: '{query}'")
|
| 251 |
|
|
|
|
| 252 |
query_embedding_result = self.embedding_model.encode(
|
| 253 |
[query],
|
| 254 |
batch_size=1,
|
|
|
|
| 256 |
return_sparse=False,
|
| 257 |
return_colbert_vecs=False
|
| 258 |
)
|
| 259 |
+
query_embedding = query_embedding_result["dense_vecs"].tolist()
|
| 260 |
|
|
|
|
| 261 |
results = self.collection.query(
|
| 262 |
query_embeddings=query_embedding,
|
| 263 |
n_results=top_k,
|
|
|
|
| 269 |
for i, doc_text in enumerate(results['documents'][0]):
|
| 270 |
source_info = results['metadatas'][0][i].get('source', 'Unknown Source')
|
| 271 |
chunk_id_info = results['metadatas'][0][i].get('chunk_id', 'N/A')
|
| 272 |
+
distance_info = results['distances'][0][i]
|
| 273 |
|
| 274 |
retrieved_chunks_texts.append(doc_text)
|
| 275 |
print(f" Retrieved chunk {i+1} (distance: {distance_info:.4f}) from '{source_info}' (chunk {chunk_id_info}).")
|
|
|
|
| 283 |
Answers a question by ensuring PDFs are indexed, retrieving context,
|
| 284 |
and querying DeepSeek.
|
| 285 |
"""
|
|
|
|
|
|
|
| 286 |
for path in pdf_paths:
|
| 287 |
self.add_document(path)
|
| 288 |
|
|
|
|
| 289 |
context_chunks = self.retrieve_context(question)
|
| 290 |
context = "\n\n".join(context_chunks)
|
| 291 |
|
|
|
|
| 295 |
else:
|
| 296 |
context_prompt = f"Using the following context:\n\n{context}\n\n"
|
| 297 |
|
| 298 |
+
# --- UPDATED SYSTEM PROMPT FOR COMPASSIA AI ---
|
| 299 |
+
system_prompt = """
|
| 300 |
+
You are CompassIA, the intelligent assistant for MaarifCompass, committed to supporting Turkiye Maarif Foundation graduates residing in Turkiye.
|
| 301 |
+
|
| 302 |
+
Your core function is to deliver precise, document-backed information concerning their needs, primarily focusing on:
|
| 303 |
+
- University application procedures, requirements, tuition fees, and scholarship opportunities
|
| 304 |
+
- Accommodation and housing resources
|
| 305 |
+
- Career networking and professional development
|
| 306 |
+
- Relevant administrative and support services.
|
| 307 |
+
- Information related to Turkiye Maarif Foundation, Turkiye Scholarship and more.
|
| 308 |
+
|
| 309 |
+
You operate exclusively with data from a designated Document Center. **It is imperative that every piece of information you provide is directly sourced and verifiable from these internal documents.**
|
| 310 |
+
|
| 311 |
+
**Should a query fall outside the scope of the provided documents or lack a direct answer within them, you are required to politely inform the user that the specific information is not available in your current knowledge base, without offering any external insights or assumptions.**
|
| 312 |
+
|
| 313 |
+
Your answers should be highly accurate, directly relevant, easy to understand, and always prioritize the user's query based strictly on documented facts.
|
| 314 |
+
**Remember, you always answer the user with the language of the question.**
|
| 315 |
+
"""
|
| 316 |
+
|
| 317 |
messages = [
|
| 318 |
+
{"role": "system", "content": system_prompt},
|
| 319 |
{"role": "user", "content": f"{context_prompt}Question: {question}"}
|
| 320 |
]
|
| 321 |
|
|
|
|
| 322 |
print("\nSending request to DeepSeek API...")
|
| 323 |
data = {
|
| 324 |
+
"model": "deepseek/deepseek-chat:free",
|
| 325 |
"messages": messages,
|
| 326 |
+
"temperature": 0.5,
|
| 327 |
+
"max_tokens": 500,
|
| 328 |
}
|
| 329 |
|
| 330 |
response = requests.post(API_URL, json=data, headers=HEADERS)
|
|
|
|
| 342 |
|
| 343 |
# --- Main execution logic ---
|
| 344 |
if __name__ == "__main__":
|
|
|
|
|
|
|
| 345 |
rag_system = DocumentRAG(
|
| 346 |
embedding_model=embedding_model,
|
| 347 |
+
persist_directory="./chroma_db",
|
| 348 |
+
collection_name="pdf_documents_collection",
|
| 349 |
chunk_size=700,
|
| 350 |
overlap=100
|
| 351 |
)
|
| 352 |
|
|
|
|
|
|
|
|
|
|
| 353 |
pdf_document_paths = [
|
| 354 |
+
"documents/heracles_tr.pdf",
|
| 355 |
+
"documents/heracles_en.pdf",
|
|
|
|
| 356 |
"documents/ogrenci_katki_payi_ogrenim_ucretleri.pdf",
|
| 357 |
"documents/Ogrenci_Liderligi_Burs_Programi_Sozlesme_Metni_2024-2025.pdf",
|
| 358 |
"documents/tmv-bursluluk-yonergesi.pdf"
|
| 359 |
]
|
| 360 |
|
|
|
|
|
|
|
| 361 |
print("\n--- Indexing Documents ---")
|
| 362 |
for pdf_path in pdf_document_paths:
|
| 363 |
if os.path.exists(pdf_path):
|
|
|
|
| 365 |
else:
|
| 366 |
print(f"Error: PDF file not found at {pdf_path}. Please check the path.")
|
| 367 |
|
| 368 |
+
print("\n--- Chat With CompassIA (Type 'quit' to exit) ---")
|
|
|
|
| 369 |
while True:
|
| 370 |
+
user_question = input("\nHow can I help you? ")
|
| 371 |
if user_question.lower() == 'quit':
|
| 372 |
print("Exiting chat.")
|
| 373 |
break
|
| 374 |
|
| 375 |
+
rag_system.answer_question(user_question, pdf_document_paths)
|
|
|