Nebulixlabs commited on
Commit
9d49e1b
·
verified ·
1 Parent(s): cf12de7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +302 -512
app.py CHANGED
@@ -1,8 +1,3 @@
1
- # ==============================================================================
2
- # NEBULIX SEARCH ENGINE v6 FINAL (PRODUCTION READY)
3
- # Target: 2 vCPU • 16 GB RAM • True Agentic RAG • AI Streaming • SQLite Analytics
4
- # Includes Hugging Face Dataset Integration & SmolLM2 (Million Params) integration
5
- # ==============================================================================
6
  import json
7
  import uuid
8
  import asyncio
@@ -10,542 +5,337 @@ import os
10
  import uvicorn
11
  import re
12
  import time
13
- import pickle
14
- import sqlite3
15
- import unicodedata
16
  import multiprocessing
17
- import numpy as np
18
- import psutil
19
  from datetime import datetime
20
  from contextlib import asynccontextmanager
21
- from fastapi import FastAPI, Query, Request, Form, BackgroundTasks
22
- from fastapi.responses import JSONResponse, StreamingResponse
23
  from fastapi.middleware.cors import CORSMiddleware
 
24
  from rank_bm25 import BM25Okapi
25
- from cachetools import TTLCache, LRUCache
26
- import hashlib
27
-
28
- # --- ML & Data Imports ---
29
- try:
30
- from datasets import load_dataset
31
- from llama_cpp import Llama
32
- from sentence_transformers import SentenceTransformer
33
- ML_AVAILABLE = True
34
- except ImportError:
35
- ML_AVAILABLE = False
36
- print("⚠️ ML packages missing. Run: pip install llama-cpp-python sentence-transformers psutil numpy datasets")
37
-
38
- # ==============================================================================
39
- # 25. CPU ALLOCATION (60% Search, 40% AI on 2 vCPUs)
40
- # ==============================================================================
41
- os.environ["OMP_NUM_THREADS"] = "1"
42
- os.environ["OPENBLAS_NUM_THREADS"] = "1"
43
- os.environ["MKL_NUM_THREADS"] = "1"
44
-
45
- # ==============================================================================
46
- # 22. SQLITE ANALYTICS SETUP
47
- # ==============================================================================
48
- DB_PATH = "analytics.db"
49
- def init_db():
50
- conn = sqlite3.connect(DB_PATH)
51
- conn.execute('CREATE TABLE IF NOT EXISTS clicks (url TEXT, type TEXT, ts DATETIME)')
52
- conn.execute('CREATE TABLE IF NOT EXISTS logs (query TEXT, latency REAL, ts DATETIME)')
53
- conn.execute('CREATE TABLE IF NOT EXISTS planner_logs (query TEXT, hops INTEGER, decision_log TEXT, ts DATETIME)')
54
- conn.commit()
55
- conn.close()
56
-
57
- def log_to_db(query, args=()):
58
- try:
59
- conn = sqlite3.connect(DB_PATH)
60
- conn.execute(query, args)
61
- conn.commit()
62
- conn.close()
63
- except Exception as e: pass
64
-
65
- # ==============================================================================
66
- # GLOBAL DATA & CACHES
67
- # ==============================================================================
68
  raw_data = []
69
- url_to_data = {}
70
  bm25_index = None
71
- trie_root = {}
72
-
73
- SEARCH_CACHE = TTLCache(maxsize=10000, ttl=1800) # 30 Min TTL
74
- AI_ANSWER_CACHE = LRUCache(maxsize=2000)
75
-
76
- doc_embeddings = None
77
- text_embedder = None
78
- llm = None
79
- AI_SEMAPHORE = asyncio.Semaphore(1)
80
-
81
- CLICK_TRACKER = {}
82
- STOP_WORDS = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "is"}
83
-
84
- # ==============================================================================
85
- # MATH & SPECIAL TAGS
86
- # ==============================================================================
87
- def update_dynamic_fields(doc):
88
- human = doc.get("human_opens", 0)
89
- ai = doc.get("ai_opens", 0)
90
- crawl = doc.get("crawler_opens", 0)
91
- bot = doc.get("bot_opens", 0)
92
-
93
- doc["total_opens"] = human + ai + crawl + bot
94
- doc["popularity"] = human + (ai * 2) + (crawl * 0.1) + (bot * 0)
95
-
96
- total_interactive = human + ai
97
- ai_percent = (ai / total_interactive * 100) if total_interactive > 0 else 0
98
- doc["ai_open_percent"] = ai_percent
99
-
100
- tags = set()
101
- if doc.get("trust_score", 50) >= 80: tags.add("Trusted")
102
- if doc.get("quality_score", 50) >= 80: tags.add("High Quality")
103
- if doc.get("domain_quality", 50) >= 90: tags.add("Verified")
104
- if doc.get("total_opens", 0) >= 500: tags.add("Popular")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- if ai >= 25: tags.add("Opened by AI")
107
- if total_interactive > 10 and ai_percent >= 80: tags.add("AI Favorite")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
- doc["special_tags"] = list(tags)
110
-
111
- # ==============================================================================
112
- # TOKENIZER & UTILS
113
- # ==============================================================================
114
- def normalize_text(text: str) -> str:
115
- if not text: return ""
116
- text = unicodedata.normalize('NFKD', str(text)).encode('ASCII', 'ignore').decode('utf-8')
117
- return re.sub(r'<[^>]+>', '', text.lower())
118
 
