Nebulixlabs commited on
Commit
411eefc
·
verified ·
1 Parent(s): 7d8e569

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +234 -32
app.py CHANGED
@@ -3,6 +3,7 @@
3
  # Target: Hugging Face Spaces | 2 vCPU • 16 GB RAM • True Agentic RAG
4
  # Model: Qwen2.5-0.5B-Instruct (Inference Only)
5
  # Ranking: Advanced Algorithmic (Domain Trust, Proxy Penalty) + BM25 + Semantic
 
6
  # ==============================================================================
7
  import json
8
  import uuid
@@ -11,6 +12,7 @@ import os
11
  import uvicorn
12
  import re
13
  import time
 
14
  import sqlite3
15
  import unicodedata
16
  import threading
@@ -39,6 +41,7 @@ try:
39
  from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, TextIteratorStreamer
40
  from sentence_transformers import SentenceTransformer
41
 
 
42
  torch.set_num_threads(1)
43
  torch.set_grad_enabled(False) # Strict Inference Mode
44
  ML_AVAILABLE = True
@@ -47,13 +50,14 @@ except ImportError:
47
  print("⚠️ ML packages missing. Check requirements.txt")
48
 
49
  # ==============================================================================
50
- # GLOBAL SETTINGS & DATABASES
51
  # ==============================================================================
52
  DB_PATH = "analytics.db"
53
  def init_db():
54
  conn = sqlite3.connect(DB_PATH)
55
  conn.execute('CREATE TABLE IF NOT EXISTS clicks (url TEXT, type TEXT, ts DATETIME)')
56
  conn.execute('CREATE TABLE IF NOT EXISTS logs (query TEXT, latency REAL, ts DATETIME)')
 
57
  conn.commit()
58
  conn.close()
59
 
@@ -65,6 +69,9 @@ def log_to_db(query, args=()):
65
  conn.close()
66
  except: pass
67
 
 
 
 
68
  raw_data = []
69
  url_to_data = {}
70
  bm25_index = None
@@ -72,7 +79,7 @@ trie_root = {}
72
  doc_embeddings = None
73
  text_embedder = None
74
 
75
- SEARCH_CACHE = TTLCache(maxsize=10000, ttl=1800)
76
  AI_ANSWER_CACHE = LRUCache(maxsize=2000)
77
 
78
  tokenizer = None
@@ -91,6 +98,30 @@ def is_adult_content(text: str) -> bool:
91
  text_lower = text.lower()
92
  return any(word in text_lower for word in ADULT_KEYWORDS)
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  def normalize_text(text: str) -> str:
95
  if not text: return ""
96
  text = unicodedata.normalize('NFKD', str(text)).encode('ASCII', 'ignore').decode('utf-8')
@@ -122,28 +153,44 @@ def build_searchable_text(doc):
122
  parts = [doc.get("title", ""), doc.get("description", ""), doc.get("tag", "")]
123
  for k, v in doc.get("meta_tags", {}).items():
124
  if isinstance(v, str): parts.append(v)
 
 
125
  return " ".join(p for p in parts if p).strip()
126
 
127
  def build_runtime_record(doc):
128
  r = doc.copy()
129
  r["meta_tags"] = parse_json_field(doc.get("meta_tags"))
 
 
 
 
 
 
 
130
  r["searchable_text"] = build_searchable_text(r)
131
 
132
  # Safe Search Tagging
133
  r["is_adult"] = is_adult_content(r["searchable_text"] + " " + r.get("url", ""))
134
 
 
 
 
 
135
  r["trust_score"] = r.get("trust_score", 50.0)
136
- r["popularity"] = r.get("popularity", 0.0)
137
-
138
- tags = set()
139
- if r["trust_score"] >= 80: tags.add("Trusted")
140
- if r.get("domain_quality", 50) >= 90: tags.add("Verified")
141
- r["special_tags"] = list(tags)
 
 
142
 
 
143
  return r
144
 
145
  # ==============================================================================
146
- # DIRECT DATASET LOADING
147
  # ==============================================================================
148
  def load_dataset_direct():
149
  global bm25_index, raw_data, url_to_data
@@ -185,18 +232,19 @@ def load_dataset_direct():
185
  def init_system():
186
  global tokenizer, pipe, text_embedder, doc_embeddings
187
  init_db()
 
188
 
189
  texts_to_embed = load_dataset_direct()
190
 
191
  if ML_AVAILABLE:
