fjrmhri commited on
Commit
b9c66c9
·
verified ·
1 Parent(s): c0c3259

Update rag.py

Browse files
Files changed (1) hide show
  1. rag.py +183 -16
rag.py CHANGED
@@ -34,10 +34,32 @@ USER_AGENT = os.getenv(
34
  "IndoHoaxDetector-EvidenceBot/1.0 (+kontak: isi-email-anda@domain.com)",
35
  )
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  _rag_ready: bool = False
39
  _last_fetch_ok: Optional[bool] = None
40
  _last_fetch_error: Optional[str] = None
 
 
41
 
42
 
43
  # Kata pertama pada slug URL artikel (mis. /articles/123-salah-...) menentukan verdict.
@@ -291,27 +313,92 @@ def start_rag_background_update(
291
  threading.Thread(target=_loop, daemon=True, name="rag-updater").start()
292
 
293
 
294
- def retrieve_evidence(
295
- query_text: str,
296
- embed_fn: Callable[[List[str]], np.ndarray],
297
- top_k: Optional[int] = None,
298
- threshold: Optional[float] = None,
299
- ) -> List[Dict[str, Any]]:
300
- if not RAG_ENABLED or not _rag_ready or not query_text:
 
 
 
 
 
 
 
 
 
 
 
 
301
  return []
302
- top_k = top_k or RAG_TOP_K
303
- threshold = RAG_SIM_THRESHOLD if threshold is None else threshold
304
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  with _store.lock:
306
  if _store.embeddings is None or not _store.metadata:
307
  return []
308
- try:
309
- q_emb = embed_fn([query_text])[0]
310
- except Exception as e:
311
- print(f"[WARN][RAG] Gagal encode query: {e}")
312
- return []
313
-
314
- sims = _store.embeddings @ np.asarray(q_emb, dtype="float32")
315
  top_idx = np.argsort(-sims)[:top_k]
316
 
317
  results = []
@@ -327,10 +414,86 @@ def retrieve_evidence(
327
  "date": meta.get("date", ""),
328
  "similarity": round(score, 4),
329
  "excerpt": meta.get("excerpt", ""),
 
330
  })
331
  return results
332
 
333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  def rag_status() -> Dict[str, Any]:
335
  return {
336
  "enabled": RAG_ENABLED,
@@ -339,4 +502,8 @@ def rag_status() -> Dict[str, Any]:
339
  "last_updated": _store.last_updated,
340
  "last_fetch_ok": _last_fetch_ok,
341
  "last_fetch_error": _last_fetch_error,
 
 
 
 
342
  }
 
34
  "IndoHoaxDetector-EvidenceBot/1.0 (+kontak: isi-email-anda@domain.com)",
35
  )
36
 
37
+ # Sumber evidence kedua: API resmi MAFINDO (https://mafindodocs.netlify.app/v2/),
38
+ # dipakai berdampingan dengan scraping TurnBackHoax di atas. Nonaktif secara default.
39
+ MAFINDO_API_ENABLED = os.getenv("MAFINDO_API_ENABLED", "0") == "1"
40
+ MAFINDO_API_KEY = os.getenv("MAFINDO_API_KEY", "")
41
+ MAFINDO_API_BASE_URL = os.getenv(
42
+ "MAFINDO_API_BASE_URL", "https://yudistira.turnbackhoax.id/api"
43
+ ).rstrip("/")
44
+ MAFINDO_API_TIMEOUT = float(os.getenv("MAFINDO_API_TIMEOUT", "10"))
45
+ # Jumlah kandidat yang diminta ke endpoint search MAFINDO sebelum di-ranking ulang
46
+ # pakai cosine similarity yang sama dengan index scraping (biar sebanding).
47
+ MAFINDO_API_LIMIT = int(os.getenv("MAFINDO_API_LIMIT", "10"))
48
+ # /antihoax/search method=content kemungkinan besar pencarian berbasis kata kunci
49
+ # (bukan semantik), jadi query yang dikirim dipotong dulu supaya peluang match lebih baik.
50
+ MAFINDO_API_QUERY_MAX_CHARS = int(os.getenv("MAFINDO_API_QUERY_MAX_CHARS", "150"))
51
+
52
+ # Strategi menggabungkan 2 sumber evidence saat keduanya aktif:
53
+ # "fallback" (default): coba API dulu, kalau API nonaktif/gagal/0 hasil relevan -> pakai scraping.
54
+ # "union" : selalu panggil dua-duanya, gabungkan & dedup hasilnya berdasarkan similarity.
55
+ RAG_SOURCE_MODE = os.getenv("RAG_SOURCE_MODE", "fallback").strip().lower()
56
+
57
 
