RoshaanT1 commited on
Commit
e845e3f
Β·
verified Β·
1 Parent(s): 5eb5123

Update flask_api.py

Browse files
Files changed (1) hide show
  1. flask_api.py +70 -36
flask_api.py CHANGED
@@ -21,6 +21,11 @@ app = Flask(__name__)
21
  UPLOAD_FOLDER = '/tmp/uploads'
22
  ALLOWED_EXTENSIONS = {'pdf', 'txt', 'csv'}
23
 
 
 
 
 
 
24
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
25
 
26
  # Global variables
@@ -29,9 +34,9 @@ current_pdf = None
29
 
30
  # ── Progress tracking ────────────────────────────────────────────────────────
31
  processing_progress = {
32
- "stage": "idle", # human-readable stage name
33
- "percent": 0, # 0-100
34
- "message": "", # optional detail line
35
  }
36
 
37
  def set_progress(stage: str, percent: int, message: str = ""):
@@ -47,11 +52,11 @@ def allowed_file(filename):
47
 
48
 
49
  def process_pdf(pdf_path):
50
- """Process the uploaded PDF and create the RAG index"""
51
  global query_engine, current_pdf
52
 
53
  set_progress("Setting up models", 5)
54
- print(f"πŸ“‚ Processing PDF: {pdf_path}")
55
  print("⏳ This may take 2-5 minutes...")
56
 
57
  llm = Groq(model="openai/gpt-oss-120b", temperature=0.0)
@@ -65,20 +70,16 @@ def process_pdf(pdf_path):
65
  Settings.node_parser = SentenceSplitter(chunk_size=1024, chunk_overlap=150)
66
 
67
  set_progress("Creating vector store", 25)
68
- print("πŸ”§ Creating vector store...")
69
  client = QdrantClient(":memory:")
70
  vector_store = QdrantVectorStore(client=client, collection_name="active_document")
71
  storage_context = StorageContext.from_defaults(vector_store=vector_store)
72
 
73
  set_progress("Loading document pages", 40)
74
- print("πŸ“„ Loading documents...")
75
  documents = SimpleDirectoryReader(input_files=[pdf_path]).load_data()
76
 
77
  set_progress("Generating embeddings", 55, f"{len(documents)} chunks to embed")
78
  print("πŸ”„ Creating embeddings (this is the slow part)...")
79
 
80
- # Smoothly interpolate progress from 55β†’88% while embeddings run,
81
- # so the bar doesn't freeze on 55% for 2+ minutes.
82
  import threading, time as _time
83
 
84
  stop_interpolation = threading.Event()
@@ -88,9 +89,8 @@ def process_pdf(pdf_path):
88
  while not stop_interpolation.is_set():
89
  _time.sleep(1)
90
  elapsed = _time.monotonic() - start
91
- # Assume embeddings take ~180 s; clamp interpolated value to 88%
92
  frac = min(elapsed / 180, 1.0)
93
- pct = int(55 + frac * 33) # 55 β†’ 88
94
  set_progress("Generating embeddings", pct, f"{len(documents)} chunks to embed")
95
 
96
  interp_thread = threading.Thread(target=_interpolate, daemon=True)
@@ -102,7 +102,7 @@ def process_pdf(pdf_path):
102
  show_progress=True
103
  )
104
 
105
- stop_interpolation.set() # embeddings done β€” stop the interpolation thread
106
 
107
  set_progress("Building query engine", 90)
108
 
@@ -132,11 +132,32 @@ def process_pdf(pdf_path):
132
  return True
133
 
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  # ── Routes ───────────────────────────────────────────────────────────────────
136
 
137
  @app.route('/progress', methods=['GET'])
138
  def get_progress():
139
- """Return current processing progress"""
140
  return jsonify(processing_progress), 200
141
 
142
 
@@ -151,7 +172,7 @@ def upload_pdf():
151
  return jsonify({'success': False, 'message': 'No file selected'}), 400
152
 
153
  if not allowed_file(file.filename):
154
- return jsonify({'success': False, 'message': 'Only PDF files are allowed'}), 400
155
 
156
  try:
157
  filename = secure_filename(file.filename)
@@ -163,14 +184,14 @@ def upload_pdf():
163
 
164
  return jsonify({
165
  'success': True,
166
- 'message': 'PDF uploaded and processed successfully',
167
  'filename': filename
168
  }), 200
169
 
170
  except Exception as e:
171
  set_progress("Error", 0, str(e))
172
- print(f"❌ Error processing PDF: {str(e)}")
173
- return jsonify({'success': False, 'message': f'Error processing PDF: {str(e)}'}), 500
174
 
175
 
176
  @app.route('/chat', methods=['POST'])
@@ -178,7 +199,7 @@ def chat():
178
  global query_engine, current_pdf