119
- def tokenize(text: str) -> list:
120
- words = re.findall(r'\b[a-z0-9]+\b', normalize_text(text))
121
- return [w for w in words if w not in STOP_WORDS]
122
 
123
- def add_to_trie(word):
124
- node = trie_root
125
- for char in word:
126
- if char not in node: node[char] = {}
127
- node = node[char]
128
- node['_end'] = True
129
 
130
- def cosine_similarity(vec_a, mat_b):
131
- if len(mat_b) == 0: return np.array([])
132
- return np.dot(mat_b, vec_a) / (np.linalg.norm(mat_b, axis=1) * np.linalg.norm(vec_a) + 1e-10)
133
 
134
- def detect_language(text):
135
- return "English"
136
 
137
- # ==============================================================================
138
- # DATASET LOADING & NEW SCHEMA MAPPING
139
- # ==============================================================================
140
- def build_searchable_text(doc):
141
- """Combines title, description, tag, and string values from meta/og tags into one text."""
142
- parts = [doc.get("title", ""), doc.get("description", ""), doc.get("tag", "")]
143
-
144
- for k, v in doc.get("meta_tags", {}).items():
145
- if isinstance(v, str): parts.append(v)
146
-
147
- for k, v in doc.get("og_tags", {}).items():
148
- if isinstance(v, str): parts.append(v)
149
 
150
- return " ".join(p for p in parts if p).strip()
151
-
152
- def build_runtime_record(doc):
153
- r = doc.copy()
154
-
155
- r["meta_tags"] = doc.get("meta_tags", {})
156
- r["og_tags"] = doc.get("og_tags", {})
157
- r["images"] = doc.get("images", [])
158
- r["videos"] = doc.get("videos", [])
159
- r["documents"] = doc.get("documents", [])
160
- r["tag"] = doc.get("tag", "general")
161
- r["specific_data"] = doc.get("specific_data", {})
162
- r["searchable_text"] = build_searchable_text(r)
163
-
164
- r["human_opens"] = r.get("human_opens", 0)
165
- r["ai_opens"] = r.get("ai_opens", 0)
166
- r["crawler_opens"] = r.get("crawler_opens", 0)
167
- r["bot_opens"] = r.get("bot_opens", 0)
168
- r["trust_score"] = r.get("trust_score", 50.0)
169
- r["quality_score"] = r.get("quality_score", 50.0)
170
- r["learning_score"] = r.get("learning_score", 50.0)
171
- r["search_boost"] = r.get("search_boost", 0.0)
172
- r["spam_score"] = r.get("spam_score", 0.0)
173
- r["domain_quality"] = r.get("domain_quality", 50.0)
174
- r["last_opened"] = r.get("last_opened", datetime.utcnow().isoformat())
175
- r["language"] = r.get("language", detect_language(r.get("title", "")))
176
- r["country"] = r.get("country", "Global")
177
-
178
- update_dynamic_fields(r)
179
- return r
180
-
181
- def init_system():
182
- global bm25_index, doc_embeddings, text_embedder, llm, raw_data, url_to_data
183
- init_db()
184
- print("🚀 [1/4] Fetching Dataset & Building Indexes...")
185
-
186
- CACHE_FILE = "cached_dataset.jsonl"
187
-
188
- if not os.path.exists(CACHE_FILE):
189
- hf_token = os.getenv("HF_TOKEN")
190
- if not hf_token:
191
- raise ValueError("❌ HF_TOKEN environment variable is missing. Cannot fetch dataset.")
192
- print("☁️ Downloading dataset from Hugging Face...")
193
- dataset = load_dataset("Nebulixlabs/Ultimate-Search-Engine-Dataset", split="train", token=hf_token)
194
- dataset.to_json(CACHE_FILE)
195
- print("✅ Dataset successfully cached locally.")
196
-
197
- if os.path.exists(CACHE_FILE):
198
- corpus_tokens = []
199
- texts_to_embed = []
200
- with open(CACHE_FILE, "r", encoding="utf-8") as f:
201
- for line in f:
202
- if line.strip():
203
- doc = build_runtime_record(json.loads(line))
204
- raw_data.append(doc)
205
- url_to_data[doc["url"]] = doc
206
-
207
- tokens = tokenize(doc["searchable_text"])
208
- corpus_tokens.append(tokens)
209
- texts_to_embed.append(doc["searchable_text"])
210
- for t in tokens: add_to_trie(t)
211
-
212
- bm25_index = BM25Okapi(corpus_tokens)
213
 
