HamidOmarov commited on
Commit
edc48fd
·
1 Parent(s): 7df5ef1

Fix HF Spaces cache permissions and set model cache

Browse files
Files changed (2) hide show
  1. Dockerfile +28 -0
  2. app/rag_system.py +131 -39
Dockerfile CHANGED
@@ -2,6 +2,34 @@ FROM python:3.11-slim
2
  WORKDIR /app
3
  COPY requirements.txt .
4
  RUN pip install --no-cache-dir -r requirements.txt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  COPY . .
6
  RUN mkdir -p /app/data/uploads /app/data/index
7
  ENV PORT=7860
 
2
  WORKDIR /app
3
  COPY requirements.txt .
4
  RUN pip install --no-cache-dir -r requirements.txt
5
+ FROM python:3.11-slim
6
+
7
+ ENV PYTHONDONTWRITEBYTECODE=1 \
8
+ PYTHONUNBUFFERED=1 \
9
+ HOME=/app \
10
+ HF_HOME=/app/.cache \
11
+ TRANSFORMERS_CACHE=/app/.cache \
12
+ HUGGINGFACE_HUB_CACHE=/app/.cache \
13
+ SENTENCE_TRANSFORMERS_HOME=/app/.cache
14
+
15
+ WORKDIR /app
16
+
17
+ RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ COPY requirements.txt .
21
+ RUN pip install --no-cache-dir -r requirements.txt
22
+
23
+ COPY . .
24
+
25
+ # Cache və data qovluqları
26
+ RUN mkdir -p /app/.cache /app/data/uploads /app/data/index && chmod -R 777 /app/.cache /app/data
27
+
28
+ ENV PORT=7860
29
+ EXPOSE 7860
30
+
31
+ CMD ["uvicorn", "app.api:app", "--host", "0.0.0.0", "--port", "7860"]
32
+
33
  COPY . .
34
  RUN mkdir -p /app/data/uploads /app/data/index
35
  ENV PORT=7860
app/rag_system.py CHANGED
@@ -1,87 +1,167 @@
1
  # app/rag_system.py
 
 
 
2
  from pathlib import Path
3
  from typing import List, Tuple
4
- import os
5
  import faiss
6
  import numpy as np
7
- from sentence_transformers import SentenceTransformer
8
  from pypdf import PdfReader
 
9
 
10
- DATA_DIR = Path(__file__).resolve().parent.parent / "data"
 
 
 
 
 
11
  UPLOAD_DIR = DATA_DIR / "uploads"
12
  INDEX_DIR = DATA_DIR / "index"
13
- INDEX_DIR.mkdir(parents=True, exist_ok=True)
14
- UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
15
 
 
 
 
 
 
 
