Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -4,42 +4,49 @@ import asyncio
|
|
| 4 |
import os
|
| 5 |
import uvicorn
|
| 6 |
import re
|
|
|
|
|
|
|
| 7 |
import multiprocessing
|
| 8 |
import urllib.parse
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
| 10 |
from contextlib import asynccontextmanager
|
|
|
|
|
|
|
| 11 |
from fastapi import FastAPI, Query, Form
|
| 12 |
from fastapi.responses import JSONResponse
|
| 13 |
from fastapi.middleware.cors import CORSMiddleware
|
| 14 |
from huggingface_hub import hf_hub_download
|
| 15 |
-
from rank_bm25 import BM25Okapi
|
| 16 |
-
import torch
|
| 17 |
import aiohttp
|
| 18 |
from bs4 import BeautifulSoup
|
|
|
|
| 19 |
|
| 20 |
# Hugging Face imports for Q4 Quantization
|
| 21 |
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, BitsAndBytesConfig
|
| 22 |
|
| 23 |
-
# ---
|
| 24 |
TOTAL_CORES = multiprocessing.cpu_count()
|
| 25 |
AI_CORES = max(1, int(TOTAL_CORES * 0.7))
|
| 26 |
torch.set_num_threads(AI_CORES)
|
| 27 |
print(f"🖥️ Total Cores: {TOTAL_CORES} | Dedicated AI Cores: {AI_CORES}")
|
| 28 |
|
| 29 |
-
raw_data = []
|
| 30 |
-
suggestion_tuples = []
|
| 31 |
-
bm25_index = None
|
| 32 |
MAX_CONCURRENT_AI = max(1, AI_CORES // 2)
|
| 33 |
ai_semaphore = asyncio.Semaphore(MAX_CONCURRENT_AI)
|
| 34 |
ai_generator = None
|
| 35 |
-
|
|
|
|
|
|
|
| 36 |
AI_RESPONSE_CACHE = {}
|
| 37 |
-
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
|
|
|
|
|
|
| 43 |
ADULT_KEYWORDS = {
|
| 44 |
"porn", "sex", "nude", "nsfw", "xxx", "adult", "erotic", "onlyfans",
|
| 45 |
"mms", "chudai", "bhabhi", "devar", "savita bhabhi", "xvideos", "pornhub",
|
|
@@ -49,29 +56,332 @@ ADULT_KEYWORDS = {
|
|
| 49 |
"sex tape", "milf", "stepmom", "incest", "playboy", "scandal", "leaked"
|
| 50 |
}
|
| 51 |
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
if not text: return []
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
def load_and_index_dataset():
|
| 57 |
-
global bm25_index, raw_data, suggestion_tuples
|
| 58 |
-
print("📥 Loading Dataset & Building Fast Index...")
|
| 59 |
dataset_path = "dataset.jsonl"
|
| 60 |
-
|
| 61 |
if HF_TOKEN:
|
| 62 |
-
try:
|
| 63 |
-
|
| 64 |
-
repo_id="Nebulixlabs/Ultimate-Search-Engine-Dataset",
|
| 65 |
-
filename="dataset.jsonl",
|
| 66 |
-
repo_type="dataset",
|
| 67 |
-
token=HF_TOKEN
|
| 68 |
-
)
|
| 69 |
-
except Exception:
|
| 70 |
-
pass
|
| 71 |
|
| 72 |
-
|
| 73 |
-
suggestion_counter = Counter()
|
| 74 |
-
|
| 75 |
if os.path.exists(dataset_path):
|
| 76 |
with open(dataset_path, "r", encoding="utf-8") as f:
|
| 77 |
for i, line in enumerate(f):
|
|
@@ -82,40 +392,18 @@ def load_and_index_dataset():
|
|
| 82 |
url = doc.get("url", "")
|
| 83 |
title_str = doc.get("title", "")
|
| 84 |
desc_str = doc.get("description", "")
|
| 85 |
-
|
| 86 |
parsed_url = urllib.parse.urlparse(url)
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
doc_text = f"{title_str} {desc_str} {url}".lower()
|
| 91 |
-
is_adult = doc.get("tag", "general").lower() in ["adult", "nsfw"] or any(aw in doc_text for aw in ADULT_KEYWORDS)
|
| 92 |
-
|
| 93 |
-
doc["_fast_domain"] = domain
|
| 94 |
doc["_fast_path"] = parsed_url.path.strip("/")
|
| 95 |
-
doc["_fast_title"] = title_str
|
| 96 |
-
doc["_fast_desc"] = desc_str
|
| 97 |
-
doc["_is_adult"] =
|
| 98 |
raw_data.append(doc)
|
| 99 |
-
|
| 100 |
-
if not is_adult:
|
| 101 |
-
if len(clean_domain_name) > 2 and clean_domain_name not in ADULT_KEYWORDS:
|
| 102 |
-
suggestion_counter[clean_domain_name] += 5
|
| 103 |
-
|
| 104 |
-
clean_t_tokens = [w for w in tokenize(title_str) if w not in ADULT_KEYWORDS]
|
| 105 |
-
for w in clean_t_tokens:
|
| 106 |
-
if len(w) > 2: suggestion_counter[w] += 1
|
| 107 |
-
for j in range(len(clean_t_tokens) - 1):
|
| 108 |
-
suggestion_counter[f"{clean_t_tokens[j]} {clean_t_tokens[j+1]}"] += 2
|
| 109 |
-
|
| 110 |
-
tokenized_corpus.append((tokenize(title_str) * 5) + (tokenize(desc_str) * 3) + tokenize(url))
|
| 111 |
except: pass
|
| 112 |
-
|
| 113 |
-
if tokenized_corpus:
|
| 114 |
-
suggestion_tuples = suggestion_counter.most_common(40000)
|
| 115 |
-
bm25_index = BM25Okapi(tokenized_corpus)
|
| 116 |
-
print(f"🚀 Indexing Complete! {len(raw_data)} records ready.")
|
| 117 |
|
| 118 |
-
# --- QWEN
|
| 119 |
def load_ai_models():
|
| 120 |
global ai_generator
|
| 121 |
print("🤖 Loading Qwen/Qwen2.5-0.5B-Instruct...")
|
|
@@ -123,224 +411,137 @@ def load_ai_models():
|
|
| 123 |
model_id = "Qwen/Qwen2.5-0.5B-Instruct"
|
| 124 |
tokenizer = AutoTokenizer.from_pretrained(model_id)
|
| 125 |
|
| 126 |
-
# Smart Hardware Check for 4-bit (Q4) Quantization
|
| 127 |
if torch.cuda.is_available():
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
load_in_4bit=True,
|
| 131 |
-
bnb_4bit_compute_dtype=torch.float16,
|
| 132 |
-
bnb_4bit_quant_type="nf4",
|
| 133 |
-
)
|
| 134 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 135 |
-
model_id,
|
| 136 |
-
quantization_config=quant_config,
|
| 137 |
-
device_map="auto"
|
| 138 |
-
)
|
| 139 |
else:
|
| 140 |
-
|
| 141 |
-
# For CPU, 4-bit BitsAndBytes isn't natively optimal, so we load in standard float32
|
| 142 |
-
# Qwen 0.5B is already tiny (~1GB) so it runs perfectly fast on CPU too.
|
| 143 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 144 |
-
model_id,
|
| 145 |
-
device_map="cpu",
|
| 146 |
-
torch_dtype=torch.float32
|
| 147 |
-
)
|
| 148 |
|
| 149 |
-
ai_generator = pipeline(
|
| 150 |
-
|
| 151 |
-
model=model,
|
| 152 |
-
tokenizer=tokenizer
|
| 153 |
-
)
|
| 154 |
-
print("✅ Qwen2.5-0.5B-Instruct (Superfast Mode) Ready!")
|
| 155 |
except Exception as e:
|
| 156 |
print(f"❌ Model Loading Failed: {e}")
|
| 157 |
|
| 158 |
-
|
| 159 |
-
def _generate_text(messages, max_tokens: int):
|
| 160 |
-
tokenizer = ai_generator.tokenizer
|
| 161 |
-
|
| 162 |
-
# 1. Use Native Chat Template for Qwen (Makes answers highly accurate & stops hallucinations)
|
| 163 |
-
try:
|
| 164 |
-
prompt = tokenizer.apply_chat_template(
|
| 165 |
-
messages,
|
| 166 |
-
tokenize=False,
|
| 167 |
-
add_generation_prompt=True
|
| 168 |
-
)
|
| 169 |
-
except Exception:
|
| 170 |
-
prompt = "\n".join([f"{m['role'].capitalize()}: {m['content']}" for m in messages]) + "\nAssistant: "
|
| 171 |
-
|
| 172 |
-
# 2. Greedy Decoding (do_sample=False) + Return Setup
|
| 173 |
-
result = ai_generator(
|
| 174 |
-
prompt,
|
| 175 |
-
max_new_tokens=max_tokens,
|
| 176 |
-
do_sample=False,
|
| 177 |
-
truncation=True,
|
| 178 |
-
return_full_text=False, # ❌ FIX: No more repeating the query in answers!
|
| 179 |
-
pad_token_id=tokenizer.eos_token_id if tokenizer.eos_token_id is not None else 0
|
| 180 |
-
)
|
| 181 |
-
|
| 182 |
-
text_out = result[0]['generated_text'].strip()
|
| 183 |
-
return text_out
|
| 184 |
-
|
| 185 |
-
async def run_ai_task(messages, max_tokens: int, query_key: str):
|
| 186 |
if not ai_generator: return "⚠️ AI Model is starting up."
|
| 187 |
-
if query_key in AI_RESPONSE_CACHE: return AI_RESPONSE_CACHE[query_key]
|
| 188 |
-
|
| 189 |
async with ai_semaphore:
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
return response
|
| 194 |
|
| 195 |
-
|
| 196 |
-
words = text.split()
|
| 197 |
-
return [" ".join(words[i:i+chunk_size]) for i in range(0, len(words), chunk_size)]
|
| 198 |
-
|
| 199 |
-
async def scrape_and_find_best_chunk(url, query):
|
| 200 |
-
try:
|
| 201 |
-
async with aiohttp.ClientSession() as session:
|
| 202 |
-
async with session.get(url, timeout=1.5) as resp:
|
| 203 |
-
if resp.status == 200:
|
| 204 |
-
html = await resp.text()
|
| 205 |
-
soup = BeautifulSoup(html, "html.parser")
|
| 206 |
-
text = " ".join([tag.get_text(strip=True) for tag in soup.find_all(['p', 'h1', 'h2'])])
|
| 207 |
-
if not text: return ""
|
| 208 |
-
|
| 209 |
-
chunks = chunk_text(text)
|
| 210 |
-
query_tokens = set(tokenize(query))
|
| 211 |
-
|
| 212 |
-
best_chunk, max_score = "", 0
|
| 213 |
-
for chunk in chunks:
|
| 214 |
-
score = len(query_tokens.intersection(tokenize(chunk)))
|
| 215 |
-
if score > max_score:
|
| 216 |
-
max_score, best_chunk = score, chunk
|
| 217 |
-
if max_score >= max(1, len(query_tokens) // 2): break
|
| 218 |
-
return best_chunk
|
| 219 |
-
except:
|
| 220 |
-
return ""
|
| 221 |
-
return ""
|
| 222 |
-
|
| 223 |
-
async def perform_core_search(raw_query: str):
|
| 224 |
-
if not raw_data or not bm25_index: return [], "Search index not ready.", 0
|
| 225 |
-
|
| 226 |
-
query = raw_query.strip().lower()
|
| 227 |
-
query_tokens = tokenize(query)
|
| 228 |
-
user_wants_adult = any(aw in query for aw in ADULT_KEYWORDS)
|
| 229 |
-
|
| 230 |
-
scores = bm25_index.get_scores(query_tokens) if query_tokens else [1.0] * len(raw_data)
|
| 231 |
-
scored_results, seen_urls = [], set()
|
| 232 |
-
query_clean = query.replace(" ", "")
|
| 233 |
-
|
| 234 |
-
for index, score in enumerate(scores):
|
| 235 |
-
if score > 0.1 or not query_tokens:
|
| 236 |
-
doc = raw_data[index]
|
| 237 |
-
doc_url = doc["url"]
|
| 238 |
-
if doc_url in seen_urls: continue
|
| 239 |
-
if not user_wants_adult and doc["_is_adult"]: continue
|
| 240 |
-
|
| 241 |
-
final_score, domain = score, doc["_fast_domain"]
|
| 242 |
-
domain_clean = domain.replace(" ", "")
|
| 243 |
-
doc_title, doc_desc = doc["_fast_title"], doc["_fast_desc"]
|
| 244 |
-
|
| 245 |
-
if query_clean == domain_clean or domain_clean.startswith(query_clean + "."):
|
| 246 |
-
final_score *= 50.0 if not doc["_fast_path"] else 10.0
|
| 247 |
-
elif query_clean in domain_clean:
|
| 248 |
-
final_score *= 25.0 if not doc["_fast_path"] else 5.0
|
| 249 |
-
|
| 250 |
-
if query in doc_title: final_score *= 5.0
|
| 251 |
-
elif all(w in doc_title for w in query_tokens): final_score *= 3.0
|
| 252 |
-
if query in doc_desc: final_score *= 2.0
|
| 253 |
-
|
| 254 |
-
stats = URL_STATS.get(doc_url, {"user_visits": 0, "ai_visits": 0, "is_trusted": False})
|
| 255 |
-
if stats.get("is_trusted", False): final_score *= 2.0
|
| 256 |
-
final_score += (stats.get("user_visits", 0) * 0.05) + (stats.get("ai_visits", 0) * 0.1)
|
| 257 |
-
|
| 258 |
-
seen_urls.add(doc_url)
|
| 259 |
-
doc_copy = {k: v for k, v in doc.items() if not k.startswith("_")}
|
| 260 |
-
doc_copy["trust_stats"] = stats
|
| 261 |
-
scored_results.append((final_score, doc_copy))
|
| 262 |
-
|
| 263 |
-
scored_results.sort(key=lambda x: x[0], reverse=True)
|
| 264 |
-
best_score = scored_results[0][0] if scored_results else 0
|
| 265 |
-
prediction = "High confidence" if best_score > 15 else "Medium confidence" if best_score > 5 else "Low confidence"
|
| 266 |
-
|
| 267 |
-
return [{"match_score": round(s, 2), **doc} for s, doc in scored_results[:100]], prediction, best_score
|
| 268 |
-
|
| 269 |
@asynccontextmanager
|
| 270 |
async def lifespan(app: FastAPI):
|
| 271 |
asyncio.create_task(asyncio.to_thread(load_and_index_dataset))
|
| 272 |
asyncio.create_task(asyncio.to_thread(load_ai_models))
|
| 273 |
yield
|
| 274 |
|
| 275 |
-
app = FastAPI(title="Nebulix Fast
|
| 276 |
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
| 277 |
|
| 278 |
@app.get("/suggest")
|
| 279 |
async def get_suggestions(q: str = Query(..., min_length=1)):
|
| 280 |
-
q =
|
| 281 |
-
matches = [word for word, count in
|
| 282 |
return {"query": q, "suggestions": matches}
|
| 283 |
|
| 284 |
-
@app.get("/
|
| 285 |
-
async def
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
return
|
| 289 |
|
| 290 |
-
@app.get("/
|
| 291 |
-
async def
|
| 292 |
req_id = str(uuid.uuid4())
|
| 293 |
-
results,
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
|
|
|
|
|
|
| 303 |
|
| 304 |
-
async def
|
| 305 |
req_id = str(uuid.uuid4())
|
| 306 |
-
needs_rag = any(w in q.lower() for w in ["what", "who", "where", "how", "why", "tell me", "explain", "latest", "best", "news"])
|
| 307 |
-
results, _, _ = await perform_core_search(q)
|
| 308 |
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
used_sources = []
|
| 311 |
|
| 312 |
-
if
|
| 313 |
-
|
| 314 |
-
tasks = [
|
| 315 |
-
|
| 316 |
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
context_parts.append(f"Source [{i+1}] ({url}): {chunk}")
|
| 323 |
-
used_sources.append(doc)
|
| 324 |
-
else:
|
| 325 |
-
context_parts.append(f"Source [{i+1}] ({url}): {doc.get('description', '')}")
|
| 326 |
-
used_sources.append(doc)
|
| 327 |
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
messages = [
|
| 332 |
{"role": "system", "content": system_prompt},
|
| 333 |
-
{"role": "user", "content": f"Live Context Data:\n{rag_context}\n\nQuestion: {q}" if
|
| 334 |
]
|
| 335 |
|
| 336 |
-
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
|
| 339 |
-
@app.get("/
|
| 340 |
-
async def
|
|
|
|
| 341 |
|
| 342 |
-
@app.post("/
|
| 343 |
-
async def
|
|
|
|
| 344 |
|
| 345 |
if __name__ == "__main__":
|
| 346 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 4 |
import os
|
| 5 |
import uvicorn
|
| 6 |
import re
|
| 7 |
+
import math
|
| 8 |
+
import time
|
| 9 |
import multiprocessing
|
| 10 |
import urllib.parse
|
| 11 |
+
import unicodedata
|
| 12 |
+
import difflib
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
from collections import Counter, defaultdict
|
| 15 |
from contextlib import asynccontextmanager
|
| 16 |
+
from typing import List, Dict, Any, Tuple, Set
|
| 17 |
+
|
| 18 |
from fastapi import FastAPI, Query, Form
|
| 19 |
from fastapi.responses import JSONResponse
|
| 20 |
from fastapi.middleware.cors import CORSMiddleware
|
| 21 |
from huggingface_hub import hf_hub_download
|
|
|
|
|
|
|
| 22 |
import aiohttp
|
| 23 |
from bs4 import BeautifulSoup
|
| 24 |
+
import torch
|
| 25 |
|
| 26 |
# Hugging Face imports for Q4 Quantization
|
| 27 |
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, BitsAndBytesConfig
|
| 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 |
+
# Cache Structures
|
| 41 |
AI_RESPONSE_CACHE = {}
|
| 42 |
+
MAX_AI_CACHE_SIZE = 5000
|
| 43 |
|
| 44 |
+
CONTENT_CACHE = {}
|
| 45 |
+
MAX_CONTENT_CONCURRENCY = 10
|
| 46 |
+
content_semaphore = asyncio.Semaphore(MAX_CONTENT_CONCURRENCY)
|
| 47 |
|
| 48 |
+
# --- 1. DETERMINISTIC PREPROCESSING & CONSTANTS ---
|
| 49 |
+
STOP_WORDS = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "is", "are", "by", "this", "that", "it"}
|
| 50 |
ADULT_KEYWORDS = {
|
| 51 |
"porn", "sex", "nude", "nsfw", "xxx", "adult", "erotic", "onlyfans",
|
| 52 |
"mms", "chudai", "bhabhi", "devar", "savita bhabhi", "xvideos", "pornhub",
|
|
|
|
| 56 |
"sex tape", "milf", "stepmom", "incest", "playboy", "scandal", "leaked"
|
| 57 |
}
|
| 58 |
|
| 59 |
+
EXPANSION_DICT = {
|
| 60 |
+
"usa": ["united", "states"],
|
| 61 |
+
"uk": ["united", "kingdom"],
|
| 62 |
+
"ai": ["artificial", "intelligence"],
|
| 63 |
+
"ml": ["machine", "learning"],
|
| 64 |
+
"nlp": ["natural", "language", "processing"],
|
| 65 |
+
"js": ["javascript"]
|
| 66 |
+
}
|
| 67 |
+
|
| 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
|
| 75 |
+
STRICT_RESULT_LIMIT = 20
|
| 76 |
+
|
| 77 |
+
def normalize_text(text: str) -> str:
|
| 78 |
+
if not text: return ""
|
| 79 |
+
text = unicodedata.normalize('NFKD', str(text)).lower()
|
| 80 |
+
text = re.sub(r'[\'’]', '', text)
|
| 81 |
+
text = re.sub(r'[^a-z0-9\-]', ' ', text)
|
| 82 |
+
return " ".join(text.split())
|
| 83 |
+
|
| 84 |
+
def tokenize(text: str, remove_stopwords: bool = True) -> List[str]:
|
| 85 |
+
tokens = normalize_text(text).split()
|
| 86 |
+
if remove_stopwords:
|
| 87 |
+
tokens = [t for t in tokens if t not in STOP_WORDS]
|
| 88 |
+
return tokens
|
| 89 |
+
|
| 90 |
+
# --- 2. DETERMINISTIC QUERY INTENT CLASSIFIER ---
|
| 91 |
+
def detect_query_intent(query_tokens: List[str]) -> Dict[str, Any]:
|
| 92 |
+
intents = []
|
| 93 |
+
|
| 94 |
+
news_terms = {"latest", "news", "today", "breaking", "recent", "updates", "current"}
|
| 95 |
+
if any(term in query_tokens for term in news_terms): intents.append("NEWS")
|
| 96 |
+
|
| 97 |
+
def_terms = {"what", "who", "definition", "meaning"}
|
| 98 |
+
if any(term in query_tokens for term in def_terms): intents.append("DEFINITION")
|
| 99 |
+
|
| 100 |
+
how_terms = {"how", "tutorial", "guide", "steps", "learn", "documentation"}
|
| 101 |
+
if any(term in query_tokens for term in how_terms): intents.append("HOW_TO")
|
| 102 |
+
|
| 103 |
+
comp_terms = {"vs", "versus", "compare", "comparison", "difference", "best"}
|
| 104 |
+
if any(term in query_tokens for term in comp_terms): intents.append("COMPARISON")
|
| 105 |
+
|
| 106 |
+
local_terms = {"near", "restaurant", "restaurants", "location", "weather"}
|
| 107 |
+
if any(term in query_tokens for term in local_terms): intents.append("LOCAL")
|
| 108 |
+
|
| 109 |
+
if len(query_tokens) <= 2 and not intents: intents.append("NAVIGATIONAL")
|
| 110 |
+
|
| 111 |
+
return {"intent": intents[0] if intents else "GENERAL", "signals": intents}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# --- 3. DEDICATED WEB CONTENT EXTRACTION ---
|
| 115 |
+
async def extract_page_content(url: str) -> Dict[str, Any]:
|
| 116 |
+
"""Downloads webpage safely, extracts pure text, and caches it."""
|
| 117 |
+
if url in CONTENT_CACHE:
|
| 118 |
+
return CONTENT_CACHE[url]
|
| 119 |
+
|
| 120 |
+
async with content_semaphore:
|
| 121 |
+
try:
|
| 122 |
+
async with aiohttp.ClientSession() as session:
|
| 123 |
+
async with session.get(url, timeout=4.0, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) NebulixBot"}) as resp:
|
| 124 |
+
if resp.status != 200:
|
| 125 |
+
return {"url": url, "title": "", "content": "", "success": False, "error": f"HTTP {resp.status}"}
|
| 126 |
+
|
| 127 |
+
html = await resp.text()
|
| 128 |
+
soup = BeautifulSoup(html, "html.parser")
|
| 129 |
+
|
| 130 |
+
# Clean irrelevant elements
|
| 131 |
+
for tag in soup(['script', 'style', 'nav', 'footer', 'header', 'aside', 'noscript', 'form', 'button', 'iframe']):
|
| 132 |
+
tag.decompose()
|
| 133 |
+
|
| 134 |
+
blocks = []
|
| 135 |
+
for tag in soup.find_all(['h1', 'h2', 'h3', 'p', 'article', 'main', 'li']):
|
| 136 |
+
text = tag.get_text(separator=' ', strip=True)
|
| 137 |
+
if len(text) > 30: # Skip very tiny menu stubs missed by decompose
|
| 138 |
+
blocks.append(text)
|
| 139 |
+
|
| 140 |
+
# Deduplicate contiguous identical text
|
| 141 |
+
clean_blocks = []
|
| 142 |
+
seen_blocks = set()
|
| 143 |
+
for b in blocks:
|
| 144 |
+
if b not in seen_blocks:
|
| 145 |
+
seen_blocks.add(b)
|
| 146 |
+
clean_blocks.append(b)
|
| 147 |
+
|
| 148 |
+
content_str = "\n\n".join(clean_blocks)
|
| 149 |
+
page_title = soup.title.string.strip() if soup.title and soup.title.string else ""
|
| 150 |
+
|
| 151 |
+
result = {
|
| 152 |
+
"url": url,
|
| 153 |
+
"title": page_title,
|
| 154 |
+
"content": content_str,
|
| 155 |
+
"success": True,
|
| 156 |
+
"timestamp": time.time()
|
| 157 |
+
}
|
| 158 |
+
CONTENT_CACHE[url] = result
|
| 159 |
+
return result
|
| 160 |
+
except asyncio.TimeoutError:
|
| 161 |
+
return {"url": url, "title": "", "content": "", "success": False, "error": "Timeout"}
|
| 162 |
+
except Exception as e:
|
| 163 |
+
return {"url": url, "title": "", "content": "", "success": False, "error": str(e)}
|
| 164 |
+
|
| 165 |
+
def rank_chunks_for_rag(text: str, query_tokens: List[str]) -> List[str]:
|
| 166 |
+
"""Deterministically extracts the most relevant chunks from an article for context."""
|
| 167 |
if not text: return []
|
| 168 |
+
chunks = text.split("\n\n")
|
| 169 |
+
q_set = set(query_tokens)
|
| 170 |
+
scored_chunks = []
|
| 171 |
+
|
| 172 |
+
for c in chunks:
|
| 173 |
+
c_toks = set(tokenize(c))
|
| 174 |
+
score = len(q_set.intersection(c_toks))
|
| 175 |
+
if score > 0:
|
| 176 |
+
scored_chunks.append((score, c))
|
| 177 |
+
|
| 178 |
+
if not scored_chunks and chunks:
|
| 179 |
+
scored_chunks = [(0, c) for c in chunks[:3]] # Fallback to top paragraphs
|
| 180 |
+
|
| 181 |
+
scored_chunks.sort(reverse=True, key=lambda x: x[0])
|
| 182 |
+
return [c for score, c in scored_chunks[:3]] # Max 3 context chunks per source
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
# --- 4. CORE DETERMINISTIC SEARCH INDEX ---
|
| 186 |
+
class HybridSearchIndex:
|
| 187 |
+
def __init__(self):
|
| 188 |
+
self.docs = []
|
| 189 |
+
self.total_docs = 0
|
| 190 |
+
self.vocab = set()
|
| 191 |
+
self.df = defaultdict(int)
|
| 192 |
+
self.avg_doc_len = 0.0
|
| 193 |
+
self.inverted_index = defaultdict(set)
|
| 194 |
+
self.suggestions = []
|
| 195 |
+
self.k1 = 1.5
|
| 196 |
+
self.b = 0.75
|
| 197 |
+
|
| 198 |
+
def build(self, raw_data: List[dict]):
|
| 199 |
+
print("📥 Building Advanced Deterministic Search Index...")
|
| 200 |
+
total_len = 0
|
| 201 |
+
suggestion_counter = Counter()
|
| 202 |
+
|
| 203 |
+
for doc_id, doc in enumerate(raw_data):
|
| 204 |
+
self.docs.append(doc)
|
| 205 |
+
title_tokens = tokenize(doc.get("_fast_title", ""))
|
| 206 |
+
desc_tokens = tokenize(doc.get("_fast_desc", ""))
|
| 207 |
+
domain_tokens = tokenize(doc.get("_fast_domain", ""))
|
| 208 |
+
all_tokens = title_tokens + desc_tokens + domain_tokens
|
| 209 |
+
|
| 210 |
+
doc["_token_len"] = len(all_tokens)
|
| 211 |
+
total_len += len(all_tokens)
|
| 212 |
+
|
| 213 |
+
for term in set(all_tokens):
|
| 214 |
+
self.inverted_index[term].add(doc_id)
|
| 215 |
+
self.df[term] += 1
|
| 216 |
+
self.vocab.add(term)
|
| 217 |
+
|
| 218 |
+
if not doc.get("_is_adult", False):
|
| 219 |
+
clean_title = [w for w in title_tokens if w not in ADULT_KEYWORDS and len(w) > 2]
|
| 220 |
+
for w in clean_title: suggestion_counter[w] += 1
|
| 221 |
+
for j in range(len(clean_title) - 1): suggestion_counter[f"{clean_title[j]} {clean_title[j+1]}"] += 2
|
| 222 |
+
d_clean = doc.get("_fast_domain", "").split('.')[0]
|
| 223 |
+
if len(d_clean) > 2 and d_clean not in ADULT_KEYWORDS: suggestion_counter[d_clean] += 5
|
| 224 |
+
|
| 225 |
+
self.total_docs = len(self.docs)
|
| 226 |
+
self.avg_doc_len = total_len / max(1, self.total_docs)
|
| 227 |
+
self.suggestions = suggestion_counter.most_common(40000)
|
| 228 |
+
print(f"🚀 Indexing Complete! {self.total_docs} records ready.")
|
| 229 |
+
|
| 230 |
+
def idf(self, term: str) -> float:
|
| 231 |
+
df = self.df.get(term, 0)
|
| 232 |
+
return math.log(1.0 + (self.total_docs - df + 0.5) / (df + 0.5))
|
| 233 |
+
|
| 234 |
+
def _get_candidates(self, query_tokens: List[str]) -> Set[int]:
|
| 235 |
+
candidates = set()
|
| 236 |
+
for term in query_tokens:
|
| 237 |
+
if term in self.inverted_index: candidates.update(self.inverted_index[term])
|
| 238 |
+
|
| 239 |
+
if len(candidates) < 10:
|
| 240 |
+
for term in query_tokens:
|
| 241 |
+
if term not in self.inverted_index:
|
| 242 |
+
matches = difflib.get_close_matches(term, self.vocab, n=2, cutoff=0.8)
|
| 243 |
+
for m in matches: candidates.update(self.inverted_index[m])
|
| 244 |
+
return candidates
|
| 245 |
+
|
| 246 |
+
def calculate_features(self, doc: dict, query_tokens: List[str], query_str: str) -> Dict[str, float]:
|
| 247 |
+
title = doc.get("_fast_title", "")
|
| 248 |
+
desc = doc.get("_fast_desc", "")
|
| 249 |
+
domain = doc.get("_fast_domain", "")
|
| 250 |
+
url = doc.get("_fast_path", "")
|
| 251 |
+
|
| 252 |
+
title_toks = tokenize(title, False)
|
| 253 |
+
desc_toks = tokenize(desc, False)
|
| 254 |
+
query_toks_full = tokenize(query_str, False)
|
| 255 |
+
|
| 256 |
+
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}
|
| 257 |
+
|
| 258 |
+
doc_len = doc.get("_token_len", 1)
|
| 259 |
+
for term in query_tokens:
|
| 260 |
+
tf = title_toks.count(term) * 2 + desc_toks.count(term)
|
| 261 |
+
if tf > 0:
|
| 262 |
+
tf_num = tf * (self.k1 + 1)
|
| 263 |
+
tf_den = tf + self.k1 * (1 - self.b + self.b * (doc_len / self.avg_doc_len))
|
| 264 |
+
scores["bm25"] += self.idf(term) * (tf_num / tf_den)
|
| 265 |
+
|
| 266 |
+
q_norm = normalize_text(query_str)
|
| 267 |
+
if q_norm and q_norm in normalize_text(title): scores["exact"] = 1.0
|
| 268 |
+
elif q_norm and q_norm in normalize_text(desc): scores["exact"] = 0.5
|
| 269 |
+
|
| 270 |
+
if len(query_toks_full) > 1:
|
| 271 |
+
phrase_overlap = 0
|
| 272 |
+
for i in range(len(query_toks_full) - 1):
|
| 273 |
+
bigram = f"{query_toks_full[i]} {query_toks_full[i+1]}"
|
| 274 |
+
if bigram in normalize_text(title): phrase_overlap += 1.0
|
| 275 |
+
elif bigram in normalize_text(desc): phrase_overlap += 0.5
|
| 276 |
+
scores["phrase"] = phrase_overlap / (len(query_toks_full) - 1)
|
| 277 |
+
|
| 278 |
+
title_overlap = sum(1 for t in query_tokens if t in title_toks)
|
| 279 |
+
scores["title"] = title_overlap / max(1, len(query_tokens))
|
| 280 |
+
|
| 281 |
+
q_joined = "".join(query_tokens)
|
| 282 |
+
domain_clean = domain.split('.')[0]
|
| 283 |
+
if q_joined == domain_clean or domain.startswith(q_joined + "."): scores["domain"] = 1.0
|
| 284 |
+
elif q_joined in domain: scores["domain"] = 0.5
|
| 285 |
+
if any(t in url for t in query_tokens): scores["url"] = 0.5
|
| 286 |
+
|
| 287 |
+
if len(query_tokens) > 1:
|
| 288 |
+
positions = [title_toks.index(t) for t in query_tokens if t in title_toks]
|
| 289 |
+
if len(positions) > 1:
|
| 290 |
+
scores["proximity"] = 1.0 / (1.0 + (max(positions) - min(positions)))
|
| 291 |
+
|
| 292 |
+
year_match = re.search(r'\b(202[0-6])\b', url + " " + desc)
|
| 293 |
+
if year_match:
|
| 294 |
+
scores["freshness"] = math.exp(-(datetime.now().year - int(year_match.group(1))) / 2.0)
|
| 295 |
+
else: scores["freshness"] = 0.3
|
| 296 |
+
|
| 297 |
+
q_score = 0.0
|
| 298 |
+
if len(title_toks) >= 3: q_score += 0.2
|
| 299 |
+
if len(desc_toks) >= 10: q_score += 0.2
|
| 300 |
+
if doc.get("url", "").startswith("https"): q_score += 0.2
|
| 301 |
+
if len(title_toks) > 0 and (len(set(title_toks)) / len(title_toks)) < 0.5: q_score -= 0.3
|
| 302 |
+
scores["quality"] = max(0.0, min(1.0, q_score))
|
| 303 |
+
|
| 304 |
+
return scores
|
| 305 |
+
|
| 306 |
+
def search(self, query: str, debug: bool = False) -> Tuple[List[dict], Dict[str, Any]]:
|
| 307 |
+
query_norm = normalize_text(query)
|
| 308 |
+
query_tokens = tokenize(query_norm)
|
| 309 |
+
|
| 310 |
+
expanded_tokens = list(query_tokens)
|
| 311 |
+
for t in query_tokens:
|
| 312 |
+
if t in EXPANSION_DICT: expanded_tokens.extend(EXPANSION_DICT[t])
|
| 313 |
+
query_tokens = list(dict.fromkeys(expanded_tokens))
|
| 314 |
+
|
| 315 |
+
intent_info = detect_query_intent(query_tokens)
|
| 316 |
+
intent = intent_info["intent"]
|
| 317 |
+
user_wants_adult = any(aw in query_norm for aw in ADULT_KEYWORDS)
|
| 318 |
+
|
| 319 |
+
candidates_ids = self._get_candidates(query_tokens)
|
| 320 |
+
scored_results = []
|
| 321 |
+
seen_domains = defaultdict(int)
|
| 322 |
+
seen_urls = set()
|
| 323 |
+
|
| 324 |
+
for doc_id in candidates_ids:
|
| 325 |
+
doc = self.docs[doc_id]
|
| 326 |
+
if not user_wants_adult and doc.get("_is_adult"): continue
|
| 327 |
+
|
| 328 |
+
base_url = doc.get("url", "").split("?")[0].rstrip("/")
|
| 329 |
+
if base_url in seen_urls: continue
|
| 330 |
+
|
| 331 |
+
features = self.calculate_features(doc, query_tokens, query_norm)
|
| 332 |
+
norm_bm25 = features["bm25"] / (features["bm25"] + 10.0)
|
| 333 |
+
|
| 334 |
+
w_freshness = WEIGHTS["W_FRESHNESS"] + (3.0 if intent == "NEWS" else 0.0)
|
| 335 |
+
w_domain = WEIGHTS["W_DOMAIN"] + (4.0 if intent == "NAVIGATIONAL" else 0.0)
|
| 336 |
+
w_title = WEIGHTS["W_TITLE"] + (2.0 if intent == "DEFINITION" else 0.0)
|
| 337 |
+
w_phrase = WEIGHTS["W_PHRASE"] + (1.5 if intent in ("DEFINITION", "COMPARISON") else 0.0)
|
| 338 |
+
|
| 339 |
+
final_score = (
|
| 340 |
+
WEIGHTS["W_BM25"] * norm_bm25 + w_title * features["title"] + w_phrase * features["phrase"] +
|
| 341 |
+
WEIGHTS["W_EXACT"] * features["exact"] + WEIGHTS["W_URL"] * features["url"] +
|
| 342 |
+
w_domain * features["domain"] + w_freshness * features["freshness"] +
|
| 343 |
+
WEIGHTS["W_QUALITY"] * features["quality"] + WEIGHTS["W_PROXIMITY"] * features["proximity"]
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
if final_score > 0.05:
|
| 347 |
+
doc_out = {
|
| 348 |
+
"title": doc.get("_fast_title", ""),
|
| 349 |
+
"description": doc.get("_fast_desc", ""),
|
| 350 |
+
"url": doc.get("url", ""),
|
| 351 |
+
"domain": doc.get("_fast_domain", ""),
|
| 352 |
+
"match_score": round(final_score, 4)
|
| 353 |
+
}
|
| 354 |
+
if debug:
|
| 355 |
+
doc_out["_debug_features"] = features
|
| 356 |
+
doc_out["_debug_intent"] = intent_info
|
| 357 |
+
|
| 358 |
+
scored_results.append((final_score, doc_out, base_url, doc["_fast_domain"]))
|
| 359 |
+
|
| 360 |
+
scored_results.sort(key=lambda x: x[0], reverse=True)
|
| 361 |
+
|
| 362 |
+
diversified_results = []
|
| 363 |
+
for score, doc_info, b_url, domain in scored_results:
|
| 364 |
+
if seen_domains[domain] >= MAX_RESULTS_PER_DOMAIN: continue
|
| 365 |
+
seen_domains[domain] += 1
|
| 366 |
+
seen_urls.add(b_url)
|
| 367 |
+
diversified_results.append(doc_info)
|
| 368 |
+
if len(diversified_results) >= STRICT_RESULT_LIMIT: break # STRICT LIMIT
|
| 369 |
+
|
| 370 |
+
# Assign final rank
|
| 371 |
+
for idx, item in enumerate(diversified_results): item["rank"] = idx + 1
|
| 372 |
+
|
| 373 |
+
return diversified_results, intent_info
|
| 374 |
+
|
| 375 |
+
# --- INITIALIZATION ---
|
| 376 |
+
search_index = HybridSearchIndex()
|
| 377 |
|
| 378 |
def load_and_index_dataset():
|
|
|
|
|
|
|
| 379 |
dataset_path = "dataset.jsonl"
|
|
|
|
| 380 |
if HF_TOKEN:
|
| 381 |
+
try: dataset_path = hf_hub_download(repo_id="Nebulixlabs/Ultimate-Search-Engine-Dataset", filename="dataset.jsonl", repo_type="dataset", token=HF_TOKEN)
|
| 382 |
+
except Exception: pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
|
| 384 |
+
raw_data = []
|
|
|
|
|
|
|
| 385 |
if os.path.exists(dataset_path):
|
| 386 |
with open(dataset_path, "r", encoding="utf-8") as f:
|
| 387 |
for i, line in enumerate(f):
|
|
|
|
| 392 |
url = doc.get("url", "")
|
| 393 |
title_str = doc.get("title", "")
|
| 394 |
desc_str = doc.get("description", "")
|
|
|
|
| 395 |
parsed_url = urllib.parse.urlparse(url)
|
| 396 |
+
|
| 397 |
+
doc["_fast_domain"] = parsed_url.netloc.lower().replace("www.", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
doc["_fast_path"] = parsed_url.path.strip("/")
|
| 399 |
+
doc["_fast_title"] = title_str
|
| 400 |
+
doc["_fast_desc"] = desc_str
|
| 401 |
+
doc["_is_adult"] = doc.get("tag", "general").lower() in ["adult", "nsfw"] or any(aw in (title_str+desc_str+url).lower() for aw in ADULT_KEYWORDS)
|
| 402 |
raw_data.append(doc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
except: pass
|
| 404 |
+
search_index.build(raw_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
|
| 406 |
+
# --- QWEN AI GENERATION MODULE (FOR RAG ONLY) ---
|
| 407 |
def load_ai_models():
|
| 408 |
global ai_generator
|
| 409 |
print("🤖 Loading Qwen/Qwen2.5-0.5B-Instruct...")
|
|
|
|
| 411 |
model_id = "Qwen/Qwen2.5-0.5B-Instruct"
|
| 412 |
tokenizer = AutoTokenizer.from_pretrained(model_id)
|
| 413 |
|
|
|
|
| 414 |
if torch.cuda.is_available():
|
| 415 |
+
quant_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_quant_type="nf4")
|
| 416 |
+
model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=quant_config, device_map="auto")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
else:
|
| 418 |
+
model = AutoModelForCausalLM.from_pretrained(model_id, device_map="cpu", torch_dtype=torch.float32)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
|
| 420 |
+
ai_generator = pipeline("text-generation", model=model, tokenizer=tokenizer)
|
| 421 |
+
print("✅ Qwen AI Ready!")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
except Exception as e:
|
| 423 |
print(f"❌ Model Loading Failed: {e}")
|
| 424 |
|
| 425 |
+
async def run_ai_task(messages, max_tokens: int, query_key: str, skip_cache: bool = False):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
if not ai_generator: return "⚠️ AI Model is starting up."
|
| 427 |
+
if not skip_cache and query_key in AI_RESPONSE_CACHE: return AI_RESPONSE_CACHE[query_key]
|
| 428 |
+
|
| 429 |
async with ai_semaphore:
|
| 430 |
+
def _gen():
|
| 431 |
+
tokenizer = ai_generator.tokenizer
|
| 432 |
+
try: prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 433 |
+
except: prompt = "\n".join([f"{m['role'].capitalize()}: {m['content']}" for m in messages]) + "\nAssistant: "
|
| 434 |
+
|
| 435 |
+
result = ai_generator(prompt, max_new_tokens=max_tokens, do_sample=False, truncation=True, return_full_text=False)
|
| 436 |
+
return result[0]['generated_text'].strip()
|
| 437 |
+
|
| 438 |
+
response = await asyncio.to_thread(_gen)
|
| 439 |
+
if not skip_cache:
|
| 440 |
+
if len(AI_RESPONSE_CACHE) > MAX_AI_CACHE_SIZE: AI_RESPONSE_CACHE.clear()
|
| 441 |
+
AI_RESPONSE_CACHE[query_key] = response
|
| 442 |
return response
|
| 443 |
|
| 444 |
+
# --- FASTAPI APP ENDPOINTS ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
@asynccontextmanager
|
| 446 |
async def lifespan(app: FastAPI):
|
| 447 |
asyncio.create_task(asyncio.to_thread(load_and_index_dataset))
|
| 448 |
asyncio.create_task(asyncio.to_thread(load_ai_models))
|
| 449 |
yield
|
| 450 |
|
| 451 |
+
app = FastAPI(title="Nebulix Fast Hybrid Search Engine", lifespan=lifespan)
|
| 452 |
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
| 453 |
|
| 454 |
@app.get("/suggest")
|
| 455 |
async def get_suggestions(q: str = Query(..., min_length=1)):
|
| 456 |
+
q = normalize_text(q).strip()
|
| 457 |
+
matches = [word for word, count in search_index.suggestions if word.startswith(q)][:7]
|
| 458 |
return {"query": q, "suggestions": matches}
|
| 459 |
|
| 460 |
+
@app.get("/content")
|
| 461 |
+
async def get_content_endpoint(url: str = Query(...)):
|
| 462 |
+
"""Dedicated endpoint to just extract webpage content."""
|
| 463 |
+
res = await extract_page_content(url)
|
| 464 |
+
return res
|
| 465 |
|
| 466 |
+
@app.get("/search")
|
| 467 |
+
async def pure_search(q: str = Query(...), content: bool = Query(False), debug: bool = Query(False)):
|
| 468 |
req_id = str(uuid.uuid4())
|
| 469 |
+
results, intent_info = await asyncio.to_thread(search_index.search, q, debug)
|
| 470 |
+
|
| 471 |
+
# Strictly limit to 20 results output
|
| 472 |
+
top_20 = results[:STRICT_RESULT_LIMIT]
|
| 473 |
+
|
| 474 |
+
if content and top_20:
|
| 475 |
+
tasks = [extract_page_content(r["url"]) for r in top_20]
|
| 476 |
+
extracted_data = await asyncio.gather(*tasks)
|
| 477 |
+
for r, ext in zip(top_20, extracted_data):
|
| 478 |
+
r["content"] = ext.get("content", "") if ext.get("success") else ""
|
| 479 |
+
|
| 480 |
+
return {"request_id": req_id, "query": q, "results_count": len(top_20), "results": top_20}
|
| 481 |
|
| 482 |
+
async def process_aimode(q: str, include_content: bool, debug: bool):
|
| 483 |
req_id = str(uuid.uuid4())
|
|
|
|
|
|
|
| 484 |
|
| 485 |
+
# 1. Deterministic Search (Max 20 internally outputted)
|
| 486 |
+
results, intent_info = await asyncio.to_thread(search_index.search, q, debug)
|
| 487 |
+
top_20 = results[:STRICT_RESULT_LIMIT]
|
| 488 |
+
query_tokens = tokenize(q)
|
| 489 |
+
|
| 490 |
+
# Bypass AI cache if it's a News/Latest query
|
| 491 |
+
is_news = "NEWS" in intent_info["signals"]
|
| 492 |
+
|
| 493 |
+
rag_context_parts = []
|
| 494 |
used_sources = []
|
| 495 |
|
| 496 |
+
if top_20:
|
| 497 |
+
# 2. Extract content ONLY for Top ranked sources
|
| 498 |
+
tasks = [extract_page_content(r["url"]) for r in top_20]
|
| 499 |
+
extracted_data = await asyncio.gather(*tasks)
|
| 500 |
|
| 501 |
+
# 3. Deterministic Chunk Selection for Context
|
| 502 |
+
valid_sources_count = 0
|
| 503 |
+
for i, (r, ext) in enumerate(zip(top_20, extracted_data)):
|
| 504 |
+
if include_content:
|
| 505 |
+
r["content"] = ext.get("content", "") if ext.get("success") else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
|
| 507 |
+
if ext.get("success") and ext.get("content") and valid_sources_count < 5: # Build context from top 5 valid pages
|
| 508 |
+
best_chunks = rank_chunks_for_rag(ext["content"], query_tokens)
|
| 509 |
+
if best_chunks:
|
| 510 |
+
rag_context_parts.append(f"[Source {valid_sources_count+1}]\nTitle: {r['title']}\nURL: {r['url']}\nRelevant Content: {' ... '.join(best_chunks)}")
|
| 511 |
+
used_sources.append({"id": valid_sources_count+1, "title": r["title"], "url": r["url"]})
|
| 512 |
+
valid_sources_count += 1
|
| 513 |
+
|
| 514 |
+
rag_context = "\n\n".join(rag_context_parts)
|
| 515 |
+
system_prompt = (
|
| 516 |
+
"You are an intelligent search assistant. Answer the user's query DIRECTLY based ONLY on the provided Live Context Data. "
|
| 517 |
+
"Do not invent facts. If the context contains the answer, summarize it clearly and cite the sources used (e.g., [Source 1]). "
|
| 518 |
+
"If the context does not contain the answer, state that you do not have enough information."
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
messages = [
|
| 522 |
{"role": "system", "content": system_prompt},
|
| 523 |
+
{"role": "user", "content": f"Live Context Data:\n{rag_context}\n\nQuestion: {q}" if rag_context else f"Question: {q}"}
|
| 524 |
]
|
| 525 |
|
| 526 |
+
# 4. Generate AI Answer (Only Step utilizing LLM)
|
| 527 |
+
ai_reply = await run_ai_task(messages, max_tokens=150, query_key=f"ai_{q}", skip_cache=is_news)
|
| 528 |
+
|
| 529 |
+
return {
|
| 530 |
+
"request_id": req_id,
|
| 531 |
+
"query": q,
|
| 532 |
+
"ai_response": ai_reply,
|
| 533 |
+
"results_count": len(top_20),
|
| 534 |
+
"results": top_20,
|
| 535 |
+
"sources": used_sources
|
| 536 |
+
}
|
| 537 |
|
| 538 |
+
@app.get("/aimode")
|
| 539 |
+
async def ai_mode_get(q: str = Query(...), content: bool = Query(False), debug: bool = Query(False)):
|
| 540 |
+
return await process_aimode(q, content, debug)
|
| 541 |
|
| 542 |
+
@app.post("/aimode")
|
| 543 |
+
async def ai_mode_post(q: str = Form(...), content: bool = Form(False), debug: bool = Form(False)):
|
| 544 |
+
return await process_aimode(q, content, debug)
|
| 545 |
|
| 546 |
if __name__ == "__main__":
|
| 547 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|