Spaces:
Sleeping
Sleeping
File size: 16,168 Bytes
5a7d213 03ee111 5a7d213 a989689 d8d3708 5a7d213 03ee111 d8d3708 841a1ea 03ee111 5a7d213 03ee111 d8d3708 03ee111 5a7d213 d8d3708 03ee111 5a7d213 d8d3708 5a7d213 d8d3708 5a7d213 d8d3708 3e8a039 fd4240c 03ee111 d8d3708 3e8a039 fd4240c d8d3708 fd4240c d8d3708 fd4240c 03ee111 5a7d213 d8d3708 160bc01 d8d3708 03ee111 5a7d213 03ee111 5a7d213 d8d3708 160bc01 d8d3708 03ee111 5a7d213 2d15f15 03ee111 d8d3708 03ee111 5a7d213 03ee111 5a7d213 a989689 d8d3708 03ee111 d8d3708 03ee111 d8d3708 5a7d213 03ee111 d8d3708 5a7d213 d8d3708 03ee111 d8d3708 03ee111 d8d3708 a989689 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 160bc01 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 03ee111 d8d3708 fd4240c 5a7d213 d8d3708 03ee111 d8d3708 03ee111 3e8a039 160bc01 03ee111 5a7d213 03ee111 | 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 | import gradio as gr
import PyPDF2
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import FAISS
from langchain.llms import HuggingFacePipeline
from transformers import pipeline, AutoTokenizer, GPT2LMHeadModel, BitsAndBytesConfig
import os
import torch
import re
import unicodedata # Unicode normalizasyonu için
# --- 0. Global Değişkenler ve Ayarlar ---
PDF_PATH = "mevzuat.pdf"
# Mevzuat için daha uygun Chunk boyutları
CHUNK_SIZE = 700 # Daha da artırıldı
CHUNK_OVERLAP = 150 # Daha da artırıldı
EMBEDDING_MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
LLM_MODEL_NAME = "ytu-ce-cosmos/turkish-gpt2-medium"
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
MODEL_MAX_LENGTH = 1024
MAX_NEW_TOKENS = 350 # Daha detaylı cevaplar için artırıldı
# --- Yardımcı Metin Temizleme ve Normalizasyon Fonksiyonu (Daha Kapsamlı) ---
def normalize_text(text):
if not isinstance(text, str):
return ""
# Unicode normalizasyonu (aksansız karakterlere çevirme, vb.)
text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('utf-8', 'ignore')
# Küçük harfe çevir
text = text.lower()
# Noktalama işaretlerini kaldır (kelime bazlı arama için önemli)
text = re.sub(r'[^\w\s]', '', text)
# Birden fazla boşluğu tek boşluğa çevir ve baştaki/sondaki boşlukları kaldır
text = re.sub(r'\s+', ' ', text).strip()
return text
# --- 1. PDF'i İşle ve Vektör Veritabanı Oluştur ---
def create_vector_db_from_pdf(pdf_path):
print(f"[{os.path.basename(__file__)}] PDF okunuyor: {pdf_path}")
chunks_with_metadata = []
try:
with open(pdf_path, "rb") as file:
reader = PyPDF2.PdfReader(file)
total_pages = len(reader.pages)
print(f"[{os.path.basename(__file__)}] Toplam {total_pages} sayfa bulundu.")
for page_num, page in enumerate(reader.pages):
try:
page_text = page.extract_text()
if page_text:
# Metin temizliği ve normalizasyonunu uygula
cleaned_and_normalized_page_text = normalize_text(page_text)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
length_function=len,
)
page_chunks = text_splitter.split_text(cleaned_and_normalized_page_text)
for chunk in page_chunks:
chunks_with_metadata.append({
"content": chunk,
"source_page": page_num + 1
})
except Exception as page_e:
print(f"[{os.path.basename(__file__)}] Sayfa {page_num + 1} okunurken veya işlenirken hata oluştu: {page_e}")
continue # Bir sonraki sayfaya geç
except Exception as e:
print(f"[{os.path.basename(__file__)}] PDF dosyası okunurken genel hata oluştu: {e}")
return None
if not chunks_with_metadata:
print(f"[{os.path.basename(__file__)}] PDF'ten metin çıkarılamadı veya parçalanamadı. Lütfen PDF içeriğini kontrol edin.")
return None
print(f"[{os.path.basename(__file__)}] Metin {len(chunks_with_metadata)} parçaya ayrıldı (metadata dahil).")
print(f"[{os.path.basename(__file__)}] Embedding modeli yükleniyor: {EMBEDDING_MODEL_NAME}")
try:
embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL_NAME)
except Exception as e:
print(f"[{os.path.basename(__file__)}] Embedding modeli yüklenirken hata oluştu: {e}")
return None
texts = [item["content"] for item in chunks_with_metadata]
metadatas = [{"source_page": item["source_page"]} for item in chunks_with_metadata]
print(f"[{os.path.basename(__file__)}] FAISS vektör veritabanı oluşturuluyor...")
try:
db = FAISS.from_texts(texts, embeddings, metadatas=metadatas)
print(f"[{os.path.basename(__file__)}] Vektör veritabanı başarıyla oluşturuldu.")
return db, embeddings
except Exception as e:
print(f"[{os.path.basename(__file__)}] FAISS veritabanı oluşturulurken hata oluştu: {e}")
return None
# --- Vektör veritabanını başlatma ---
vector_db, embeddings_model = None, None
if os.path.exists(PDF_PATH):
vector_db, embeddings_model = create_vector_db_from_pdf(PDF_PATH)
else:
print(f"[{os.path.basename(__file__)}] Hata: {PDF_PATH} bulunamadı. Lütfen PDF dosyasını projenizin kök dizinine yükleyin ve doğru yolda olduğundan emin olun.")
# --- LLM modelini yükleme ---
print(f"[{os.path.basename(__file__)}] LLM modeli yükleniyor: {LLM_MODEL_NAME} - Cihaz: {DEVICE}")
tokenizer = None
llm = None
try:
tokenizer = AutoTokenizer.from_pretrained(LLM_MODEL_NAME)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
if DEVICE == "cuda":
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16, # Doğru format "torch.float16" veya torch.float16
bnb_4bit_use_double_quant=True,
)
model = GPT2LMHeadModel.from_pretrained(LLM_MODEL_NAME,
quantization_config=quantization_config,
device_map="auto")
else:
model = GPT2LMHeadModel.from_pretrained(LLM_MODEL_NAME).to(DEVICE)
text_generation_pipeline = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
max_new_tokens=MAX_NEW_TOKENS,
temperature=0.3, # Daha kesin ve tutarlı cevaplar için biraz daha düşürüldü
do_sample=True,
top_k=30, # Daha odaklı cevaplar için biraz daha düşürüldü
num_return_sequences=1,
device=0 if DEVICE == "cuda" else -1,
pad_token_id=tokenizer.pad_token_id,
return_full_text=False # Sadece üretilen metni döndür
)
llm = HuggingFacePipeline(pipeline=text_generation_pipeline)
print(f"[{os.path.basename(__file__)}] LLM modeli başarıyla yüklendi.")
except Exception as e:
print(f"[{os.path.basename(__file__)}] LLM modeli yüklenirken kritik hata oluştu: {e}")
llm = None # Hata durumunda LLM'i None olarak bırak
# --- 2. Gradio Arayüzü İçin Soru Cevaplama Fonksiyonu ---
def answer_question(question, chat_history):
global tokenizer
if vector_db is None or llm is None or tokenizer is None:
error_msg = "Üzgünüm, sistem başlatılamadı (PDF, LLM veya Tokenizer yüklenemedi). Lütfen logları kontrol edin ve yöneticinizle iletişime geçin."
print(f"[{os.path.basename(__file__)}] Hata: {error_msg}")
return "", chat_history + [[question, error_msg]]
print(f"[{os.path.basename(__file__)}] Gelen soru: '{question}'")
# Soruyu da veritabanındaki metinlerle aynı şekilde normalize et
normalized_question = normalize_text(question)
print(f"[{os.path.basename(__file__)}] Normalize edilmiş soru: '{normalized_question}'")
if not normalized_question.strip():
return "", chat_history + [[question, "Lütfen geçerli bir soru girin."]]
# Daha fazla potansiyel ilgili belge getir
retrieved_docs_with_scores = vector_db.similarity_search_with_score(normalized_question, k=6) # k değeri 4'ten 6'ya çıkarıldı
if not retrieved_docs_with_scores:
print(f"[{os.path.basename(__file__)}] Sorunuza uygun ilgili belge bulunamadı.")
return "", chat_history + [[question, "Üzgünüm, sorunuzla ilgili bağlamda herhangi bir bilgi bulunamadı. Lütfen sorunuzu farklı bir şekilde ifade etmeyi deneyin veya daha spesifik olun."]]
context_parts = []
source_pages = set() # Kullanılan sayfa numaralarını tutmak için
for i, (doc, score) in enumerate(retrieved_docs_with_scores):
# Yüksek benzerlik skoruna sahip (düşük mesafe) belgeleri tercih et
# Çok alakasız görünenleri eleyebiliriz, eşik değeri deneyerek ayarlanmalı
# Örneğin, mesafe 0.5'ten büyükse ele (FAISS'te daha küçük mesafe daha iyi benzerliktir)
# print(f"Doc {i+1} Score: {score}, Content: {doc.page_content[:100]}...") # Debug için
# Eğer embedding modeli cosine similarity döndürüyorsa, 0'a yakınlık yüksek benzerlik demektir.
# Eğer L2 mesafesi döndürüyorsa, 0'a yakınlık yüksek benzerlik demektir.
# sentence-transformers genellikle cosine similarity döndürür, bu yüzden 1'e yakın değerler daha iyi.
# Ancak FAISS genellikle L2 mesafesi kullanır (veya cosine distance'ı 1-cos(theta) olarak temsil eder).
# Genellikle 0'a yakın değerler daha iyi (daha küçük mesafe).
# Eğer score çok yüksek (büyük bir sayı) ise, bu doküman çok alakasız olabilir.
# Basit bir filtreleme: İlk 3 dokümanı kesin al, diğerlerini skorlarına göre değerlendir
if i < 3 or score < 0.6: # Bu eşik değeri denemelerle ayarlanmalı
context_parts.append(doc.page_content)
if 'source_page' in doc.metadata:
source_pages.add(doc.metadata['source_page'])
else:
print(f"[{os.path.basename(__file__)}] Doküman {i+1} çok alakasız olduğu için bağlama eklenmedi (Skor: {score:.4f}).")
context = "\n\n".join(context_parts)
if not context.strip():
print(f"[{os.path.basename(__file__)}] Filtreleme sonrası bağlam boş kaldı.")
return "", chat_history + [[question, "Üzgünüm, sorunuzla ilgili yeterince alakalı bilgi bulunamadı. Lütfen sorunuzu farklı bir şekilde ifade etmeyi deneyin veya daha spesifik olun."]]
print(f"[{os.path.basename(__file__)}] Bağlam için {len(context_parts)} belge parçası kullanıldı. İlk 200 karakter:\n'{context[:200]}...'")
# Geliştirilmiş, daha detaylı ve kısıtlayıcı Prompt
prompt = f"""Sen bir Fırat Üniversitesi mevzuat uzmanısın. Görevin, yalnızca ve kesinlikle aşağıdaki 'Bağlam' bölümünde verilen bilgilere dayanarak 'Soru'yu eksiksiz, net, anlaşılır ve madde madde olacak şekilde cevaplamaktır.
Eğer 'Bağlam'da soruya cevap verecek yeterli ve doğrudan bilgi bulunmuyorsa, kesinlikle kendi bilginizden uydurma yapmayın. Bu durumda, 'Üzgünüm, bu soruya bağlamda yeterli bilgi bulunmamaktadır.' şeklinde açıkça belirtin.
Cevabın sonunda, bilginin alındığı sayfa numaralarını 'Kaynak: Sayfa X, Y, Z' formatında açıkça ve sadece bağlamda belirtilen sayfa numaralarını kullanarak listeleyin. Bağlamda sayfa bilgisi yoksa (ki bu olmamalı), kaynak belirtme.
Bağlam:
{context}
Soru: {question}
Cevap:
"""
print(f"[{os.path.basename(__file__)}] LLM'e gönderilen prompt'ın başlangıcı: '{prompt[:500]}...'")
# --- Prompt uzunluğunu MODEL_MAX_LENGTH - MAX_NEW_TOKENS ile sınırlayalım ---
max_input_tokens = MODEL_MAX_LENGTH - MAX_NEW_TOKENS
prompt_tokens = tokenizer.encode(prompt, return_tensors='pt')[0]
tokenized_prompt_length = prompt_tokens.shape[0]
print(f"[{os.path.basename(__file__)}] Oluşturulan prompt'un token uzunluğu: {tokenized_prompt_length}")
if tokenized_prompt_length > max_input_tokens:
print(f"[{os.path.basename(__file__)}] UYARI: Prompt'un token uzunluğu ({tokenized_prompt_length}) modelin kabul edebileceği maksimum girdi uzunluğunu ({max_input_tokens}) aşıyor! Kısaltılıyor...")
# Token bazında kısaltma
prompt_tokens = prompt_tokens[:max_input_tokens]
prompt = tokenizer.decode(prompt_tokens, skip_special_tokens=True)
print(f"[{os.path.basename(__file__)}] Prompt kısaltıldı. Yeni token uzunluğu: {len(tokenizer.encode(prompt))}")
print(f"[{os.path.basename(__file__)}] Kısaltılmış prompt'ın başlangıcı: '{prompt[:500]}...'")
final_answer = ""
try:
response_data = llm(prompt)
if isinstance(response_data, list) and len(response_data) > 0 and 'generated_text' in response_data[0]:
final_answer = response_data[0]['generated_text'].strip()
else:
final_answer = str(response_data).strip()
# LLM'in prompt'u tekrar etme veya gereksiz kısımları kaldırma
# Çoklu kontrol noktası
if final_answer.lower().startswith(prompt.lower()):
final_answer = final_answer[len(prompt):].strip()
# "Cevap:" kısmı LLM tarafından tekrar edildiyse temizle
if "Cevap:" in final_answer:
final_answer = final_answer.split("Cevap:", 1)[-1].strip()
# Prompt'un başlangıcından kalıntı kaldıysa temizle
if final_answer.lower().startswith("sen bir fırat üniversitesi mevzuat uzmanısın.") or \
final_answer.lower().startswith("aşağıdaki bağlamı kullanarak"):
# Daha sofistike bir temizleme: "Cevap:" ifadesini arayarak o kısımdan sonrasını al
match = re.search(r"cevap:\s*(.*)", final_answer, re.IGNORECASE | re.DOTALL)
if match:
final_answer = match.group(1).strip()
else: # Bulamazsa yine de bir kısmı sil
final_answer = final_answer.split("Soru:", 1)[-1].strip() if "Soru:" in final_answer else final_answer.strip()
final_answer = final_answer.split("Cevap:", 1)[-1].strip() if "Cevap:" in final_answer else final_answer.strip()
# Cevabın boş veya anlamsız olup olmadığını kontrol et
if not final_answer or final_answer.strip().lower() in ["üzgünüm, bu soruya bağlamda yeterli bilgi bulunmamaktadır.", "üzgünüm, bu soruya bağlamda yeterli bilgi bulunmamaktadır", "bilgi bulunamadı.", "bilgi bulunamadı"]:
final_answer = "Üzgünüm, bu soruya bağlamda yeterli bilgi bulunmamaktadır."
# Kaynakları cevaba ekle
if source_pages and final_answer != "Üzgünüm, bu soruya bağlamda yeterli bilgi bulunmamaktadır.":
sorted_pages = sorted(list(source_pages))
final_answer += f"\n\n**Kaynak:** Sayfa {', '.join(map(str, sorted_pages))}"
print(f"[{os.path.basename(__file__)}] Üretilen son cevap: '{final_answer[:500]}...'")
except Exception as e:
final_answer = f"Cevap üretilirken kritik bir hata oluştu: {e}. Lütfen daha sonra tekrar deneyin veya farklı bir soru sorun."
print(f"[{os.path.basename(__file__)}] Kritik cevap üretim hatası: {e}")
chat_history.append((question, final_answer))
return "", chat_history
# --- 3. Gradio Arayüz Tanımı ---
with gr.Blocks() as demo:
gr.Markdown(
"""
# Fırat Üniversitesi Mini RAG Botu
Fırat Üniversitesi mevzuat.firat.edu.tr adresinden alınan PDF dosyası üzerinde soru-cevap yapın.
Sorularınıza belgedeki bilgilere dayanarak cevaplar verilecektir.
Bağlamda bilgi yoksa, bot kesinlikle uydurma yapmayacaktır ve bunu açıkça belirtecektir.
**Cevaplar artık kaynak sayfa numarasını da içermektedir.**
"""
)
chatbot = gr.Chatbot(height=400, label="Sohbet")
msg = gr.Textbox(label="Sorunuzu buraya yazın:", placeholder="Örn: 'Belgede tez jürisi kuralı ne?'")
with gr.Row():
submit_btn = gr.Button("Gönder")
clear_btn = gr.Button("Sohbeti Temizle")
msg.submit(answer_question, [msg, chatbot], [msg, chatbot])
submit_btn.click(answer_question, [msg, chatbot], [msg, chatbot])
clear_btn.click(lambda: (None, []), outputs=[msg, chatbot])
if __name__ == "__main__":
demo.launch() |