179
 
180
  if query_engine is None:
181
- return jsonify({'success': False, 'message': 'No PDF uploaded yet.'}), 400
182
 
183
  data = request.get_json()
184
  if not data or 'question' not in data:
@@ -189,22 +210,8 @@ def chat():
189
  return jsonify({'success': False, 'message': 'Question cannot be empty'}), 400
190
 
191
  try:
192
- # Detect conversational/casual questions and short-circuit before
193
- # hitting the query engine β€” these should never return sources
194
- CONVERSATIONAL_TRIGGERS = (
195
- "how are you", "how r u", "hey", "hi", "hello", "good morning",
196
- "good afternoon", "good evening", "what's up", "whats up",
197
- "who are you", "what are you", "thanks", "thank you", "bye",
198
- "goodbye", "ok", "okay", "cool", "nice", "great", "awesome",
199
- )
200
- q_lower = question.lower().strip()
201
- is_conversational = (
202
- len(question.split()) <= 6 and # short questions only
203
- any(trigger in q_lower for trigger in CONVERSATIONAL_TRIGGERS)
204
- )
205
-
206
- if is_conversational:
207
- # Use the LLM directly without the query engine (no document context)
208
  from llama_index.core.llms import ChatMessage
209
  chat_response = Settings.llm.chat([
210
  ChatMessage(role="user", content=question)
@@ -216,13 +223,34 @@ def chat():
216
  'document': current_pdf
217
  }), 200
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  response = query_engine.query(question)
220
 
221
  answer = response.response
222
  if len(answer) > 2000:
223
  answer = answer[:2000] + "..."
224
 
225
- # Only return sources with a real score (not None)
226
  sources = []
227
  for node in response.source_nodes:
228
  score = float(node.score) if node.score is not None else 0.0
@@ -235,7 +263,13 @@ def chat():
235
  'text_snippet': node.text[:100] + '...' if len(node.text) > 100 else node.text
236
  })
237
 
238
- return jsonify({'success': True, 'answer': answer, 'sources': sources, 'document': current_pdf}), 200
 
 
 
 
 
 
239
 
240
  except Exception as e:
241
  return jsonify({'success': False, 'message': f'Error: {str(e)}'}), 500
 
21
  UPLOAD_FOLDER = '/tmp/uploads'
22
  ALLOWED_EXTENSIONS = {'pdf', 'txt', 'csv'}
23
 
24
+ # Minimum cosine similarity score for a retrieved chunk to be considered
25
+ # relevant. Scores are in [0, 1]. 0.60 means the question must share at
26
+ # least ~60% semantic overlap with the best matching passage in the doc.
27
+ RELEVANCE_THRESHOLD = 0.60
28
+
29
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
30
 
31
  # Global variables
 
34
 
35
  # ── Progress tracking ────────────────────────────────────────────────────────
36
  processing_progress = {
37
+ "stage": "idle",
38
+ "percent": 0,
39
+ "message": "",
40
  }
41
 
42
  def set_progress(stage: str, percent: int, message: str = ""):
 
52
 
53
 
54
  def process_pdf(pdf_path):
55
+ """Process the uploaded document and create the RAG index."""
56
  global query_engine, current_pdf
57
 
58
  set_progress("Setting up models", 5)
59
+ print(f"πŸ“‚ Processing file: {pdf_path}")
60
  print("⏳ This may take 2-5 minutes...")
61
 
62
  llm = Groq(model="openai/gpt-oss-120b", temperature=0.0)
 
70
  Settings.node_parser = SentenceSplitter(chunk_size=1024, chunk_overlap=150)
71
 
72
  set_progress("Creating vector store", 25)
 
73
  client = QdrantClient(":memory:")
74
  vector_store = QdrantVectorStore(client=client, collection_name="active_document")
75
  storage_context = StorageContext.from_defaults(vector_store=vector_store)
76
 
77
  set_progress("Loading document pages", 40)
 
78
  documents = SimpleDirectoryReader(input_files=[pdf_path]).load_data()
79
 
80
  set_progress("Generating embeddings", 55, f"{len(documents)} chunks to embed")
81
  print("πŸ”„ Creating embeddings (this is the slow part)...")
82
 
 
 
83
  import threading, time as _time
84
 
85
  stop_interpolation = threading.Event()
 
89
  while not stop_interpolation.is_set():
90
  _time.sleep(1)
91
  elapsed = _time.monotonic() - start
 
92
  frac = min(elapsed / 180, 1.0)
93
+ pct = int(55 + frac * 33)
94
  set_progress("Generating embeddings", pct, f"{len(documents)} chunks to embed")
95
 
96
  interp_thread = threading.Thread(target=_interpolate, daemon=True)
 