192
- print("🧠 [1/2] Loading Fast Semantic Embedder (all-MiniLM-L6-v2)...")
193
  text_embedder = SentenceTransformer('all-MiniLM-L6-v2', device='cpu')
194
 
195
  # In-memory fast semantic indexing
196
  if texts_to_embed:
197
  doc_embeddings = text_embedder.encode(texts_to_embed, convert_to_numpy=True)
198
 
199
- print("🔥 [2/2] Loading Qwen2.5-0.5B-Instruct for Agentic RAG...")
200
  try:
201
  model_id = "Qwen/Qwen2.5-0.5B-Instruct"
202
  tokenizer = AutoTokenizer.from_pretrained(model_id)
@@ -221,28 +269,27 @@ def calculate_domain_trust(url: str, query_terms: list):
221
  trust_score = 0
222
  domain_parts = domain.split('.')
223
 
224
- # 1. Exact Brand/Domain Match (Massive Boost)
225
- # Ex: if query is "github", and domain is "github.com"
226
  for qt in query_terms:
227
  if qt in domain_parts and len(qt) > 2:
228
- trust_score += 150 # Very high boost for official domains
229
 
230
  # 2. Proxy / Fake Mirror Penalty
231
  proxy_indicators = ["proxy", "mirror", "unblock", "alt", "fake", "ip"]
232
  if any(p in domain for p in proxy_indicators):
233
- trust_score -= 200 # Kill proxies from top ranking
234
 
235
  # 3. High Authority TLDs
236
  if domain.endswith(".gov") or domain.endswith(".edu"): trust_score += 30
237
  elif domain.endswith(".org") or domain.endswith(".com"): trust_score += 10
238
 
239
- # 4. Domain Length Penalty (Long subdomains are usually spam)
240
  if len(domain_parts) > 3: trust_score -= 20
241
 
242
  return trust_score
243
 
244
  def get_ranked_results(query: str):
245
- # 1. Adult Query Check
246
  is_safe_query = not is_adult_content(query)
247
 
248
  q_tokens = tokenize(query)
@@ -259,32 +306,39 @@ def get_ranked_results(query: str):
259
  scored_results = []
260
 
261
  for i, doc in enumerate(raw_data):
262
- # Apply SafeSearch Filter
263
  if is_safe_query and doc.get("is_adult"):
264
- continue # Skip adult content if query is clean
265
 
266
  domain_trust = calculate_domain_trust(doc["url"], query_terms_lower)
267
 
268
- # Hybrid Formula
269
  score = (bm25_scores[i] * 2.0) + \
270
  (semantic_scores[i] * 15.0) + \
271
  domain_trust + \
272
  (doc.get("trust_score", 50) * 0.1) + \
273
- (doc.get("popularity", 0) * 0.05)
 
274
 
275
- if score > 0:
276
  scored_results.append((score, doc))
277
 
278
  scored_results.sort(key=lambda x: x[0], reverse=True)
279
 
280
  formatted = []
281
- for s, d in scored_results[:50]:
282
  formatted.append({
283
  "url": d["url"], "title": d["title"], "description": d["description"],
284
- "domain": get_domain(d["url"]),
 
 
 
285
  "is_adult": d.get("is_adult", False),
286
- "trust_score": round(d.get("trust_score", 50), 2),
287
- "special_tags": d.get("special_tags", []),
 
 
 
288
  "match_score": round(s, 2),
289
  "searchable_text": d.get("searchable_text", "")
290
  })
@@ -304,7 +358,7 @@ def get_best_chunks(query, full_text):
304
  return chunks[:3]
305
 
306
  # ==============================================================================
307
- # AGENTIC RAG (QWEN)
308
  # ==============================================================================
309
  async def agent_planner_call(prompt):
310
  if not pipe: return "ACTION: ANSWER"
@@ -320,7 +374,7 @@ async def execute_agentic_rag(query, candidates, debug=False):
320
  visited_idx = set()
321
  debug_log = []
322
 
323
- metadata_list = "\n".join([f"ID {i} | URL: {d['url']} | Title: {d['title']}" for i, d in enumerate(candidates[:15])])
324
 
325
  async with AI_SEMAPHORE:
326
  for hop in range(5):
@@ -337,25 +391,47 @@ async def execute_agentic_rag(query, candidates, debug=False):
337
  doc = candidates[idx]
