Nebulixlabs commited on
Commit
88f13f9
·
verified ·
1 Parent(s): bf17016

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +46 -25
app.py CHANGED
@@ -28,16 +28,18 @@ from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, BitsAndB
28
 
29
  # --- CONFIGURATION & MULTIPROCESSING ---
30
  TOTAL_CORES = multiprocessing.cpu_count()
31
- AI_CORES = max(1, int(TOTAL_CORES * 0.7))
 
32
  torch.set_num_threads(AI_CORES)
33
- print(f"🖥️ Total Cores: {TOTAL_CORES} | Dedicated AI Cores: {AI_CORES}")
34
 
35
- MAX_CONCURRENT_AI = max(1, AI_CORES // 2)
 
36
  ai_semaphore = asyncio.Semaphore(MAX_CONCURRENT_AI)
37
  ai_generator = None
38
  HF_TOKEN = os.getenv("HF_TOKEN")
39
 
40
- # Strict Cache Structures (Guarantees no re-runs for identical queries)
41
  AI_RESPONSE_CACHE = {}
42
  MAX_AI_CACHE_SIZE = 10000
43
 
@@ -68,7 +70,8 @@ EXPANSION_DICT = {
68
  WEIGHTS = {
69
  "W_BM25": 1.0, "W_TITLE": 4.0, "W_HEADINGS": 3.0, "W_DESCRIPTION": 2.0, "W_BODY": 1.0,
70
  "W_URL": 1.5, "W_DOMAIN": 1.5, "W_PHRASE": 2.5, "W_EXACT": 3.0, "W_FRESHNESS": 1.0,
71
- "W_QUALITY": 1.0, "W_POPULARITY": 0.5, "W_PROXIMITY": 1.5
 
72
  }
73
 
74
  MAX_RESULTS_PER_DOMAIN = 3
@@ -163,7 +166,7 @@ def rank_chunks_for_rag(text: str, query_tokens: List[str]) -> List[str]:
163
 
164
  if not scored_chunks and chunks: scored_chunks = [(0, c) for c in chunks[:3]]
165
  scored_chunks.sort(reverse=True, key=lambda x: x[0])
166
- return [c for score, c in scored_chunks[:2]] # Strictly reduced for token limits
167
 
168
 
169
  # --- 4. CORE DETERMINISTIC SEARCH INDEX ---
@@ -237,7 +240,7 @@ class HybridSearchIndex:
237
  desc_toks = tokenize(desc, False)
238
  query_toks_full = tokenize(query_str, False)
239
 
240
- scores = {"bm25": 0.0, "title": 0.0, "phrase": 0.0, "exact": 0.0, "url": 0.0, "domain": 0.0, "quality": 0.0, "freshness": 0.0, "proximity": 0.0}
241
 
242
  doc_len = doc.get("_token_len", 1)
243
  for term in query_tokens:
@@ -284,6 +287,10 @@ class HybridSearchIndex:
284
  if len(title_toks) > 0 and (len(set(title_toks)) / len(title_toks)) < 0.5: q_score -= 0.3
285
  scores["quality"] = max(0.0, min(1.0, q_score))
286
 
 
 
 
 
287
  return scores
288
 
289
  def search(self, query: str, debug: bool = False) -> Tuple[List[dict], Dict[str, Any]]:
@@ -323,7 +330,8 @@ class HybridSearchIndex:
323
  WEIGHTS["W_BM25"] * norm_bm25 + w_title * features["title"] + w_phrase * features["phrase"] +
324
  WEIGHTS["W_EXACT"] * features["exact"] + WEIGHTS["W_URL"] * features["url"] +
325
  w_domain * features["domain"] + w_freshness * features["freshness"] +
326
- WEIGHTS["W_QUALITY"] * features["quality"] + WEIGHTS["W_PROXIMITY"] * features["proximity"]
 
327
  )
328
 
329
  if final_score > 0.05:
@@ -383,33 +391,43 @@ def load_and_index_dataset():
383
  except: pass
384
  search_index.build(raw_data)
385
 
386
- # --- AI GENERATION MODULE (SMOLLM 135M) ---
 
387
  def load_ai_models():
388
  global ai_generator
389
- print("🤖 Loading HuggingFaceTB/SmolLM-135M-Instruct (Ultra Fast)...")
390
  try:
391
- model_id = "HuggingFaceTB/SmolLM-135M-Instruct"
 
392
  tokenizer = AutoTokenizer.from_pretrained(model_id)
393
 
 
 
 
394
  if torch.cuda.is_available():
395
- quant_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_quant_type="nf4")
396
  model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=quant_config, device_map="auto")
397
  else:
398
  model = AutoModelForCausalLM.from_pretrained(model_id, device_map="cpu", torch_dtype=torch.float32)
399
 
