precison9 commited on
Commit
2933ea7
·
verified ·
1 Parent(s): 90113b3

Update app/rag/rag_processor.py

Browse files
Files changed (1) hide show
  1. app/rag/rag_processor.py +56 -14
app/rag/rag_processor.py CHANGED
@@ -1,5 +1,7 @@
1
  import os
2
  import re
 
 
3
  import logging
4
  from typing import List, Tuple, Optional
5
  import faiss
@@ -12,6 +14,7 @@ import io
12
  import openpyxl
13
  import pandas as pd
14
  from duckduckgo_search import DDGS
 
15
  from fastapi import UploadFile
16
 
17
  logger = logging.getLogger(__name__)
@@ -19,6 +22,7 @@ logger = logging.getLogger(__name__)
19
  _EMBED_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
20
  _embedder: Optional[SentenceTransformer] = None
21
 
 
22
  def _get_embedder() -> SentenceTransformer:
23
  global _embedder
24
  if _embedder is None:
@@ -26,7 +30,7 @@ def _get_embedder() -> SentenceTransformer:
26
  _embedder = SentenceTransformer(_EMBED_MODEL_NAME)
27
  return _embedder
28
 
29
- # Enhanced File Extraction
30
  def extract_text(file: UploadFile) -> str:
31
  ext = os.path.splitext(file.filename)[1].lower()
32
  content = file.file.read()
@@ -63,25 +67,27 @@ def extract_text(file: UploadFile) -> str:
63
  except Exception as e:
64
  logger.error(f"CSV extract failed: {e}")
65
  return ""
66
- elif ext in [".jpg", ".jpeg", ".png", ".gif"]: # OCR for images
67
  try:
68
  img = Image.open(file_bytes)
69
  return pytesseract.image_to_string(img)
70
  except Exception as e:
71
  logger.error(f"Image OCR failed: {e}")
72
  return ""
73
- else: # Fallback text
74
  try:
75
  return content.decode("utf-8", errors="ignore")
76
  except Exception as e:
77
  logger.error(f"Text extract failed: {e}")
78
  return ""
79
 
 
80
  def clean_text(text: str) -> str:
81
  t = re.sub(r"[ \t]+", " ", text)
82
  t = re.sub(r"\n{3,}", "\n\n", t)
83
  return t.strip()
84
 
 
85
  def chunk_text(text: str, max_tokens: int = 400, overlap: int = 50) -> List[str]:
86
  text = clean_text(text)
87
  if not text:
@@ -98,12 +104,14 @@ def chunk_text(text: str, max_tokens: int = 400, overlap: int = 50) -> List[str]
98
  start = max(0, end - overlap)
99
  return chunks
100
 
 
101
  class RagIndex:
102
  def __init__(self, index: faiss.IndexFlatIP, dim: int, chunks: List[str]):
103
  self.index = index
104
  self.dim = dim
105
  self.chunks = chunks
106
 
 
107
  def build_faiss_index(chunks: List[str]) -> RagIndex:
108
  emb = _get_embedder()
109
  vectors = emb.encode(chunks, convert_to_numpy=True, normalize_embeddings=True)
@@ -112,6 +120,7 @@ def build_faiss_index(chunks: List[str]) -> RagIndex:
112
  index.add(vectors)
113
  return RagIndex(index=index, dim=dim, chunks=chunks)
114
 
 
115
  def search(index: RagIndex, query: str, top_k: int = 6) -> List[Tuple[str, float]]:
116
  emb = _get_embedder()
117
  q = emb.encode([query], convert_to_numpy=True, normalize_embeddings=True)
