adAstra144 commited on
Commit
6ab2222
·
1 Parent(s): 2718be8

OR implementation 1

Browse files
Files changed (4) hide show
  1. __pycache__/app.cpython-313.pyc +0 -0
  2. app.py +301 -2
  3. requirements.txt +3 -1
  4. test_api.py +28 -0
__pycache__/app.cpython-313.pyc ADDED
Binary file (43.7 kB). View file
 
app.py CHANGED
@@ -12,6 +12,9 @@ import sqlite3
12
  import re
13
  from langdetect import detect, DetectorFactory
14
  from urllib.parse import urlparse, urlunparse
 
 
 
15
 
16
  # Make langdetect deterministic
17
  DetectorFactory.seed = 0
@@ -32,6 +35,16 @@ model = None
32
  translator_tokenizer = None
33
  translator_model = None
34
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
 
 
 
 
 
 
 
 
 
35
 
36
  # -----------------------
37
  # DB + URL helpers
@@ -355,6 +368,117 @@ def predict_phishing(text: str):
355
  logger.error(f"Error predicting phishing: {e}")
356
  return "Error", 0.0
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  # -----------------------
359
  # Unified pipeline
360
  # -----------------------
@@ -487,7 +611,18 @@ def home():
487
 
488
  @app.route("/health", methods=["GET"])
489
  def health():
490
- return jsonify({"status": "healthy", "model_loaded": model is not None})
 
 
 
 
 
 
 
 
 
 
 
491
 
492
 
493
 
@@ -508,6 +643,158 @@ def analyze():
508
  return jsonify({"error": "Internal server error"}), 500
509
 
510
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  # =============================
512
  # /evaluate (GET form + POST CSV)
513
  # =============================
@@ -698,4 +985,16 @@ def internal_error(error):
698
  # -----------------------
699
  # Startup
700
  # -----------------------
701
- load_model()
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  import re
13
  from langdetect import detect, DetectorFactory
14
  from urllib.parse import urlparse, urlunparse
15
+ import time
16
+ from openrouter import OpenRouter
17
+ import requests
18
 
19
  # Make langdetect deterministic
20
  DetectorFactory.seed = 0
 
35
  translator_tokenizer = None
36
  translator_model = None
37
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
38
+ # OpenRouter client + model ids (env-configurable placeholders)
39
+ or_client = None
40
+ OR_MODEL_1 = os.environ.get("OR_MODEL_1", "or-model-1")
41
+ OR_MODEL_2 = os.environ.get("OR_MODEL_2", "or-model-2")
42
+ OR_TIMEOUT = int(os.environ.get("OR_TIMEOUT", "15"))
43
+ OR_CONSECUTIVE_FAILURES = 0
44
+ OR_CB_THRESHOLD = int(os.environ.get("OR_CB_THRESHOLD", "5"))
45
+ OR_CB_OPEN_SECONDS = int(os.environ.get("OR_CB_OPEN_SECONDS", "300"))
46
+ OR_LAST_ERROR = None
47
+ OR_CB_OPEN_UNTIL = 0
48
 
49
  # -----------------------
50
  # DB + URL helpers
 
368
  logger.error(f"Error predicting phishing: {e}")
369
  return "Error", 0.0
370
 