400
  ai_generator = pipeline("text-generation", model=model, tokenizer=tokenizer)
401
- print("✅ SmolLM AI Ready!")
402
  except Exception as e:
403
  print(f"❌ Model Loading Failed: {e}")
404
 
405
  async def run_ai_task(messages, max_tokens: int, query_key: str):
406
  if not ai_generator: return "⚠️ AI Model is starting up."
407
 
408
- # EXACT CACHE MATCH - Guarantees User 1 and User 2 get identical cached response
409
  if query_key in AI_RESPONSE_CACHE:
410
  return AI_RESPONSE_CACHE[query_key]
411
 
412
- async with ai_semaphore:
 
 
 
 
 
 
 
413
  def _gen():
414
  tokenizer = ai_generator.tokenizer
415
  try: prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
@@ -420,11 +438,13 @@ async def run_ai_task(messages, max_tokens: int, query_key: str):
420
 
421
  response = await asyncio.to_thread(_gen)
422
 
423
- # Unconditionally cache to save computing power
424
  if len(AI_RESPONSE_CACHE) > MAX_AI_CACHE_SIZE: AI_RESPONSE_CACHE.clear()
425
  AI_RESPONSE_CACHE[query_key] = response
426
 
427
  return response
 
 
 
428
 
429
  # --- FASTAPI APP ENDPOINTS ---
430
  @asynccontextmanager
