File size: 6,257 Bytes
64fd9b7
edc48fd
 
 
64fd9b7
 
edc48fd
64fd9b7
 
 
edc48fd
64fd9b7
edc48fd
 
 
 
 
 
64fd9b7
 
 
edc48fd
 
 
 
 
 
64fd9b7
 
edc48fd
64fd9b7
edc48fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64fd9b7
edc48fd
64fd9b7
 
edc48fd
 
 
 
 
64fd9b7
edc48fd
 
 
 
 
 
 
64fd9b7
edc48fd
 
 
 
 
 
 
 
 
 
64fd9b7
edc48fd
64fd9b7
edc48fd
64fd9b7
 
 
edc48fd
 
 
64fd9b7
edc48fd
64fd9b7
edc48fd
64fd9b7
 
 
edc48fd
 
 
 
64fd9b7
edc48fd
 
 
64fd9b7
 
edc48fd
 
 
64fd9b7
 
 
 
edc48fd
 
 
 
 
 
 
64fd9b7
edc48fd
64fd9b7
 
 
edc48fd
 
 
64fd9b7
edc48fd
 
 
64fd9b7
edc48fd
 
 
 
 
64fd9b7
 
 
 
 
edc48fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64fd9b7
 
 
 
 
 
 
edc48fd
64fd9b7
edc48fd
 
 
 
 
 
 
 
 
 
 
 
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
# app/rag_system.py
from __future__ import annotations

import os
from pathlib import Path
from typing import List, Tuple

import faiss
import numpy as np
from pypdf import PdfReader
from sentence_transformers import SentenceTransformer


# -----------------------------
# Konfiqurasiya & qovluqlar
# -----------------------------
ROOT_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = ROOT_DIR / "data"
UPLOAD_DIR = DATA_DIR / "uploads"
INDEX_DIR = DATA_DIR / "index"

# HF Spaces-də yazma icazəsi olan cache qovluğu
CACHE_DIR = Path(os.getenv("HF_HOME", str(ROOT_DIR / ".cache")))
for d in (DATA_DIR, UPLOAD_DIR, INDEX_DIR, CACHE_DIR):
    d.mkdir(parents=True, exist_ok=True)

# Model adı ENV-dən dəyişdirilə bilər
MODEL_NAME = os.getenv("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")


