import os import spaces import gradio as gr from fastapi import HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import joblib import pandas as pd import numpy as np # --- 1. LOAD MODEL AI --- # Mengecek di root folder atau di dalam folder models/ MODEL_PATH = "deteks_isolation_forest.pkl" if not os.path.exists(MODEL_PATH) and os.path.exists("models/deteks_isolation_forest.pkl"): MODEL_PATH = "models/deteks_isolation_forest.pkl" try: ai_model = joblib.load(MODEL_PATH) print(f"Model AI berhasil di-load dari {MODEL_PATH}!") except Exception as e: ai_model = None print(f"Error loading model: {e}") FITUR_AI = [ 'n_event_kunjungan', 'n_faskes_kunjungan', 'same_day_hits', 'unbundling_hits', 'n_event_obat', 'n_faskes_obat', 'qty_30d', 'min_interval', 'overlap_days' ] # --- 2. FUNGSI UTAMA DENGAN @spaces.GPU (Terhubung Langsung ke Gradio) --- @spaces.GPU def calculate_risk_fusion( participant_key: str, n_event_kunjungan: float, n_faskes_kunjungan: float, same_day_hits: float, unbundling_hits: float, n_event_obat: float, n_faskes_obat: float, qty_30d: float, min_interval: float, overlap_days: float, rules_score_tambahan: float = 0.0 ) -> dict: if ai_model is None: raise RuntimeError("Model belum ter-load. Pastikan file .pkl ada di repositori.") features_dict = { 'n_event_kunjungan': float(n_event_kunjungan), 'n_faskes_kunjungan': float(n_faskes_kunjungan), 'same_day_hits': float(same_day_hits), 'unbundling_hits': float(unbundling_hits), 'n_event_obat': float(n_event_obat), 'n_faskes_obat': float(n_faskes_obat), 'qty_30d': float(qty_30d), 'min_interval': float(min_interval), 'overlap_days': float(overlap_days) } df_input = pd.DataFrame([features_dict])[FITUR_AI] raw_score = float(-ai_model.score_samples(df_input)[0]) # Konversi raw_score ke persentil (skala 0 - 100) ai_percentile = float(np.clip((raw_score - 0.35) / 0.40 * 100, 0, 100)) # Hitung & Normalisasi Skor Rules (skala 0 - 100) total_rules = float(same_day_hits) + float(unbundling_hits) + float(rules_score_tambahan) rules_norm = float(min((total_rules / 140.0) * 100, 100.0)) # Risk Fusion (60% Rules + 40% AI) review_priority = (0.6 * rules_norm) + (0.4 * ai_percentile) # Reason Codes (Explainability) reasons = [] if same_day_hits > 0: reasons.append(f"Kunjungan di hari yang sama ({int(same_day_hits)}x)") if unbundling_hits > 0: reasons.append(f"Indikasi unbundling kunjungan ({int(unbundling_hits)}x)") if min_interval < 25: reasons.append(f"Early refill obat (interval {int(min_interval)} hari)") if overlap_days >= 3: reasons.append(f"Overlap stok obat tinggi ({int(overlap_days)} hari)") if n_faskes_obat > 1: reasons.append(f"Pengambilan obat lintas faskes ({int(n_faskes_obat)} faskes)") if not reasons: reasons.append("Pola terpantau normal / risiko rendah") if review_priority >= 70: status = "Tinggi" elif review_priority >= 40: status = "Sedang" else: status = "Rendah" return { "participant_key": str(participant_key), "review_priority": round(review_priority, 2), "risk_band": status, "reasons": reasons, "detail_scores": { "total_rules_raw": round(total_rules, 2), "rules_score_normalized": round(rules_norm, 2), "ai_anomaly_percentile": round(ai_percentile, 2), "raw_if_score": round(raw_score, 4) } } # --- 3. ANTARMUKA GRADIO --- with gr.Blocks(title="DETEKS AI Engine") as demo: gr.Markdown("## 🏥 DETEKS - AI Risk Scoring Service (ZeroGPU)") gr.Markdown("Antarmuka uji coba model **Hybrid Rules + Isolation Forest**. Endpoint REST API tersedia di `POST /api/v1/analyze_risk`.") with gr.Row(): with gr.Column(): p_key = gr.Textbox(label="ID Peserta (participant_key)", value="P-0951") n_ev_kunj = gr.Number(label="Total Kunjungan RJTL (n_event_kunjungan)", value=5) n_fas_kunj = gr.Number(label="Jumlah Faskes Kunjungan (n_faskes_kunjungan)", value=1) same_day = gr.Number(label="Same Day Hits", value=0) unbundling = gr.Number(label="Unbundling Hits", value=1) with gr.Column(): n_ev_obat = gr.Number(label="Frekuensi Tebus Obat (n_event_obat)", value=2) n_fas_obat = gr.Number(label="Jumlah Faskes Obat (n_faskes_obat)", value=2) qty_30d = gr.Number(label="Kuantitas Obat 30 Hari (qty_30d)", value=2) min_int = gr.Number(label="Interval Terpendek Ambil Obat (min_interval)", value=20) overlap = gr.Number(label="Hari Overlap Obat (overlap_days)", value=5) rules_ext = gr.Number(label="Skor Rules Obat Tambahan", value=10) btn = gr.Button("🔍 Analisis Risiko Pasien", variant="primary") output_json = gr.JSON(label="Hasil Output DETEKS (JSON)") # Fungsi @spaces.GPU didaftarkan langsung ke komponen Gradio btn.click( fn=calculate_risk_fusion, inputs=[p_key, n_ev_kunj, n_fas_kunj, same_day, unbundling, n_ev_obat, n_fas_obat, qty_30d, min_int, overlap, rules_ext], outputs=output_json, api_name="analyze_risk" ) # --- 4. TAMBAHKAN CUSTOM FASTAPI ENDPOINT KE SERVER GRADIO --- class PatientFeatures(BaseModel): participant_key: str = "P-0001" n_event_kunjungan: float = 2.0 n_faskes_kunjungan: float = 1.0 same_day_hits: float = 0.0 unbundling_hits: float = 0.0 n_event_obat: float = 2.0 n_faskes_obat: float = 1.0 qty_30d: float = 2.0 min_interval: float = 27.0 overlap_days: float = 4.0 rules_score_tambahan: float = 0.0 # Mengaktifkan CORS pada aplikasi FastAPI internal milik Gradio demo.app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @demo.app.post("/api/v1/analyze_risk") def analyze_risk_api(patient: PatientFeatures): try: return calculate_risk_fusion( participant_key=patient.participant_key, n_event_kunjungan=patient.n_event_kunjungan, n_faskes_kunjungan=patient.n_faskes_kunjungan, same_day_hits=patient.same_day_hits, unbundling_hits=patient.unbundling_hits, n_event_obat=patient.n_event_obat, n_faskes_obat=patient.n_faskes_obat, qty_30d=patient.qty_30d, min_interval=patient.min_interval, overlap_days=patient.overlap_days, rules_score_tambahan=patient.rules_score_tambahan ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # --- 5. JALANKAN DENGAN DEMO.LAUNCH() --- if __name__ == "__main__": demo.launch()