Spaces:
Sleeping
Sleeping
| 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() |