class SimpleRAG:
    """
    Sadə RAG nüvəsi:
    - PDF -> mətn parçalama
    - Sentence-Transformers embeddings
    - FAISS Index (IP / cosine bərabərləşdirilmiş)
    """

    def __init__(
        self,
        index_path: Path = INDEX_DIR / "faiss.index",
        meta_path: Path = INDEX_DIR / "meta.npy",
        model_name: str = MODEL_NAME,
        cache_dir: Path = CACHE_DIR,
    ):
        self.index_path = Path(index_path)
        self.meta_path = Path(meta_path)
        self.model_name = model_name
        self.cache_dir = Path(cache_dir)

        # Model
        # cache_folder Spaces-də /.cache icazə xətasının qarşısını alır
        self.model = SentenceTransformer(self.model_name, cache_folder=str(self.cache_dir))
        self.embed_dim = self.model.get_sentence_embedding_dimension()

        # FAISS index və meta (chunks)
        self.index: faiss.Index = None  # type: ignore
        self.chunks: List[str] = []

        self._load()

    # -----------------------------
    # Yükləmə / Saxlama
    # -----------------------------
    def _load(self) -> None:
        # Chunks (meta) yüklə
        if self.meta_path.exists():
            try:
                self.chunks = np.load(self.meta_path, allow_pickle=True).tolist()
            except Exception:
                # zədələnmişsə sıfırla
                self.chunks = []

        # FAISS index yüklə
        if self.index_path.exists():
            try:
                idx = faiss.read_index(str(self.index_path))
                # ölçü uyğunluğunu yoxla
                if hasattr(idx, "d") and idx.d == self.embed_dim:
                    self.index = idx
                else:
                    # uyğunsuzluqda sıfırdan qur
                    self.index = faiss.IndexFlatIP(self.embed_dim)
            except Exception:
                self.index = faiss.IndexFlatIP(self.embed_dim)
        else:
            self.index = faiss.IndexFlatIP(self.embed_dim)

    def _persist(self) -> None:
        faiss.write_index(self.index, str(self.index_path))
        np.save(self.meta_path, np.array(self.chunks, dtype=object))

    # -----------------------------
    # PDF -> Mətn -> Parçalama
    # -----------------------------
    @staticmethod
    def _pdf_to_texts(pdf_path: Path, step: int = 800) -> List[str]:
        reader = PdfReader(str(pdf_path))
        pages_text: List[str] = []
        for page in reader.pages:
            t = page.extract_text() or ""
            if t.strip():
                pages_text.append(t)

        chunks: List[str] = []
        for txt in pages_text:
            for i in range(0, len(txt), step):
                chunk = txt[i : i + step].strip()
                if chunk:
                    chunks.append(chunk)
        return chunks

    # -----------------------------
    # Index-ə əlavə
    # -----------------------------
    def add_pdf(self, pdf_path: Path) -> int:
        texts = self._pdf_to_texts(pdf_path)
        if not texts:
            return 0

        emb = self.model.encode(
            texts, convert_to_numpy=True, normalize_embeddings=True, show_progress_bar=False
        )
        # FAISS-ə əlavə
        self.index.add(emb.astype(np.float32))
        # Meta-ya əlavə
        self.chunks.extend(texts)
        # Diskə yaz
        self._persist()
        return len(texts)

    # -----------------------------
    # Axtarış
    # -----------------------------
    def search(self, query: str, k: int = 5) -> List[Tuple[str, float]]:
        if self.index is None:
            return []

        q = self.model.encode([query], convert_to_numpy=True, normalize_embeddings=True)
        # FAISS float32 gözləyir
        D, I = self.index.search(q.astype(np.float32), min(k, max(1, self.index.ntotal)))
        results: List[Tuple[str, float]] = []

        if I.size > 0 and self.chunks:
            for idx, score in zip(I[0], D[0]):
                if 0 <= idx < len(self.chunks):
                    results.append((self.chunks[idx], float(score)))
        return results

    # -----------------------------
    # Cavab Sinttezi (LLM-siz demo)
    # -----------------------------
    def synthesize_answer(self, question: str, contexts: List[str]) -> str:
        if not contexts:
            return "Kontekst tapılmadı. Sualı daha dəqiq verin və ya PDF yükləyin."
        joined = "\n---\n".join(contexts[:3])
        return (
            f"Sual: {question}\n\n"
            f"Cavab (kontekstdən çıxarış):\n{joined}\n\n"
            f"(Qeyd: Demo rejimi — LLM inteqrasiyası üçün sonradan OpenAI/Groq və s. əlavə edilə bilər.)"
        )


# Köhnə import yolunu dəstəkləmək üçün eyni funksiyanı modul səviyyəsində də saxlayırıq
def synthesize_answer(question: str, contexts: List[str]) -> str:
    if not contexts:
        return "Kontekst tapılmadı. Sualı daha dəqiq verin və ya PDF yükləyin."
    joined = "\n---\n".join(contexts[:3])
    return (
        f"Sual: {question}\n\n"
        f"Cavab (kontekstdən çıxarış):\n{joined}\n\n"
        f"(Qeyd: Demo rejimi — LLM inteqrasiyası üçün sonradan OpenAI/Groq və s. əlavə edilə bilər.)"
    )


# Faylı import edən tərəfin rahatlığı üçün bu qovluqları export edirik
__all__ = [
    "SimpleRAG",
    "synthesize_answer",
    "DATA_DIR",
    "UPLOAD_DIR",
    "INDEX_DIR",
    "CACHE_DIR",
    "MODEL_NAME",
]