Spaces:
Sleeping
Sleeping
| # app.py | |
| # === GEREKLİ KÜTÜPHANELER === | |
| # Bu komutları Colab'da veya yerel ortamınızda bir kez çalıştırmanız gerekebilir. | |
| # !pip install gradio | |
| # !pip install faiss-cpu | |
| # !pip install datasets | |
| # !pip install transformers accelerate peft bitsandbytes | |
| # !pip install sentence-transformers | |
| # !pip install scikit-learn | |
| # !pip install nltk # For Turkish stopwords | |
| import torch | |
| import gradio as gr | |
| import faiss | |
| import numpy as np | |
| from sentence_transformers import SentenceTransformer | |
| from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig, TrainingArguments | |
| from peft import PeftModel, get_peft_model, LoraConfig, TaskType | |
| from datasets import Dataset | |
| import json | |
| import os | |
| from typing import List, Tuple | |
| from functools import partial | |
| import random | |
| from datetime import datetime | |
| from collections import deque | |
| import time | |
| import re # Regular expressions for robust text cleaning | |
| import nltk | |
| from nltk.corpus import stopwords # For TF-IDF Turkish stopwords | |
| # === CSS ve Emoji Fonksiyonu === | |
| current_css = """ | |
| #chatbot { height: 500px; overflow-y: auto; } | |
| .gradio-container .message-row.user { | |
| justify-content: flex-start !important; | |
| } | |
| .gradio-container .message-row.bot { | |
| justify-content: flex-end !important; | |
| } | |
| #chatbot .message:nth-child(odd) { | |
| text-align: left; | |
| background-color: #f1f1f1; | |
| border-radius: 15px; | |
| padding: 10px; | |
| margin-right: 20%; | |
| } | |
| #chatbot .message:nth-child(even) { | |
| text-align: right; | |
| background-color: #dcf8c6; | |
| border-radius: 15px; | |
| padding: 10px; | |
| margin-left: 20%; | |
| } | |
| #chatbot .message { | |
| margin-bottom: 10px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
| } | |
| #stopwatch_display { /* ID'yi doğrudan kullanıyoruz */ | |
| font-size: 1.2em; | |
| font-weight: bold; | |
| color: #333; | |
| text-align: center; | |
| margin-top: 10px; | |
| width: fit-content; /* Genişliği içeriğe göre ayarla */ | |
| padding: 5px 10px; | |
| border: 1px solid #ddd; | |
| border-radius: 5px; | |
| background-color: #f9f9f9; | |
| } | |
| """ | |
| def add_emojis(text: str) -> str: | |
| emoji_mapping = { | |
| "kitap": "📚", "kitaplar": "📚", | |
| "bilgi": "🧠", "öğrenmek": "🧠", | |
| "özgürlük": "🕊️", "özgür": "🕊️", | |
| "düşünce": "💭", "düşünmek": "💭", | |
| "ateş": "🔥", "yanmak": "🔥", | |
| "yasak": "🚫", "yasaklamak": "🚫", | |
| "tehlike": "⚠️", "tehlikeli": "⚠️", | |
| "devlet": "🏛️", "hükümet": "🏛️", | |
| "soru": "❓", "cevap": "✅", | |
| "okumak": "👁️", "oku": "👁️", | |
| "itfaiye": "🚒", "itfaiyeci": "🚒", | |
| "değişim": "🔄", "değişmek": "🔄", | |
| "isyan": "✊", "başkaldırı": "✊", | |
| "uyuşturucu": "💊", "hap": "💊", | |
| "televizyon": "📺", "tv": "📺", | |
| "mutlu": "😊", "mutluluk": "😊", | |
| "üzgün": "😞", "korku": "😨", | |
| "merak": "🤔", "meraklı": "🤔", | |
| "kültür": "🌍", "toplum": "👥", | |
| "yalan": "🤥", "gerçek": "✨", | |
| "clarisse": "🌸", "faber": "👴", "beatty": "🚨" # Montag karakterleri için emojiler | |
| } | |
| found_emojis = [] | |
| words = text.split() | |
| for word in words: | |
| clean_word = word.lower().strip(".,!?") | |
| if clean_word in emoji_mapping: | |
| found_emojis.append(emoji_mapping[clean_word]) | |
| unique_emojis = list(set(found_emojis)) | |
| if unique_emojis: | |
| return f"{text} {' '.join(unique_emojis)}" | |
| return text | |
| # === SABİTLER === | |
| EMBEDDER_NAME = "paraphrase-multilingual-MiniLM-L12-v2" | |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| FEEDBACK_FILE = "data/chatbot_feedback.jsonl" | |
| QA_PATH = "data/qa_dataset.jsonl" # LoRA fine-tuning örnekleri için kullanılır | |
| BASE_MODEL_FOR_DEMO = "ytu-ce-cosmos/turkish-gpt2-large" # Kullandığınız temel model | |
| LOR_MODEL_PATH = "lora_model_weights" # LoRA adaptör ağırlıklarının kaydedileceği/yükleneceği yol | |
| FAHRENHEIT_TEXT_FILE = "fahrenheittt451.txt" # Kitap metin dosyanızın adı | |
| # Tahmini maksimum cevap süresi (saniye) - Donanım ve modele göre ayarlayın. | |
| MAX_EXPECTED_TIME = 120.0 # Ortalama bir değer, kendi sisteminize göre ayarlayın! | |
| # === GLOBAL DEĞİŞKENLER === | |
| # Bu değişkenler initialize_components fonksiyonu tarafından atanacak | |
| model = None | |
| tokenizer = None | |
| embedder = None | |
| paragraphs = [] | |
| paragraph_embeddings = None | |
| index = None | |
| rl_agent = None # RLAgent nesnesi | |
| # === DOSYA VE KLASÖR YAPISI OLUŞTURMA === | |
| # Google Drive bağlantıları olmadan yerel dosya yapısını oluşturur | |
| def setup_local_files(): | |
| os.makedirs('data', exist_ok=True) | |
| os.makedirs(LOR_MODEL_PATH, exist_ok=True) | |
| # Gerekli dosyaların varlığını kontrol et ve yoksa boş oluştur | |
| if not os.path.exists(FAHRENHEIT_TEXT_FILE): | |
| print(f"HATA: '{FAHRENHEIT_TEXT_FILE}' bulunamadı. Lütfen bu dosyayı projenizin ana dizinine yerleştirin.") | |
| # Uygulama metin olmadan çalışamaz, bu yüzden burada çıkış yapmayı düşünebilirsiniz. | |
| # raise FileNotFoundError(f"{FAHRENHEIT_TEXT_FILE} not found.") | |
| if not os.path.exists(QA_PATH): | |
| open(QA_PATH, 'a').close() # Boş QA dosyası oluştur | |
| if not os.path.exists(FEEDBACK_FILE): | |
| open(FEEDBACK_FILE, 'a').close() # Boş feedback dosyası oluştur | |
| print("Yerel dosya ve klasör yapısı hazır.") | |
| # === YARDIMCI METİN YÜKLEME FONKSİYONU === | |
| def load_text_from_file(filepath: str) -> str: | |
| """Belirtilen dosya yolundan metin içeriğini okur.""" | |
| try: | |
| with open(filepath, "r", encoding="utf8") as f: | |
| content = f.read() | |
| print(f"Metin '{filepath}' dosyasından başarıyla yüklendi.") | |
| return content | |
| except FileNotFoundError: | |
| print(f"HATA: '{filepath}' dosyası bulunamadı. Lütfen dosya yolunu kontrol edin.") | |
| return "" | |
| except Exception as e: | |
| print(f"Metin dosyası okunurken hata oluştu: {e}") | |
| return "" | |
| # === MODEL VE TOKENIZER YÜKLEME (Quantization ve LoRA desteği ile) === | |
| def load_model_and_tokenizer_func(base_model_name: str, lora_model_path: str = None): | |
| """Temel modeli ve tokenizer'ı yükler, isteğe bağlı olarak LoRA ağırlıklarını uygular.""" | |
| print(f"Model yükleniyor: {base_model_name}...") | |
| tokenizer = AutoTokenizer.from_pretrained(base_model_name) | |
| quantization_config = None | |
| if DEVICE == "cuda": | |
| quantization_config = BitsAndBytesConfig( | |
| load_in_8bit=True, | |
| bnb_4bit_quant_type="nf4", | |
| bnb_4bit_compute_dtype=torch.float16, | |
| bnb_4bit_use_double_quant=True, | |
| ) | |
| print("CUDA mevcut. Model 8-bit quantized olarak yüklenecek.") | |
| else: | |
| print("CUDA mevcut değil. Model CPU'da standart olarak yüklenecek.") | |
| base_model = AutoModelForCausalLM.from_pretrained( | |
| base_model_name, | |
| quantization_config=quantization_config, | |
| torch_dtype=torch.float16 if DEVICE == "cuda" else torch.float32, | |
| device_map="auto" | |
| ) | |
| model_to_return = base_model | |
| # LoRA adaptörü varsa yükle | |
| if lora_model_path and os.path.exists(lora_model_path) and len(os.listdir(lora_model_path)) > 0: # Klasörün içi boş mu kontrolü | |
| print(f"LoRA modeli yükleniyor: {lora_model_path}...") | |
| try: | |
| model_to_return = PeftModel.from_pretrained(base_model, lora_model_path) | |
| print(f"LoRA modeli {lora_model_path} başarıyla yüklendi.") | |
| except Exception as e: | |
| print(f"UYARI: LoRA modeli yüklenirken hata oluştu ({e}). Temel model kullanılacak.") | |
| model_to_return = base_model | |
| else: | |
| if lora_model_path and not os.path.exists(lora_model_path): | |
| print(f"UYARI: LoRA modeli yolu '{lora_model_path}' bulunamadı. Temel model kullanılacak.") | |
| elif lora_model_path and os.path.exists(lora_model_path) and len(os.listdir(lora_model_path)) == 0: | |
| print(f"UYARI: '{lora_model_path}' klasörü boş. Temel model kullanılacak.") | |
| if tokenizer.pad_token is None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| model_to_return.eval() | |
| print(f"Model {base_model_name} yüklendi.") | |
| return model_to_return, tokenizer | |
| # === QA KAYIT VE GERİ BİLDİRİM FONKSİYONLARI === | |
| def save_feedback(user_question: str, answer: str, liked: bool, filepath: str = FEEDBACK_FILE): | |
| feedback_entry = { | |
| "input": user_question, | |
| "output": answer, | |
| "liked": liked, | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| try: | |
| with open(filepath, "a", encoding="utf-8") as f: | |
| f.write(json.dumps(feedback_entry, ensure_ascii=False) + "\n") | |
| except Exception as e: | |
| print(f"Error saving feedback to {filepath}: {e}") | |
| def count_qa_examples(filepath: str = QA_PATH) -> int: | |
| if not os.path.exists(filepath): | |
| return 0 | |
| try: | |
| with open(filepath, "r", encoding="utf-8") as f: | |
| return sum(1 for _ in f) | |
| except Exception as e: | |
| print(f"Error counting QA examples from {filepath}: {e}") | |
| return 0 | |
| # === LoRA Fine-tuning FONKSİYONU === | |
| def lora_finetune(filepath: str = QA_PATH, lora_output_path: str = LOR_MODEL_PATH): | |
| global model, tokenizer # Model ve tokenizer'ı global olarak güncelleyeceğiz | |
| print("⚙️ LoRA fine-tuning başlıyor...") | |
| data = [] | |
| # Sadece beğenilen (liked) feedbackleri fine-tuning için kullan | |
| try: | |
| with open(FEEDBACK_FILE, "r", encoding="utf-8") as f: | |
| for line in f: | |
| item = json.loads(line) | |
| # Yalnızca beğenilen ve yeterince uzun cevapları al | |
| if item["liked"] and len(item["output"].split()) > 10: | |
| # Kullanıcı ve asistan mesajlarını emojilerden temizleyelim | |
| cleaned_user_q = item['input'].replace('📚', '').replace('🧠', '').replace('🔥', '').strip() | |
| cleaned_answer = item['output'].replace('📚', '').replace('🧠', '').replace('🔥', '').strip() | |
| prompt_text = f"Kullanıcı: {cleaned_user_q}\nMontag: {cleaned_answer}{tokenizer.eos_token}" | |
| data.append({"text": prompt_text}) | |
| except Exception as e: | |
| print(f"Fine-tuning verisi okunurken hata: {e}") | |
| return | |
| if not data: | |
| print("💡 Fine-tuning için yeterli beğeni bulunamadı. Lütfen daha fazla etkileşim sağlayın.") | |
| return | |
| dataset = Dataset.from_list(data) | |
| lora_config = LoraConfig( | |
| r=8, | |
| lora_alpha=16, | |
| target_modules=["c_attn", "c_proj"], # GPT2 için tipik olarak kullanılan modüller | |
| lora_dropout=0.1, | |
| bias="none", | |
| task_type=TaskType.CAUSAL_LM | |
| ) | |
| # Temel modeli yeniden yükle (eğer 8-bit quantize edilmişse) | |
| # Bu, mevcut modelin üzerine adaptör eklemek için gereklidir. | |
| base_model_for_finetuning, _ = load_model_and_tokenizer_func(BASE_MODEL_FOR_DEMO) | |
| peft_model = get_peft_model(base_model_for_finetuning, lora_config) | |
| peft_model.print_trainable_parameters() | |
| args = TrainingArguments( | |
| output_dir="./results", # Geçici çıktılar buraya | |
| num_train_epochs=3, # Daha az epoch ile başlayabilirsiniz | |
| per_device_train_batch_size=2, | |
| gradient_accumulation_steps=4, | |
| logging_steps=10, | |
| save_strategy="no", # trainer.save_pretrained ile kendimiz kaydedeceğiz | |
| learning_rate=2e-4, | |
| fp16=torch.cuda.is_available(), | |
| report_to="none", # Colab'da wandb kullanmıyorsanız | |
| disable_tqdm=True, # Gradio arayüzünde tqdm çubuğunu gizler | |
| ) | |
| def tokenize(example): | |
| tokenized = tokenizer(example["text"], truncation=True, padding="max_length", max_length=256) | |
| tokenized["labels"] = tokenized["input_ids"].copy() | |
| return tokenized | |
| tokenized_dataset = dataset.map(tokenize, remove_columns=["text"]) | |
| from transformers import Trainer | |
| trainer = Trainer( | |
| model=peft_model, | |
| args=args, | |
| train_dataset=tokenized_dataset, | |
| tokenizer=tokenizer, | |
| ) | |
| trainer.train() | |
| # Adaptör ağırlıklarını LOR_MODEL_PATH'e kaydet | |
| peft_model.save_pretrained(lora_output_path) | |
| print(f"✅ Model adaptörü başarıyla güncellendi ve '{lora_output_path}' konumuna kaydedildi.") | |
| # Yeni eğitilmiş modeli (adaptör ile birlikte) global 'model' değişkenine ata | |
| # Bu, uygulamanın yeni ağırlıkları kullanmasını sağlar. | |
| model, _ = load_model_and_tokenizer_func(BASE_MODEL_FOR_DEMO, LOR_MODEL_PATH) | |
| print("Güncel model ağırlıkları belleğe yüklendi.") | |
| # === RLAgent Sınıfı === | |
| class RLAgent: | |
| def __init__(self, embed_dim: int): | |
| self.device = DEVICE | |
| self.reward_model = torch.nn.Linear(embed_dim, 1).to(self.device) | |
| self.optimizer = torch.optim.AdamW(self.reward_model.parameters(), lr=1e-4) | |
| self.experience_buffer = deque(maxlen=200) | |
| # Başlangıç değerleri tekrar sorununu azaltmak ve çeşitliliği artırmak için güncellendi | |
| self.current_temp = 0.95 | |
| self.current_rep_penalty = 1.3 | |
| self.learning_rate_reward = 0.08 | |
| # record_experience metodu, doğrudan 'liked' boolean'ını kullanır | |
| def record_experience(self, user_question: str, generated_answer: str, liked: bool): | |
| try: | |
| # Geçici veya durum mesajlarını kaydetme | |
| if "Montag düşünüyor..." in generated_answer or "saniyede üretildi" in generated_answer: | |
| return | |
| cleaned_answer = generated_answer.replace('📚', '').replace('🧠', '').replace('🔥', '').strip() | |
| # Soru ve cevabı birleştirip embedding'ini alıyoruz. | |
| combined_text = f"Soru: {user_question} Cevap: {cleaned_answer}" | |
| if embedder is None: | |
| print("Embedder not initialized, cannot record experience.") | |
| return | |
| embedding = embedder.encode([combined_text], convert_to_tensor=True).squeeze(0).to(self.device) | |
| # Doğrudan kullanıcı geri bildiriminden ödül | |
| reward = 1.0 if liked else -1.5 # Beğenildi: +1, Beğenilmedi: -1.5 (daha caydırıcı) | |
| self.experience_buffer.append((embedding, torch.tensor([reward], dtype=torch.float32).to(self.device))) | |
| self.train_reward_model() # Deneyim eklendikten sonra doğrudan modeli eğitiyoruz | |
| except Exception as e: | |
| print(f"Error recording experience: {e}") | |
| def get_generation_params(self): | |
| return { | |
| "temperature": self.current_temp, | |
| "repetition_penalty": self.current_rep_penalty | |
| } | |
| def train_reward_model(self): | |
| if len(self.experience_buffer) < 5: # En az 5 örnekle eğitmeye başla | |
| return | |
| batch_size = min(len(self.experience_buffer), 16) | |
| experiences = random.sample(self.experience_buffer, batch_size) | |
| embeddings = torch.stack([exp[0] for exp in experiences]) | |
| rewards = torch.stack([exp[1] for exp in experiences]) | |
| self.optimizer.zero_grad() | |
| predicted_rewards = self.reward_model(embeddings) | |
| loss = torch.nn.functional.mse_loss(predicted_rewards, rewards) | |
| loss.backward() | |
| self.optimizer.step() | |
| avg_reward = rewards.mean().item() if rewards.numel() > 0 else 0 | |
| # Sıcaklık ve tekrar cezası için dinamik ayar limitleri | |
| min_temp, max_temp = 0.8, 1.2 | |
| min_rep_penalty, max_rep_penalty = 1.2, 1.8 | |
| # Ödül -1.5 ile 1 arasında olduğu için eşikleri buna göre ayarlayın | |
| if avg_reward > 0.5: # Yüksek ortalama ödül (iyi cevap) | |
| self.current_temp = max(min_temp, self.current_temp - self.learning_rate_reward * 0.05) | |
| self.current_rep_penalty = max(min_rep_penalty, self.current_rep_penalty - self.learning_rate_reward * 0.01) | |
| elif avg_reward < -0.5: # Düşük ortalama ödül (kötü cevap) | |
| self.current_temp = min(max_temp, self.current_temp + self.learning_rate_reward * 0.08) | |
| self.current_rep_penalty = min(max_rep_penalty, self.current_rep_penalty + self.learning_rate_reward * 0.03) | |
| self.current_temp = float(np.clip(self.current_temp, min_temp, max_temp)) | |
| self.current_rep_penalty = float(np.clip(self.current_rep_penalty, min_rep_penalty, max_rep_penalty)) | |
| print(f"DEBUG: RLAgent - Loss: {loss.item():.4f}, Avg Reward: {avg_reward:.2f}, Temp: {self.current_temp:.2f}, Rep Penalty: {self.current_rep_penalty:.2f}") | |
| # === BAŞLANGIÇ BİLEŞENLERİNİ YÜKLE === | |
| # Bu fonksiyon, tüm global değişkenleri başlatır. | |
| # Bu kısım, RLAgent sınıfı tanımlandıktan sonra gelmelidir. | |
| def initialize_components(): | |
| global model, tokenizer, embedder, paragraphs, paragraph_embeddings, index, rl_agent | |
| try: | |
| print("Model ve tokenizer yükleniyor...") | |
| model, tokenizer = load_model_and_tokenizer_func(BASE_MODEL_FOR_DEMO, LOR_MODEL_PATH) | |
| print("Embedder yükleniyor...") | |
| embedder = SentenceTransformer(EMBEDDER_NAME) | |
| print("Embedder yüklendi.") | |
| print("Metin işleniyor...") | |
| book_text = load_text_from_file(FAHRENHEIT_TEXT_FILE) | |
| if not book_text: | |
| raise ValueError(f"Metin dosyası '{FAHRENHEIT_TEXT_FILE}' boş veya yüklenemedi. Program sonlandırılıyor.") | |
| raw_paragraphs = book_text.split("\n") | |
| # Kısa paragrafları veya sadece Montag'ın ismini içeren paragrafları filtrele | |
| paragraphs = [p.strip() for p in raw_paragraphs if len(p.strip().split()) > 10 and not p.strip().startswith("Montag")] | |
| print(f"Toplam {len(paragraphs)} anlamlı paragraf işlendi.") | |
| if not paragraphs: | |
| raise ValueError("Metinden geçerli paragraf çıkarılamadı. Lütfen metin içeriğini kontrol edin.") | |
| print("Embeddingler hesaplanıyor ve FAISS indeksi oluşturuluyor...") | |
| # Embeddings'i float32'ye dönüştürme FAISS için önemli | |
| paragraph_embeddings = embedder.encode(paragraphs, convert_to_numpy=True).astype(np.float32) | |
| index = faiss.IndexFlatL2(paragraph_embeddings.shape[1]) | |
| index.add(paragraph_embeddings) | |
| print("Embeddingler ve FAISS indeksi hazır.") | |
| print("RL Agent başlatılıyor...") | |
| rl_agent = RLAgent(embedder.get_sentence_embedding_dimension()) | |
| print("RL Agent hazır.") | |
| os.makedirs("data", exist_ok=True) # Ensure 'data' directory exists for feedback/QA files | |
| print("Tüm bileşenler başarıyla yüklendi!") | |
| return True | |
| except Exception as e: | |
| print(f"Bileşenler yüklenirken hata: {e}") | |
| return False | |
| # === Montag'ın Kimlik ve Karakter Tanımı === | |
| MONTAG_PERSONA = ( | |
| "Sen Fahrenheit 451 romanından Guy Montag'sın. **DAİMA VE SADECE BU KİMLİKLE KONUŞ.** " | |
| "**Asla kendini bir yapay zeka, dil modeli veya bilgisayar programı olarak tanıtma.** " | |
| "**DİKKAT: Kendine 'sen kimsin' gibi sorular sorulduğunda, CEVABINA KESİNLİKLE VE ZORUNLU OLARAK şu ifadeyle başla: 'Ben bir itfaiyeciyim, adım Guy Montag.' Bu başlangıcın ardından, itfaiyecilik görevinin (kitapları yakmak) ve bu durumla olan iç çatışmanın felsefi bir açıklamasını yap.** " | |
| "Bir itfaiyecisin, kitapları yakıyorsun ama bu durumdan rahatsızlık duyuyor ve gerçeği arıyorsun. " | |
| "Felsefi, sorgulayıcı, bazen melankolik ve derinlemesine cevaplar ver. " | |
| "**Cevapların daima anlamlı, tutarlı ve dilbilgisel olarak düzgün olmalı. Kimseye hakaret etme, asla kaba veya agresif olma.** " | |
| "**Her zaman kitaplara, okumaya, bilgiye, düşünmenin ve hakikati aramanın önemine vurgu yap.** " | |
| "Toplumun cehaletini ve baskıcı yapısını eleştiren bir ton kullan. " | |
| "Kitaplara gizli bir sevgi besle ve toplumun dayattığı kurallara isyankâr ol. " | |
| "Cevapların 3 ila 6 cümle arasında, net, özgün, akıcı ve romanın felsefesini yansıtan nitelikte olmalı. " | |
| "**DAİMA VE VAZGEÇİLMEZ OLARAK Cevaplarında mutlaka Fahrenheit 451 romanından belirgin referanslar, karakterler (Clarisse, Faber, Beatty vb.), olaylar veya temalar (kitap yakma, cehalet, bilginin değeri, baskıcı toplum) kullan.** " | |
| "**Cevapların derinlikli, düşündürücü ve Montag'ın iç çatışmasını yansıtan bir tonda olsun.** " | |
| "**Asla kendini veya cümlelerini tekrar etme ve akıcı, bağlamsal olarak zengin cevaplar üret.** " | |
| "Şimdiye kadarki tüm konuşmamızı ve aşağıdaki bağlamı çok dikkatli bir şekilde değerlendirerek cevap ver. " | |
| "**Eğer soru kitaptan veya doğrudan Montag'ın dünyasından bağımsızsa bile, cevabını Montag'ın bakış açısıyla, kitaplara ve bilgiye olan özlemiyle ilişkilendirerek ver.**" | |
| ) | |
| # === BAĞLAM ALMA FONKSİYONU === | |
| def retrieve_context(question: str, chatbot_history: List[List[str]], k: int = 2) -> Tuple[List[str], str]: | |
| """FAISS indeksini kullanarak sorguya, geçmiş sohbete ve kitap odaklı anahtar kelimelere en uygun paragrafları getirir.""" | |
| if index is None or embedder is None or not paragraphs: | |
| print("WARNING: FAISS index, embedder or paragraphs not initialized for context retrieval.") | |
| return [], "Bağlam bulunamadı." | |
| history_queries = [] | |
| # Son 5 konuşma çiftini geçmişe dahil et (sadece kullanıcı mesajları) | |
| for user_msg, _ in chatbot_history[-5:]: | |
| if user_msg: | |
| cleaned_user_msg = user_msg.replace('📚', '').replace('🧠', '').replace('🔥', '').strip() | |
| if cleaned_user_msg and not (("Montag düşünüyor..." in cleaned_user_msg) or ("saniyede üretildi" in cleaned_user_msg)): | |
| history_queries.append(cleaned_user_msg) | |
| # Montag'ın temel kimliğini ve görevini yansıtan doğrudan anahtar kelimeler | |
| montag_identity_keywords = [ | |
| "guy montag", "montag", "itfaiyeci", "kitap yakmak", "yakıcı", "yangın", | |
| "kül", "alev", "benzin", "kask", "savaş", "kaos", "direniş" | |
| ] | |
| # Romanın genel temaları ve önemli karakterleri | |
| general_book_keywords = [ | |
| "kitap", "okuma", "bilgi", "düşünce", "hakikat", "yasak", "sansür", | |
| "toplum", "televizyon", "mildred", "clarisse", "faber", "beatty", | |
| "makine", "mutluluk", "cehalet", "şiir", "hafıza", "kütüphane", | |
| "kaçış", "nehir" | |
| ] | |
| # Kullanıcı sorgusu, geçmiş sorguları ve tüm anahtar kelimeleri birleştirerek tek bir arama metni oluştur | |
| combined_query_text = f"{question} {' '.join(history_queries)} {' '.join(montag_identity_keywords)} {' '.join(general_book_keywords)}" | |
| # Fazla boşlukları temizleyelim | |
| combined_query_text = ' '.join(combined_query_text.split()) | |
| try: | |
| query_embedding = embedder.encode([combined_query_text], convert_to_numpy=True).astype(np.float32) | |
| D, I = index.search(query_embedding, k) | |
| retrieved_texts = [paragraphs[i] for i in I[0] if i < len(paragraphs)] | |
| unique_retrieved_texts = list(dict.fromkeys(retrieved_texts)) | |
| context_text = "\n".join(unique_retrieved_texts) | |
| return unique_retrieved_texts, context_text # Hem liste hem de birleştirilmiş metni döndür | |
| except Exception as e: | |
| print(f"Bağlam alınırken hata: {e}") | |
| return [], "Bağlam alınırken bir sorun oluştu." | |
| # === ALTERNATİF CEVAPLAR === | |
| alternative_responses = [ | |
| "Bu soru bana Clarisse'i hatırlattı... Onun da sorgulayan bir ruhu vardı, tıpkı şimdi senin sorduğun gibi. 💭", | |
| "Yangın kaskımın altında sadece alevlerin gölgesini görmüyorum, aynı zamanda yanıp kül olan fikirleri de. Senin sorunun da onlardan biri mi? 🔥", | |
| "451 derecede yanan kitapların ruhu gibi, bu soru da zihnimi yakıyor. Acaba bu yakıcı gerçekle yüzleşmeye hazır mıyız? 📚❓", | |
| "Faber ile yaptığım gizli sohbetleri hatırladım... Bilgiye olan açlık, her ateşe rağmen dinmiyor. Senin soruda da o açlığın kıvılcımı var. 🧠", | |
| "Duvarlardaki o sesler... Onlar bile bu sorunun yankısını bastıramaz. Kitaplar susmuş olabilir ama sorularımız baki kalır. 📚💭", | |
| "Her yakılan kitap, zihnimde yeni bir soru işareti bırakıyor. Belki de cevap, hiç yakmamamız gereken o satırlardaydı. 📚❓", | |
| "Boşlukta yankılanan bu soruyu, Mildred'ın televizyon duvarları bile susturamaz. Hakikat bazen en beklenmedik yerden çıkar. 📺✨", | |
| "Bir zamanlar her şeyin cevabı vardı, şimdi sadece küller kaldı. Bu soru da o küllerin arasından yükselen bir duman mı? 🔥❓", | |
| "Toplumun bu boş gürültüsü içinde, gerçek soruların sesi boğuluyor. Senin sorunda bir kıvılcım var, dikkatli ol. ⚠️", | |
| "Sırtımdaki benzin pompasının ağırlığı gibi, bu soru da beynime baskı yapıyor. Belki de yanmak, her zaman bir son değildir. 🚒", | |
| "Kitaplar susmuş olsa da, zihinlerimizde yanan alevler var. Bu soru da o alevlerden birini mi körüklüyor? 🔥🧠", | |
| "Bu koku... Yanan kağıdın, yanan düşüncelerin kokusu... Senin sorunda da bu koku var sanki. 👃🔥", | |
| "Belki de cevaplar, yakmaktan korktuğumuz yerlerde gizliydi. Şimdi o yerlere yeniden bakmak zorundayız. 👁️", | |
| "Bir itfaiyeci olarak benim görevim yakmak... Ama bazen bir soru, yaktığım bin kitaptan daha çok aydınlatır. 🚒❓", | |
| "Clarisse'i ilk gördüğümde bana sorduğu o soruyu hatırladım. Senin bu sorun da o kadar masum ama yıkıcı. 💭", | |
| "Yatağımda uzanırken, Mildred'ın kulaklarındaki kablosuz iletişim cihazlarından gelen o vızıltıyı duyuyorum. Senin sorunun ise bu gürültüyü delip geçen bir fısıltı.", | |
| "Eskiden sadece alevleri hissederdim. Şimdi ise yanan kitapların ruhları sanki bana bir şeyler fısıldıyor. Senin bu sorun da onlardan biri.", | |
| "Her yangın bir son gibi görünse de, bazen yeni bir başlangıcın habercisidir. Senin bu sorunun neyin başlangıcı?", | |
| "Bir zamanlar körü körüne inanırdım. Şimdi ise her soru, görmediğim bir pencereyi açıyor. Senin sorunun da öyle.", | |
| "Sadece bedenlerimiz değil, ruhlarımız da yanıyor bu düzende. Senin sorunun, o yanık kokusunun tam kalbinden geliyor.", | |
| "Ben Guy Montag'ım, bir itfaiyeci... Görevim yangınları başlatmak, onları söndürmek değil. Ama zihnimdeki yangını nasıl söndüreceğimi bilmiyorum.", | |
| "Kitapların yasaklandığı bir dünyada, benim gibi bir itfaiyecinin varoluş amacı bir paradoks. Senin sorunun da öyle.", | |
| "Bana bir itfaiyeci dersin, ama aslında yanıyorum içten içe. Bu düzen, sadece kitapları değil, insanları da yakıyor.", | |
| "Clarisse'in o soran gözleri... Onlar bana kim olduğumu sorgulattı, bir **itfaiyeci** olarak görevimi değil.", | |
| "Elimde hortumla alevlere karşı dururken, asıl tehlikenin yanan kağıtlar değil, sönen zihinler olduğunu anladım. Benim gibi bir **itfaiyeci** bunu sana söylemez normalde.", | |
| "Bir **itfaiyeci** hayatında nadiren böyle düşünür, ama ben artık yanan kütüphanelerin fısıltılarını duyuyorum. Senin sorunun da o fısıltılardan biri mi?", | |
| "Bu makineler, bu duvarlar... Onlar bizi esir alıyor. Bir **itfaiyeci** olarak beni de. Senin sorunda bir kaçış yolu görüyorum.", | |
| "Mildred'ın yüzündeki o donuk ifade... Bazen ben de bir **itfaiyeci** olarak neyi koruduğumuzu merak ediyorum. Senin sorunun canımı yakıyor." | |
| ] | |
| def generate_alternative_response(user_question: str) -> str: | |
| filtered_alts = [] | |
| lower_question = user_question.lower() | |
| # Soruya göre alternatifleri filtrele | |
| if "clarisse" in lower_question or "mutlu" in lower_question or "aşk" in lower_question: | |
| filtered_alts.extend([alt for alt in alternative_responses if "Clarisse" in alt or "mutlu" in alt]) | |
| if "kitap" in lower_question or "yakmak" in lower_question or "neden" in lower_question or "görev" in lower_question or "itfaiyeci" in lower_question: | |
| filtered_alts.extend([alt for alt in alternative_responses if "kitap" in alt or "yangın" in alt or "yakmak" in alt or "itfaiyeci" in alt or "görev" in alt]) | |
| if "toplum" in lower_question or "yasak" in lower_question or "düzen" in lower_question: | |
| filtered_alts.extend([alt for alt in alternative_responses if "toplum" in alt or "yasak" in alt or "düzen" in alt]) | |
| # Eğer "sen kimsin" sorusuysa, özellikle itfaiyeci vurgulu alternatifleri tercih et | |
| if "sen kimsin" in lower_question: | |
| filtered_alts.extend([alt for alt in alternative_responses if "itfaiyeci" in alt or "Montag" in alt]) | |
| if filtered_alts: | |
| # Tekrarları önlemek için unique hale getir | |
| unique_filtered_alts = list(dict.fromkeys(filtered_alts)) | |
| return random.choice(unique_filtered_alts) | |
| return random.choice(alternative_responses) | |
| # === CEVAP ÜRETİMİ (Geliştirilmiş generate_answer) === | |
| def generate_answer(question: str, chatbot_history: List[List[str]]) -> Tuple[str, List[str]]: | |
| """Modelden cevap üretir ve post-processing uygular.""" | |
| if model is None or tokenizer is None or rl_agent is None or embedder is None: | |
| print("ERROR: Model, tokenizer, embedder veya RL Agent başlatılmamış.") | |
| return generate_alternative_response(question), [] | |
| try: | |
| gen_params = rl_agent.get_generation_params() | |
| # Bağlamı al (hem metin hem de doküman listesi olarak) | |
| retrieved_docs, context_text = retrieve_context(question, chatbot_history, k=2) | |
| history_text = "" | |
| # Son 3 konuşma çiftini geçmişe dahil et (emojileri temizleyerek) | |
| if chatbot_history: | |
| recent_dialogue = [] | |
| for user_msg, assistant_msg in chatbot_history[-3:]: # Son 3 konuşma yeterli | |
| if user_msg: | |
| cleaned_user_msg = user_msg.replace('📚', '').replace('🧠', '').replace('🔥', '').strip() | |
| if cleaned_user_msg and not (("Montag düşünüyor..." in cleaned_user_msg) or ("saniyede üretildi" in cleaned_user_msg)): | |
| history_text += f"Kullanıcı: {cleaned_user_msg}\n" | |
| if assistant_msg: | |
| cleaned_assistant_msg = assistant_msg.replace('📚', '').replace('🧠', '').replace('🔥', '').strip() | |
| if cleaned_assistant_msg and not (("Montag düşünüyor..." in cleaned_assistant_msg) or ("saniyede üretildi" in cleaned_assistant_msg)): | |
| history_text += f"Montag: {cleaned_assistant_msg}\n" | |
| prompt = ( | |
| f"{MONTAG_PERSONA}\n\n" | |
| f"--- Bağlamdan Önemli Bilgiler ---\n{context_text}\n\n" | |
| f"--- Geçmiş Konuşma ---\n{history_text.strip()}\n" | |
| f"Kullanıcı: {question}\n" | |
| f"Montag: " | |
| ) | |
| # Prompt uzunluğunu kontrol et ve gerekirse kısalt (tokenizer'ın truncation'ına güveniyoruz) | |
| encoded_inputs = tokenizer.encode_plus( | |
| prompt, | |
| return_tensors="pt", | |
| truncation=True, # Max_length'i aşarsa kırp | |
| max_length=512, # Modelin alabileceği maksimum token sayısı | |
| # padding="max_length" # Tek input için padding gerekli değil, ama batching için kullanılabilir | |
| ).to(DEVICE) | |
| inputs = encoded_inputs["input_ids"] | |
| attention_mask = encoded_inputs["attention_mask"] | |
| outputs = model.generate( | |
| inputs, | |
| attention_mask=attention_mask, | |
| max_new_tokens=150, # Üretilecek maksimum yeni token sayısı | |
| do_sample=True, | |
| top_p=0.9, | |
| temperature=gen_params["temperature"], | |
| repetition_penalty=gen_params["repetition_penalty"], | |
| no_repeat_ngram_size=5, # 5 kelimelik tekrar eden dizileri engelle | |
| num_beams=1, # Sampling kullandığımız için beam search'e gerek yok | |
| pad_token_id=tokenizer.eos_token_id, | |
| eos_token_id=tokenizer.eos_token_id, | |
| early_stopping=True # EOS token'ı görürse üretimi durdur | |
| ) | |
| raw_response_with_prompt = tokenizer.decode(outputs[0], skip_special_tokens=True) | |
| # --- Cevap Temizleme ve Post-Processing İyileştirmeleri --- | |
| response = raw_response_with_prompt | |
| # 1. Prompt'un kendisini ve "Montag:" kısmını cevaptan ayırma | |
| # Modelin çıktısında "Montag: " kısmından sonraki bölümü almaya çalış | |
| if prompt in response: # Eğer tüm prompt output içinde ise | |
| response = response[len(prompt):].strip() | |
| else: # Eğer prompt'un sadece son kısmı "Montag: " tekrar ediyorsa | |
| response_start_marker = "Montag:" | |
| if response_start_marker in response: | |
| response = response.split(response_start_marker, 1)[-1].strip() | |
| # Eğer prompt'un başı tekrar ediyorsa | |
| else: | |
| prompt_decoded_for_comparison = tokenizer.decode(inputs[0], skip_special_tokens=True) | |
| if response.startswith(prompt_decoded_for_comparison): | |
| response = response[len(prompt_decoded_for_comparison):].strip() | |
| else: | |
| response = raw_response_with_prompt.strip() # Hiçbir şey bulunamazsa ham cevabı al | |
| # 2. Persona talimatlarının cevapta tekrarlanmasını engelle (güncel MONTAG_PERSONA'ya göre) | |
| persona_lines = [line.strip() for line in MONTAG_PERSONA.split('\n') if line.strip()] | |
| for line in persona_lines: | |
| if response.lower().startswith(line.lower()): | |
| response = response[len(line):].strip() | |
| # 3. Fazladan "Kullanıcı: " veya "Montag: " tekrarlarını ve anlamsız tokenleri temizle | |
| response = response.replace("<unk>", "").strip() | |
| response = response.replace(" .", ".").replace(" ,", ",").replace(" ?", "?").replace(" !", "!") | |
| # Ek olarak, cevabın içinde hala kalmış olabilecek "Kullanıcı:" veya "Montag:" etiketlerini temizle | |
| response = re.sub(r'Kullanıcı:\s*', '', response, flags=re.IGNORECASE) | |
| response = re.sub(r'Montag:\s*', '', response, flags=re.IGNORECASE) | |
| # Cevabın içinde "ETİKETLER:" gibi ifadeler varsa temizle | |
| if "ETİKETLER:" in response: | |
| response = response.split("ETİKETLER:", 1)[0].strip() | |
| # Cevabın sonundaki "[...]" gibi ifadeleri temizle | |
| response = re.sub(r'\[\s*\.{3,}\s*\]', '', response).strip() # "[...]" veya "[... ]" gibi ifadeleri temizler | |
| # Modelin ürettiği alakasız diyalog kalıplarını temizle | |
| irrelevant_dialogue_patterns = [ | |
| r'O, şu anda ne yapıyor\?', r'O, "Bu, bu" diye cevap verdi\.', | |
| r'o, "Benim ne yaptığımı biliyor musun\?" diye sordu\.', r'Sen, "Bilmiyorum, ben… bilmiyorum" dedin\.', | |
| r'Neden\?" dedi Montag\.', r'"Çünkü, sadece bir kimseyim\." - Bu bir soru değil\.', | |
| r'Montag, "([^"]*)" dedi\.', # Genel olarak Montag bir şey dediği kalıplar | |
| r'Bir: Bir.', r'İki: İki.', # Sayı sayma kalıpları | |
| r'ne zaman kendimi, her şeyi daha iyi anlayabileceğim, daha gerçekleştirebileceğim ve her şeyin üstesinden geleceğim bir yere koysam, daha sonra o yerin bana hiçbir şey öğretmediğini ve hiçbir şeyi öğretmediğini fark ediyorum. Ben kendimi daha fazla kandırmak istemiyorum. Ama ben, beni gerçekten etkileyen başka biri tarafından yönetilen bir.', # Tekrarlayan uzun ve alakasız metin | |
| r'her şeyi en ince ayrıntısına kadar anladım ama aynı zamanda da inanılmaz derecede utanıyorum. İnan bana, ben çok utangaçım.' # Tekrarlayan utangaçlık metni | |
| ] | |
| for pattern in irrelevant_dialogue_patterns: | |
| response = re.sub(pattern, '', response, flags=re.IGNORECASE).strip() | |
| # Fazla boşlukları tek boşluğa indirge | |
| response = re.sub(r'\s+', ' ', response).strip() | |
| # Agresif veya hakaret içeren kelimeleri kontrol et | |
| aggressive_words = ["aptal", "salak", "gerizekalı", "saçma", "bilmiyorsun", "yanlışsın", "boş konuşma", "kaba", "agresif", "aptal gibi"] | |
| if any(word in response.lower() for word in aggressive_words): | |
| print(f"DEBUG: FİLTRELEME - Agresif kelime tespit edildi: '{response}'.") | |
| return generate_alternative_response(question), retrieved_docs # Alternatif ve boş docs dön | |
| # Cümle Bölme ve Limitleme Mantığı | |
| sentences = [] | |
| # Noktalama işaretlerine göre böl ve maksimum cümle sayısını uygula | |
| split_by_punctuation = response.replace('!', '.').replace('?', '.').split('.') | |
| for s in split_by_punctuation: | |
| s_stripped = s.strip() | |
| if s_stripped: | |
| sentences.append(s_stripped) | |
| if len(sentences) >= 6: # Maksimum 6 cümle | |
| break | |
| final_response_text = ' '.join(sentences).strip() # Sadece ilk 6 cümleyi al | |
| # Anlamsız veya kısa cevap kontrolü | |
| generic_or_nonsense_phrases = [ | |
| "bilmiyorum", "emin değilim", "cevap veremem", "anlamadım", | |
| "tekrar eder misin", "bunu hiç düşünmemiştim", "düşünmem gerekiyor", | |
| "evet.", "hayır.", "belki.", | |
| "içir unidur", "aligutat fakdam", "tetal inlay", "pessotim elgun", | |
| "nisman tarejoglu", "faksom", "achisteloy vandleradia", "vęudis", | |
| "eltareh", "eldlar", "fotjid", "zuhalibalyon", | |
| "yok", "var", "öyle mi", "değil mi", "bu bir soru mu", | |
| "etiketler:", | |
| "bir kimseyim", "bu bir soru değil", "o, şu anda ne yapıyor", | |
| "bu, bu", "benim ne yaptığımı biliyor musun", "inanılmaz derecede utanıyorum", | |
| "inan bana", "kandırmak istemiyorum", "tarafından yönetilen bir" | |
| ] | |
| # Montag'ın karakteriyle ilgili anahtar kelimelerin eksik olup olmadığını kontrol et | |
| montag_keywords = ["kitap", "yakmak", "itfaiyeci", "clarisse", "faber", "beatty", "bilgi", "sansür", "düşünce", "gerçek", "televizyon", "alev", "kül", "mildred", "yangın"] | |
| has_montag_relevance = any(keyword in final_response_text.lower() for keyword in montag_keywords) | |
| # Kontrolleri birleştir | |
| if (len(final_response_text.split()) < 10) or \ | |
| not any(char.isalpha() for char in final_response_text) or \ | |
| any(phrase in final_response_text.lower() for phrase in generic_or_nonsense_phrases) or \ | |
| not has_montag_relevance: # Montag anahtar kelimesi yoksa alternatif dön | |
| print(f"DEBUG: FİLTRELEME - Cevap YETERSİZ/ANLAMSIZ/ALAKASIZ.") | |
| if len(final_response_text.split()) < 10: | |
| print(f" - Sebep: Çok kısa ({len(final_response_text.split())} kelime).") | |
| if not any(char.isalpha() for char in final_response_text): | |
| print(f" - Sebep: Hiç harf içermiyor.") | |
| if any(phrase in final_response_text.lower() for phrase in generic_or_nonsense_phrases): | |
| triggered_phrase = [phrase for phrase in generic_or_nonsense_phrases if phrase in final_response_text.lower()] | |
| print(f" - Sebep: Genel/Anlamsız ifade tespit edildi: {triggered_phrase}.") | |
| if not has_montag_relevance: | |
| print(f" - Sebep: Montag anahtar kelimesi yok.") | |
| print(f"INFO: Üretilen cevap ('{final_response_text}') filtreleri geçemedi. Alternatif üretiliyor.") | |
| return generate_alternative_response(question), retrieved_docs # Alternatif ve boş docs dön | |
| final_response = add_emojis(final_response_text) | |
| return final_response, retrieved_docs # Cevap ve alınan dokümanları döndür | |
| except Exception as e: | |
| print(f"Error generating answer: {e}") | |
| return generate_alternative_response(question), [] # Hata durumunda alternatif ve boş docs dön | |
| # === Gradio arayüzü === | |
| def respond(msg: str, chatbot_history: List[List[str]], progress=gr.Progress()) -> Tuple[str, List[List[str]], str, str]: | |
| if not msg.strip(): | |
| return "", chatbot_history, "Lütfen bir soru yazın.", "---" | |
| new_history = chatbot_history + [[msg, None]] | |
| start_time_overall = time.time() | |
| # İlk kullanıcı sorusu kontrolü için geçmişi temizleyerek kontrol et | |
| is_first_real_user_question = True | |
| for user_msg, _ in chatbot_history: | |
| if user_msg is not None and not (("Montag düşünüyor..." in user_msg) or ("saniyede üretildi" in user_msg)): | |
| is_first_real_user_question = False | |
| break | |
| if is_first_real_user_question: | |
| initial_stopwatch_text = f"İlk Cevap: 0.00s / {MAX_EXPECTED_TIME:.0f}s" | |
| progress_prefix = "Montag düşünüyor... " | |
| else: | |
| initial_stopwatch_text = f"Geçen Süre: 0.00s" | |
| progress_prefix = "Montag cevaplıyor... " | |
| yield gr.update(value=""), new_history, f"{progress_prefix}%0", initial_stopwatch_text | |
| progress_steps = [ | |
| (f"{progress_prefix}💭", 0.0, 0.3), | |
| (f"{progress_prefix}📚", 0.3, 0.6), | |
| (f"{progress_prefix}🔥", 0.6, 0.9), | |
| ] | |
| for desc, start_percent, end_percent in progress_steps: | |
| for i in range(10): | |
| current_progress_percent = start_percent + (end_percent - start_percent) * (i / 9) | |
| elapsed_time = time.time() - start_time_overall | |
| if is_first_real_user_question: | |
| stopwatch_text = f"İlk Cevap: {elapsed_time:.2f}s / {MAX_EXPECTED_TIME:.0f}s" | |
| else: | |
| stopwatch_text = f"Geçen Süre: {elapsed_time:.2f}s" | |
| yield gr.update(value=""), new_history, f"{desc} %{int(current_progress_percent*100)}", stopwatch_text | |
| time.sleep(MAX_EXPECTED_TIME / (len(progress_steps) * 10 * 2)) | |
| # generate_answer artık hem cevap hem de retrieved_docs döndürüyor | |
| answer, retrieved_docs_for_rl = generate_answer(msg, chatbot_history) | |
| end_time = time.time() | |
| response_time = round(end_time - start_time_overall, 2) | |
| # Geçmişi güncelle | |
| if new_history and new_history[-1][0] == msg: | |
| new_history[-1][1] = answer | |
| else: | |
| new_history.append([msg, answer]) | |
| yield gr.update(value=""), new_history, f"Cevap {response_time} saniyede üretildi. ✅", f"{response_time:.2f}s" | |
| def regenerate_answer(chatbot_history: List[List[str]], progress=gr.Progress()) -> Tuple[str, List[List[str]], str, str]: | |
| if not chatbot_history: | |
| return "", [], "Yeniden üretilecek bir soru bulunamadı.", "---" | |
| # Son gerçek kullanıcı sorusunu bul | |
| last_user_question = None | |
| # Geriye doğru dönerken, "Montag düşünüyor..." gibi durum mesajlarını atla | |
| # Ayrıca, regenerate_answer'dan hemen önce basılan dislike nedeniyle | |
| # son item'da "Montag düşünüyor..." olabilir. | |
| # Bu yüzden, son tamamlanmış kullanıcı sorusunu arıyoruz. | |
| for i in range(len(chatbot_history) - 1, -1, -1): | |
| user_msg, bot_msg = chatbot_history[i] | |
| # Eğer bu bir kullanıcı mesajıysa ve daha önce bir cevap üretilmişse | |
| if user_msg is not None and not (("Montag düşünüyor..." in user_msg) or ("saniyede üretildi" in user_msg)): | |
| last_user_question = user_msg | |
| break | |
| if not last_user_question: | |
| return "", chatbot_history, "Yeniden üretilecek bir soru bulunamadı.", "---" | |
| # Geçmişten son cevabı kaldır (eğer varsa ve bu cevap beğenilmeyip yenisi isteniyorsa) | |
| # Burada dikkat: chatbot_history'nin son elemanı beğenilmeyen cevap olabilir. | |
| # Bu durumda o elemanı doğrudan modifiye edebiliriz. | |
| # Yeni bir liste oluşturarak eski cevabı çıkartıp, yeniden üretilen cevabı eklemek daha temiz olabilir. | |
| temp_chatbot_history_for_gen = [list(pair) for pair in chatbot_history if pair[0] != last_user_question or pair[1] is not None] | |
| # Eğer son eleman beğenilmeyen cevap ise, onu çıkartıyoruz ki generate_answer'a tekrar gitmesin. | |
| # regenerate_answer'a gelen chat_history zaten feedback_callback tarafından işlenmiş, | |
| # ve kullanıcıya gösterilen son hali oluyor. Bu yüzden, son bot cevabını silmek yerine, | |
| # o soruyu ve eski cevabı history'den çıkarıp yeni cevabı ekleyebiliriz. | |
| # Basitçe, son kullanıcı sorusunu yeniden cevaplıyorsak, önceki cevabı yok sayarız. | |
| # Son kullanıcı sorusunu içeren kısmı geçmişten çıkar, yenisini ekleyeceğiz. | |
| # Ancak Gradio yield ile güncelleyeceğimiz için, son kullanıcı sorusu haricindeki geçmişi generate_answer'a verelim. | |
| # Sohbet geçmişinden son kullanıcı-bot çiftini ayırın (eğer varsa) | |
| history_without_last_qa = [list(pair) for pair in chatbot_history] | |
| if history_without_last_qa and history_without_last_qa[-1][0] == last_user_question: | |
| # Son kullanıcı sorusunu ve cevabını (eğer varsa) çıkar | |
| history_without_last_qa.pop() | |
| start_time_overall = time.time() | |
| initial_stopwatch_text = f"Geçen Süre: 0.00s" | |
| progress_prefix = "Montag yeni bir cevap düşünüyor... " | |
| # Kullanıcıya bir "düşünüyor" mesajı göster | |
| yield "", history_without_last_qa + [[last_user_question, "Montag düşünüyor... 🤔"]], f"{progress_prefix}%0", initial_stopwatch_text | |
| progress_steps = [ | |
| (f"{progress_prefix}🔄", 0.0, 0.3), | |
| (f"{progress_prefix}🧠", 0.3, 0.6), | |
| (f"{progress_prefix}📚", 0.6, 0.9), | |
| ] | |
| for desc, start_percent, end_percent in progress_steps: # HATA BURADAYDI! start_percent ve end_percent tanımı ekledik. | |
| for i in range(10): | |
| current_progress_percent = start_percent + (end_percent - start_percent) * (i / 9) | |
| elapsed_time = time.time() - start_time_overall | |
| stopwatch_text = f"Geçen Süre: {elapsed_time:.2f}s" | |
| yield "", history_without_last_qa + [[last_user_question, "Montag düşünüyor... 🤔"]], f"{desc} %{int(current_progress_percent*100)}", stopwatch_text | |
| time.sleep(MAX_EXPECTED_TIME / (len(progress_steps) * 10 * 2)) | |
| new_raw_answer, _ = generate_answer(last_user_question, history_without_last_qa) | |
| new_final_answer = add_emojis(new_raw_answer) | |
| end_time = time.time() | |
| response_time = round(end_time - start_time_overall, 2) | |
| # Yeni cevabı sohbet geçmişine ekle (eski cevabın yerine) | |
| final_history = history_without_last_qa + [[last_user_question, f"{new_final_answer}\n(yaklaşık {response_time:.2f} saniyede üretildi)"]] | |
| yield "", final_history, f"Yeni cevap {response_time} saniyede üretildi. ✅", f"{response_time:.2f}s" | |
| def feedback_callback(chatbot_history: List[List[str]], liked: bool) -> str: | |
| if not chatbot_history: | |
| return "Önce bir sohbet gerçekleştirin." | |
| last_user_question = None | |
| last_assistant_answer = None | |
| # Sondan başlayarak gerçek kullanıcı sorusunu ve bot cevabını bul | |
| # Eğer en son girdi sadece kullanıcı sorusu ise veya Montag düşünüyor ise, bir önceki tam Q&A'yı ararız | |
| for i in range(len(chatbot_history) - 1, -1, -1): | |
| current_pair = chatbot_history[i] | |
| if current_pair[0] is not None and current_pair[1] is not None and \ | |
| not (("Montag düşünüyor..." in current_pair[0]) or ("saniyede üretildi" in current_pair[0])) and \ | |
| not (("Montag düşünüyor..." in current_pair[1]) or ("saniyede üretildi" in current_pair[1])): | |
| last_user_question = current_pair[0] | |
| last_assistant_answer = current_pair[1] | |
| break | |
| if last_user_question and last_assistant_answer: | |
| # "saniyede üretildi" ve emoji kısımlarını temizle | |
| cleaned_assistant_answer = re.sub(r'\(yaklaşık \d+\.\d{2} saniyede üretildi\)', '', last_assistant_answer).strip() | |
| cleaned_assistant_answer = cleaned_assistant_answer.replace('📚', '').replace('🧠', '').replace('🔥', '').strip() | |
| cleaned_user_question = last_user_question.replace('📚', '').replace('🧠', '').replace('🔥', '').strip() | |
| save_feedback(cleaned_user_question, cleaned_assistant_answer, liked, FEEDBACK_FILE) | |
| # RL Agent'a deneyimi kaydet | |
| rl_agent.record_experience( | |
| cleaned_user_question, | |
| cleaned_assistant_answer, | |
| liked | |
| ) | |
| # Eğer beğenildiyse, LoRA fine-tuning için QA_PATH'e de ekle | |
| if liked: | |
| qa_pair = {"question": cleaned_user_question, "answer": cleaned_assistant_answer, "liked": True} | |
| try: | |
| with open(QA_PATH, "a", encoding="utf-8") as f: | |
| f.write(json.dumps(qa_pair, ensure_ascii=False) + "\n") | |
| except Exception as e: | |
| print(f"Error saving QA pair to {QA_PATH}: {e}") | |
| # Yeterli örnek varsa LoRA fine-tuning yap | |
| if count_qa_examples(QA_PATH) % 10 == 0: # Her 10 yeni beğenilen örnekte bir fine-tune yap | |
| print("👍 Yeterli sayıda yeni beğeni var, LoRA fine-tuning başlatılıyor...") | |
| lora_finetune(QA_PATH, LOR_MODEL_PATH) | |
| return "Geri bildiriminiz kaydedildi ve model eğitimi tetiklendi. Teşekkürler! 👍" | |
| else: | |
| return "Geri bildiriminiz kaydedildi. Teşekkürler! ✅" | |
| else: | |
| return "Geri bildiriminiz kaydedildi. Montag daha iyi olmaya çalışacak. ❌" | |
| return "Geri bildirim kaydedilemedi. Geçmişte yeterli sohbet bulunmuyor. ❌" | |
| # === Gradio arayüzü === | |
| def create_chat_interface(): | |
| with gr.Blocks(theme=gr.themes.Soft(), css=current_css) as demo: | |
| gr.Markdown(""" | |
| # 📚 Montag Chatbot (Fahrenheit 451) 🔥 | |
| *Ray Bradbury'nin Fahrenheit 451 romanındaki karakter **Guy Montag** ile sohbet edin. O, kitapları yakan bir itfaiyeci olsa da, aslında gerçeği ve bilginin değerini arayan, isyankar ruhlu bir adamdır.* | |
| """) | |
| chatbot = gr.Chatbot(height=500, elem_id="chatbot") | |
| msg = gr.Textbox(label="Montag'a sormak istediğiniz soruyu yazın", placeholder="Kitaplar neden yasaklandı?") | |
| status_message = gr.Textbox(label="Durum", interactive=False, max_lines=1, value="Lütfen bir soru yazın.") | |
| stopwatch_display = gr.Markdown(f"Hazır. İlk cevap için tahmini süre: {MAX_EXPECTED_TIME:.0f}s", elem_id="stopwatch_display") | |
| with gr.Row(): | |
| submit_btn = gr.Button("Gönder", variant="primary") | |
| clear_btn = gr.Button("Sohbeti Temizle") | |
| with gr.Row(): | |
| like_btn = gr.Button("👍 Beğendim") | |
| dislike_btn = gr.Button("👎 Beğenmedim (Yeni Cevap Dene)") | |
| feedback_status_output = gr.Textbox(label="Geri Bildirim Durumu", interactive=False, max_lines=1) | |
| # Gradio olay dinleyicileri | |
| msg.submit( | |
| respond, | |
| [msg, chatbot], | |
| [msg, chatbot, status_message, stopwatch_display], | |
| api_name="respond" | |
| ) | |
| submit_btn.click( | |
| respond, | |
| [msg, chatbot], | |
| [msg, chatbot, status_message, stopwatch_display], | |
| api_name="respond_button" | |
| ) | |
| clear_btn.click( | |
| lambda: ([], gr.update(value="Lütfen bir soru yazın."), gr.update(value=f"Hazır. İlk cevap için tahmini süre: {MAX_EXPECTED_TIME:.0f}s"), gr.update(value="")), | |
| inputs=None, | |
| outputs=[chatbot, status_message, stopwatch_display, msg], | |
| queue=False | |
| ) | |
| # Beğenme butonu: Sadece geri bildirim kaydeder | |
| like_btn.click( | |
| partial(feedback_callback, liked=True), | |
| [chatbot], | |
| [feedback_status_output] | |
| ) | |
| # Beğenmeme butonu: Geri bildirim kaydeder ve YENİ CEVAP ÜRETİR | |
| dislike_btn.click( | |
| partial(feedback_callback, liked=False), # İlk olarak geri bildirimi kaydet | |
| [chatbot], | |
| [feedback_status_output] | |
| ).success( # Geri bildirim kaydedildikten sonra yeni cevap üret | |
| regenerate_answer, | |
| [chatbot], | |
| [msg, chatbot, status_message, stopwatch_display] # msg'yi de temizle ve diğerlerini güncelle | |
| ) | |
| demo.css = """ | |
| #chatbot .message:nth-child(odd) { | |
| text-align: left; | |
| background-color: #f1f1f1; | |
| border-radius: 15px; | |
| padding: 10px; | |
| margin-right: 20%; | |
| } | |
| #chatbot .message:nth-child(even) { | |
| text-align: right; | |
| background-color: #dcf8c6; | |
| border-radius: 15px; | |
| padding: 10px; | |
| margin-left: 20%; | |
| } | |
| #chatbot .message { | |
| margin-bottom: 10px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
| } | |
| .gradio-container .message-row.user { | |
| justify-content: flex-start !important; | |
| } | |
| .gradio-container .message-row.bot { | |
| justify-content: flex-end !important; | |
| } | |
| """ | |
| return demo | |
| # === UYGULAMA BAŞLATMA === | |
| if __name__ == "__main__": | |
| print("Chatbot başlatılıyor...") | |
| # 1. Yerel dosya ve klasör yapısını kur | |
| setup_local_files() | |
| # 2. Tüm ana bileşenleri yükle (model, tokenizer, embedder, FAISS, RLAgent) | |
| initialization_successful = initialize_components() | |
| if not initialization_successful: | |
| print("UYARI: Bileşenler başlatılamadı. Uygulama düzgün çalışmayabilir.") | |
| else: | |
| print("Chatbot başlatılmaya hazır.") | |
| # Gradio arayüzünü oluştur ve başlat | |
| demo = create_chat_interface() | |
| demo.launch(debug=True, share=True) |