214
- if ML_AVAILABLE:
215
- print("🧠 [2/4] Loading BAAI/bge-small-en-v1.5...")
216
- text_embedder = SentenceTransformer('BAAI/bge-small-en-v1.5', device='cpu')
217
-
218
- if os.path.exists("embeddings.bin"):
219
- print("💾 [3/4] Loading Precomputed Embeddings...")
220
- with open("embeddings.bin", "rb") as fb: doc_embeddings = pickle.load(fb)
221
- else:
222
- print("⚙️ [3/4] Generating Embeddings (One-Time)...")
223
- doc_embeddings = text_embedder.encode(texts_to_embed, convert_to_numpy=True)
224
- with open("embeddings.bin", "wb") as fb: pickle.dump(doc_embeddings, fb)
225
- with open("metadata.json", "w") as fm: json.dump({"count": len(doc_embeddings)}, fm)
226
-
227
- # =============== CHANGED TO SMOLLM2 (360 Million Params) ===============
228
- print("🔥 [4/4] Loading Llama.cpp SmolLM2-360M GGUF (Lightweight CPU Model)...")
229
- try:
230
- llm = Llama(
231
- model_path="./models/smollm2-360m-instruct-q8_0.gguf", # Update filename if downloaded differently
232
- n_threads=1,
233
- n_ctx=3000,
234
- verbose=False
235
- )
236
- except:
237
- print("⚠️ LLM not found locally in ./models/, mocked mode active.")
238
- print("✅ Nebulix v6 Ready.")
239
-
240
- # ==============================================================================
241
- # RANKING FORMULA
242
- # ==============================================================================
243
- def get_ranked_results(query: str):
244
- q_tokens = tokenize(query)
245
- if not q_tokens: return []
246
-
247
- bm25_scores = bm25_index.get_scores(q_tokens) if bm25_index else np.zeros(len(raw_data))
248
- semantic_scores = np.zeros(len(raw_data))
249
-
250
- if text_embedder and doc_embeddings is not None:
251
- q_vec = text_embedder.encode([query])[0]
252
- semantic_scores = cosine_similarity(q_vec, doc_embeddings)
253
-
254
- scored_results = []
255
-
256
- for i, doc in enumerate(raw_data):
257
- score = (bm25_scores[i] * 0.35) + \
258
- (semantic_scores[i] * 0.45) + \
259
- (doc["trust_score"] * 0.05) + \
260
- (doc["quality_score"] * 0.05) + \
261
- (doc["learning_score"] * 0.03) + \
262
- (doc["popularity"] * 0.05) + \
263
- doc["search_boost"] - \
264
- doc["spam_score"]
265
-
266
- scored_results.append((score, doc))
267
-
268
- scored_results.sort(key=lambda x: x[0], reverse=True)
269
-
270
- formatted = []
271
- for s, d in scored_results[:100]:
272
- formatted.append({
273
- "url": d["url"], "title": d["title"], "description": d["description"],
274
- "favicon": d.get("favicon", ""), "language": d["language"], "country": d["country"],
275
- "images": d.get("images", []), "videos": d.get("videos", []),
276
- "tag": d.get("tag", "general"),
277
- "last_opened": d["last_opened"], "trust_score": round(d["trust_score"], 2),
278
- "quality_score": round(d["quality_score"], 2), "learning_score": round(d["learning_score"], 2),
279
- "human_opens": d["human_opens"], "ai_opens": d["ai_opens"],
280
- "total_opens": d["total_opens"], "special_tags": d["special_tags"],
281
- "match_score": round(s, 2),
282
- "searchable_text": d.get("searchable_text", "")
283
- })
284
- return formatted
285
-
286
- # ==============================================================================
287
- # CHUNKING
288
- # ==============================================================================
289
- def get_best_chunks(query, full_text):
290
- if not full_text: return []
291
- words = full_text.split()
292
- chunks = []
293
- i = 0
294
- while i < len(words):
295
- chunks.append(" ".join(words[i:i + 512]))
296
- i += (512 - 64)
297
 
298
- if not chunks: return []
299
- if text_embedder:
300
- q_emb = text_embedder.encode([query], convert_to_numpy=True)[0]
301
- c_embs = text_embedder.encode(chunks, convert_to_numpy=True)
302
- scores = cosine_similarity(q_emb, c_embs)
303
- best_idx = np.argsort(scores)[::-1][:5]
304
- return [chunks[idx] for idx in best_idx]
305
- return chunks[:5]
306
-
307
- # ==============================================================================
308
- # TRUE AGENTIC RAG PLANNER (Max 10 Hops)
309
- # ==============================================================================
310
- async def agent_planner_call(prompt):
311
- if not llm: return "ACTION: ANSWER"
312
- res = await asyncio.to_thread(llm, prompt, max_tokens=20, temperature=0.1, stop=["\n"])
313
- return res["choices"][0]["text"].strip()
314
-
315
- async def execute_agentic_rag(query, candidates, debug=False):
316
- context_memory = ""
317
- sources_used = []
318
- visited_idx = set()
319
- debug_log = []
320
-
321
- metadata_list = "\n".join([f"ID {i} | URL: {d['url']} | Title: {d['title']}" for i, d in enumerate(candidates[:20])])
322
-
323
- async with AI_SEMAPHORE:
324
- for hop in range(10):
325
- # Prompt simplified slightly for million-parameter models to ensure strict output
326
- sys_prompt = f"""System: You are a Planner. Output ONLY the action command.
327
- Query: {query}
328
- URLs:
329
- {metadata_list}
330
- Memory:
331
- {context_memory if context_memory else 'Empty'}
332
- To read a URL, type: OPEN(id)
333
- To answer, type: ANSWER
334
- Action: """
335
-
336
- decision = await agent_planner_call(sys_prompt)
337
- debug_log.append(f"Hop {hop+1}: {decision}")
338
-
339
- open_match = re.search(r'OPEN\((\d+)\)', decision, re.IGNORECASE)
340
- if open_match:
341
- idx = int(open_match.group(1))
342
- if idx < len(candidates) and idx not in visited_idx:
343
- visited_idx.add(idx)
344
- doc = candidates[idx]
345
-
346
- target_text = doc.get("searchable_text", "") or doc.get("description", "")
347
- best_chunks = get_best_chunks(query, target_text)
348
- context_memory += f"\n[From {doc['url']}]:\n" + "\n".join(best_chunks) + "\n"
349
-
350
- if doc["url"] in url_to_data:
351
- url_to_data[doc["url"]]["ai_opens"] += 1
352
- url_to_data[doc["url"]]["learning_score"] += 1.0
353
- update_dynamic_fields(url_to_data[doc["url"]])
354
-
355
- sources_used.append({
356
- "url": doc["url"], "title": doc["title"], "language": doc["language"],
357
- "country": doc["country"], "last_opened": doc["last_opened"],
358
- "human_opens": doc["human_opens"], "ai_opens": doc["ai_opens"],
359
- "trust_score": doc["trust_score"], "quality_score": doc["quality_score"],
360
- "learning_score": doc["learning_score"], "special_tags": doc["special_tags"],
361
- "favicon": doc["favicon"]
362
- })
363
- else:
364
- context_memory += "\n(System: ID already read. Choose another or ANSWER.)"
365
- else:
366
- break
367
 