58
  _rag_ready: bool = False
59
  _last_fetch_ok: Optional[bool] = None
60
  _last_fetch_error: Optional[str] = None
61
+ _mafindo_warned_no_key: bool = False
62
+ _last_mafindo_error: Optional[str] = None
63
 
64
 
65
  # Kata pertama pada slug URL artikel (mis. /articles/123-salah-...) menentukan verdict.
 
313
  threading.Thread(target=_loop, daemon=True, name="rag-updater").start()
314
 
315
 
316
+ def _fetch_evidence_from_api(query_text: str) -> Optional[List[Dict[str, Any]]]:
317
+ """
318
+ Ambil kandidat evidence dari API resmi MAFINDO (POST /antihoax/search, method=content).
319
+ Return None = API nonaktif/gagal total (sinyal ke pemanggil utk fallback ke scraping).
320
+ Return [] = API berhasil dipanggil tapi tidak ada artikel yang cocok.
321
+ """
322
+ global _mafindo_warned_no_key, _last_mafindo_error
323
+
324
+ if not MAFINDO_API_ENABLED:
325
+ return None
326
+
327
+ if not MAFINDO_API_KEY:
328
+ if not _mafindo_warned_no_key:
329
+ print("[WARN][RAG] MAFINDO_API_ENABLED=1 tapi MAFINDO_API_KEY kosong, API dilewati.")
330
+ _mafindo_warned_no_key = True
331
+ return None
332
+
333
+ query_value = str(query_text or "").strip()[:MAFINDO_API_QUERY_MAX_CHARS]
334
+ if not query_value:
335
  return []
 
 
336
 
337
+ try:
338
+ resp = requests.post(
339
+ f"{MAFINDO_API_BASE_URL}/antihoax/search",
340
+ data={
341
+ "key": MAFINDO_API_KEY,
342
+ "method": "content",
343
+ "value": query_value,
344
+ "limit": MAFINDO_API_LIMIT,
345
+ },
346
+ headers={"Accept": "application/json"},
347
+ timeout=MAFINDO_API_TIMEOUT,
348
+ )
349
+ if resp.status_code != 200:
350
+ _last_mafindo_error = f"HTTP {resp.status_code}"
351
+ print(f"[WARN][RAG] MAFINDO API status {resp.status_code}, lanjut tanpa hasil API.")
352
+ return None
353
+ payload = resp.json()
354
+ except Exception as e:
355
+ _last_mafindo_error = str(e)
356
+ print(f"[WARN][RAG] MAFINDO API gagal: {e}")
357
+ return None
358
+
359
+ if not isinstance(payload, list):
360
+ # Key salah / parameter salah biasanya direspons sbg dict/string, bukan list artikel.
361
+ _last_mafindo_error = f"Format tak terduga: {str(payload)[:150]}"
362
+ print(f"[WARN][RAG] MAFINDO API format tak terduga, dianggap 0 hasil: {str(payload)[:150]}")
363
+ return []
364
+
365
+ _last_mafindo_error = None
366
+ articles: List[Dict[str, Any]] = []
367
+ for item in payload:
368
+ if not isinstance(item, dict):
369
+ continue
370
+ news_id = str(item.get("id", "")).strip()
371
+ title = str(item.get("title", "")).strip()
372
+ if not news_id or not title:
373
+ continue
374
+ status = str(item.get("status", "")).strip()
375
+ verdict = "hoax" if status == "2" else "fakta" if status == "1" else "tidak diketahui"
376
+ url = str(item.get("references") or item.get("source_link") or "").strip()
377
+ excerpt_src = item.get("conclusion") or item.get("fact") or item.get("content") or ""
378
+ articles.append({
379
+ "id": f"mafindo-{news_id}",
380
+ "title": title,
381
+ "excerpt": str(excerpt_src).strip()[:300],
382
+ "url": url,
383
+ "date": str(item.get("tanggal", "")).strip(),
384
+ "verdict": verdict,
385
+ "source": "api",
386
+ })
387
+ return articles
388
+
389
+
390
+ def _retrieve_from_local_index(
391
+ q_emb: np.ndarray,
392
+ top_k: int,
393
+ threshold: float,
394
+ ) -> List[Dict[str, Any]]:
395
+ """Cari evidence dari index lokal hasil scraping TurnBackHoax (embeddings di _store)."""
396
+ if not RAG_ENABLED or not _rag_ready:
397
+ return []
398
  with _store.lock:
399
  if _store.embeddings is None or not _store.metadata:
400
  return []