@@ -465,7 +485,7 @@ async def process_aimode(q: str, include_content: bool, debug: bool, max_ai_toke
465
  req_id = str(uuid.uuid4())
466
  cache_key = f"ai_{normalize_text(q)}_{max_ai_tokens}"
467
 
468
- # 1. IMMEDIATE CACHE CHECK (0s latency if previously asked)
469
  if cache_key in AI_RESPONSE_CACHE:
470
  results, _ = await asyncio.to_thread(search_index.search, q, debug)
471
  return {
@@ -478,7 +498,7 @@ async def process_aimode(q: str, include_content: bool, debug: bool, max_ai_toke
478
  "cached": True
479
  }
480
 
481
- # 2. Deterministic Search
482
  results, intent_info = await asyncio.to_thread(search_index.search, q, debug)
483
  top_20 = results[:STRICT_RESULT_LIMIT]
484
  query_tokens = tokenize(q)
@@ -487,7 +507,7 @@ async def process_aimode(q: str, include_content: bool, debug: bool, max_ai_toke
487
  used_sources = []
488
 
489
  if top_20:
490
- tasks = [extract_page_content(r["url"]) for r in top_20[:3]] # Only fetch top 3 to keep it fast
491
  extracted_data = await asyncio.gather(*tasks)
492
 
493
  valid_sources_count = 0
@@ -501,14 +521,15 @@ async def process_aimode(q: str, include_content: bool, debug: bool, max_ai_toke
501
  used_sources.append({"id": valid_sources_count+1, "title": r["title"], "url": r["url"]})
502
  valid_sources_count += 1
503
 
504
- # 3. STRICT 100 TOKEN INPUT LIMIT (User Query + RAG)
505
- system_prompt = "Answer directly based on context. Cite sources."
 
506
  combined_input = f"Context:\n{chr(10).join(rag_context_parts)}\n\nQuery: {q}"
507
 
508
  if ai_generator:
509
  tokenizer = ai_generator.tokenizer
510
  encoded_input = tokenizer.encode(combined_input, add_special_tokens=False)
511
- if len(encoded_input) > 90: # 90 + 10 for system prompt = 100 max tokens input
512
  encoded_input = encoded_input[:90]
513
  combined_input = tokenizer.decode(encoded_input, skip_special_tokens=True)
514
 
@@ -517,7 +538,7 @@ async def process_aimode(q: str, include_content: bool, debug: bool, max_ai_toke
517
  {"role": "user", "content": combined_input}
518
  ]
519
 
520
- # 4. Generate Answer (Restricted to 20 or 50 tokens based on GET/POST)
521
  ai_reply = await run_ai_task(messages, max_tokens=max_ai_tokens, query_key=cache_key)
522
 
523
  return {
@@ -532,11 +553,11 @@ async def process_aimode(q: str, include_content: bool, debug: bool, max_ai_toke
532
 
533
  @app.get("/aimode")
534
  async def ai_mode_get(q: str = Query(...), content: bool = Query(False), debug: bool = Query(False)):
535
- return await process_aimode(q, content, debug, max_ai_tokens=20) # Strictly 20 Tokens
536
 
537
  @app.post("/aimode")
538
  async def ai_mode_post(q: str = Form(...), content: bool = Form(False), debug: bool = Form(False)):
539
- return await process_aimode(q, content, debug, max_ai_tokens=50) # Strictly 50 Tokens
540
 
541
  if __name__ == "__main__":
542
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
28
 
29
  # --- CONFIGURATION & MULTIPROCESSING ---
30
  TOTAL_CORES = multiprocessing.cpu_count()
31
+ # STRICTLY 40% CPU FOR AI, 60% FOR SEARCH
32
+ AI_CORES = max(1, int(TOTAL_CORES * 0.40))
33
  torch.set_num_threads(AI_CORES)
34
+ print(f"🖥️ Total Cores: {TOTAL_CORES} | Dedicated AI Cores (40%): {AI_CORES} | Dedicated Search Cores: {TOTAL_CORES - AI_CORES}")
35
 
36
+ # First come first serve configuration
37
+ MAX_CONCURRENT_AI = max(1, AI_CORES)
38
  ai_semaphore = asyncio.Semaphore(MAX_CONCURRENT_AI)
39
  ai_generator = None
40
  HF_TOKEN = os.getenv("HF_TOKEN")
41
 
42
+ # Strict Cache Structures
43
  AI_RESPONSE_CACHE = {}
44
  MAX_AI_CACHE_SIZE = 10000
45
 
 
70
  WEIGHTS = {
71
  "W_BM25": 1.0, "W_TITLE": 4.0, "W_HEADINGS": 3.0, "W_DESCRIPTION": 2.0, "W_BODY": 1.0,
72
  "W_URL": 1.5, "W_DOMAIN": 1.5, "W_PHRASE": 2.5, "W_EXACT": 3.0, "W_FRESHNESS": 1.0,
73
+ "W_QUALITY": 1.0, "W_POPULARITY": 0.5, "W_PROXIMITY": 1.5,
74
+ "W_ROOT_DOMAIN": 8.0 # Extreme priority for root domains (domain.com over domain.com/path)
75
  }
76
 
77
  MAX_RESULTS_PER_DOMAIN = 3
 
166
 
167
  if not scored_chunks and chunks: scored_chunks = [(0, c) for c in chunks[:3]]
168
  scored_chunks.sort(reverse=True, key=lambda x: x[0])
169
+ return [c for score, c in scored_chunks[:2]]
170
 
171
 
172
  # --- 4. CORE DETERMINISTIC SEARCH INDEX ---
 
240
  desc_toks = tokenize(desc, False)
241
  query_toks_full = tokenize(query_str, False)
242
 
243
+ scores = {"bm25": 0.0, "title": 0.0, "phrase": 0.0, "exact": 0.0, "url": 0.0, "domain": 0.0, "quality": 0.0, "freshness": 0.0, "proximity": 0.0, "root_domain": 0.0}
244
 
245
  doc_len = doc.get("_token_len", 1)
246
  for term in query_tokens:
 
287
  if len(title_toks) > 0 and (len(set(title_toks)) / len(title_toks)) < 0.5: q_score -= 0.3
288
  scores["quality"] = max(0.0, min(1.0, q_score))
289
 
290
+ # ROOT DOMAIN DETECTION (Domain.com is always higher than Domain.com/path)
291
+ if url == "" or url == "/":
292
+ scores["root_domain"] = 1.0
293
+
294
  return scores
295
 
296
  def search(self, query: str, debug: bool = False) -> Tuple[List[dict], Dict[str, Any]]:
 
330
  WEIGHTS["W_BM25"] * norm_bm25 + w_title * features["title"] + w_phrase * features["phrase"] +
331
  WEIGHTS["W_EXACT"] * features["exact"] + WEIGHTS["W_URL"] * features["url"] +
332
  w_domain * features["domain"] + w_freshness * features["freshness"] +
333
+ WEIGHTS["W_QUALITY"] * features["quality"] + WEIGHTS["W_PROXIMITY"] * features["proximity"] +
334
+ WEIGHTS["W_ROOT_DOMAIN"] * features["root_domain"] # Hugely boosts root domains
335
  )
336
 
337
  if final_score > 0.05:
 
391
  except: pass
392
  search_index.build(raw_data)
393
 
394
+
395
+ # --- AI GENERATION MODULE (~200-360M Params, 4-bit Quantized) ---
396
  def load_ai_models():
397
  global ai_generator
398
+ print("🤖 Loading HuggingFaceTB/SmolLM2-360M-Instruct (Ultra Fast 4-Bit)...")
399
  try:
400
+ # Chosen ~360M parameter model. Extremely fast and follows instructions aggressively.
401
+ model_id = "HuggingFaceTB/SmolLM2-360M-Instruct"
402
  tokenizer = AutoTokenizer.from_pretrained(model_id)
403
 
404
+ # Loading strictly in 4-bit config for maximum efficiency
405
+ quant_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_quant_type="nf4")
406
+
407
  if torch.cuda.is_available():
 
408
  model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=quant_config, device_map="auto")
409
  else:
410
  model = AutoModelForCausalLM.from_pretrained(model_id, device_map="cpu", torch_dtype=torch.float32)
411
 
412
  ai_generator = pipeline("text-generation", model=model, tokenizer=tokenizer)
413
+ print("✅ Fast 4-Bit AI Ready!")
414
  except Exception as e:
415
  print(f"❌ Model Loading Failed: {e}")
416
 
417
  async def run_ai_task(messages, max_tokens: int, query_key: str):
418
  if not ai_generator: return "⚠️ AI Model is starting up."
419
 
 
420
  if query_key in AI_RESPONSE_CACHE:
421
  return AI_RESPONSE_CACHE[query_key]
422
 
423
+ # --- FCFS / GRACEFUL DEGRADATION LOGIC ---
424
+ # Try to enter the AI queue. If CPU is busy (locked after 0.1s), instantly fallback to Search!
425
+ try:
426
+ await asyncio.wait_for(ai_semaphore.acquire(), timeout=0.1)
427
+ except asyncio.TimeoutError:
428
+ return "⚠️ AI Servers are fully occupied right now. Displaying your ultra-fast search results below! (Please try AI Mode again in a few seconds)"
429
+
430
+ try:
431
  def _gen():
432
  tokenizer = ai_generator.tokenizer
433
  try: prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
 
438
 
439
  response = await asyncio.to_thread(_gen)
440
 
 
441
  if len(AI_RESPONSE_CACHE) > MAX_AI_CACHE_SIZE: AI_RESPONSE_CACHE.clear()
442
  AI_RESPONSE_CACHE[query_key] = response
443
 
444
  return response
445
+ finally:
446
+ ai_semaphore.release()
447
+
448
 
449
  # --- FASTAPI APP ENDPOINTS ---
450
  @asynccontextmanager
 
485
  req_id = str(uuid.uuid4())
486
  cache_key = f"ai_{normalize_text(q)}_{max_ai_tokens}"
487
 
488
+ # 1. IMMEDIATE CACHE CHECK
489
  if cache_key in AI_RESPONSE_CACHE:
490
  results, _ = await asyncio.to_thread(search_index.search, q, debug)
491
  return {
 
498
  "cached": True
499
  }
500
 
501
+ # 2. Deterministic Search (Now prioritizing Domain.com natively via Deep Analytics)
502
  results, intent_info = await asyncio.to_thread(search_index.search, q, debug)
503
  top_20 = results[:STRICT_RESULT_LIMIT]
504
  query_tokens = tokenize(q)
 
507
  used_sources = []
508
 
509
  if top_20:
510
+ tasks = [extract_page_content(r["url"]) for r in top_20[:3]]
511
  extracted_data = await asyncio.gather(*tasks)
512
 
513
  valid_sources_count = 0
 
521
  used_sources.append({"id": valid_sources_count+1, "title": r["title"], "url": r["url"]})
522
  valid_sources_count += 1
523
 
524
+ # 3. STRICT TOKEN INSTRUCTIONS (1 Token = ~4 words mapping handled contextually)
525
+ max_words_instruction = max_ai_tokens * 4
526
+ system_prompt = f"Answer concisely using the given context. You MUST conclude your entire response exactly within {max_ai_tokens} tokens (which is roughly {max_words_instruction} words limit). Do not leave sentences incomplete. Summarize directly."
527
  combined_input = f"Context:\n{chr(10).join(rag_context_parts)}\n\nQuery: {q}"
528
 
529
  if ai_generator:
530
  tokenizer = ai_generator.tokenizer
531
  encoded_input = tokenizer.encode(combined_input, add_special_tokens=False)
532
+ if len(encoded_input) > 90:
533
  encoded_input = encoded_input[:90]
534
  combined_input = tokenizer.decode(encoded_input, skip_special_tokens=True)
535
 
 
538
  {"role": "user", "content": combined_input}
539
  ]
540
 
541
+ # 4. Generate Answer FCFS
542
  ai_reply = await run_ai_task(messages, max_tokens=max_ai_tokens, query_key=cache_key)
543
 
544
  return {
 
553
 
554
  @app.get("/aimode")
555
  async def ai_mode_get(q: str = Query(...), content: bool = Query(False), debug: bool = Query(False)):
556
+ return await process_aimode(q, content, debug, max_ai_tokens=20) # Strictly 20 Tokens Limit (~80 Words)
557
 
558
  @app.post("/aimode")
559
  async def ai_mode_post(q: str = Form(...), content: bool = Form(False), debug: bool = Form(False)):
560
+ return await process_aimode(q, content, debug, max_ai_tokens=50) # Strictly 50 Tokens Limit (~200 Words)
561
 
562
  if __name__ == "__main__":
563
  uvicorn.run(app, host="0.0.0.0", port=7860)