368
- log_to_db("INSERT INTO planner_logs (query, hops, decision_log, ts) VALUES (?, ?, ?, ?)",
369
- (query, len(visited_idx), " | ".join(debug_log), datetime.utcnow()))
 
370
 
371
- final_prompt = f"System: Use ONLY the provided Memory to answer the query.\nQuery: {query}\nMemory:\n{context_memory}\n\nProvide Answer:\n"
 
 
 
 
 
 
372
 
373
- return final_prompt, sources_used, debug_log
374
-
375
- # ==============================================================================
376
- # FASTAPI APP & ENDPOINTS
377
- # ==============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  @asynccontextmanager
379
  async def lifespan(app: FastAPI):
380
- asyncio.create_task(asyncio.to_thread(init_system))
381
- yield
382
-
383
- app = FastAPI(title="Nebulix v6 Core", lifespan=lifespan)
384
- app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
385
-
386
  @app.middleware("http")
387
- async def analytics_middleware(request: Request, call_next):
388
- start_time = time.perf_counter()
389
- response = await call_next(request)
390
- latency = (time.perf_counter() - start_time) * 1000
391
- if "/search" in request.url.path or "/aimode" in request.url.path:
392
- q = request.query_params.get("q", "")
393
- log_to_db("INSERT INTO logs (query, latency, ts) VALUES (?, ?, ?)", (q, latency, datetime.utcnow()))
394
- return response
395
-
 
 
 
 
 
 
 
 
 
 
 
 
396
  @app.get("/search")
397
- async def api_search(q: str = Query(...)):
398
- cache_key = q.lower()
399
- if cache_key in SEARCH_CACHE: return SEARCH_CACHE[cache_key]
400
-
401
- results = get_ranked_results(q)
402
- for r in results: r.pop("searchable_text", None)
403
-
404
- resp = {"query": q, "results": results}
405
- SEARCH_CACHE[cache_key] = resp
406
- return resp
407
-
408
  @app.get("/aimode")
409
- async def api_aimode(q: str = Query(...), debug: bool = False):
410
- q_hash = hashlib.md5(q.encode()).hexdigest()
411
- if q_hash in AI_ANSWER_CACHE: return AI_ANSWER_CACHE[q_hash]
412
-
413
- results = get_ranked_results(q)
414
- final_prompt, sources, debug_log = await execute_agentic_rag(q, results, debug)
415
-
416
- if not llm: return {"Answer": "Mocked", "Sources": sources}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
- async with AI_SEMAPHORE:
419
- res = await asyncio.to_thread(llm, final_prompt, max_tokens=1024, temperature=0.2)
420
- answer = res["choices"][0]["text"].strip()
421
 
422
- resp = {
423
- "Answer": answer[:150] + "...",
424
- "Summary": answer[:300],
425
- "Detailed Explanation": answer,
426
- "Sources": sources
427
- }
428
- if debug: resp["Debug_Planner"] = debug_log
429
- AI_ANSWER_CACHE[q_hash] = resp
430
- return resp
431
-
432
- @app.get("/aimodechat")
433
- async def api_aimodechat(q: str = Query(...)):
434
- results = get_ranked_results(q)
435
- final_prompt, sources, _ = await execute_agentic_rag(q, results)
436
 
437
- async def true_event_stream():
438
- if llm:
439
- async with AI_SEMAPHORE:
440
- for chunk in llm(final_prompt, max_tokens=1024, stream=True, temperature=0.2):
441
- text = chunk["choices"][0]["text"]
442
- if text: yield f"data: {text}\n\n"
443
- else:
444
- yield "data: [Mocked stream due to missing LLM model]\n\n"
445
- yield f"data: [SOURCES_JSON] {json.dumps(sources)}\n\n"
446
 