371
+
372
+ def generate_text(prompt: str, model_id: str, timeout: int = OR_TIMEOUT):
373
+ """Send prompt to OpenRouter HTTP API and return dict {ok, text, error, status}.
374
+ Uses chat completions endpoint and returns the assistant message content.
375
+ Falls back gracefully on network or parsing errors.
376
+ """
377
+ global OR_CONSECUTIVE_FAILURES, OR_LAST_ERROR, OR_CB_OPEN_UNTIL
378
+ api_key = os.environ.get("OPENROUTER_API_KEY")
379
+ # circuit breaker: short-circuit if open
380
+ if OR_CB_OPEN_UNTIL and time.time() < OR_CB_OPEN_UNTIL:
381
+ return {"ok": False, "text": None, "error": "circuit_open", "status": None}
382
+ if not api_key:
383
+ OR_CONSECUTIVE_FAILURES += 1
384
+ OR_LAST_ERROR = "Missing OPENROUTER_API_KEY"
385
+ return {"ok": False, "text": None, "error": "Missing OPENROUTER_API_KEY", "status": None}
386
+ url = "https://api.openrouter.ai/v1/chat/completions"
387
+ headers = {
388
+ "Authorization": f"Bearer {api_key}",
389
+ "Content-Type": "application/json"
390
+ }
391
+ body = {
392
+ "model": model_id,
393
+ "messages": [
394
+ {"role": "system", "content": "You are an automated phishing detector. Respond with only valid JSON."},
395
+ {"role": "user", "content": prompt}
396
+ ],
397
+ "temperature": 0.0,
398
+ "max_tokens": 256
399
+ }
400
+ try:
401
+ resp = requests.post(url, headers=headers, json=body, timeout=timeout)
402
+ text = resp.text
403
+ if resp.status_code != 200:
404
+ logger.error(f"OpenRouter API error {resp.status_code}: {text}")
405
+ OR_CONSECUTIVE_FAILURES += 1
406
+ OR_LAST_ERROR = f"status {resp.status_code}"
407
+ if OR_CONSECUTIVE_FAILURES >= OR_CB_THRESHOLD:
408
+ OR_CB_OPEN_UNTIL = time.time() + OR_CB_OPEN_SECONDS
409
+ logger.warning("OpenRouter circuit opened due to repeated failures")
410
+ return {"ok": False, "text": text, "error": f"status {resp.status_code}", "status": resp.status_code}
411
+ # try to extract assistant content
412
+ j = resp.json()
413
+ content = None
414
+ if isinstance(j, dict) and "choices" in j and isinstance(j["choices"], list) and j["choices"]:
415
+ first = j["choices"][0]
416
+ # chat-style: message.content
417
+ if "message" in first and isinstance(first["message"], dict):
418
+ content = first["message"].get("content")
419
+ # or text field
420
+ if not content:
421
+ content = first.get("text")
422
+ # success -> reset failure counter
423
+ OR_CONSECUTIVE_FAILURES = 0
424
+ OR_LAST_ERROR = None
425
+ return {"ok": True, "text": content or text, "error": None, "status": resp.status_code}
426
+ except Exception as e:
427
+ logger.error(f"OpenRouter request error: {e}")
428
+ OR_CONSECUTIVE_FAILURES += 1
429
+ OR_LAST_ERROR = str(e)
430
+ if OR_CONSECUTIVE_FAILURES >= OR_CB_THRESHOLD:
431
+ OR_CB_OPEN_UNTIL = time.time() + OR_CB_OPEN_SECONDS
432
+ logger.warning("OpenRouter circuit opened due to repeated exceptions")
433
+ return {"ok": False, "text": None, "error": str(e), "status": None}
434
+
435
+
436
+ def predict_phishing_or(text: str, model_id: str):
437
+ """Call OpenRouter to classify text. Return (label, confidence_percent, raw_response).
438
+ Falls back to HF `predict_phishing` on error or parse failure.
439
+ """
440
+ # craft a strict prompt that asks for JSON
441
+ prompt = (
442
+ "Classify the following text as either 'Phishing' or 'Safe'.\n"
443
+ "Return ONLY a single JSON object with exactly these fields: {\"label\": \"Phishing\"|\"Safe\", \"confidence\": <0-100 float>}\n"
444
+ "Do not add any other text or explanation.\n\n"
445
+ f"TEXT:\n{text}"
446
+ )
447
+ resp = generate_text(prompt, model_id)
448
+ logger.info(f"OpenRouter raw response: {resp}")
449
+ if not resp.get("ok") or not resp.get("text"):
450
+ # fallback to HF
451
+ label, conf = predict_phishing(text)
452
+ return label, conf, {"fallback": True, "reason": resp.get("error"), "raw": resp.get("text")}
453
+
454
+ # try to parse JSON object from resp['text']
455
+ txt = resp.get("text")
456
+ # extract the first {...} block
457
+ import json, re
458
+ m = re.search(r"\{.*\}", txt, re.DOTALL)
459
+ if not m:
460
+ label, conf = predict_phishing(text)
461
+ return label, conf, {"fallback": True, "reason": "no_json", "raw": txt}
462
+ try:
463
+ obj = json.loads(m.group(0))
464
+ lab = obj.get("label")
465
+ conf = obj.get("confidence")
466
+ if isinstance(conf, str):
467
+ try:
468
+ conf = float(conf)
469
+ except Exception:
470
+ conf = 0.0
471
+ if lab and (lab.lower() in ("phishing", "safe")):
472
+ label = "Phishing" if lab.lower() == "phishing" else "Safe"
473
+ conf_val = round(float(conf), 1) if conf is not None else 0.0
474
+ return label, conf_val, {"fallback": False, "raw": txt}
475
+ else:
476
+ raise ValueError("Invalid label")
477
+ except Exception as e:
478
+ logger.error(f"Error parsing OpenRouter JSON: {e} -- raw: {txt}")
479
+ label, conf = predict_phishing(text)
480
+ return label, conf, {"fallback": True, "reason": str(e), "raw": txt}
481
+
482
  # -----------------------