102
  show_progress=True
103
  )
104
 
105
+ stop_interpolation.set()
106
 
107
  set_progress("Building query engine", 90)
108
 
 
132
  return True
133
 
134
 
135
+ # ── Helpers ──────────────────────────────────────────────────────────────────
136
+
137
+ CONVERSATIONAL_TRIGGERS = (
138
+ "how are you", "how r u", "hey", "hi", "hello", "good morning",
139
+ "good afternoon", "good evening", "what's up", "whats up",
140
+ "who are you", "what are you", "thanks", "thank you", "bye",
141
+ "goodbye", "ok", "okay", "cool", "nice", "great", "awesome",
142
+ )
143
+
144
+ def is_conversational(question: str) -> bool:
145
+ q = question.lower().strip()
146
+ return (
147
+ len(question.split()) <= 6 and
148
+ any(trigger in q for trigger in CONVERSATIONAL_TRIGGERS)
149
+ )
150
+
151
+ def top_score(source_nodes) -> float:
152
+ """Return the highest similarity score among retrieved nodes."""
153
+ scores = [n.score for n in source_nodes if n.score is not None]
154
+ return max(scores) if scores else 0.0
155
+
156
+
157
  # ── Routes ───────────────────────────────────────────────────────────────────
158
 
159
  @app.route('/progress', methods=['GET'])
160
  def get_progress():
 
161
  return jsonify(processing_progress), 200
162
 
163
 
 
172
  return jsonify({'success': False, 'message': 'No file selected'}), 400
173
 
174
  if not allowed_file(file.filename):
175
+ return jsonify({'success': False, 'message': 'Only PDF, TXT, and CSV files are allowed'}), 400
176
 
177
  try:
178
  filename = secure_filename(file.filename)
 
184
 
185
  return jsonify({
186
  'success': True,
187
+ 'message': 'File uploaded and processed successfully',
188
  'filename': filename
189
  }), 200
190
 
191
  except Exception as e:
192
  set_progress("Error", 0, str(e))
193
+ print(f"❌ Error processing file: {str(e)}")
194
+ return jsonify({'success': False, 'message': f'Error processing file: {str(e)}'}), 500
195
 
196
 
197
  @app.route('/chat', methods=['POST'])
 
199
  global query_engine, current_pdf
200
 
201
  if query_engine is None:
202
+ return jsonify({'success': False, 'message': 'No document uploaded yet.'}), 400
203
 
204
  data = request.get_json()
205
  if not data or 'question' not in data:
 
210
  return jsonify({'success': False, 'message': 'Question cannot be empty'}), 400
211
 
212
  try:
213
+ # ── 1. Conversational short-circuit (no document lookup) ──────────
214
+ if is_conversational(question):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  from llama_index.core.llms import ChatMessage
216
  chat_response = Settings.llm.chat([
217
  ChatMessage(role="user", content=question)
 
223
  'document': current_pdf
224
  }), 200
225
 
226
+ # ── 2. Retrieve nodes and check relevance BEFORE generating ───────
227
+ retriever = query_engine.retriever
228
+ source_nodes = retriever.retrieve(question)
229
+
230
+ best_score = top_score(source_nodes)
231
+ print(f"🎯 Best retrieval score: {best_score:.4f} (threshold: {RELEVANCE_THRESHOLD})")
232
+
233
+ if best_score < RELEVANCE_THRESHOLD:
234
+ # Question is out of scope β€” don't hallucinate an answer
235
+ return jsonify({
236
+ 'success': True,
237
+ 'answer': (
238
+ f"I couldn't find anything relevant to that in **{current_pdf}**. "
239
+ "Could you rephrase, or ask something more specific to the document?"
240
+ ),
241
+ 'sources': [],
242
+ 'document': current_pdf,
243
+ 'relevance_score': round(best_score, 4),
244
+ 'below_threshold': True,
245
+ }), 200
246
+
247
+ # ── 3. Score is good β€” generate answer normally ───────────────────
248
  response = query_engine.query(question)
249
 
250
  answer = response.response
251
  if len(answer) > 2000:
252
  answer = answer[:2000] + "..."
253
 
 
254
  sources = []
255
  for node in response.source_nodes:
256
  score = float(node.score) if node.score is not None else 0.0
 
263
  'text_snippet': node.text[:100] + '...' if len(node.text) > 100 else node.text
264
  })
265
 
266
+ return jsonify({
267
+ 'success': True,
268
+ 'answer': answer,
269
+ 'sources': sources,
270
+ 'document': current_pdf,
271
+ 'relevance_score': round(best_score, 4),
272
+ }), 200
273
 
274
  except Exception as e:
275
  return jsonify({'success': False, 'message': f'Error: {str(e)}'}), 500