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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +508 -252
app.py CHANGED
@@ -1,295 +1,551 @@
 
 
 
 
 
1
  import json
2
  import uuid
3
  import asyncio
4
  import os
5
  import uvicorn
6
  import re
 
 
 
 
7
  import multiprocessing
8
- import httpx
9
- import urllib.parse
10
- import xml.etree.ElementTree as ET
11
- from collections import deque
12
- from contextlib import asynccontextmanager
13
  from datetime import datetime
14
- from fastapi import FastAPI, Query, BackgroundTasks, Request
 
 
15
  from fastapi.middleware.cors import CORSMiddleware
16
- from huggingface_hub import hf_hub_download
17
-
18
- # Elasticsearch Import
19
- from elasticsearch import AsyncElasticsearch, helpers
20
-
21
- # AI Models Imports
22
- import torch
23
- from transformers import pipeline
24
-
25
- # --- 1. SETUP & CONFIGURATION ---
26
- TOTAL_CORES = multiprocessing.cpu_count()
27
- AI_CORES = max(1, int(TOTAL_CORES * 0.8))
28
- torch.set_num_threads(AI_CORES)
29
-
30
- print(f"🖥️ Allocating {AI_CORES} cores to AI.")
31
-
32
- # Initialize Elasticsearch Client
33
- es = AsyncElasticsearch(["http://localhost:9200"])
34
- INDEX_NAME = "nebulix_search_index"
35
-
36
- MAX_CONCURRENT_AI = max(1, AI_CORES // 2)
37
- ai_semaphore = asyncio.Semaphore(MAX_CONCURRENT_AI)
38
- ai_generator = None
39
-
40
- ADULT_KEYWORDS = {"porn", "sex", "nude", "nsfw", "xxx", "adult", "erotic"}
41
- HF_TOKEN = os.getenv("HF_TOKEN")
42
-
43
- # --- 2. EXTERNAL SMART APIs ---
44
- async def get_currency_conversion(amount, base, target):
45
- try:
46
- url = f"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/{base}.json"
47
- async with httpx.AsyncClient() as client:
48
- resp = await client.get(url, timeout=5.0)
49
- if resp.status_code == 200:
50
- rate = resp.json()[base][target]
51
- return {"type": "currency", "content": f"💱 {amount} {base.upper()} = {float(amount) * rate:.2f} {target.upper()}"}
52
- except Exception: pass
53
- return None
54
-
55
- async def get_stock_crypto_price(query):
56
  try:
57
- search_url = f"https://query2.finance.yahoo.com/v1/finance/search?q={urllib.parse.quote(query)}"
58
- async with httpx.AsyncClient() as client:
59
- resp = await client.get(search_url, headers={'User-Agent': 'Mozilla/5.0'}, timeout=5.0)
60
- if resp.status_code == 200 and resp.json()['quotes']:
61
- quote = resp.json()['quotes'][0]
62
- symbol = quote.get('symbol')
63
- price_url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
64
- price_resp = await client.get(price_url, headers={'User-Agent': 'Mozilla/5.0'}, timeout=5.0)
65
- if price_resp.status_code == 200:
66
- data = price_resp.json()['chart']['result'][0]['meta']
67
- return {"type": "finance", "content": f"📈 {quote.get('shortname')} ({symbol}) Price: {data['regularMarketPrice']} {data['currency']}"}
68
- except Exception: pass
69
- return None
70
-
71
- async def perform_translation(text: str, target_lang: str, source_lang: str = "auto"):
72
- try:
73
- url = "https://translate.googleapis.com/translate_a/single"
74
- params = {"client": "gtx", "sl": source_lang, "tl": target_lang, "dt": "t", "q": text}
75
- async with httpx.AsyncClient() as client:
76
- resp = await client.get(url, params=params, timeout=5.0)
77
- if resp.status_code == 200:
78
- translated_text = "".join([s[0] for s in resp.json()[0]])
79
- return {"source_lang": source_lang, "target_lang": target_lang, "original": text, "translated": translated_text}
80
- except Exception: pass
81
- return None
82
-
83
- async def analyze_query_for_apis(query: str):
84
- q_lower = query.lower().strip()
 
 
 
 
 
85
 
86
- translate_match = re.search(r'^translate\s+(.*?)(?:\s+to\s+([a-zA-Z]+))?$', q_lower)
87
- if translate_match:
88
- return {"type": "translation", "content": f"🗣️ Translation: {(await perform_translation(translate_match.group(1).strip(), translate_match.group(2) or 'en'))['translated']}"}
89
-
90
- curr_match = re.search(r'convert\s+([0-9.]+)\s+([a-z]{3})\s+(?:to|in)\s+([a-z]{3})', q_lower)
91
- if curr_match:
92
- return await get_currency_conversion(*curr_match.groups())
93
-
94
- if any(k in q_lower for k in ["price of", "stock price", "crypto price"]):
95
- return await get_stock_crypto_price(q_lower.replace("price of", "").replace("stock price", "").replace("crypto price", "").strip())
96
-
97
- if "time" in q_lower and "now" in q_lower:
98
- return {"type": "time", "content": f"⏰ Current Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"}
99
-
100
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- # --- 3. ELASTICSEARCH DATASET LOAD & INDEX ---
103
- async def load_and_index_dataset():
104
- print("📥 Downloading Private Dataset from Hugging Face...")
105
- dataset_path = "dataset.jsonl"
106
 
107
- if HF_TOKEN:
108
- try:
109
- dataset_path = await asyncio.to_thread(
110
- hf_hub_download,
111
- repo_id="Nebulixlabs/Ultimate-Search-Engine-Dataset",
112
- filename="dataset.jsonl",
113
- repo_type="dataset",
114
- token=HF_TOKEN
115
- )
116
- print("✅ Private Dataset Downloaded Successfully!")
117
- except Exception as e:
118
- print(f"⚠️ Hugging Face Download failed: {e}")
119
-
120
- # Check if index exists, if not create with SEO boosting map
121
- if not await es.indices.exists(index=INDEX_NAME):
122
- print("⚡ Creating Elasticsearch Index...")
123
- await es.indices.create(index=INDEX_NAME, body={
124
- "mappings": {
125
- "properties": {
126
- "title": {"type": "text", "analyzer": "standard"},
127
- "description": {"type": "text", "analyzer": "standard"},
128
- "tag": {"type": "keyword"}, # Keyword is exact match
129
- "url": {"type": "keyword"}
130
- }
131
- }
132
- })
133
- else:
134
- print("⚡ Elasticsearch Index already exists. Ready.")
135
 
136
- print("🔄 Loading Data into Elasticsearch...")
137
- actions = []
 
 
138
 
139
- if os.path.exists(dataset_path):
140
- with open(dataset_path, "r", encoding="utf-8") as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  for line in f:
142
  if line.strip():
143
- try:
144
- doc = json.loads(line)
145
- action = {
146
- "_op_type": "index",
147
- "_index": INDEX_NAME,
148
- "_source": doc # Pushing ENTIRE raw dictionary
149
- }
150
- actions.append(action)
151
- except: pass
152
 
153
- if actions:
154
- await helpers.async_bulk(es, actions)
155
- print(f"🚀 Indexed {len(actions)} records into Elasticsearch!")
156
- else:
157
- print("❌ Dataset file not found!")
158
-
159
- def load_ai_models():
160
- global ai_generator
161
- print("🤖 Loading RAG AI Model...")
162
- try:
163
- ai_generator = pipeline("text-generation", model="Qwen/Qwen2.5-0.5B-Instruct", device=-1, dtype=torch.float32)
164
- print("✅ AI Model Loaded Successfully!")
165
- except Exception as e:
166
- print("❌ AI Model Failed:", e)
167
-
168
- # --- 4. ELASTICSEARCH SEARCH LOGIC (SEO & SMART FILTER) ---
169
- async def perform_core_search(query: str):
170
- if not await es.indices.exists(index=INDEX_NAME):
171
- return [], "Search index not ready.", 0
172
 
173
- user_wants_adult = any(word in query.lower() for word in ADULT_KEYWORDS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
- # 🎯 Google-Grade SEO Query Construction
176
- es_query = {
177
- "query": {
178
- "bool": {
179
- "must": [
180
- {
181
- "multi_match": {
182
- "query": query,
183
- # Field Boosting for SEO: Title gets 4x points, Desc 2x, Meta 1x
184
- "fields": ["title^4", "description^2", "meta_tags.description^1", "url"],
185
- "fuzziness": "AUTO" # Auto typo correction (e.g. githb -> github)
186
- }
187
- }
188
- ],
189
- "must_not": []
190
- }
191
- },
192
- "size": 100 # Can fetch up to 100 dynamically based on match
193
- }
194
-
195
- # 🛡️ Smart Tag Filter (Hide adult content if user didn't ask for it)
196
- if not user_wants_adult:
197
- es_query["query"]["bool"]["must_not"].extend([
198
- {"match": {"tag": "adult"}},
199
- {"match": {"tag": "nsfw"}}
200
- ])
201
 
202
- response = await es.search(index=INDEX_NAME, body=es_query)
203
- hits = response["hits"]["hits"]
204
 
205
- best_score = hits[0]["_score"] if hits else 0
206
- prediction = "High confidence" if best_score > 10 else "Medium confidence" if best_score > 3 else "Low confidence"
207
-
208
- final_results = []
209
- for hit in hits:
210
- # RETURNING THE ENTIRE DOCUMENT (Favicon, images, specific_data, etc.)
211
- doc = hit["_source"]
212
- doc["match_score"] = hit["_score"] # Add ES score for frontend
213
- final_results.append(doc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
- return final_results, prediction, best_score
216
-
217
- # --- AI GENERATION HELPER ---
218
- def _generate_text(messages, max_tokens: int):
219
- result = ai_generator(messages, max_new_tokens=max_tokens, do_sample=True, temperature=0.3, top_p=0.85)
220
- return result[0]['generated_text'][-1]['content']
221
-
222
- async def run_ai_task(messages, max_tokens: int):
223
- if not ai_generator: return "⚠️ AI Model is starting up."
224
- async with ai_semaphore:
225
- return await asyncio.to_thread(_generate_text, messages, max_tokens)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
- # --- 5. FASTAPI APP & ENDPOINTS ---
 
 
228
  @asynccontextmanager
229
  async def lifespan(app: FastAPI):
230
- # Run setup tasks
231
- await load_and_index_dataset()
232
- asyncio.create_task(asyncio.to_thread(load_ai_models))
233
  yield
234
- await es.close()
235
 
236
- app = FastAPI(title="Nebulix Search Engine (Powered by Elasticsearch)", lifespan=lifespan)
237
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
238
 
239
- @app.get("/translate")
240
- async def translate_api(q: str = Query(...), target: str = Query("en"), source: str = Query("auto")):
241
- result = await perform_translation(q, target, source)
242
- return {"status": "success", "data": result} if result else {"status": "error"}
 
 
 
 
 
243
 
244
  @app.get("/search")
245
- async def pure_search(q: str = Query(...)):
246
- req_id = str(uuid.uuid4())
247
- smart_data = await analyze_query_for_apis(q)
248
- results, prediction, _ = await perform_core_search(q)
249
 
250
- return {
251
- "request_id": req_id,
252
- "query": q,
253
- "smart_widget": smart_data,
254
- "results_count": len(results),
255
- "results": results # Now returns ENTIRE JSON structure including Favicon, Videos, Tag, etc.
256
- }
257
 
258
  @app.get("/aimode")
259
- async def ai_mode_short(q: str = Query(...)):
260
- req_id = str(uuid.uuid4())
261
- smart_data = await analyze_query_for_apis(q)
262
- results, _, _ = await perform_core_search(q)
263
 
264
- if not results and not smart_data:
265
- return {"request_id": req_id, "ai_response": "I couldn't find any results.", "results": []}
266
-
267
- context = (f"Live Info: {smart_data['content']}. " if smart_data else "") + " ".join([f"{r.get('title', '')} - {r.get('description', '')}" for r in results[:2]])
268
- messages = [
269
- {"role": "system", "content": "Provide a brief 2-3 sentence summary based only on the provided context."},
270
- {"role": "user", "content": f"Query: {q}\nContext: {context}"}
271
- ]
272
 
273
- ai_reply = await run_ai_task(messages, max_tokens=150)
274
- return {"request_id": req_id, "ai_response": ai_reply, "smart_widget": smart_data, "results": results}
 
 
 
 
 
 
 
 
 
 
 
275
 
276
  @app.get("/aimodechat")
277
- async def ai_mode_chat(q: str = Query(...)):
278
- req_id = str(uuid.uuid4())
279
- smart_data = await analyze_query_for_apis(q)
280
- results, _, _ = await perform_core_search(q)
281
 
282
- context_parts = [f"LIVE SYSTEM DATA: {smart_data['content']}"] if smart_data else []
283
- for i, r in enumerate(results[:5]):
284
- context_parts.append(f"Source [{i+1}]: {r.get('title')} - {r.get('description')}")
 
 
 
 
 
 
285
 
286
- messages = [
287
- {"role": "system", "content": "You are a Smart AI Search Assistant. Use live data and search sources to answer accurately."},
288
- {"role": "user", "content": f"User Query: {q}\n\nContext & Sources:\n{chr(10).join(context_parts)}"}
289
- ]
290
-
291
- ai_reply = await run_ai_task(messages, max_tokens=400)
292
- return {"request_id": req_id, "ai_response": ai_reply, "smart_widget": smart_data, "results": results}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
  if __name__ == "__main__":
295
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
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
9
  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")