401
+ sims = _store.embeddings @ q_emb
 
 
 
 
 
 
402
  top_idx = np.argsort(-sims)[:top_k]
403
 
404
  results = []
 
414
  "date": meta.get("date", ""),
415
  "similarity": round(score, 4),
416
  "excerpt": meta.get("excerpt", ""),
417
+ "source": "scrape",
418
  })
419
  return results
420
 
421
 
422
+ def _rank_candidates_by_similarity(
423
+ articles: List[Dict[str, Any]],
424
+ q_emb: np.ndarray,
425
+ embed_fn: Callable[[List[str]], np.ndarray],
426
+ threshold: float,
427
+ ) -> List[Dict[str, Any]]:
428
+ """Beri skor cosine similarity ke kandidat artikel (mis. dari API) memakai embed_fn
429
+ yang sama dengan index scraping, supaya keduanya sebanding & bisa di-threshold sama."""
430
+ if not articles:
431
+ return []
432
+ texts = [f"{a.get('title', '')}. {a.get('excerpt', '')}".strip() for a in articles]
433
+ try:
434
+ embs = np.asarray(embed_fn(texts), dtype="float32")
435
+ except Exception as e:
436
+ print(f"[WARN][RAG] Gagal encode kandidat evidence API: {e}")
437
+ return []
438
+
439
+ ranked = []
440
+ for art, emb in zip(articles, embs):
441
+ score = float(emb @ q_emb)
442
+ if score < threshold:
443
+ continue
444
+ ranked.append({
445
+ "title": art.get("title", ""),
446
+ "url": art.get("url", ""),
447
+ "verdict": art.get("verdict", "tidak diketahui"),
448
+ "date": art.get("date", ""),
449
+ "similarity": round(score, 4),
450
+ "excerpt": art.get("excerpt", ""),
451
+ "source": art.get("source", "api"),
452
+ })
453
+ return ranked
454
+
455
+
456
+ def retrieve_evidence(
457
+ query_text: str,
458
+ embed_fn: Callable[[List[str]], np.ndarray],
459
+ top_k: Optional[int] = None,
460
+ threshold: Optional[float] = None,
461
+ ) -> List[Dict[str, Any]]:
462
+ if not query_text or (not RAG_ENABLED and not MAFINDO_API_ENABLED):
463
+ return []
464
+ top_k = top_k or RAG_TOP_K
465
+ threshold = RAG_SIM_THRESHOLD if threshold is None else threshold
466
+
467
+ try:
468
+ q_emb = np.asarray(embed_fn([query_text])[0], dtype="float32")
469
+ except Exception as e:
470
+ print(f"[WARN][RAG] Gagal encode query: {e}")
471
+ return []
472
+
473
+ api_ranked: Optional[List[Dict[str, Any]]] = None
474
+ if MAFINDO_API_ENABLED:
475
+ api_articles = _fetch_evidence_from_api(query_text)
476
+ if api_articles is not None:
477
+ api_ranked = _rank_candidates_by_similarity(api_articles, q_emb, embed_fn, threshold)
478
+
479
+ if RAG_SOURCE_MODE == "union":
480
+ scrape_ranked = _retrieve_from_local_index(q_emb, top_k, threshold)
481
+ combined = (api_ranked or []) + scrape_ranked
482
+ dedup: Dict[str, Dict[str, Any]] = {}
483
+ for item in combined:
484
+ key = (item.get("url") or item.get("title") or "").strip().lower()
485
+ if not key:
486
+ continue
487
+ if key not in dedup or item["similarity"] > dedup[key]["similarity"]:
488
+ dedup[key] = item
489
+ return sorted(dedup.values(), key=lambda x: -x["similarity"])[:top_k]
490
+
491
+ # Mode default "fallback": pakai hasil API kalau ada & relevan, kalau tidak coba scraping.
492
+ if api_ranked:
493
+ return sorted(api_ranked, key=lambda x: -x["similarity"])[:top_k]
494
+ return _retrieve_from_local_index(q_emb, top_k, threshold)
495
+
496
+
497
  def rag_status() -> Dict[str, Any]:
498
  return {
499
  "enabled": RAG_ENABLED,
 
502
  "last_updated": _store.last_updated,
503
  "last_fetch_ok": _last_fetch_ok,
504
  "last_fetch_error": _last_fetch_error,
505
+ "source_mode": RAG_SOURCE_MODE,
506
+ "mafindo_api_enabled": MAFINDO_API_ENABLED,
507
+ "mafindo_api_ready": MAFINDO_API_ENABLED and bool(MAFINDO_API_KEY),
508
+ "last_mafindo_error": _last_mafindo_error,
509
  }