447
- return StreamingResponse(true_event_stream(), media_type="text/event-stream")
448
-
449
- @app.get("/popular")
450
- async def api_popular():
451
- docs = sorted(raw_data, key=lambda x: x.get("popularity", 0), reverse=True)[:10]
452
- return {"popular": [{"url": d["url"], "title": d["title"], "popularity": d["popularity"]} for d in docs]}
453
-
454
- @app.get("/ai-popular")
455
- async def api_ai_popular():
456
- docs = sorted(raw_data, key=lambda x: x.get("ai_opens", 0), reverse=True)[:10]
457
- return {"ai_popular": [{"url": d["url"], "title": d["title"], "ai_opens": d["ai_opens"], "special_tags": d["special_tags"]} for d in docs]}
458
-
459
- @app.get("/trending")
460
- async def api_trending():
461
- docs = sorted(raw_data, key=lambda x: x.get("total_opens", 0), reverse=True)[:10]
462
- return {"trending": [{"url": d["url"], "title": d["title"]} for d in docs]}
463
-
464
- @app.get("/suggestions")
465
- async def api_suggestions(q: str = Query(...)):
466
- base_q = q.lower().split()[-1]
467
- words = []
468
- def dfs(node, prefix):
469
- if len(words) >= 5: return
470
- if node.get('_end'): words.append(prefix)
471
- for char, child in node.items():
472
- if char != '_end': dfs(child, prefix + char)
473
 
474
- node = trie_root
475
- for c in base_q:
476
- if c in node: node = node[c]
477
- else: break
478
- if node and node != trie_root: dfs(node, base_q)
479
- return {"suggestions": [q.replace(base_q, w) for w in words]}
480
-
481
- @app.get("/stats")
482
- async def api_stats():
483
- mem = psutil.virtual_memory()
484
  return {
485
- "engine": "Nebulix v6 HF Production (SmolLM2 360M)",
486
- "documents": len(raw_data),
487
- "cpu_usage_percent": psutil.cpu_percent(),
488
- "ram_usage_percent": mem.percent,
489
- "search_cache_items": len(SEARCH_CACHE),
490
- "ai_cache_items": len(AI_ANSWER_CACHE)
491
  }
492
-
493
- @app.get("/health")
494
- async def api_health(): return {"status": "ok", "version": "6.1"}
495
-
496
- @app.get("/url-info")
497
- async def api_url_info(url: str = Query(...)):
498
- if url in url_to_data:
499
- safe_doc = url_to_data[url].copy()
500
- safe_doc.pop("searchable_text", None)
501
- return safe_doc
502
- return {"error": "Not Found"}
503
-
504
- @app.post("/click")
505
- async def api_click(req: Request):
506
- data = await req.json()
507
- url = data.get("url")
508
- ip = req.client.host
509
- track_id = f"{ip}_{url}"
510
- if track_id in CLICK_TRACKER and time.time() - CLICK_TRACKER[track_id] < 1800:
511
- return {"status": "ignored_duplicate"}
512
-
513
- CLICK_TRACKER[track_id] = time.time()
514
- if url in url_to_data:
515
- d = url_to_data[url]
516
- d["human_opens"] += 1
517
- d["last_opened"] = datetime.utcnow().isoformat()
518
- update_dynamic_fields(d)
519
- log_to_db("INSERT INTO clicks (url, type, ts) VALUES (?, ?, ?)", (url, "human", datetime.utcnow()))
520
- return {"status": "success", "human_opens": d["human_opens"]}
521
- return {"error": "Not found"}
522
-
523
- @app.post("/ai-open")
524
- async def api_ai_open(req: Request):
525
- data = await req.json()
526
- url = data.get("url")
527
- if url in url_to_data:
528
- d = url_to_data[url]
529
- d["ai_opens"] += 1
530
- d["learning_score"] += 1.0
531
- d["last_opened"] = datetime.utcnow().isoformat()
532
- update_dynamic_fields(d)
533
- log_to_db("INSERT INTO clicks (url, type, ts) VALUES (?, ?, ?)", (url, "ai", datetime.utcnow()))
534
- return {"status": "success"}
535
- return {"error": "Not found"}
536
-
537
- @app.post("/feedback")
538
- async def api_feedback(req: Request):
539
- data = await req.json()
540
- url = data.get("url")
541
- if url in url_to_data:
542
- url_to_data[url]["trust_score"] += 2.0 if data.get("helpful") else -1.0
543
- update_dynamic_fields(url_to_data[url])
544
- return {"status": "feedback_registered"}
545
-
546
- @app.post("/search-log")
547
- async def api_search_log(req: Request):
548
- return {"status": "logged"}
549
-
550
- if __name__ == "__main__":
551
- uvicorn.run(app, host="0.0.0.0", port=7860, log_level="warning")
 
 
 
 
 
 
1
  import json
2
  import uuid
3
  import asyncio
 
5
  import uvicorn
6
  import re
7
  import time
 
 
 
8
  import multiprocessing
9
+ import urllib.parse
 
10
  from datetime import datetime
11
  from contextlib import asynccontextmanager
12
+ from fastapi import FastAPI, Query, Request, UploadFile, File, Form
13
+ from fastapi.responses import JSONResponse
14
  from fastapi.middleware.cors import CORSMiddleware
15
+ from huggingface_hub import hf_hub_download
16
  from rank_bm25 import BM25Okapi