483
  # Unified pipeline
484
  # -----------------------
 
611
 
612
  @app.route("/health", methods=["GET"])
613
  def health():
614
+ or_configured = bool(os.environ.get("OPENROUTER_API_KEY"))
615
+ return jsonify({
616
+ "status": "healthy",
617
+ "model_loaded": model is not None,
618
+ "openrouter": {
619
+ "configured": or_configured,
620
+ "client_initialized": or_client is not None,
621
+ "consecutive_failures": OR_CONSECUTIVE_FAILURES,
622
+ "circuit_open": bool(OR_CB_OPEN_UNTIL and time.time() < OR_CB_OPEN_UNTIL),
623
+ "last_error": OR_LAST_ERROR
624
+ }
625
+ })
626
 
627
 
628
 
 
643
  return jsonify({"error": "Internal server error"}), 500
644
 
645
 
646
+ def analyze_pipeline_with_model(message: str, model_id: str):
647
+ """Same pipeline as analyze_pipeline but uses the specified OpenRouter model for classification.
648
+ Falls back to HF classifier when OR fails.
649
+ """
650
+ try:
651
+ text = (message or "").strip()
652
+ if not text:
653
+ return {"error": "empty", "blacklist": False}
654
+
655
+ # extract both full URLs and domains once
656
+ full_urls, domains = extract_urls_and_domains(text)
657
+
658
+ # --- load whitelist set (lowercased) ---
659
+ try:
660
+ conn = get_db_connection()
661
+ cursor = conn.cursor()
662
+ cursor.execute("SELECT LOWER(domain) FROM whitelist")
663
+ rows = cursor.fetchall()
664
+ whitelist_set = {r[0] for r in rows if r and r[0]}
665
+ conn.close()
666
+ except Exception as e:
667
+ logger.warning(f"Whitelist load failed: {e}")
668
+ whitelist_set = set()
669
+
670
+ # 1) domain whitelist check (allow subdomains)
671
+ if domains and whitelist_set:
672
+ for domain in domains:
673
+ d = (domain or "").lower().strip().strip('/')
674
+ if not d:
675
+ continue
676
+ # direct or subdomain match
677
+ matched_whitelist = False
678
+ if d in whitelist_set:
679
+ matched_whitelist = True
680
+ else:
681
+ for wl in whitelist_set:
682
+ if d == wl or d.endswith("." + wl):
683
+ matched_whitelist = True
684
+ break
685
+
686
+ if matched_whitelist:
687
+ # BEFORE returning Safe, check whether any full URL in the message
688
+ # is exactly present in the blacklist (normalized). If so, treat as Phishing.
689
+ if full_urls:
690
+ try:
691
+ bconn = get_blacklist_connection()
692
+ bcur = bconn.cursor()
693
+ for u in full_urls:
694
+ norm_u = normalize_full_url(u).lower()
695
+ bcur.execute("SELECT 1 FROM blacklist WHERE LOWER(url) = ?", (norm_u,))
696
+ if bcur.fetchone():
697
+ bconn.close()
698
+ return {
699
+ "result": "Phishing",
700
+ "confidence": "100.0%",
701
+ "message": text,
702
+ "blacklist": True,
703
+ "whitelist": False,
704
+ "detected_lang": None,
705
+ "translated_text": None,
706
+ "model_used": model_id
707
+ }
708
+ bconn.close()
709
+ except Exception as e:
710
+ logger.warning(f"Blacklist-full-url check failed: {e}")
711
+ # If no full-url blacklist hit, return Safe because domain is whitelisted
712
+ return {
713
+ "result": "Safe",
714
+ "confidence": "100.0%",
715
+ "message": text,
716
+ "blacklist": False,
717
+ "whitelist": True,
718
+ "detected_lang": None,
719
+ "translated_text": None,
720
+ "model_used": model_id
721
+ }
722
+
723
+ # 2) not whitelisted by domain (or whitelist didn't apply) -> regular blacklist check
724
+ is_black, _reason = is_blacklisted(text)
725
+ if is_black:
726
+ return {
727
+ "result": "Phishing",
728
+ "confidence": "100.0%",
729
+ "message": text,
730
+ "blacklist": True,
731
+ "whitelist": False,
732
+ "detected_lang": None,
733
+ "translated_text": None,
734
+ "model_used": model_id
735
+ }
736
+
737
+ # 3) language detection + optional translate
738
+ lang = detect_language(text)
739
+ translated_text = None
740
+ analysis_text = text
741
+ if lang in ("tl", "fil"):
742
+ translated_text = translate_tl_to_en(text)
743
+ analysis_text = translated_text or text
744
+
745
+ # 4) classification via OpenRouter model (with HF fallback inside)
746
+ label, conf, raw = predict_phishing_or(analysis_text, model_id)
747
+ return {
748
+ "result": label,
749
+ "confidence": f"{conf}%",
750
+ "message": text,
751
+ "blacklist": False,
752
+ "whitelist": False,
753
+ "detected_lang": lang,
754
+ "translated_text": translated_text,
755
+ "model_used": model_id,
756
+ "or_raw": raw
757
+ }
758
+
759
+ except Exception as e:
760
+ logger.error(f"Pipeline error: {e}")
761
+ return {"result": "Error", "confidence": "0%", "message": message, "blacklist": False, "whitelist": False}
762
+
763
+
764
+ @app.route("/analyze/or1", methods=["POST"])
765
+ def analyze_or1():
766
+ try:
767
+ data = request.get_json()
768
+ if not data or "message" not in data:
769
+ return jsonify({"error": "Missing 'message' field"}), 400
770
+ message = data["message"]
771
+ if not message or not message.strip():
772
+ return jsonify({"error": "Message cannot be empty"}), 400
773
+
774
+ result = analyze_pipeline_with_model(message, OR_MODEL_1)
775
+ return jsonify(result), 200
776
+ except Exception as e:
777
+ logger.error(f"Error in analyze/or1 endpoint: {e}")
778
+ return jsonify({"error": "Internal server error"}), 500
779
+
780
+
781
+ @app.route("/analyze/or2", methods=["POST"])
782
+ def analyze_or2():
783
+ try:
784
+ data = request.get_json()
785
+ if not data or "message" not in data:
786
+ return jsonify({"error": "Missing 'message' field"}), 400
787
+ message = data["message"]
788
+ if not message or not message.strip():
789
+ return jsonify({"error": "Message cannot be empty"}), 400
790
+
791
+ result = analyze_pipeline_with_model(message, OR_MODEL_2)
792
+ return jsonify(result), 200
793
+ except Exception as e:
794
+ logger.error(f"Error in analyze/or2 endpoint: {e}")
795
+ return jsonify({"error": "Internal server error"}), 500
796
+
797
+
798
  # =============================
