Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
|
|
|
| 32 |
torch.set_num_threads(AI_CORES)
|
| 33 |
-
print(f"🖥️ Total Cores: {TOTAL_CORES} | Dedicated AI Cores: {AI_CORES}")
|
| 34 |
|
| 35 |
-
|
|
|
|
| 36 |
ai_semaphore = asyncio.Semaphore(MAX_CONCURRENT_AI)
|
| 37 |
ai_generator = None
|
| 38 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 39 |
|
| 40 |
-
# Strict Cache Structures
|
| 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]]
|
| 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 |
-
|
|
|
|
| 387 |
def load_ai_models():
|
| 388 |
global ai_generator
|
| 389 |
-
print("🤖 Loading HuggingFaceTB/
|
| 390 |
try:
|
| 391 |
-
|
|
|
|
| 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("✅
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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]]
|
| 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
|
| 505 |
-
|
|
|
|
| 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:
|
| 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
|
| 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)
|