Update app.py
Browse files
app.py
CHANGED
|
@@ -1447,6 +1447,104 @@ def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg
|
|
| 1447 |
# 14) LLM + WORD
|
| 1448 |
# ============================================================
|
| 1449 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1450 |
def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_text):
|
| 1451 |
doc = Document()
|
| 1452 |
doc.add_heading(f"Laporan IPLM — {wilayah}", level=1)
|
|
@@ -1507,15 +1605,14 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
|
|
| 1507 |
doc.add_heading("Metodologi", level=2)
|
| 1508 |
doc.add_paragraph(
|
| 1509 |
"Indeks dasar dihitung per entitas menggunakan transformasi Yeo-Johnson dan normalisasi MinMax nasional per indikator. "
|
| 1510 |
-
"Nilai kemudian diagregasi per wilayah×jenis
|
| 1511 |
)
|
| 1512 |
doc.add_paragraph(
|
| 1513 |
"Penyesuaian dilakukan berbasis kecukupan sampel minimum 68% pada level wilayah, "
|
| 1514 |
-
"dengan rumus faktor = min(total_terkumpul / target_total_68, 1.0).
|
| 1515 |
-
"Indeks_Final_wilayah×jenis = Indeks_Dasar_Agregat × faktor."
|
| 1516 |
)
|
| 1517 |
doc.add_paragraph(
|
| 1518 |
-
"Nilai keseluruhan
|
| 1519 |
)
|
| 1520 |
|
| 1521 |
doc.add_heading("Analisis Naratif (LLM)", level=2)
|
|
|
|
| 1447 |
# 14) LLM + WORD
|
| 1448 |
# ============================================================
|
| 1449 |
|
| 1450 |
+
_HF_CLIENT = None
|
| 1451 |
+
|
| 1452 |
+
def get_llm_client():
|
| 1453 |
+
global _HF_CLIENT
|
| 1454 |
+
if _HF_CLIENT is not None:
|
| 1455 |
+
return _HF_CLIENT
|
| 1456 |
+
try:
|
| 1457 |
+
_HF_CLIENT = InferenceClient(model=LLM_MODEL_NAME, token=HF_TOKEN) if HF_TOKEN else InferenceClient(model=LLM_MODEL_NAME)
|
| 1458 |
+
return _HF_CLIENT
|
| 1459 |
+
except Exception:
|
| 1460 |
+
_HF_CLIENT = None
|
| 1461 |
+
return None
|
| 1462 |
+
|
| 1463 |
+
|
| 1464 |
+
def build_context(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, verif_total: pd.DataFrame, wilayah: str, kew: str) -> str:
|
| 1465 |
+
lines = []
|
| 1466 |
+
lines.append(f"Wilayah filter: {wilayah}")
|
| 1467 |
+
lines.append(f"Kewenangan: {kew}")
|
| 1468 |
+
lines.append("Metode: Indeks dasar dihitung per entitas (Yeo-Johnson + MinMax nasional per indikator), lalu diagregasi per wilayah×jenis.")
|
| 1469 |
+
lines.append("Penyesuaian: faktor = min(total_terkumpul / target_total_68, 1.0).")
|
| 1470 |
+
lines.append("FIX keseluruhan: nilai keseluruhan = rata-rata 3 jenis (sekolah+umum+khusus) ÷ 3 (missing=0, tetap ÷3).")
|
| 1471 |
+
|
| 1472 |
+
if summary_jenis is not None and not summary_jenis.empty:
|
| 1473 |
+
lines.append("\nRingkasan (jenis + keseluruhan):")
|
| 1474 |
+
for _, r in summary_jenis.iterrows():
|
| 1475 |
+
try:
|
| 1476 |
+
jenis = str(r.get("Jenis", "")).strip()
|
| 1477 |
+
jw = int(pd.to_numeric(r.get("Jumlah_Wilayah", 0), errors="coerce") or 0)
|
| 1478 |
+
tp = int(pd.to_numeric(r.get("Total_Perpus", 0), errors="coerce") or 0)
|
| 1479 |
+
fin = float(pd.to_numeric(r.get("Indeks_Final_Disesuaikan_0_100", 0), errors="coerce") or 0)
|
| 1480 |
+
das = float(pd.to_numeric(r.get("Indeks_Dasar_0_100", 0), errors="coerce") or 0)
|
| 1481 |
+
cov = float(pd.to_numeric(r.get("Coverage_Target68_Jenis_%", 0), errors="coerce") or 0)
|
| 1482 |
+
lines.append(f"- {jenis}: wilayah={jw}, total_perpus={tp}, dasar={das:.2f}, final={fin:.2f}, coverage_target68={cov:.2f}%")
|
| 1483 |
+
except Exception:
|
| 1484 |
+
continue
|
| 1485 |
+
|
| 1486 |
+
if agg_total is not None and not agg_total.empty:
|
| 1487 |
+
label_col = "Kab/Kota" if "Kab/Kota" in agg_total.columns else ("Provinsi" if "Provinsi" in agg_total.columns else None)
|
| 1488 |
+
if label_col:
|
| 1489 |
+
lines.append("\nTop 5 wilayah (Final tertinggi):")
|
| 1490 |
+
top = agg_total.sort_values("Indeks_Final_Wilayah_0_100", ascending=False).head(5)
|
| 1491 |
+
for _, r in top.iterrows():
|
| 1492 |
+
wl = str(r.get(label_col, "(wilayah)"))
|
| 1493 |
+
fin = float(pd.to_numeric(r.get("Indeks_Final_Wilayah_0_100", 0), errors="coerce") or 0)
|
| 1494 |
+
lines.append(f"- {wl}: Final={fin:.2f}")
|
| 1495 |
+
|
| 1496 |
+
return "\n".join(lines)
|
| 1497 |
+
|
| 1498 |
+
|
| 1499 |
+
def generate_llm_analysis(summary_jenis, agg_total, verif_total, wilayah, kew):
|
| 1500 |
+
ctx = build_context(summary_jenis, agg_total, verif_total, wilayah, kew)
|
| 1501 |
+
|
| 1502 |
+
# kalau LLM dimatikan / token gak ada -> return teks aman
|
| 1503 |
+
client = get_llm_client()
|
| 1504 |
+
if (client is None) or (not USE_LLM):
|
| 1505 |
+
return (
|
| 1506 |
+
"Analisis otomatis (LLM) tidak tersedia.\n\n"
|
| 1507 |
+
"Catatan: Set USE_LLM=True dan pastikan HF_TOKEN tersedia bila ingin mengaktifkan analisis LLM."
|
| 1508 |
+
)
|
| 1509 |
+
|
| 1510 |
+
system_prompt = (
|
| 1511 |
+
"Anda adalah analis kebijakan perpustakaan dan literasi di Indonesia. "
|
| 1512 |
+
"Tugas Anda menyusun analisis berbasis data IPLM secara formal, tajam, dan operasional."
|
| 1513 |
+
)
|
| 1514 |
+
|
| 1515 |
+
user_prompt = f"""
|
| 1516 |
+
DATA RINGKAS IPLM:
|
| 1517 |
+
|
| 1518 |
+
{ctx}
|
| 1519 |
+
|
| 1520 |
+
TULISKAN ANALISIS BAHASA INDONESIA FORMAL, STRUKTUR:
|
| 1521 |
+
1) Gambaran umum hasil wilayah (1 paragraf).
|
| 1522 |
+
2) Analisis jenis sekolah, umum, khusus serta indeks keseluruhan (2 paragraf).
|
| 1523 |
+
3) Penjelasan makna penyesuaian berbasis target 68% (1 paragraf, netral).
|
| 1524 |
+
4) Rekomendasi program 3–5 tahun (2 paragraf, konkret dan dapat dieksekusi).
|
| 1525 |
+
|
| 1526 |
+
ATURAN:
|
| 1527 |
+
- Jangan memakai label eksplisit "rendah/sedang/tinggi".
|
| 1528 |
+
- Gunakan frasa netral: "memerlukan penguatan", "memerlukan konsolidasi", dsb.
|
| 1529 |
+
"""
|
| 1530 |
+
|
| 1531 |
+
try:
|
| 1532 |
+
resp = client.chat_completion(
|
| 1533 |
+
model=LLM_MODEL_NAME,
|
| 1534 |
+
messages=[
|
| 1535 |
+
{"role": "system", "content": system_prompt},
|
| 1536 |
+
{"role": "user", "content": user_prompt},
|
| 1537 |
+
],
|
| 1538 |
+
max_tokens=1100,
|
| 1539 |
+
temperature=0.25,
|
| 1540 |
+
top_p=0.9,
|
| 1541 |
+
)
|
| 1542 |
+
text = resp.choices[0].message.content.strip()
|
| 1543 |
+
return text if text else "LLM mengembalikan respon kosong."
|
| 1544 |
+
except Exception as e:
|
| 1545 |
+
return f"⚠️ Error saat memanggil LLM: {repr(e)}"
|
| 1546 |
+
|
| 1547 |
+
|
| 1548 |
def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_text):
|
| 1549 |
doc = Document()
|
| 1550 |
doc.add_heading(f"Laporan IPLM — {wilayah}", level=1)
|
|
|
|
| 1605 |
doc.add_heading("Metodologi", level=2)
|
| 1606 |
doc.add_paragraph(
|
| 1607 |
"Indeks dasar dihitung per entitas menggunakan transformasi Yeo-Johnson dan normalisasi MinMax nasional per indikator. "
|
| 1608 |
+
"Nilai kemudian diagregasi per wilayah×jenis."
|
| 1609 |
)
|
| 1610 |
doc.add_paragraph(
|
| 1611 |
"Penyesuaian dilakukan berbasis kecukupan sampel minimum 68% pada level wilayah, "
|
| 1612 |
+
"dengan rumus faktor = min(total_terkumpul / target_total_68, 1.0)."
|
|
|
|
| 1613 |
)
|
| 1614 |
doc.add_paragraph(
|
| 1615 |
+
"Nilai keseluruhan (FIX) dihitung sebagai rata-rata 3 jenis (sekolah+umum+khusus) ÷ 3, dengan missing dianggap 0."
|
| 1616 |
)
|
| 1617 |
|
| 1618 |
doc.add_heading("Analisis Naratif (LLM)", level=2)
|