16
  MODEL_NAME = os.getenv("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
17
 
 
18
  class SimpleRAG:
19
- def __init__(self, index_path: Path = INDEX_DIR / "faiss.index", meta_path: Path = INDEX_DIR / "meta.npy"):
20
- self.model = SentenceTransformer(MODEL_NAME)
21
- self.index_path = index_path
22
- self.meta_path = meta_path
23
- self.index = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  self.chunks: List[str] = []
 
25
  self._load()
26
 
27
- def _load(self):
28
- # meta (chunks) yüklə
 
 
 
29
  if self.meta_path.exists():
30
- self.chunks = np.load(self.meta_path, allow_pickle=True).tolist()
31
- # faiss index yüklə
 
 
 
 
 
32
  if self.index_path.exists():
33
- # dim modelin çıxış ölçüsü
34
- dim = self.model.get_sentence_embedding_dimension()
35
- self.index = faiss.read_index(str(self.index_path))
36
- # təhlükəsizlik: ölçüsü uyğun olmalıdır
37
- if self.index.d != dim:
38
- # uyğunsuzluqda sıfırdan başla
39
- self.index = faiss.IndexFlatIP(dim)
 
 
 
40
  else:
41
- dim = self.model.get_sentence_embedding_dimension()
42
- self.index = faiss.IndexFlatIP(dim)
43
 
44
- def _persist(self):
45
  faiss.write_index(self.index, str(self.index_path))
46
  np.save(self.meta_path, np.array(self.chunks, dtype=object))
47
 
 
 
 
48
  @staticmethod
49
- def _pdf_to_texts(pdf_path: Path) -> List[str]:
50
  reader = PdfReader(str(pdf_path))
51
- full_text = []
52
  for page in reader.pages:
53
  t = page.extract_text() or ""
54
  if t.strip():
55
- full_text.append(t)
56
- # sadə parçalama: ~500 hərf
57
- chunks = []
58
- for txt in full_text:
59
- step = 800
60
  for i in range(0, len(txt), step):
61
- chunks.append(txt[i:i+step])
 
 
62
  return chunks
63
 
 
 
 
64
  def add_pdf(self, pdf_path: Path) -> int:
65
  texts = self._pdf_to_texts(pdf_path)
66
  if not texts:
67
  return 0
68
- emb = self.model.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
69
- self.index.add(emb)
 
 
 
 
 
70
  self.chunks.extend(texts)
 
71
  self._persist()
72
  return len(texts)
73
 
 
 
 
74
  def search(self, query: str, k: int = 5) -> List[Tuple[str, float]]:
 
 
 
75
  q = self.model.encode([query], convert_to_numpy=True, normalize_embeddings=True)
76
- D, I = self.index.search(q, k)
77
- results = []
78
- if I.size > 0 and len(self.chunks) > 0:
 
 
79
  for idx, score in zip(I[0], D[0]):
80
  if 0 <= idx < len(self.chunks):
81
  results.append((self.chunks[idx], float(score)))
82
  return results
83
 
84
- # sadə cavab formalaşdırıcı (LLM yoxdursa, kontekst + heuristika)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  def synthesize_answer(question: str, contexts: List[str]) -> str:
86
  if not contexts:
87
  return "Kontekst tapılmadı. Sualı daha dəqiq verin və ya PDF yükləyin."
@@ -89,5 +169,17 @@ def synthesize_answer(question: str, contexts: List[str]) -> str:
89
  return (
90
  f"Sual: {question}\n\n"
91
  f"Cavab (kontekstdən çıxarış):\n{joined}\n\n"
92
- f"(Qeyd: Demo rejimi — LLM inteqrasiyası üçün / later: OpenAI/Groq və s.)"
93
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # app/rag_system.py
2
+ from __future__ import annotations
3
+
4
+ import os
5
  from pathlib import Path
6
  from typing import List, Tuple
7
+
8
  import faiss
9
  import numpy as np
 
10
  from pypdf import PdfReader
11
+ from sentence_transformers import SentenceTransformer
12
 
13
+
14
+ # -----------------------------
15
+ # Konfiqurasiya & qovluqlar
16
+ # -----------------------------
17
+ ROOT_DIR = Path(__file__).resolve().parent.parent
18
+ DATA_DIR = ROOT_DIR / "data"
19
  UPLOAD_DIR = DATA_DIR / "uploads"
20
  INDEX_DIR = DATA_DIR / "index"
 
 
21
 
22
+ # HF Spaces-də yazma icazəsi olan cache qovluğu
23
+ CACHE_DIR = Path(os.getenv("HF_HOME", str(ROOT_DIR / ".cache")))
24
+ for d in (DATA_DIR, UPLOAD_DIR, INDEX_DIR, CACHE_DIR):
25
+ d.mkdir(parents=True, exist_ok=True)
26
+
27
+ # Model adı ENV-dən dəyişdirilə bilər
28
  MODEL_NAME = os.getenv("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
29
 
30
+
31
  class SimpleRAG:
32
+ """
33
+ Sadə RAG nüvəsi:
34
+ - PDF -> mətn parçalama
35
+ - Sentence-Transformers embeddings
36
+ - FAISS Index (IP / cosine bərabərləşdirilmiş)
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ index_path: Path = INDEX_DIR / "faiss.index",
42
+ meta_path: Path = INDEX_DIR / "meta.npy",
43
+ model_name: str = MODEL_NAME,
44
+ cache_dir: Path = CACHE_DIR,
45
+ ):
46
+ self.index_path = Path(index_path)
47
+ self.meta_path = Path(meta_path)
48
+ self.model_name = model_name
49
+ self.cache_dir = Path(cache_dir)
50
+
51
+ # Model
52
+ # cache_folder Spaces-də /.cache icazə xətasının qarşısını alır
53
+ self.model = SentenceTransformer(self.model_name, cache_folder=str(self.cache_dir))
54
+ self.embed_dim = self.model.get_sentence_embedding_dimension()
55
+
56
+ # FAISS index və meta (chunks)
57
+ self.index: faiss.Index = None # type: ignore
58
  self.chunks: List[str] = []
59
+
60
  self._load()
61
 
62
+ # -----------------------------
63
+ # Yükləmə / Saxlama
64
+ # -----------------------------
65
+ def _load(self) -> None:
66
+ # Chunks (meta) yüklə
67
  if self.meta_path.exists():
68
+ try:
69
+ self.chunks = np.load(self.meta_path, allow_pickle=True).tolist()
70
+ except Exception:
71
+ # zədələnmişsə sıfırla
72
+ self.chunks = []
73
+
74
+ # FAISS index yüklə
75
  if self.index_path.exists():
76
+ try:
77
+ idx = faiss.read_index(str(self.index_path))
78
+ # ölçü uyğunluğunu yoxla
79
+ if hasattr(idx, "d") and idx.d == self.embed_dim:
80
+ self.index = idx
81
+ else:
82
+ # uyğunsuzluqda sıfırdan qur
83
+ self.index = faiss.IndexFlatIP(self.embed_dim)
84
+ except Exception:
85
+ self.index = faiss.IndexFlatIP(self.embed_dim)
86
  else:
87
+ self.index = faiss.IndexFlatIP(self.embed_dim)
 
88
 
89
+ def _persist(self) -> None:
90
  faiss.write_index(self.index, str(self.index_path))
91
  np.save(self.meta_path, np.array(self.chunks, dtype=object))
92
 
93
+ # -----------------------------
94
+ # PDF -> Mətn -> Parçalama
95
+ # -----------------------------
96
  @staticmethod
97
+ def _pdf_to_texts(pdf_path: Path, step: int = 800) -> List[str]:
98
  reader = PdfReader(str(pdf_path))
99
+ pages_text: List[str] = []
100
  for page in reader.pages:
101
  t = page.extract_text() or ""
102
  if t.strip():
103
+ pages_text.append(t)
104
+
105
+ chunks: List[str] = []
106
+ for txt in pages_text:
 
107
  for i in range(0, len(txt), step):
108
+ chunk = txt[i : i + step].strip()
109
+ if chunk:
110
+ chunks.append(chunk)
111
  return chunks
112
 
113
+ # -----------------------------
114
+ # Index-ə əlavə
115
+ # -----------------------------
116
  def add_pdf(self, pdf_path: Path) -> int:
117
  texts = self._pdf_to_texts(pdf_path)
118
  if not texts:
119
  return 0
120
+
121
+ emb = self.model.encode(
122
+ texts, convert_to_numpy=True, normalize_embeddings=True, show_progress_bar=False
123
+ )
124
+ # FAISS-ə əlavə
125
+ self.index.add(emb.astype(np.float32))
126
+ # Meta-ya əlavə
127
  self.chunks.extend(texts)
128
+ # Diskə yaz
129
  self._persist()
130
  return len(texts)
131
 
132
+ # -----------------------------
133
+ # Axtarış
134
+ # -----------------------------
135
  def search(self, query: str, k: int = 5) -> List[Tuple[str, float]]:
136
+ if self.index is None:
137
+ return []
138
+
139
  q = self.model.encode([query], convert_to_numpy=True, normalize_embeddings=True)
140
+ # FAISS float32 gözləyir
141
+ D, I = self.index.search(q.astype(np.float32), min(k, max(1, self.index.ntotal)))
142
+ results: List[Tuple[str, float]] = []
143
+
144
+ if I.size > 0 and self.chunks:
145
  for idx, score in zip(I[0], D[0]):
146
  if 0 <= idx < len(self.chunks):
147
  results.append((self.chunks[idx], float(score)))
148
  return results
149
 
150
+ # -----------------------------
151
+ # Cavab Sinttezi (LLM-siz demo)
152
+ # -----------------------------
153
+ def synthesize_answer(self, question: str, contexts: List[str]) -> str:
154
+ if not contexts:
155
+ return "Kontekst tapılmadı. Sualı daha dəqiq verin və ya PDF yükləyin."
156
+ joined = "\n---\n".join(contexts[:3])
157
+ return (
158
+ f"Sual: {question}\n\n"
159
+ f"Cavab (kontekstdən çıxarış):\n{joined}\n\n"
160
+ f"(Qeyd: Demo rejimi — LLM inteqrasiyası üçün sonradan OpenAI/Groq və s. əlavə edilə bilər.)"
161
+ )
162
+
163
+
164
+ # Köhnə import yolunu dəstəkləmək üçün eyni funksiyanı modul səviyyəsində də saxlayırıq
165
  def synthesize_answer(question: str, contexts: List[str]) -> str:
166
  if not contexts:
167
  return "Kontekst tapılmadı. Sualı daha dəqiq verin və ya PDF yükləyin."
 
169
  return (
170
  f"Sual: {question}\n\n"
171
  f"Cavab (kontekstdən çıxarış):\n{joined}\n\n"
172
+ f"(Qeyd: Demo rejimi — LLM inteqrasiyası üçün sonradan OpenAI/Groq və s. əlavə edilə bilər.)"
173
  )
174
+
175
+
176
+ # Faylı import edən tərəfin rahatlığı üçün bu qovluqları export edirik
177
+ __all__ = [
178
+ "SimpleRAG",
179
+ "synthesize_answer",
180
+ "DATA_DIR",
181
+ "UPLOAD_DIR",
182
+ "INDEX_DIR",
183
+ "CACHE_DIR",
184
+ "MODEL_NAME",
185
+ ]