338
  best_chunks = get_best_chunks(query, doc.get("searchable_text", ""))
339
  context_memory += f"\n[From {doc['domain']}]:\n" + "\n".join(best_chunks) + "\n"
340
- sources_used.append({"url": doc["url"], "title": doc["title"], "domain": doc["domain"]})
 
 
 
 
 
 
 
 
 
341
  else: break
342
  else: break
343
 
 
 
 
344
  final_prompt = f"You are a helpful AI assistant. Use ONLY the provided Memory to answer the query.\nQuery: {query}\nMemory:\n{context_memory}\n\nAnswer:\n"
345
 
346
  return final_prompt, sources_used, debug_log
347
 
348
  # ==============================================================================
349
- # FASTAPI ENDPOINTS
350
  # ==============================================================================
351
  @asynccontextmanager
352
  async def lifespan(app: FastAPI):
353
  asyncio.create_task(asyncio.to_thread(init_system))
354
  yield
355
 
356
- app = FastAPI(title="Nebulix v6 Qwen", lifespan=lifespan)
357
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
358
 
 
 
 
 
 
 
 
 
 
 
359
  @app.get("/search")
360
  async def api_search(q: str = Query(...)):
361
  cache_key = q.lower()
@@ -368,6 +444,32 @@ async def api_search(q: str = Query(...)):
368
  SEARCH_CACHE[cache_key] = resp
369
  return resp
370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  @app.get("/aimodechat")
372
  async def api_aimodechat(q: str = Query(...)):
373
  results = get_ranked_results(q)
@@ -395,8 +497,108 @@ async def api_aimodechat(q: str = Query(...)):
395
 
396
  return StreamingResponse(true_event_stream(), media_type="text/event-stream")
