AaronTekle commited on
Commit
2d7100b
·
verified ·
1 Parent(s): 133a510

Update rag_engine.py

Browse files
Files changed (1) hide show
  1. rag_engine.py +122 -54
rag_engine.py CHANGED
@@ -1,10 +1,26 @@
1
  from __future__ import annotations
 
2
  import html
3
- import spaces
4
- import re
5
  import os
 
6
  import threading
7
  from dataclasses import dataclass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  import faiss
9
  import numpy as np
10
  import pandas as pd
@@ -83,7 +99,7 @@ class KnowledgeBase:
83
  self.dimension
84
  )
85
 
86
- # Prevent accidental mixing of embeddings with different vector dimensions
87
  if embeddings.shape[1] != self.dimension:
88
  raise ValueError(
89
  "Embedding dimension changed during this session"
@@ -210,18 +226,63 @@ _LOCAL_MODEL_LOCK = threading.Lock()
210
 
211
  def get_embedding_device() -> str:
212
  """
213
- embedding device
 
 
 
 
 
 
 
214
  """
215
 
216
- if os.getenv("SPACES_ZERO_GPU"):
217
- return "cpu"
218
 
219
  return "cuda" if torch.cuda.is_available() else "cpu"
220
 
221
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  def get_embedder() -> SentenceTransformer:
223
  """
224
- Load the embedding model once and reuse it
 
 
 
 
 
 
225
  """
226
 
227
  global _EMBEDDER
@@ -230,36 +291,22 @@ def get_embedder() -> SentenceTransformer:
230
  return _EMBEDDER
231
 
232
  with _EMBEDDER_LOCK:
233
-
234
  if _EMBEDDER is None:
235
-
236
- device = get_embedding_device()
237
-
238
- print(
239
- f"[embedding] Loading "
240
- f"{EMBEDDING_MODEL_ID} "
241
- f"on {device}",
242
- flush=True,
243
- )
244
-
245
- _EMBEDDER = SentenceTransformer(
246
- EMBEDDING_MODEL_ID,
247
- device=device,
248
- )
249
-
250
- print(
251
- "[embedding] Model ready.",
252
- flush=True,
253
- )
254
 
255
  return _EMBEDDER
256
 
257
 
258
  def _encode(
259
  texts: list[str],
 
 
260
  ) -> np.ndarray:
261
  """
262
- Encode text passages into normalized embeddings
 
 
 
263
  """
264
 
265
  if not texts:
@@ -271,40 +318,46 @@ def _encode(
271
 
272
  vectors = model.encode(
273
  texts,
274
- batch_size=8,
275
  normalize_embeddings=True,
276
  convert_to_numpy=True,
277
  show_progress_bar=False,
278
  )
279
 
280
- # FAISS expects float32 arrays
281
  vectors = vectors.astype("float32")
282
 
 
283
  return np.ascontiguousarray(vectors)
284
 
285
 
 
286
  def embed_passages(
287
  texts: list[str],
288
  ) -> np.ndarray:
289
  """
290
- Embed document passages
 
 
 
291
  """
292
 
293
- return _encode(texts)
 
 
 
294
 
295
 
 
296
  def embed_query(
297
  query: str,
298
  ) -> np.ndarray:
299
  """
300
- Embed a search query
301
 
302
- Qwen embedding models can expose a query-specific prompt
303
- If unavailable, a generic retrieval instruction is used
304
  """
305
 
306
- model = get_embedder()
307
-
308
  clean = query.strip()
309
 
310
  if not clean:
@@ -312,6 +365,8 @@ def embed_query(
312
  "Query cannot be empty"
313
  )
314
 
 
 
315
  prompts = getattr(
316
  model,
317
  "prompts",
@@ -319,7 +374,6 @@ def embed_query(
319
  )
320
 
321
  if "query" in prompts:
322
-
323
  vectors = model.encode(
324
  [clean],
325
  prompt_name="query",
@@ -337,14 +391,14 @@ def embed_query(
337
  vectors
338
  )
339
 
340
- # compatibility path for models
341
  instructed = (
342
  "Represent this sentence for searching "
343
  f"relevant passages: {clean}"
344
  )
345
 
346
  return _encode(
347
- [instructed]
 
348
  )
349
 
350
 
@@ -582,7 +636,7 @@ def build_context(
582
  + chunk.text.strip()
583
  )
584
 
585
- # Stop adding context once the configured character budget has been reached
586
  if (
587
  used_chars + len(block) > max_chars
588
  and blocks
@@ -607,6 +661,12 @@ def _get_local_model():
607
  (CUDA is used automatically when available)
608
  """
609
 
 
 
 
 
 
 
610
  global _LOCAL_MODEL
611
  global _LOCAL_TOKENIZER
612
 
@@ -851,17 +911,18 @@ def _generate_local(
851
  return answer.strip()
852
 
853
 
854
- # Backend selection
855
 
856
  def choose_backend() -> str:
857
  """
858
- Choose between hosted Hugging Face inference
859
- and local generation
 
 
 
860
 
861
- LLM_BACKEND values:
862
- hf_api
863
- local
864
- auto
865
  """
866
 
867
  configured = (
@@ -870,22 +931,29 @@ def choose_backend() -> str:
870
  .lower()
871
  )
872
 
 
 
 
 
 
 
 
 
 
 
873
  if configured in {
874
  "hf_api",
875
  "local",
876
  }:
877
  return configured
878
 
879
- # Auto behavior:
880
- # use HF API when a token exists,
881
- # otherwise use the local model
882
  if HF_TOKEN:
883
  return "hf_api"
884
 
885
  return "local"
886
 
887
 
888
- # Citation validation
889
 
890
  def _extract_citation_ids(
891
  answer: str,
@@ -902,7 +970,7 @@ def _extract_citation_ids(
902
  )
903
 
904
 
905
- # Main RAG answer function
906
 
907
  def answer_question(
908
  question: str,
@@ -1206,4 +1274,4 @@ def render_sources(
1206
  "<div class='sources-grid'>"
1207
  + "".join(cards)
1208
  + "</div>"
1209
- )
 
1
  from __future__ import annotations
2
+
3
  import html
 
 
4
  import os
5
+ import re
6
  import threading
7
  from dataclasses import dataclass
8
+
9
+ IS_HF_SPACE = bool(os.getenv("SPACE_ID"))
10
+
11
+ if IS_HF_SPACE:
12
+ import spaces
13
+ else:
14
+
15
+ class _SpacesShim:
16
+ @staticmethod
17
+ def GPU(duration: int = 60, **_kwargs):
18
+ def decorator(fn):
19
+ return fn
20
+ return decorator
21
+
22
+ spaces = _SpacesShim()
23
+
24
  import faiss
25
  import numpy as np
26
  import pandas as pd
 
99
  self.dimension
100
  )
101
 
102
+ # prevent accidental mixing of embeddings with different vector dimensions
103
  if embeddings.shape[1] != self.dimension:
104
  raise ValueError(
105
  "Embedding dimension changed during this session"
 
226
 
227
  def get_embedding_device() -> str:
228
  """
229
+ Return the device used by the embedding model.
230
+
231
+ On a Hugging Face ZeroGPU Space the model is registered on CUDA at
232
+ module scope. ZeroGPU intercepts that placement and attaches a real
233
+ GPU only while a @spaces.GPU function is running.
234
+
235
+ During local development, use the local NVIDIA GPU when available
236
+ and otherwise fall back to CPU.
237
  """
238
 
239
+ if IS_HF_SPACE:
240
+ return "cuda"
241
 
242
  return "cuda" if torch.cuda.is_available() else "cpu"
243
 
244
 
245
+ def _load_embedder() -> SentenceTransformer:
246
+ """
247
+ Construct the embedding model.
248
+
249
+ ZeroGPU requires CUDA model placement at module scope rather than
250
+ lazy-loading the model inside a GPU-decorated request.
251
+ """
252
+
253
+ device = get_embedding_device()
254
+
255
+ print(
256
+ f"[embedding] Loading {EMBEDDING_MODEL_ID} on {device}",
257
+ flush=True,
258
+ )
259
+
260
+ model = SentenceTransformer(
261
+ EMBEDDING_MODEL_ID,
262
+ device=device,
263
+ )
264
+
265
+ print(
266
+ "[embedding] Model ready.",
267
+ flush=True,
268
+ )
269
+
270
+ return model
271
+
272
+
273
+ if IS_HF_SPACE:
274
+ _EMBEDDER = _load_embedder()
275
+
276
+
277
  def get_embedder() -> SentenceTransformer:
278
  """
279
+ Return the shared embedding model.
280
+
281
+ Hugging Face Space:
282
+ The model was loaded at module scope for ZeroGPU.
283
+
284
+ Local machine:
285
+ Load once on first use and reuse it afterward.
286
  """
287
 
288
  global _EMBEDDER
 
291
  return _EMBEDDER
292
 
293
  with _EMBEDDER_LOCK:
 
294
  if _EMBEDDER is None:
295
+ _EMBEDDER = _load_embedder()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
  return _EMBEDDER
298
 
299
 
300
  def _encode(
301
  texts: list[str],
302
+ *,
303
+ batch_size: int = 16,
304
  ) -> np.ndarray:
305
  """
306
+ Encode text into normalized float32 vectors for FAISS.
307
+
308
+ This helper performs the actual model computation. On ZeroGPU it
309
+ must only be reached from a @spaces.GPU-decorated function.
310
  """
311
 
312
  if not texts:
 
318
 
319
  vectors = model.encode(
320
  texts,
321
+ batch_size=int(batch_size),
322
  normalize_embeddings=True,
323
  convert_to_numpy=True,
324
  show_progress_bar=False,
325
  )
326
 
 
327
  vectors = vectors.astype("float32")
328
 
329
+ # return ordinary CPU/NumPy data across the ZeroGPU process boundary.
330
  return np.ascontiguousarray(vectors)
331
 
332
 
333
+ @spaces.GPU(duration=120)
334
  def embed_passages(
335
  texts: list[str],
336
  ) -> np.ndarray:
337
  """
338
+ Embed document/FAR passages.
339
+
340
+ Only the embedding computation runs on ZeroGPU. The FAISS index and
341
+ session knowledge base remain in the main application process.
342
  """
343
 
344
+ return _encode(
345
+ texts,
346
+ batch_size=16,
347
+ )
348
 
349
 
350
+ @spaces.GPU(duration=30)
351
  def embed_query(
352
  query: str,
353
  ) -> np.ndarray:
354
  """
355
+ Embed a retrieval query on ZeroGPU.
356
 
357
+ Qwen embedding models may expose a query-specific prompt. If that
358
+ prompt is unavailable, use a generic retrieval instruction.
359
  """
360
 
 
 
361
  clean = query.strip()
362
 
363
  if not clean:
 
365
  "Query cannot be empty"
366
  )
367
 
368
+ model = get_embedder()
369
+
370
  prompts = getattr(
371
  model,
372
  "prompts",
 
374
  )
375
 
376
  if "query" in prompts:
 
377
  vectors = model.encode(
378
  [clean],
379
  prompt_name="query",
 
391
  vectors
392
  )
393
 
 
394
  instructed = (
395
  "Represent this sentence for searching "
396
  f"relevant passages: {clean}"
397
  )
398
 
399
  return _encode(
400
+ [instructed],
401
+ batch_size=1,
402
  )
403
 
404
 
 
636
  + chunk.text.strip()
637
  )
638
 
639
+ # stop adding context once the configured character budget has been reached
640
  if (
641
  used_chars + len(block) > max_chars
642
  and blocks
 
661
  (CUDA is used automatically when available)
662
  """
663
 
664
+ if IS_HF_SPACE:
665
+ raise RuntimeError(
666
+ "Local LLM fallback is disabled on this ZeroGPU Space. "
667
+ "Use LLM_BACKEND=hf_api with HF_TOKEN configured as a Space Secret."
668
+ )
669
+
670
  global _LOCAL_MODEL
671
  global _LOCAL_TOKENIZER
672
 
 
911
  return answer.strip()
912
 
913
 
914
+ # backend selection
915
 
916
  def choose_backend() -> str:
917
  """
918
+ Choose the generation backend.
919
+
920
+ Hugging Face ZeroGPU deployment:
921
+ Use hosted Hugging Face inference for generation. ZeroGPU is
922
+ reserved for the embedding model.
923
 
924
+ Local development:
925
+ Respect LLM_BACKEND = hf_api, local, or auto.
 
 
926
  """
927
 
928
  configured = (
 
931
  .lower()
932
  )
933
 
934
+ if IS_HF_SPACE:
935
+ if not HF_TOKEN:
936
+ raise RuntimeError(
937
+ "HF_TOKEN is required on the Hugging Face Space. "
938
+ "Configure it as a Space Secret and use "
939
+ "LLM_BACKEND=hf_api."
940
+ )
941
+
942
+ return "hf_api"
943
+
944
  if configured in {
945
  "hf_api",
946
  "local",
947
  }:
948
  return configured
949
 
 
 
 
950
  if HF_TOKEN:
951
  return "hf_api"
952
 
953
  return "local"
954
 
955
 
956
+ # citation validation
957
 
958
  def _extract_citation_ids(
959
  answer: str,
 
970
  )
971
 
972
 
973
+ # main RAG answer function
974
 
975
  def answer_question(
976
  question: str,
 
1274
  "<div class='sources-grid'>"
1275
  + "".join(cards)
1276
  + "</div>"
1277
+ )