17
+ import torch
18
+ from PIL import Image
19
+ import io
20
+ DIRECT IMPORTS TO FIX THE PIPELINE BUG
21
+ from transformers import pipeline, BlipProcessor, BlipForConditionalGeneration
22
+ --- 1. SETUP & CPU ALLOCATION (40% for AI) ---
23
+ TOTAL_CORES = multiprocessing.cpu_count()
24
+ AI_CORES = max(1, int(TOTAL_CORES * 0.4))
25
+ torch.set_num_threads(AI_CORES)
26
+ print(f"🖥️ Total Cores: {TOTAL_CORES} | Allocating {AI_CORES} cores strictly to AI.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  raw_data = []
 
28
  bm25_index = None
29
+ MAX_CONCURRENT_AI = max(1, AI_CORES)
30
+ ai_semaphore = asyncio.Semaphore(MAX_CONCURRENT_AI)
31
+ ai_generator = None
32
+ vision_processor = None
33
+ vision_model = None
34
+ --- AI CACHING SYSTEM ---
35
+ AI_RESPONSE_CACHE = {}
36
+ MAX_CACHE_SIZE = 5000
37
+ ADULT_KEYWORDS = {"porn", "sex", "nude", "nsfw", "xxx", "adult", "erotic", "onlyfans"}
38
+ STOP_WORDS = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "is", "are"}
39
+ HF_TOKEN = os.getenv("HF_TOKEN")
40
+ BANNED_IPS = set()
41
+ IP_TRACKER = {}
42
+ --- 2. ADVANCED NLP & IN-MEMORY INDEXING ---
43
+ def tokenize(text):
44
+ if not text: return []
45
+ tokens = re.findall(r'[a-z0-9]+', str(text).lower())
46
+ return [t for t in tokens if t not in STOP_WORDS]
47
+ def extract_nested_data(obj):
48
+ if isinstance(obj, dict):
49
+ return " ".join(extract_nested_data(v) for v in obj.values())
50
+ elif isinstance(obj, list):
51
+ return " ".join(extract_nested_data(v) for v in obj)
52
+ return str(obj)
53
+ def load_and_index_dataset():
54
+ global bm25_index, raw_data
55
+ print("📥 Loading Dataset & Building Deep Index...")
56
+ code
57
+ Code
58
+ dataset_path = "dataset.jsonl"
59
+ if HF_TOKEN:
60
+ try:
61
+ dataset_path = hf_hub_download(
62
+ repo_id="Nebulixlabs/Ultimate-Search-Engine-Dataset",
63
+ filename="dataset.jsonl",
64
+ repo_type="dataset",
65
+ token=HF_TOKEN
66
+ )
67
+ except Exception as e:
68
+ print(f"⚠️ Hugging Face Download failed: {e}")
69
+
70
+ tokenized_corpus = []
71
+
72
+ if os.path.exists(dataset_path):
73
+ with open(dataset_path, "r", encoding="utf-8") as f:
74
+ for line in f:
75
+ if line.strip():
76
+ try:
77
+ doc = json.loads(line)
78
+ raw_data.append(doc)
79
+
80
+ title_tokens = tokenize(doc.get("title", ""))
81
+ desc_tokens = tokenize(doc.get("description", ""))
82
+ meta_tokens = tokenize(doc.get("meta_tags", {}).get("description", ""))
83
+ url_tokens = tokenize(doc.get("url", ""))
84
+ specific_tokens = tokenize(extract_nested_data(doc.get("specific_data", {})))
85
+
86
+ boosted_tokens = (title_tokens * 5) + (desc_tokens * 3) + meta_tokens + url_tokens + (specific_tokens * 2)
87
+ tokenized_corpus.append(boosted_tokens)
88
+ except: pass
89
+
90
+ if tokenized_corpus:
91
+ print("⚡ Building High-Speed BM25 Okapi Index...")
92
+ bm25_index = BM25Okapi(tokenized_corpus)
93
+ print(f"🚀 Indexing Complete! {len(raw_data)} records ready.")
94
+ else:
95
+ print("❌ Dataset file not found!")
96
+ def load_ai_models():
97
+ global ai_generator, vision_processor, vision_model
98
+ print("🤖 Loading Lightweight AI Models (Text & Vision)...")
99
+ try:
100
+ # 1. Text AI
101
+ ai_generator = pipeline("text-generation", model="Qwen/Qwen2.5-0.5B-Instruct", device=-1, dtype=torch.float32)
102
+ code
103
+ Code
104
+ # 2. Vision AI
105
+ print("👁️ Loading Vision AI...")
106
+ vision_processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base")
107
+ vision_model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-base")
108
 
109
+ print("✅ All AI Models Loaded! System ready.")
110
+ except Exception as e:
111
+ print(f"❌ Model Loading Failed: {e}")
112
+ --- 3. CORE SEARCH ALGORITHM ---
113
+ async def perform_core_search(raw_query: str):
114
+ if not raw_data or not bm25_index:
115
+ return [], "Search index not ready.", 0
116
+ code
117
+ Code
118
+ site_filter = None
119
+ tag_filter = None
120
+
121
+ site_match = re.search(r'site:([^\s]+)', raw_query, re.IGNORECASE)
122
+ if site_match:
123
+ site_filter = site_match.group(1).lower()
124
+ raw_query = raw_query.replace(site_match.group(0), "")
125
 
126
+ tag_match = re.search(r'tag:([^\s]+)', raw_query, re.IGNORECASE)
127
+ if tag_match:
128
+ tag_filter = tag_match.group(1).lower()
129
+ raw_query = raw_query.replace(tag_match.group(0), "")
 
 
 
 
 
130
 
131
+ query = raw_query.strip()
132
+ query_tokens = tokenize(query)
 
133
 
134
+ if not query_tokens and not site_filter and not tag_filter:
135
+ return [], "Invalid Query", 0
 
 
 
 
136
 
137
+ user_wants_adult = any(word in ADULT_KEYWORDS for word in query_tokens)
138
+ scores = bm25_index.get_scores(query_tokens) if query_tokens else [1.0] * len(raw_data)
 
139
 
140
+ scored_results = []
141
+ seen_urls = set()
142
 