799
  # /evaluate (GET form + POST CSV)
800
  # =============================
 
985
  # -----------------------
986
  # Startup
987
  # -----------------------
988
+ load_model()
989
+ try:
990
+ api_key = os.environ.get("OPENROUTER_API_KEY")
991
+ if api_key:
992
+ try:
993
+ or_client = OpenRouter(api_key=api_key)
994
+ logger.info("OpenRouter client initialized.")
995
+ except Exception as e:
996
+ logger.warning(f"Failed to initialize OpenRouter client object: {e}")
997
+ else:
998
+ logger.warning("OPENROUTER_API_KEY not set; OpenRouter endpoints will still attempt HTTP calls but client object is not created.")
999
+ except Exception as e:
1000
+ logger.warning(f"OpenRouter init warning: {e}")
requirements.txt CHANGED
@@ -10,4 +10,6 @@ google-auth-oauthlib
10
  google-auth-httplib2
11
  langdetect
12
  sentencepiece
13
- sacremoses
 
 
 
10
  google-auth-httplib2
11
  langdetect
12
  sentencepiece
13
+ sacremoses
14
+ openrouter
15
+ requests
test_api.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import sys
4
+ import json
5
+
6
+ API_BASE = os.getenv("API_BASE", "http://localhost:8080")
7
+ MESSAGE = os.getenv("TEST_MESSAGE", "Please verify your account at http://example.com/login")
8
+ TIMEOUT = int(os.getenv("TEST_TIMEOUT", "10"))
9
+
10
+ endpoints = ["/analyze", "/analyze/or1", "/analyze/or2"]
11
+
12
+ print(f"Testing API at {API_BASE} with message: {MESSAGE}\n")
13
+ for ep in endpoints:
14
+ url = API_BASE.rstrip("/") + ep
15
+ payload = {"message": MESSAGE}
16
+ try:
17
+ resp = requests.post(url, json=payload, timeout=TIMEOUT)
18
+ try:
19
+ body = resp.json()
20
+ except Exception:
21
+ body = resp.text
22
+ print(f"Endpoint: {ep} -> Status: {resp.status_code}")
23
+ print(json.dumps(body, indent=2, ensure_ascii=False))
24
+ except requests.exceptions.RequestException as e:
25
+ print(f"Endpoint: {ep} -> Request failed: {e}")
26
+ print("---")
27
+
28
+ print("Done.")