@@ -123,13 +132,14 @@ def search(index: RagIndex, query: str, top_k: int = 6) -> List[Tuple[str, float
123
  hits.append((index.chunks[idx], float(score)))
124
  return hits
125
 
 
126
  def build_context_from_files(files: List[UploadFile], prompt: str, top_k: int = 6) -> str:
127
  all_text = []
128
  for file in files:
129
  txt = extract_text(file)
130
  if txt:
131
  all_text.append(txt)
132
- file.file.seek(0) # Reset
133
  big_text = "\n\n".join(all_text)
134
  chunks = chunk_text(big_text, max_tokens=450, overlap=80)
135
  if not chunks:
@@ -139,13 +149,45 @@ def build_context_from_files(files: List[UploadFile], prompt: str, top_k: int =
139
  context_sections = [f"[DOC#{i} score={score:.3f}]\n{chunk}" for i, (chunk, score) in enumerate(hits, 1)]
140
  return "\n\n".join(context_sections)
141
 
142
- # Web search tool
143
- def web_search(query: str) -> str:
144
- try:
145
- with DDGS() as ddgs:
146
- results = [r for r in ddgs.text(query, max_results=5)]
147
- sections = [f"[WEB#{i}] Title: {r['title']}\nSnippet: {r['body']}\nURL: {r['href']}" for i, r in enumerate(results, 1)]
148
- return "\n\n".join(sections) if sections else "No results found."
149
- except Exception as e:
150
- logger.error(f"Web search failed: {e}")
151
- return "Web search error."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import re
3
+ import time
4
+ import asyncio
5
  import logging
6
  from typing import List, Tuple, Optional
7
  import faiss
 
14
  import openpyxl
15
  import pandas as pd
16
  from duckduckgo_search import DDGS
17
+ from duckduckgo_search.exceptions import RatelimitException, DuckDuckGoSearchException
18
  from fastapi import UploadFile
19
 
20
  logger = logging.getLogger(__name__)
 
22
  _EMBED_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
23
  _embedder: Optional[SentenceTransformer] = None
24
 
25
+
26
  def _get_embedder() -> SentenceTransformer:
27
  global _embedder
28
  if _embedder is None:
 
30
  _embedder = SentenceTransformer(_EMBED_MODEL_NAME)
31
  return _embedder
32
 
33
+
34
  def extract_text(file: UploadFile) -> str:
35
  ext = os.path.splitext(file.filename)[1].lower()
36
  content = file.file.read()
 
67
  except Exception as e:
68
  logger.error(f"CSV extract failed: {e}")
69
  return ""
70
+ elif ext in [".jpg", ".jpeg", ".png", ".gif"]:
71
  try:
72
  img = Image.open(file_bytes)
73
  return pytesseract.image_to_string(img)
74
  except Exception as e:
75
  logger.error(f"Image OCR failed: {e}")
76
  return ""
77
+ else:
78
  try:
79
  return content.decode("utf-8", errors="ignore")
80
  except Exception as e:
81
  logger.error(f"Text extract failed: {e}")
82
  return ""
83
 
84
+
85
  def clean_text(text: str) -> str:
86
  t = re.sub(r"[ \t]+", " ", text)
87
  t = re.sub(r"\n{3,}", "\n\n", t)
88
  return t.strip()
89
 
90
+
91
  def chunk_text(text: str, max_tokens: int = 400, overlap: int = 50) -> List[str]:
92
  text = clean_text(text)
93
  if not text:
 
104
  start = max(0, end - overlap)
105
  return chunks
106
 
107
+
108
  class RagIndex:
109
  def __init__(self, index: faiss.IndexFlatIP, dim: int, chunks: List[str]):
110
  self.index = index
111
  self.dim = dim
112
  self.chunks = chunks
113
 
114
+
115
  def build_faiss_index(chunks: List[str]) -> RagIndex:
116
  emb = _get_embedder()
117
  vectors = emb.encode(chunks, convert_to_numpy=True, normalize_embeddings=True)
 
120
  index.add(vectors)
121
  return RagIndex(index=index, dim=dim, chunks=chunks)
122
 
123
+
124
  def search(index: RagIndex, query: str, top_k: int = 6) -> List[Tuple[str, float]]:
125
  emb = _get_embedder()
126
  q = emb.encode([query], convert_to_numpy=True, normalize_embeddings=True)
 
132
  hits.append((index.chunks[idx], float(score)))
133
  return hits
134
 
135
+
136
  def build_context_from_files(files: List[UploadFile], prompt: str, top_k: int = 6) -> str:
137
  all_text = []
138
  for file in files:
139
  txt = extract_text(file)
140
  if txt:
141
  all_text.append(txt)
142
+ file.file.seek(0)
143
  big_text = "\n\n".join(all_text)
144
  chunks = chunk_text(big_text, max_tokens=450, overlap=80)
145
  if not chunks:
 
149
  context_sections = [f"[DOC#{i} score={score:.3f}]\n{chunk}" for i, (chunk, score) in enumerate(hits, 1)]
150
  return "\n\n".join(context_sections)
151
 
152
+
153
+ def _web_search_sync(query: str, max_results: int = 5) -> str:
154
+ """
155
+ DDGS is an unofficial DuckDuckGo scraper with no SLA — it rate-limits
156
+ datacenter/cloud IPs (like Hugging Face Spaces) intermittently and
157
+ without warning. Retrying with backoff smooths over most transient
158
+ hits; it can't fix a sustained block, which is a hosting-environment
159
+ limitation, not a code bug.
160
+ """
161
+ last_error = None
162
+ for attempt in range(3):
163
+ try:
164
+ with DDGS() as ddgs:
165
+ results = list(ddgs.text(query, max_results=max_results, backend="lite"))
166
+ if not results:
167
+ return "No results found."
168
+ sections = [
169
+ f"[WEB#{i}] Title: {r['title']}\nSnippet: {r['body']}\nURL: {r['href']}"
170
+ for i, r in enumerate(results, 1)
171
+ ]
172
+ return "\n\n".join(sections)
173
+ except RatelimitException as e:
174
+ last_error = e
175
+ logger.warning(f"Web search rate-limited, attempt {attempt + 1}/3: {e}")
176
+ time.sleep(1.5 * (attempt + 1))
177
+ except DuckDuckGoSearchException as e:
178
+ last_error = e
179
+ logger.error(f"Web search failed: {e}")
180
+ break
181
+ except Exception as e:
182
+ last_error = e
183
+ logger.error(f"Web search unexpected error: {e}")
184
+ break
185
+
186
+ logger.error(f"Web search exhausted retries: {last_error}")
187
+ return f"[Web search unavailable: {type(last_error).__name__ if last_error else 'unknown error'}]"
188
+
189
+
190
+ async def web_search(query: str) -> str:
191
+ # DDGS is synchronous/blocking — offload it so it can't freeze
192
+ # the event loop (and therefore every other user's request) while waiting.
193
+ return await asyncio.to_thread(_web_search_sync, query)