chat-bpjs / app_gradio.py
muhammad.sutariya
improve search similarity
3945ef3
import gradio as gr
import json
import psycopg2
import pandas as pd
from datetime import datetime
import re
from difflib import SequenceMatcher
from llm_service import LLMService
import os
import random
import time
from typing import Dict, Optional, List
# Configuration for direct service integration
class BPJSService:
"""Direct service implementation without API calls"""
def __init__(self):
print("πŸš€ Initializing BPJS Service...")
self.faq_data = self.load_faq_data()
self.conversation_history = {}
self.registration_state = {} # Track registration flow state
try:
self.llm_service = LLMService()
if self.llm_service and self.llm_service.client:
print("βœ“ LLM service initialized")
else:
print("⚠ LLM service disabled (missing configuration)")
self.llm_service = None
except Exception as e:
print(f"⚠ Warning: LLM service not available: {e}")
self.llm_service = None
print("βœ“ BPJS Service ready")
def load_faq_data(self) -> Dict:
"""Load FAQ data from JSON file"""
try:
with open('bpjs_faq_complete.json', 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
print("File bpjs_faq_complete.json tidak ditemukan!")
return {"data": []}
def connect_to_postgres(self):
"""Connect to PostgreSQL database with timeout"""
try:
conn = psycopg2.connect(
host=os.getenv("DB_HOST", "162.55.210.105"),
database=os.getenv("DB_NAME", "chatbot"),
user=os.getenv("DB_USER", "poc_chatbot"),
password=os.getenv("DB_PASSWORD", "taiwan-stars-hop-mart"),
port=os.getenv("DB_PORT", 5432),
connect_timeout=10
)
return conn
except psycopg2.OperationalError as e:
print(f"⚠ Database connection failed (timeout/firewall): {e}")
return None
except Exception as e:
print(f"⚠ Database error: {e}")
return None
def similarity(self, a: str, b: str) -> float:
"""Calculate similarity between two strings"""
return SequenceMatcher(None, a.lower(), b.lower()).ratio()
def search_faq(self, question: str, threshold: float = 0.3):
"""
Search for relevant FAQ based on question
Args:
question: User's question
threshold: Minimum similarity score (0.0-1.0). Default 0.3.
- Lower = more results but less accurate (e.g., 0.2)
- Higher = fewer but more accurate results (e.g., 0.5)
"""
best_match = None
best_score = 0
question_lower = question.lower()
for faq in self.faq_data.get("data", []):
question_text = faq.get("pertanyaan", "")
title_text = faq.get("title", "")
# Calculate base similarity scores
question_score = self.similarity(question, question_text)
title_score = self.similarity(question, title_text)
# Bonus score for exact keyword matches (helps find specific FAQs)
keywords = ['denda', 'iuran', 'bayar', 'keterlambatan', 'rawat inap', 'phk', 'klaim']
keyword_bonus = 0
for keyword in keywords:
if keyword in question_lower and keyword in question_text.lower():
keyword_bonus += 0.1
# Combine scores
score = max(question_score, title_score) + keyword_bonus
if score > best_score and score >= threshold:
best_score = score
best_match = faq
return best_match if best_match else None
def check_user_registration(self, nomor_peserta: str):
"""Check user registration status in PostgreSQL"""
conn = self.connect_to_postgres()
if not conn:
return None
try:
cursor = conn.cursor()
query = """
SELECT nomor_peserta, tanggal_lahir, jenis_kelamin,
provinsi, status_keaktifan, "PSTV18"
FROM public.kepesertaan_bpjs
WHERE nomor_peserta = %s
"""
cursor.execute(query, (nomor_peserta,))
result = cursor.fetchone()
if result:
return {
"nomor_peserta": result[0],
"tanggal_lahir": result[1],
"jenis_kelamin": result[2],
"provinsi": result[3],
"status_keaktifan": result[4],
"pstv18": result[5]
}
return None
except Exception as e:
print(f"Error querying database: {e}")
return None
finally:
conn.close()
def extract_registration_number(self, message: str):
"""Extract registration number from message"""
patterns = [
r"nomor\s+peserta\s+(\d+)",
r"peserta\s+(\d+)",
r"nomor\s+(\d+)",
r"\b(\d{8,12})\b"
]
for pattern in patterns:
match = re.search(pattern, message.lower())
if match:
return match.group(1)
return None
def classify_question_type(self, message: str) -> str:
"""Classify the type of question"""
message_lower = message.lower()
new_registration_keywords = [
"daftar bpjs", "registrasi baru", "pendaftaran baru", "daftar peserta",
"buat akun bpjs", "daftar sebagai peserta", "pendaftaran bpjs",
"saya ingin daftar", "cara daftar", "daftar baru"
]
status_check_keywords = [
"nomor peserta", "peserta", "status", "aktif", "tidak aktif",
"cek peserta", "data peserta", "status saya", "data saya"
]
faq_keywords = [
"iuran", "denda", "phk", "kepesertaan", "rawat inap",
"bpjs", "jaminan kesehatan", "pembayaran", "klaim"
]
new_reg_score = sum(1 for keyword in new_registration_keywords if keyword in message_lower)
if new_reg_score > 0:
return "new_registration"
reg_number = self.extract_registration_number(message)
if reg_number:
return "status_check"
status_score = sum(1 for keyword in status_check_keywords if keyword in message_lower)
faq_score = sum(1 for keyword in faq_keywords if keyword in message_lower)
if status_score > faq_score and status_score > 0:
return "status_check"
elif faq_score > 0:
return "faq"
else:
return "general"
def extract_context_from_history(self, session_id: str):
"""Extract useful context information from conversation history"""
history = self.get_history(session_id)
context = {
"user_name": None,
"last_peserta_number": None,
"user_preferences": []
}
# Get registered name from registration flow (if exists)
if session_id in self.registration_state:
if "data" in self.registration_state[session_id] and "nama" in self.registration_state[session_id]["data"]:
context["user_name"] = self.registration_state[session_id]["data"]["nama"]
for conv in history:
user_msg = conv['user'].lower()
peserta_num = self.extract_registration_number(user_msg)
if peserta_num:
context["last_peserta_number"] = peserta_num
return context
def get_general_response(self, question: str, context: dict = None) -> str:
"""Generate response for general questions using basic knowledge and context"""
question_lower = question.lower()
if context and context.get("user_name"):
name = context["user_name"]
if "nama saya siapa" in question_lower or "siapa nama saya" in question_lower:
return f"Nama Anda adalah {name}. Ada yang bisa saya bantu mengenai BPJS?"
if "halo" in question_lower or "hai" in question_lower:
return f"Halo {name}! Ada yang bisa saya bantu mengenai FAQ BPJS atau cek status kepesertaan?"
general_responses = {
"siapa presiden indonesia": "Presiden Indonesia saat ini adalah Joko Widodo (Jokowi).",
"apa itu bpjs": "BPJS Kesehatan adalah Badan Penyelenggara Jaminan Sosial yang menyelenggarakan program jaminan kesehatan untuk seluruh rakyat Indonesia.",
"terima kasih": f"Sama-sama{', ' + context.get('user_name', '') if context and context.get('user_name') else ''}! Apakah ada yang bisa saya bantu lagi?",
"halo": "Halo! Saya adalah chatbot BPJS. Ada yang bisa saya bantu mengenai FAQ BPJS atau cek status kepesertaan?",
"selamat": "Selamat! Terima kasih telah menggunakan layanan chatbot BPJS."
}
for key, response in general_responses.items():
if key in question_lower:
return response
if "nama saya" in question_lower:
match = re.search(r"nama saya (\w+)", question_lower)
if match:
name = match.group(1).title()
return f"Senang berkenalan dengan Anda, {name}! Saya akan mengingat nama Anda. Ada yang bisa saya bantu mengenai BPJS?"
return "Maaf, saya hanya dapat membantu dengan pertanyaan seputar FAQ BPJS dan pengecekan status kepesertaan. Silakan tanyakan hal-hal terkait BPJS Kesehatan."
def add_to_history(self, session_id: str, user_message: str, bot_response: str):
"""Add conversation to history"""
if session_id not in self.conversation_history:
self.conversation_history[session_id] = []
self.conversation_history[session_id].append({
"timestamp": datetime.now(),
"user": user_message,
"bot": bot_response
})
if len(self.conversation_history[session_id]) > 10:
self.conversation_history[session_id] = self.conversation_history[session_id][-10:]
def get_history(self, session_id: str):
"""Get conversation history for a session_id"""
return self.conversation_history.get(session_id, [])
def generate_nomor_peserta(self):
"""Generate a new unique nomor peserta"""
timestamp_part = str(int(time.time()))[-6:]
random_part = str(random.randint(100, 999))
nomor_peserta = timestamp_part + random_part
existing = self.check_user_registration(nomor_peserta)
if existing:
return self.generate_nomor_peserta()
return nomor_peserta
def save_new_registration(self, nama: str, tanggal_lahir: str,
jenis_kelamin: str, provinsi: str):
"""Save new registration to PostgreSQL database"""
conn = self.connect_to_postgres()
if not conn:
return False, "Gagal terhubung ke database", None
try:
nomor_peserta = self.generate_nomor_peserta()
cursor = conn.cursor()
query = """
INSERT INTO public.kepesertaan_bpjs
(nomor_peserta, tanggal_lahir, jenis_kelamin, provinsi, status_keaktifan, "PSTV18")
VALUES (%s, %s, %s, %s, %s, %s)
"""
cursor.execute(query, (
nomor_peserta,
tanggal_lahir,
jenis_kelamin,
provinsi,
'AKTIF',
None
))
conn.commit()
return True, "Registrasi berhasil disimpan", nomor_peserta
except Exception as e:
conn.rollback()
return False, f"Error menyimpan registrasi: {e}", None
finally:
conn.close()
def handle_registration_flow(self, session_id: str, message: str) -> str:
"""Handle step-by-step registration flow"""
state = self.registration_state[session_id]
current_step = state["step"]
data = state["data"]
# Allow user to cancel registration
if message.lower() in ['batal', 'cancel', 'stop', 'keluar']:
del self.registration_state[session_id]
return "❌ Registrasi dibatalkan. Anda bisa memulai registrasi baru kapan saja dengan mengetik 'daftar BPJS'."
if current_step == "nama":
# Save nama and ask for tanggal lahir
data["nama"] = message.strip()
state["step"] = "tanggal_lahir"
response = f"""βœ… Nama: **{data['nama']}**
**Langkah 2 dari 4**
Sekarang masukkan **tanggal lahir** Anda:
Format: DD/MM/YYYY
_Contoh: 15/08/1990_
_(Ketik 'batal' jika ingin membatalkan registrasi)_"""
elif current_step == "tanggal_lahir":
# Validate and save tanggal lahir
tanggal_lahir = message.strip()
# Simple validation
if not re.match(r'\d{2}/\d{2}/\d{4}', tanggal_lahir):
return "❌ Format tanggal tidak valid. Gunakan format DD/MM/YYYY (contoh: 15/08/1990). Silakan coba lagi:"
data["tanggal_lahir"] = tanggal_lahir
state["step"] = "jenis_kelamin"
response = f"""βœ… Tanggal Lahir: **{data['tanggal_lahir']}**
**Langkah 3 dari 4**
Pilih **jenis kelamin** Anda:
Ketik:
- **L** atau **Laki-laki**
- **P** atau **Perempuan**
_(Ketik 'batal' jika ingin membatalkan registrasi)_"""
elif current_step == "jenis_kelamin":
# Validate and save jenis kelamin
jk = message.strip().lower()
if jk in ['l', 'laki-laki', 'laki laki', 'pria']:
data["jenis_kelamin"] = "Laki-laki"
elif jk in ['p', 'perempuan', 'wanita']:
data["jenis_kelamin"] = "Perempuan"
else:
return "❌ Input tidak valid. Silakan ketik **L** untuk Laki-laki atau **P** untuk Perempuan:"
state["step"] = "provinsi"
response = f"""βœ… Jenis Kelamin: **{data['jenis_kelamin']}**
**Langkah 4 dari 4 (Terakhir!)**
Masukkan **provinsi** tempat tinggal Anda:
_Contoh: DKI Jakarta, Jawa Barat, Jawa Timur, dll._
_(Ketik 'batal' jika ingin membatalkan registrasi)_"""
elif current_step == "provinsi":
# Save provinsi and complete registration
data["provinsi"] = message.strip()
# Show confirmation
state["step"] = "konfirmasi"
response = f"""## πŸ“‹ Konfirmasi Data Registrasi
Mohon periksa data Anda:
| Field | Value |
|-------|-------|
| **Nama** | {data['nama']} |
| **Tanggal Lahir** | {data['tanggal_lahir']} |
| **Jenis Kelamin** | {data['jenis_kelamin']} |
| **Provinsi** | {data['provinsi']} |
Apakah data sudah benar?
- Ketik **YA** untuk melanjutkan registrasi
- Ketik **TIDAK** atau **BATAL** untuk membatalkan
_(Data ini akan disimpan ke database)_"""
elif current_step == "konfirmasi":
konfirmasi = message.strip().lower()
if konfirmasi in ['ya', 'y', 'yes', 'ok', 'oke', 'benar']:
# Save to database
success, msg, nomor_peserta = self.save_new_registration(
nama=data["nama"],
tanggal_lahir=data["tanggal_lahir"],
jenis_kelamin=data["jenis_kelamin"],
provinsi=data["provinsi"]
)
# Clear registration state
del self.registration_state[session_id]
if success:
response = f"""## πŸŽ‰ Registrasi BPJS Berhasil!
**NOMOR PESERTA ANDA:** `{nomor_peserta}`
### πŸ“‹ Data yang Tersimpan:
- **Nama:** {data['nama']}
- **Tanggal Lahir:** {data['tanggal_lahir']}
- **Jenis Kelamin:** {data['jenis_kelamin']}
- **Provinsi:** {data['provinsi']}
- **Status:** AKTIF βœ…
⚠️ **PENTING:** Simpan nomor peserta Anda dengan baik!
Anda sekarang bisa:
- Mengecek status dengan: "Cek peserta {nomor_peserta}"
- Bertanya tentang FAQ BPJS
- Bertanya tentang layanan kesehatan
Ada yang bisa saya bantu lagi? 😊"""
else:
response = f"❌ **Error:** {msg}\n\nSilakan coba daftar lagi dengan ketik 'daftar BPJS'."
else:
# Cancel registration
del self.registration_state[session_id]
response = "❌ Registrasi dibatalkan. Anda bisa memulai registrasi baru kapan saja dengan mengetik 'daftar BPJS'."
return response
def process_message(self, session_id: str, message: str) -> str:
"""Main method to process user message and return response"""
# Check if user is in registration flow
if session_id in self.registration_state:
return self.handle_registration_flow(session_id, message)
context = self.extract_context_from_history(session_id)
question_type = self.classify_question_type(message)
if question_type == "faq":
faq_result = self.search_faq(message)
if faq_result:
if self.llm_service:
try:
response = self.llm_service.generate_faq_response(message, faq_result)
if context.get('user_name'):
response += f"\n\nSemoga informasi ini membantu, {context['user_name']}! 😊"
except Exception as e:
response = f"**{faq_result['title']}**\n\n"
response += f"**Pertanyaan:** {faq_result['pertanyaan']}\n\n"
response += f"**Jawaban:** {faq_result['jawaban']}\n\n"
if faq_result.get('tanggal'):
response += f"*Tanggal: {faq_result['tanggal']}*"
if context.get('user_name'):
response += f"\n\nSemoga informasi ini membantu, {context['user_name']}!"
else:
response = f"**{faq_result['title']}**\n\n"
response += f"**Pertanyaan:** {faq_result['pertanyaan']}\n\n"
response += f"**Jawaban:** {faq_result['jawaban']}\n\n"
if faq_result.get('tanggal'):
response += f"*Tanggal: {faq_result['tanggal']}*"
if context.get('user_name'):
response += f"\n\nSemoga informasi ini membantu, {context['user_name']}!"
else:
if self.llm_service:
try:
response = self.llm_service.generate_faq_response(message)
if context.get('user_name'):
response += f"\n\n{context['user_name']}, jika ada pertanyaan lain, silakan tanyakan! 😊"
except Exception as e:
base_response = "Maaf, saya tidak menemukan FAQ yang sesuai dengan pertanyaan Anda. Silakan coba dengan kata kunci yang berbeda."
if context.get('user_name'):
response = f"{context['user_name']}, {base_response.lower()}"
else:
response = base_response
else:
base_response = "Maaf, saya tidak menemukan FAQ yang sesuai dengan pertanyaan Anda. Silakan coba dengan kata kunci yang berbeda."
if context.get('user_name'):
response = f"{context['user_name']}, {base_response.lower()}"
else:
response = base_response
elif question_type == "new_registration":
# Start registration flow
self.registration_state[session_id] = {
"step": "nama",
"data": {}
}
response = """## πŸ“ Registrasi Peserta BPJS Baru
Baik! Saya akan membantu Anda mendaftar sebagai peserta BPJS.
Prosesnya akan saya tanyakan satu per satu ya.
**Langkah 1 dari 4**
Silakan masukkan **nama lengkap** Anda:
_Contoh: Budi Santoso_"""
elif question_type == "status_check":
reg_number = self.extract_registration_number(message)
if not reg_number and context.get('last_peserta_number'):
if any(word in message.lower() for word in ['status saya', 'data saya', 'kepesertaan saya']):
reg_number = context['last_peserta_number']
if reg_number:
user_data = self.check_user_registration(reg_number)
if user_data:
response = "## πŸ“‹ Data Peserta BPJS\n\n"
response += "---\n\n"
response += f"| **Field** | **Value** |\n"
response += f"|-----------|----------|\n"
response += f"| **Nomor Peserta** | `{user_data['nomor_peserta']}` |\n"
response += f"| **Tanggal Lahir** | {user_data['tanggal_lahir']} |\n"
response += f"| **Jenis Kelamin** | {user_data['jenis_kelamin']} |\n"
response += f"| **Provinsi** | {user_data['provinsi']} |\n"
status_emoji = "βœ…" if user_data['status_keaktifan'] == "AKTIF" else "❌"
response += f"| **Status Keaktifan** | {status_emoji} {user_data['status_keaktifan']} |\n"
if user_data.get('pstv18'):
response += f"| **PSTV18** | {user_data['pstv18']} |\n"
response += "\n---\n"
if context.get('user_name'):
response += f"\nπŸ’‘ **Info:** Ini adalah data kepesertaan untuk **{context['user_name']}**."
else:
if context.get('user_name'):
response = f"Maaf {context['user_name']}, nomor peserta {reg_number} tidak ditemukan dalam database. Pastikan nomor peserta benar."
else:
response = f"Maaf, nomor peserta {reg_number} tidak ditemukan dalam database. Pastikan nomor peserta benar."
else:
base_response = "Untuk mengecek status kepesertaan, silakan sertakan nomor peserta Anda. Contoh: 'Cek status peserta 12345678'"
if context.get('user_name'):
response = f"{context['user_name']}, {base_response.lower()}"
else:
response = base_response
else:
if self.llm_service:
try:
user_name = context.get('user_name')
history = self.get_history(session_id)
if history:
response = self.llm_service.generate_response(
message,
context=f"Nama pengguna: {user_name}" if user_name else "",
conversation_history=history[-3:]
)
else:
response = self.llm_service.generate_contextual_response(
message,
user_name=user_name
)
except Exception as e:
response = self.get_general_response(message, context)
else:
response = self.get_general_response(message, context)
self.add_to_history(session_id, message, response)
return response
# Initialize service
try:
bpjs_service = BPJSService()
print("=" * 60)
print("βœ“ BPJS Chatbot initialized successfully!")
print("=" * 60)
except Exception as e:
print("=" * 60)
print(f"❌ Error initializing BPJS Service: {e}")
print("=" * 60)
raise
# Gradio chat function
def chat_function(message, history):
"""Process chat message for Gradio interface"""
# Generate session ID from Gradio session (use consistent ID per session)
session_id = "gradio_session_default"
# Check for /reset command
if message.strip().lower() == "/reset":
# Clear backend state
cleared_items = []
if session_id in bpjs_service.conversation_history:
del bpjs_service.conversation_history[session_id]
cleared_items.append("conversation history")
if session_id in bpjs_service.registration_state:
del bpjs_service.registration_state[session_id]
cleared_items.append("registration state")
if cleared_items:
response = f"βœ… **Reset Berhasil!**\n\nYang telah dihapus:\n" + "\n".join([f"- {item}" for item in cleared_items]) + "\n\nAnda bisa memulai percakapan baru sekarang. πŸ”„"
else:
response = "ℹ️ **Tidak ada data untuk direset.**\n\nHistory dan state sudah kosong."
return response
# Process normal message
response = bpjs_service.process_message(session_id, message)
return response
# Create Gradio interface using ChatInterface (simple and clean)
demo = gr.ChatInterface(
fn=chat_function,
type="messages",
title="πŸ₯ BPJS Chatbot",
description="""
Chatbot untuk FAQ BPJS dan layanan kepesertaan menggunakan Groq LLM.
**Features:**
- πŸ“‹ FAQ BPJS dengan pencarian cerdas
- πŸ‘€ Cek status kepesertaan dari database
- πŸ“ Registrasi peserta baru (step-by-step)
- πŸ’¬ Conversational AI dengan context awareness
**Contoh pertanyaan:**
- "Bagaimana cara bayar iuran BPJS?"
- "Cek peserta 12345678"
- "Saya ingin daftar BPJS" (untuk registrasi)
""",
examples=[
"Bagaimana cara bayar iuran BPJS?",
"Cek peserta 12345678",
"Saya ingin daftar BPJS",
"Berapa denda keterlambatan bayar iuran?",
"Syarat rawat inap BPJS",
],
theme=gr.themes.Soft(),
chatbot=gr.Chatbot(
height=500,
show_copy_button=True,
avatar_images=(None, "https://em-content.zobj.net/source/twitter/376/hospital_1f3e5.png"),
type="messages"
),
textbox=gr.Textbox(
placeholder="Ketik pertanyaan Anda di sini...",
container=False,
scale=7
),
)
# Launch the app
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False
)