143
+ for index, score in enumerate(scores):
144
+ if score > 0.5 or not query_tokens:
145
+ doc = raw_data[index]
146
+ doc_url = doc.get("url", "")
 
 
 
 
 
 
 
 
147
 
148
+ if doc_url in seen_urls: continue
149
+ parsed_url = urllib.parse.urlparse(doc_url)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ if site_filter and site_filter not in parsed_url.netloc.lower(): continue
152
+ doc_tag = str(doc.get("tag", "general")).lower()
153
+ if tag_filter and tag_filter != doc_tag: continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
+ is_adult = doc_tag in ["adult", "nsfw"] or any(w in str(doc).lower() for w in ADULT_KEYWORDS)
156
+ if not user_wants_adult and is_adult: continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
+ final_score = score
159
+ exact_phrase = query.lower()
160
+ doc_title = str(doc.get("title", "")).lower()
161
 
162
+ if exact_phrase == doc_title: final_score *= 5.0
163
+ elif exact_phrase in doc_title: final_score *= 3.0
164
+ if exact_phrase in str(doc.get("description", "")).lower(): final_score *= 1.8
165
+
166
+ path = parsed_url.path.strip("/")
167
+ if not path: final_score *= 1.5
168
+ else: final_score *= (0.90 ** path.count("/"))
169
 
170
+ seen_urls.add(doc_url)
171
+ scored_results.append((final_score, doc))
172
+
173
+ scored_results.sort(key=lambda x: x[0], reverse=True)
174
+ best_score = scored_results[0][0] if scored_results else 0
175
+ prediction = "High confidence" if best_score > 15 else "Medium confidence" if best_score > 5 else "Low confidence"
176
+
177
+ # Yaha par hum wahi structure bana rahe hain jo frontend ko chahiye
178
+ final_results = [{"match_score": round(s, 2), **doc} for s, doc in scored_results[:100]]
179
+ return final_results, prediction, best_score
180
+ --- AI GENERATION & CACHE HELPER ---
181
+ def _generate_text(messages, max_tokens: int):
182
+ result = ai_generator(messages, max_new_tokens=max_tokens, do_sample=True, temperature=0.3, top_p=0.85)
183
+ return result[0]['generated_text'][-1]['content']
184
+ def _generate_image_review(image: Image):
185
+ inputs = vision_processor(image, return_tensors="pt")
186
+ with torch.no_grad():
187
+ out = vision_model.generate(**inputs, max_new_tokens=50)
188
+ return vision_processor.decode(out[0], skip_special_tokens=True).capitalize()
189
+ async def run_ai_task(messages, max_tokens: int, query_key: str):
190
+ if not ai_generator: return "⚠️ AI Model is starting up."
191
+ if query_key in AI_RESPONSE_CACHE: return AI_RESPONSE_CACHE[query_key]
192
+ code
193
+ Code
194
+ async with ai_semaphore:
195
+ response = await asyncio.to_thread(_generate_text, messages, max_tokens)
196
+ if len(AI_RESPONSE_CACHE) > MAX_CACHE_SIZE: AI_RESPONSE_CACHE.clear()
197
+ AI_RESPONSE_CACHE[query_key] = response
198
+ return response
199
+ --- 4. FASTAPI APP & ENDPOINTS ---
200
  @asynccontextmanager
201
  async def lifespan(app: FastAPI):
202
+ asyncio.create_task(asyncio.to_thread(load_and_index_dataset))
203
+ asyncio.create_task(asyncio.to_thread(load_ai_models))
204
+ yield
205
+ app = FastAPI(title="Nebulix Advanced SEO & Visual API Engine", lifespan=lifespan)
206
+ app.add_middleware(CORSMiddleware, allow_origins=[""], allow_credentials=True, allow_methods=[""], allow_headers=["*"])
 
207
  @app.middleware("http")
208
+ async def rate_limit_middleware(request: Request, call_next):
209
+ client_ip = request.client.host
210
+ if client_ip in BANNED_IPS:
211
+ return JSONResponse(status_code=403, content={"error": "BANNED: Your IP is permanently banned."})
212
+ code
213
+ Code
214
+ current_time = int(time.time())
215
+ if client_ip not in IP_TRACKER:
216
+ IP_TRACKER[client_ip] = {"time": current_time, "count": 1}
217
+ else:
218
+ if IP_TRACKER[client_ip]["time"] == current_time: IP_TRACKER[client_ip]["count"] += 1
219
+ else: IP_TRACKER[client_ip] = {"time": current_time, "count": 1}
220
+
221
+ if IP_TRACKER[client_ip]["count"] >= 6:
222
+ BANNED_IPS.add(client_ip)
223
+ return JSONResponse(status_code=403, content={"error": "IP BANNED: Exceeded limit."})
224
+
225
+ return await call_next(request)
226
+ @app.get("/")
227
+ async def health_check():
228
+ return {"status": "ok", "message": "Nebulix API Engine is running smoothly."}
229
  @app.get("/search")
230
+ async def pure_search(q: str = Query(...)):
231
+ req_id = str(uuid.uuid4())
232
+ results, prediction, _ = await perform_core_search(q)
233
+ return {"request_id": req_id, "query": q, "results_count": len(results), "results": results}
 
 
 
 
 
 
 
234
  @app.get("/aimode")