397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  @app.get("/health")
399
- async def api_health(): return {"status": "ok", "model": "Qwen2.5-0.5B"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
  if __name__ == "__main__":
402
  uvicorn.run(app, host="0.0.0.0", port=7860, log_level="warning")
 
3
  # Target: Hugging Face Spaces | 2 vCPU • 16 GB RAM • True Agentic RAG
4
  # Model: Qwen2.5-0.5B-Instruct (Inference Only)
5
  # Ranking: Advanced Algorithmic (Domain Trust, Proxy Penalty) + BM25 + Semantic
6
+ # Includes ALL API Endpoints (Popular, Trending, Suggestions, Clicks, etc.)
7
  # ==============================================================================
8
  import json
9
  import uuid
 
12
  import uvicorn
13
  import re
14
  import time
15
+ import pickle
16
  import sqlite3
17
  import unicodedata
18
  import threading
 
41
  from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, TextIteratorStreamer
42
  from sentence_transformers import SentenceTransformer
43
 
44
+ # 🔥 STRICT NO-TRAINING LOCK 🔥
45
  torch.set_num_threads(1)
46
  torch.set_grad_enabled(False) # Strict Inference Mode
47
  ML_AVAILABLE = True
 
50
  print("⚠️ ML packages missing. Check requirements.txt")
51
 
52
  # ==============================================================================
53
+ # SQLITE ANALYTICS SETUP
54
  # ==============================================================================
55
  DB_PATH = "analytics.db"
56
  def init_db():
57
  conn = sqlite3.connect(DB_PATH)
58
  conn.execute('CREATE TABLE IF NOT EXISTS clicks (url TEXT, type TEXT, ts DATETIME)')
59
  conn.execute('CREATE TABLE IF NOT EXISTS logs (query TEXT, latency REAL, ts DATETIME)')
60
+ conn.execute('CREATE TABLE IF NOT EXISTS planner_logs (query TEXT, hops INTEGER, decision_log TEXT, ts DATETIME)')
61
  conn.commit()
62
  conn.close()
63
 
 
69
  conn.close()
70
  except: pass
71
 
72
+ # ==============================================================================
73
+ # GLOBAL DATA & CACHES
74
+ # ==============================================================================
75
  raw_data = []
76
  url_to_data = {}
77
  bm25_index = None
 
79
  doc_embeddings = None
80
  text_embedder = None
81
 
82
+ SEARCH_CACHE = TTLCache(maxsize=10000, ttl=1800) # 30 Min TTL
83
  AI_ANSWER_CACHE = LRUCache(maxsize=2000)
84
 
85
  tokenizer = None
 
98
  text_lower = text.lower()
99
  return any(word in text_lower for word in ADULT_KEYWORDS)
100
 
101
+ def update_dynamic_fields(doc):
102
+ human = doc.get("human_opens", 0)
103
+ ai = doc.get("ai_opens", 0)
104
+ crawl = doc.get("crawler_opens", 0)
105
+ bot = doc.get("bot_opens", 0)
106
+
107
+ doc["total_opens"] = human + ai + crawl + bot
108
+ doc["popularity"] = human + (ai * 2) + (crawl * 0.1) + (bot * 0)
109
+
110
+ total_interactive = human + ai
111
+ ai_percent = (ai / total_interactive * 100) if total_interactive > 0 else 0
112
+ doc["ai_open_percent"] = ai_percent
113
+
114
+ tags = set()
115
+ if doc.get("trust_score", 50) >= 80: tags.add("Trusted")
116
+ if doc.get("quality_score", 50) >= 80: tags.add("High Quality")
117
+ if doc.get("domain_quality", 50) >= 90: tags.add("Verified")
118
+ if doc.get("total_opens", 0) >= 500: tags.add("Popular")
119
+
120
+ if ai >= 25: tags.add("Opened by AI")
121
+ if total_interactive > 10 and ai_percent >= 80: tags.add("AI Favorite")
122
+
123
+ doc["special_tags"] = list(tags)
124
+
125
  def normalize_text(text: str) -> str:
126
  if not text: return ""
127
  text = unicodedata.normalize('NFKD', str(text)).encode('ASCII', 'ignore').decode('utf-8')
 
153
  parts = [doc.get("title", ""), doc.get("description", ""), doc.get("tag", "")]
154
  for k, v in doc.get("meta_tags", {}).items():
155
  if isinstance(v, str): parts.append(v)
156
+ for k, v in doc.get("og_tags", {}).items():
157
+ if isinstance(v, str): parts.append(v)
158
  return " ".join(p for p in parts if p).strip()
159
 
160
  def build_runtime_record(doc):
161
  r = doc.copy()
162
  r["meta_tags"] = parse_json_field(doc.get("meta_tags"))
163
+ r["og_tags"] = parse_json_field(doc.get("og_tags"))
164
+ r["specific_data"] = parse_json_field(doc.get("specific_data"))
165
+
166
+ r["images"] = doc.get("images") or []
167
+ r["videos"] = doc.get("videos") or []
168
+ r["documents"] = doc.get("documents") or []
169
+ r["tag"] = doc.get("tag", "general")
170
  r["searchable_text"] = build_searchable_text(r)
171
 
172
  # Safe Search Tagging
173
  r["is_adult"] = is_adult_content(r["searchable_text"] + " " + r.get("url", ""))
174
 
175
+ r["human_opens"] = r.get("human_opens", 0)
176
+ r["ai_opens"] = r.get("ai_opens", 0)
177
+ r["crawler_opens"] = r.get("crawler_opens", 0)
178
+ r["bot_opens"] = r.get("bot_opens", 0)
179
  r["trust_score"] = r.get("trust_score", 50.0)
180
+ r["quality_score"] = r.get("quality_score", 50.0)
181
+ r["learning_score"] = r.get("learning_score", 50.0)
182
+ r["search_boost"] = r.get("search_boost", 0.0)
183
+ r["spam_score"] = r.get("spam_score", 0.0)
184
+ r["domain_quality"] = r.get("domain_quality", 50.0)
185
+ r["last_opened"] = r.get("last_opened", datetime.utcnow().isoformat())
186
+ r["language"] = r.get("language", "English")
187
+ r["country"] = r.get("country", "Global")
188
 
189
+ update_dynamic_fields(r)
190
  return r
191
 
192
  # ==============================================================================
193
+ # DIRECT DATASET LOADING (No Train Split Errors)
194
  # ==============================================================================
195
  def load_dataset_direct():
196
  global bm25_index, raw_data, url_to_data
 
232
  def init_system():
233
  global tokenizer, pipe, text_embedder, doc_embeddings
234
  init_db()
235
+ print("🚀 [1/3] Initializing System & Downloading Data...")
236
 
237
  texts_to_embed = load_dataset_direct()
238
 
239
  if ML_AVAILABLE:
240
+ print("🧠 [2/3] Loading Fast Semantic Embedder (all-MiniLM-L6-v2)...")
241
  text_embedder = SentenceTransformer('all-MiniLM-L6-v2', device='cpu')
242
 
243
  # In-memory fast semantic indexing
244
  if texts_to_embed:
245
  doc_embeddings = text_embedder.encode(texts_to_embed, convert_to_numpy=True)
246
 
247
+ print("🔥 [3/3] Loading Qwen2.5-0.5B-Instruct for Agentic RAG...")
248
  try:
249
  model_id = "Qwen/Qwen2.5-0.5B-Instruct"
250
  tokenizer = AutoTokenizer.from_pretrained(model_id)
 
269
  trust_score = 0
270
  domain_parts = domain.split('.')
271
 
272
+ # 1. Exact Brand/Domain Match (Massive Boost for official domains)
 
273
  for qt in query_terms:
274
  if qt in domain_parts and len(qt) > 2:
275
+ trust_score += 150
276
 
277
  # 2. Proxy / Fake Mirror Penalty
278
  proxy_indicators = ["proxy", "mirror", "unblock", "alt", "fake", "ip"]
279
  if any(p in domain for p in proxy_indicators):
280
+ trust_score -= 200
281
 
282
  # 3. High Authority TLDs
283
  if domain.endswith(".gov") or domain.endswith(".edu"): trust_score += 30
284
  elif domain.endswith(".org") or domain.endswith(".com"): trust_score += 10
285
 
286
+ # 4. Spam Penalty for long subdomains
287
  if len(domain_parts) > 3: trust_score -= 20
288
 
289
  return trust_score
290
 
291
  def get_ranked_results(query: str):
292
+ # Safe Search Check
293
  is_safe_query = not is_adult_content(query)
294
 
295
  q_tokens = tokenize(query)
 
306
  scored_results = []
307
 
308
  for i, doc in enumerate(raw_data):
309
+ # Adult filter logic
310
  if is_safe_query and doc.get("is_adult"):
311
+ continue
312
 
313
  domain_trust = calculate_domain_trust(doc["url"], query_terms_lower)
314
 
315
+ # Advanced Hybrid Formula
316
  score = (bm25_scores[i] * 2.0) + \
317
  (semantic_scores[i] * 15.0) + \
318
  domain_trust + \
319
  (doc.get("trust_score", 50) * 0.1) + \
320
+ (doc.get("popularity", 0) * 0.05) + \
321
+ doc.get("search_boost", 0) - doc.get("spam_score", 0)
322
 
323
+ if score > -50: # Ignore completely heavily penalized spam
324
  scored_results.append((score, doc))
325
 
326
  scored_results.sort(key=lambda x: x[0], reverse=True)
327
 
328
  formatted = []
329
+ for s, d in scored_results[:100]:
330
  formatted.append({
331
  "url": d["url"], "title": d["title"], "description": d["description"],
332
+ "domain": get_domain(d["url"]), "favicon": d.get("favicon", ""),
333
+ "language": d.get("language", "English"), "country": d.get("country", "Global"),
334
+ "images": d.get("images", []), "videos": d.get("videos", []),
335
+ "tag": d.get("tag", "general"),
336
  "is_adult": d.get("is_adult", False),
337
+ "last_opened": d["last_opened"], "trust_score": round(d.get("trust_score", 50), 2),
338
+ "quality_score": round(d.get("quality_score", 50), 2),
339
+ "learning_score": round(d.get("learning_score", 50), 2),
340
+ "human_opens": d["human_opens"], "ai_opens": d["ai_opens"],
341
+ "total_opens": d["total_opens"], "special_tags": d.get("special_tags", []),
342
  "match_score": round(s, 2),
343
  "searchable_text": d.get("searchable_text", "")
344
  })
 
358
  return chunks[:3]
359
 
360
  # ==============================================================================
361
+ # TRUE AGENTIC RAG PLANNER (QWEN)
362
  # ==============================================================================
363
  async def agent_planner_call(prompt):
364
  if not pipe: return "ACTION: ANSWER"
 
374
  visited_idx = set()
375
  debug_log = []
376
 
377
+ metadata_list = "\n".join([f"ID {i} | URL: {d['url']} | Title: {d['title']}" for i, d in enumerate(candidates[:10])])
378
 
379
  async with AI_SEMAPHORE:
380
  for hop in range(5):
 
391
  doc = candidates[idx]
392
  best_chunks = get_best_chunks(query, doc.get("searchable_text", ""))
393
  context_memory += f"\n[From {doc['domain']}]:\n" + "\n".join(best_chunks) + "\n"
394
+
395
+ if doc["url"] in url_to_data:
396
+ url_to_data[doc["url"]]["ai_opens"] += 1
397
+ url_to_data[doc["url"]]["learning_score"] += 1.0
398
+ update_dynamic_fields(url_to_data[doc["url"]])
399
+
400
+ sources_used.append({
401
+ "url": doc["url"], "title": doc["title"], "domain": doc["domain"],
402
+ "special_tags": doc["special_tags"], "favicon": doc["favicon"]
403
+ })
404
  else: break
405
  else: break
406
 
407
+ log_to_db("INSERT INTO planner_logs (query, hops, decision_log, ts) VALUES (?, ?, ?, ?)",
408
+ (query, len(visited_idx), " | ".join(debug_log), datetime.utcnow()))
409
+
410
  final_prompt = f"You are a helpful AI assistant. Use ONLY the provided Memory to answer the query.\nQuery: {query}\nMemory:\n{context_memory}\n\nAnswer:\n"
411
 
412
  return final_prompt, sources_used, debug_log
413
 
414
  # ==============================================================================
415
+ # FASTAPI APP & ALL ENDPOINTS
416
  # ==============================================================================
417
  @asynccontextmanager
418
  async def lifespan(app: FastAPI):
419
  asyncio.create_task(asyncio.to_thread(init_system))
420
  yield
421
 
422
+ app = FastAPI(title="Nebulix v6 Qwen SafeSearch", lifespan=lifespan)
423
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
424
 
425
+ @app.middleware("http")
426
+ async def analytics_middleware(request: Request, call_next):
427
+ start_time = time.perf_counter()
428
+ response = await call_next(request)
429
+ latency = (time.perf_counter() - start_time) * 1000
430
+ if "/search" in request.url.path or "/aimode" in request.url.path:
431
+ q = request.query_params.get("q", "")
432
+ log_to_db("INSERT INTO logs (query, latency, ts) VALUES (?, ?, ?)", (q, latency, datetime.utcnow()))
433
+ return response
434
+
435
  @app.get("/search")
436
  async def api_search(q: str = Query(...)):
437
  cache_key = q.lower()
 
444
  SEARCH_CACHE[cache_key] = resp
445
  return resp
446
 
447
+ @app.get("/aimode")
448
+ async def api_aimode(q: str = Query(...), debug: bool = False):
449
+ q_hash = hashlib.md5(q.encode()).hexdigest()
450
+ if q_hash in AI_ANSWER_CACHE: return AI_ANSWER_CACHE[q_hash]
451
+
452
+ results = get_ranked_results(q)
453
+ final_prompt, sources, debug_log = await execute_agentic_rag(q, results, debug)
454
+
455
+ if not pipe: return {"Answer": "Mocked", "Sources": sources}
456
+
457
+ async with AI_SEMAPHORE:
458
+ res = await asyncio.to_thread(
459
+ pipe, final_prompt, max_new_tokens=1024, temperature=0.2, do_sample=True, return_full_text=False
460
+ )
461
+ answer = res[0]["generated_text"].strip()
462
+
463
+ resp = {
464
+ "Answer": answer[:150] + "...",
465
+ "Summary": answer[:300],
466
+ "Detailed Explanation": answer,
467
+ "Sources": sources
468
+ }
469
+ if debug: resp["Debug_Planner"] = debug_log
470
+ AI_ANSWER_CACHE[q_hash] = resp
471
+ return resp
472
+
473
  @app.get("/aimodechat")
474
  async def api_aimodechat(q: str = Query(...)):
475
  results = get_ranked_results(q)
 
497
 
498
  return StreamingResponse(true_event_stream(), media_type="text/event-stream")
499
 
500
+ @app.get("/popular")
501
+ async def api_popular():
502
+ docs = sorted(raw_data, key=lambda x: x.get("popularity", 0), reverse=True)[:10]
503
+ return {"popular": [{"url": d["url"], "title": d["title"], "popularity": d["popularity"]} for d in docs]}
504
+
505
+ @app.get("/ai-popular")
506
+ async def api_ai_popular():
507
+ docs = sorted(raw_data, key=lambda x: x.get("ai_opens", 0), reverse=True)[:10]
508
+ return {"ai_popular": [{"url": d["url"], "title": d["title"], "ai_opens": d["ai_opens"], "special_tags": d["special_tags"]} for d in docs]}
509
+
510
+ @app.get("/trending")
511
+ async def api_trending():
512
+ docs = sorted(raw_data, key=lambda x: x.get("total_opens", 0), reverse=True)[:10]
513
+ return {"trending": [{"url": d["url"], "title": d["title"]} for d in docs]}
514
+
515
+ @app.get("/suggestions")
516
+ async def api_suggestions(q: str = Query(...)):
517
+ base_q = q.lower().split()[-1]
518
+ words = []
519
+ def dfs(node, prefix):
520
+ if len(words) >= 5: return
521
+ if node.get('_end'): words.append(prefix)
522
+ for char, child in node.items():
523
+ if char != '_end': dfs(child, prefix + char)
524
+
525
+ node = trie_root
526
+ for c in base_q:
527
+ if c in node: node = node[c]
528
+ else: break
529
+ if node and node != trie_root: dfs(node, base_q)
530
+ return {"suggestions": [q.replace(base_q, w) for w in words]}
531
+
532
+ @app.get("/stats")
533
+ async def api_stats():
534
+ mem = psutil.virtual_memory()
535
+ return {
536
+ "engine": "Nebulix v6.5 HF Spaces Edition",
537
+ "llm_model": "Qwen2.5-0.5B-Instruct",
538
+ "ranking": "Hybrid (Algorithmic Domain Trust + BM25 + Semantic)",
539
+ "documents": len(raw_data),
540
+ "cpu_usage_percent": psutil.cpu_percent(),
541
+ "ram_usage_percent": mem.percent,
542
+ "search_cache_items": len(SEARCH_CACHE),
543
+ "ai_cache_items": len(AI_ANSWER_CACHE)
544
+ }
545
+
546
  @app.get("/health")
547
+ async def api_health(): return {"status": "ok", "version": "6.5"}
548
+
549
+ @app.get("/url-info")
550
+ async def api_url_info(url: str = Query(...)):
551
+ if url in url_to_data:
552
+ safe_doc = url_to_data[url].copy()
553
+ safe_doc.pop("searchable_text", None)
554
+ return safe_doc
555
+ return {"error": "Not Found"}
556
+
557
+ @app.post("/click")
558
+ async def api_click(req: Request):
559
+ data = await req.json()
560
+ url = data.get("url")
561
+ ip = req.client.host
562
+ track_id = f"{ip}_{url}"
563
+ if track_id in CLICK_TRACKER and time.time() - CLICK_TRACKER[track_id] < 1800:
564
+ return {"status": "ignored_duplicate"}
565
+
566
+ CLICK_TRACKER[track_id] = time.time()
567
+ if url in url_to_data:
568
+ d = url_to_data[url]
569
+ d["human_opens"] += 1
570
+ d["last_opened"] = datetime.utcnow().isoformat()
571
+ update_dynamic_fields(d)
572
+ log_to_db("INSERT INTO clicks (url, type, ts) VALUES (?, ?, ?)", (url, "human", datetime.utcnow()))
573
+ return {"status": "success", "human_opens": d["human_opens"]}
574
+ return {"error": "Not found"}
575
+
576
+ @app.post("/ai-open")
577
+ async def api_ai_open(req: Request):
578
+ data = await req.json()
579
+ url = data.get("url")
580
+ if url in url_to_data:
581
+ d = url_to_data[url]
582
+ d["ai_opens"] += 1
583
+ d["learning_score"] += 1.0
584
+ d["last_opened"] = datetime.utcnow().isoformat()
585
+ update_dynamic_fields(d)
586
+ log_to_db("INSERT INTO clicks (url, type, ts) VALUES (?, ?, ?)", (url, "ai", datetime.utcnow()))
587
+ return {"status": "success"}
588
+ return {"error": "Not found"}
589
+
590
+ @app.post("/feedback")
591
+ async def api_feedback(req: Request):
592
+ data = await req.json()
593
+ url = data.get("url")
594
+ if url in url_to_data:
595
+ url_to_data[url]["trust_score"] += 2.0 if data.get("helpful") else -1.0
596
+ update_dynamic_fields(url_to_data[url])
597
+ return {"status": "feedback_registered"}
598
+
599
+ @app.post("/search-log")
600
+ async def api_search_log(req: Request):
601
+ return {"status": "logged"}
602
 
603
  if __name__ == "__main__":
604
  uvicorn.run(app, host="0.0.0.0", port=7860, log_level="warning")