""" embedder.py — LawAgent Mursit Embedder API """ import argparse import json import os import sys import time import uuid import logging from pathlib import Path # services/ klasörü BACKEND/ kökünde — hangi dizinden çalıştırılırsa çalıştırılsın bulsun _BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if _BACKEND_DIR not in sys.path: sys.path.insert(0, _BACKEND_DIR) # .env dosyasını yükle (QDRANT_URL, GROQ_API_KEY vs. için) from dotenv import load_dotenv for _env_path in [ Path(_BACKEND_DIR) / ".env", Path(_BACKEND_DIR).parent / ".env", ]: if _env_path.exists(): load_dotenv(dotenv_path=_env_path) print(f"[Embedder] .env yüklendi: {_env_path}") break import torch from sentence_transformers import SentenceTransformer from qdrant_client import QdrantClient from qdrant_client.http import models as qmodels from services.qdrant_client import get_qdrant_client # Logging Yapılandırması logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s — %(message)s", datefmt="%H:%M:%S", ) log = logging.getLogger("LawAgent.Embedder") # tqdm Entegrasyonu (Yüklü değilse fallback fonksiyonu çalışır) try: from tqdm import tqdm except ImportError: def tqdm(iterable, desc=None, total=None, **kwargs): log.info(f"{desc or 'İşlem yapılıyor'}...") return iterable # Yollar ve Sabitler BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") MODEL_NAME = "newmindai/Mursit-Base-TR-Retrieval" COLLECTION_NAME = "lawagent_mursit" # Eğer zenginleştirilmiş corpus varsa öncelikli olarak onu kullan ENRICHED_CORPUS = os.path.join(DATA_DIR, "chunk_corpus_enriched.json") if os.path.exists(ENRICHED_CORPUS): CHUNK_CORPUS = ENRICHED_CORPUS else: CHUNK_CORPUS = os.path.join(DATA_DIR, "chunk_corpus.json") QUANTIZE_PATH = os.path.join(DATA_DIR, "mursit_int8.pt") DISTANCE_METRIC = qmodels.Distance.COSINE # ═══════════════════════════════════════════════════════════════════════════════ # AĞ TOLERANSI (QDRANT RETRY YARDIMCISI) # ═══════════════════════════════════════════════════════════════════════════════ def qdrant_retry(func, *args, retries: int = 5, delay: float = 1.0, **kwargs): """Qdrant işlemleri sırasında oluşabilecek ağ kesintilerine karşı üstel geri çekilmeli retry mekanizması.""" for i in range(retries): try: return func(*args, **kwargs) except Exception as e: if i == retries - 1: raise e wait_time = delay * (2 ** i) log.warning(f"Qdrant sorgu hatası (Deneme {i+1}/{retries}): {e}. {wait_time}s bekleniyor...") time.sleep(wait_time) # ═══════════════════════════════════════════════════════════════════════════════ # MURSIT EMBEDDER SINIFI # ═══════════════════════════════════════════════════════════════════════════════ class MursitEmbedder: """ Mursit-Base-TR-Retrieval için embedding sınıfı. GPU ivmelendirme (CUDA/MPS) ve CPU kuantizasyon yönetimi içerir. Parametreler: quantize : int8 dynamic quantization (sadece CPU) device : 'cpu' | 'cuda' | 'mps' | None (otomatik) model_name_or_path: HuggingFace model adı veya yerel klasör yolu. None ise MODEL_NAME sabiti kullanılır. Fine-tuned checkpoint'ler için kullanın. """ def __init__( self, quantize: bool = False, device: str = None, model_name_or_path: str = None, ): self.quantize = quantize _model_id = model_name_or_path if model_name_or_path else MODEL_NAME # Cihaz Otomatik Algılama if quantize: self.device = "cpu" if device and device != "cpu": log.warning("Kuantizasyon sadece CPU üzerinde desteklenir. Cihaz 'cpu' olarak zorlandı.") else: if device: self.device = device else: if torch.cuda.is_available(): self.device = "cuda" elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): self.device = "mps" else: self.device = "cpu" log.info( f"[Mursit] Model yükleniyor ({self.device} - {'int8' if quantize else 'float32'})..." f" model={_model_id}" ) t0 = time.time() try: self.st = SentenceTransformer(_model_id, device=self.device, local_files_only=True) except Exception as e: log.warning(f"Çevrimdışı model yükleme başarısız ({e}), çevrimiçi deneniyor...") self.st = SentenceTransformer(_model_id, device=self.device) self.vector_size = self.st.get_embedding_dimension() if quantize: self._load_or_quantize() log.info(f"[Mursit] Hazır — {time.time()-t0:.1f}s | dim={self.vector_size}") def _load_or_quantize(self) -> None: transformer_module = self.st._first_module().auto_model quantized = torch.quantization.quantize_dynamic( transformer_module, {torch.nn.Linear}, dtype=torch.qint8 ) if os.path.exists(QUANTIZE_PATH): log.info("Kaydedilmiş int8 ağırlıklar yükleniyor...") try: state = torch.load(QUANTIZE_PATH, map_location="cpu") quantized.load_state_dict(state) except Exception as e: log.warning(f"Kaydedilmiş int8 yüklenemedi, sıfırdan kuantize ediliyor: {e}") else: log.info("Model dinamik olarak kuantize ediliyor (ilk kez)...") os.makedirs(os.path.dirname(QUANTIZE_PATH) or ".", exist_ok=True) torch.save(quantized.state_dict(), QUANTIZE_PATH) log.info(f"int8 ağırlıklar kaydedildi -> {QUANTIZE_PATH}") self.st._first_module().auto_model = quantized def encode(self, texts: list[str], batch_size: int = 32, normalize: bool = True) -> list: """Liste halindeki metinleri vektöre çevirir (Python list of list[float]).""" return self.st.encode( texts, normalize_embeddings=normalize, show_progress_bar=False, batch_size=batch_size, ).tolist() def encode_single(self, text: str, normalize: bool = True) -> list[float]: """Arama sorgusunu Mürşit modeline uygun prefix ile vektöre çevirir.""" prefix = "query: " full_text = prefix + text.strip() return self.st.encode( full_text, normalize_embeddings=normalize, show_progress_bar=False, convert_to_numpy=True ).tolist() def kaydet(self, yol: str = QUANTIZE_PATH) -> None: if not self.quantize: log.warning("float32 model kaydedilmiyor. Lütfen --quantize ile çalıştırın.") return os.makedirs(os.path.dirname(yol) or ".", exist_ok=True) torch.save(self.st._first_module().auto_model.state_dict(), yol) mb = os.path.getsize(yol) / 1024 / 1024 log.info(f"int8 kaydedildi -> {yol} ({mb:.1f} MB)") # ═══════════════════════════════════════════════════════════════════════════════ # QDRANT YARDIMCI FONKSİYONLARI # ═══════════════════════════════════════════════════════════════════════════════ def _chunk_id_to_uint64(cid: str) -> int: return uuid.uuid5(uuid.NAMESPACE_DNS, str(cid)).int >> 64 def _get_existing_ids(client: QdrantClient, corpus: list) -> set: ids = [_chunk_id_to_uint64(c["chunk_id"]) for c in corpus] existing = set() for i in range(0, len(ids), 1000): try: points = qdrant_retry( client.retrieve, collection_name=COLLECTION_NAME, ids=ids[i : i + 1000], with_payload=False, with_vectors=False, ) existing.update(p.id for p in points) except Exception as e: log.warning(f"Qdrant'tan mevcut ID'ler çekilirken hata oluştu: {e}") return existing def _ensure_collection(client: QdrantClient, vector_size: int, reset: bool) -> None: collections_res = qdrant_retry(client.get_collections) existing = [c.name for c in collections_res.collections] if reset and COLLECTION_NAME in existing: qdrant_retry(client.delete_collection, COLLECTION_NAME) log.info(f"Collection silindi: {COLLECTION_NAME}") existing.remove(COLLECTION_NAME) if COLLECTION_NAME not in existing: qdrant_retry( client.create_collection, COLLECTION_NAME, vectors_config=qmodels.VectorParams( size=vector_size, distance=DISTANCE_METRIC ), ) log.info(f"Collection oluşturuldu: {COLLECTION_NAME}") else: count = qdrant_retry(client.count, COLLECTION_NAME).count log.info(f"Collection mevcut: {COLLECTION_NAME} ({count} kayıt)") # ═══════════════════════════════════════════════════════════════════════════════ # VERİ TABANI OLUŞTURMA (EMBED CORPUS) # ═══════════════════════════════════════════════════════════════════════════════ def embed_corpus( reset: bool = False, test_mode: bool = False, quantize: bool = False, batch_size: int = 32, device: str = None, ) -> None: if not os.path.exists(CHUNK_CORPUS): log.error(f"{CHUNK_CORPUS} bulunamadı. Önce legal_chunker.py çalıştırılmalı.") return with open(CHUNK_CORPUS, "r", encoding="utf-8") as f: corpus = json.load(f) if test_mode: corpus = corpus[:20] log.info("Test modu aktif: Sadece ilk 20 chunk işlenecek.") embedder = MursitEmbedder(quantize=quantize, device=device) client = get_qdrant_client() _ensure_collection(client, embedder.vector_size, reset) existing = set() if reset else _get_existing_ids(client, corpus) yeni = [c for c in corpus if _chunk_id_to_uint64(c["chunk_id"]) not in existing] log.info(f"Veri Durumu: Toplam={len(corpus)} | Mevcut={len(existing)} | Eklenecek={len(yeni)}") if not yeni: log.info("Tüm veriler güncel. Eklenecek yeni chunk yok.") return t0 = time.time() eklenen = 0 toplam = len(yeni) for i in tqdm(range(0, toplam, batch_size), desc="Vektörleştirme ve Yükleme", total=(toplam + batch_size - 1) // batch_size): batch = yeni[i : i + batch_size] enriched_texts = [] for c in batch: source = c.get("source", "") text = c.get("text", "") if source == "yargitay": enriched_texts.append(f"Yargıtay Kararı {c.get('decision_id', '')}: {text}") else: enriched_texts.append(f"{c.get('law', '')} Madde {c.get('article_no', '')}: {text}") # Vektörleri hesapla vecs = embedder.encode(enriched_texts, batch_size=batch_size) points = [ qmodels.PointStruct( id=_chunk_id_to_uint64(c["chunk_id"]), vector=vecs[idx], payload={ "chunk_id": c.get("chunk_id", ""), "text": c.get("text", ""), "law": c.get("law", ""), "article_no": c.get("article_no", ""), "source": c.get("source", ""), "decision_id": c.get("decision_id", ""), "token_len": c.get("token_len", 0), "atiflar": c.get("atiflar", []) }, ) for idx, c in enumerate(batch) ] # Qdrant'a yükle (Retry korumalı) qdrant_retry(client.upsert, collection_name=COLLECTION_NAME, points=points) eklenen += len(batch) # CUDA bellek temizliği if torch.cuda.is_available(): torch.cuda.empty_cache() log.info(f"Tamamlandı. {eklenen} chunk başarıyla yüklendi. (Süre: {time.time()-t0:.1f}s)") # ═══════════════════════════════════════════════════════════════════════════════ # CLI GİRİŞİ # ═══════════════════════════════════════════════════════════════════════════════ if __name__ == "__main__": parser = argparse.ArgumentParser(description="LawAgent Mursit Embedder v11 (GPU & Retry & tqdm)") parser.add_argument("--reset", action="store_true", help="Collection'ı sıfırla ve baştan başla") parser.add_argument("--test", action="store_true", help="Sadece ilk 20 chunk ile test et") parser.add_argument("--quantize", action="store_true", help="int8 dinamik kuantizasyon") parser.add_argument("--kaydet", action="store_true", help="Kuantize modeli kaydet") parser.add_argument("--batch-size", type=int, default=32, help="Encoding batch boyutu (varsayılan: 32)") parser.add_argument("--device", type=str, default=None, help="Cihaz zorla (cpu, cuda, mps vb.)") args = parser.parse_args() if args.kaydet: embedder = MursitEmbedder(quantize=args.quantize, device=args.device) embedder.kaydet() else: embed_corpus( reset=args.reset, test_mode=args.test, quantize=args.quantize, batch_size=args.batch_size, device=args.device, )