235
+ async def ai_mode_short(q: str = Query(...)):
236
+ req_id = str(uuid.uuid4())
237
+ results, _, _ = await perform_core_search(q)
238
+ if not results: return {"request_id": req_id, "ai_response": "I couldn't find any results.", "results": []}
239
+ code
240
+ Code
241
+ context = " ".join([f"{r.get('title', '')} - {r.get('description', '')}" for r in results[:3]])
242
+ messages = [
243
+ {"role": "system", "content": "Provide a brief 2-3 sentence summary based only on the provided context."},
244
+ {"role": "user", "content": f"Query: {q}\nContext: {context}"}
245
+ ]
246
+
247
+ ai_reply = await run_ai_task(messages, 150, query_key=f"short_{q}")
248
+ return {"request_id": req_id, "ai_response": ai_reply, "results": results}
249
+ 1. PURANA CHAT API (Current UI ke liye)
250
+ @app.get("/aimodechat")
251
+ async def ai_mode_chat(q: str = Query(...)):
252
+ req_id = str(uuid.uuid4())
253
+ results, _, _ = await perform_core_search(q)
254
+ code
255
+ Code
256
+ context_parts = [f"Source [{i+1}]: {r.get('title')} - {r.get('description')}" for i, r in enumerate(results[:5])]
257
+ messages = [
258
+ {"role": "system", "content": "You are a Smart AI Search Assistant. Use search sources to answer accurately."},
259
+ {"role": "user", "content": f"User Query: {q}\n\nContext:\n{chr(10).join(context_parts)}"}
260
+ ]
261
+
262
+ ai_reply = await run_ai_task(messages, 400, query_key=f"chat_{q}")
263
+ return {"request_id": req_id, "ai_response": ai_reply, "results": results}
264
+ 2. NAYA CHAT API (Future UI - Image + Text Chat ke liye)
265
+ @app.post("/aimodechat")
266
+ async def ai_mode_chat_vision(q: str = Form(...), file: UploadFile = File(None)):
267
+ req_id = str(uuid.uuid4())
268
+ code
269
+ Code
270
+ vision_context = ""
271
+ # Agar user ne chat me image upload ki hai, toh AI image ko decode karega
272
+ if file and vision_model:
273
+ try:
274
+ image_bytes = await file.read()
275
+ img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
276
+ async with ai_semaphore:
277
+ ai_review = await asyncio.to_thread(_generate_image_review, img)
278
+ vision_context = f"\n[User uploaded an image showing: {ai_review}]\n"
279
+ except Exception:
280
+ vision_context = "\n[User uploaded an image but it could not be analyzed.]\n"
281
+
282
+ # Search query combining text and vision
283
+ search_query = f"{q} {vision_context}".strip()
284
+ results, _, _ = await perform_core_search(search_query)
285
+
286
+ context_parts = [f"Source [{i+1}]: {r.get('title')} - {r.get('description')}" for i, r in enumerate(results[:5])]
287
+
288
+ messages = [
289
+ {"role": "system", "content": "You are a Smart AI Search Assistant capable of understanding text and visual context. Use the provided search sources and image details to answer accurately."},
290
+ {"role": "user", "content": f"User Query: {q}{vision_context}\n\nContext:\n{chr(10).join(context_parts)}"}
291
+ ]
292
+
293
+ # Generate Chat response
294
+ cache_key = f"chat_{q}_{len(vision_context)}"
295
+ ai_reply = await run_ai_task(messages, 400, query_key=cache_key)
296
+
297
+ return {"request_id": req_id, "ai_response": ai_reply, "results": results}
298
+ IMAGE SEARCH (Fixed: Same JSON structure as normal text search)
299
+ @app.post("/image-search")
300
+ async def reverse_image_search(file: UploadFile = File(...)):
301
+ if not vision_model:
302
+ return {"error": "Vision AI Model is still loading."}
303
+ code
304
+ Code
305
+ req_id = str(uuid.uuid4())
306
+
307
+ try:
308
+ image_bytes = await file.read()
309
+ img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
310
 
311
+ # 1. Vision AI process karega
312
+ async with ai_semaphore:
313
+ ai_review = await asyncio.to_thread(_generate_image_review, img)
314
 
315
+ # 2. Text Search hoga vision ke basis pe
316
+ raw_results, _, _ = await perform_core_search(ai_review)
317
+ visual_results = []
 
 
 
 
 
 
 
 
 
 
 
318
 
319
+ # 3. DIRECTLY append the original FULL document
320
+ for doc in raw_results:
321
+ is_adult = str(doc.get("tag", "general")).lower() in ["adult", "nsfw"] or any(w in str(doc).lower() for w in ADULT_KEYWORDS)
322
+ if is_adult: continue
 
 
 
 
 
323
 
324
+ # Pura document direct bhej rahe hain bina modify kiye
325
+ # jisse exact text search wala JSON format barkarar rahe
326
+ visual_results.append(doc)
327
+
328
+ if len(visual_results) >= 40: break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
+ # 4. Same structure return karenge
 
 
 
 
 
 
 
 
 
331
  return {
332
+ "request_id": req_id,
333
+ "query": f"Image Search Analysis: {ai_review}", # Taki UI me ye "query" ki tarah display ho
334
+ "results_count": len(visual_results),
335
+ "results": visual_results
 
 
336
  }
337
+
338
+ except Exception as e:
339
+ return {"error": f"Image processing failed: {str(e)}"}
340
+ if name == "main":
341
+ uvicorn.run(app, host="0.0.0.0", port=7860)