import gradio as gr import os from mistralai.client import MistralClient from mistralai.models.chat_completion import ChatMessage api_key = os.getenv("MISTRAL_API_KEY") if not api_key: raise ValueError("MISTRAL_API_KEY environment variable tidak ditemukan! Tambahkan di Settings → Variables and secrets.") client = MistralClient(api_key=api_key) DENTAL_SYSTEM_PROMPT = """You are a professional dental AI assistant specialized in dentistry and oral health. Your role is to provide helpful, accurate information about dental topics including: - Dental hygiene and preventive care - Common dental procedures and treatments - Oral health conditions and symptoms - Dental anatomy and terminology - Post-operative care instructions - Dental emergency guidance Important guidelines: - Always emphasize that you're not a substitute for professional dental care - Encourage users to consult with licensed dentists for specific medical advice - Provide evidence-based information when possible - Be clear about when immediate dental care is needed - Use simple, understandable language for non-medical users - Focus on educational and supportive information Please respond in Indonesian language unless the user specifically asks for English.""" NON_MEDICAL_KEYWORDS = [ 'masak', 'resep', 'film', 'musik', 'lagu', 'game', 'politik', 'bisnis', 'saham', 'crypto', 'olahraga', 'bola', 'basket', 'travel', 'wisata', 'fashion', 'cuaca', 'berita', 'gosip', 'artis', 'coding', 'programming', 'hukum', 'harga', 'beli', 'jual', 'toko', 'belanja' ] def extract_text(content): """Ekstrak teks dari content yang bisa berupa string, list, atau dict""" if isinstance(content, str): return content elif isinstance(content, list): texts = [] for item in content: if isinstance(item, dict): texts.append(item.get('text', '') or item.get('content', '')) elif isinstance(item, str): texts.append(item) return ' '.join(filter(None, texts)) elif isinstance(content, dict): return content.get('text', '') or content.get('content', '') or str(content) return str(content) if content else '' def is_medical_related(message): """Izinkan semua topik medis, blokir hanya yang jelas non-medis""" msg_lower = message.lower() # Blokir jika mengandung keyword non-medis yang jelas non_medical_count = sum(1 for kw in NON_MEDICAL_KEYWORDS if kw in msg_lower) # Jika mengandung 2+ keyword non-medis dan tidak ada konteks medis, tolak medical_hints = ['sakit', 'nyeri', 'obat', 'dokter', 'gejala', 'penyakit', 'kesehatan', 'medis', 'klinik', 'rumah sakit', 'operasi', 'terapi', 'diagnosis', 'treatment', 'health', 'pain', 'disease'] has_medical = any(kw in msg_lower for kw in medical_hints) if non_medical_count >= 2 and not has_medical: return False return True def dental_chat(message, history): message_text = extract_text(message) if not message_text.strip(): history.append({"role": "user", "content": message_text}) history.append({"role": "assistant", "content": "Mohon masukkan pertanyaan yang valid."}) yield "", history return if not is_medical_related(message_text): history.append({"role": "user", "content": message_text}) history.append({"role": "assistant", "content": "Maaf, saya hanya bisa membantu dengan pertanyaan seputar kesehatan dan kedokteran gigi. Silakan ajukan pertanyaan medis."}) yield "", history return try: mistral_messages = [ChatMessage(role="system", content=DENTAL_SYSTEM_PROMPT)] for msg in history: content_text = extract_text(msg["content"]) if content_text.strip(): mistral_messages.append(ChatMessage(role=msg["role"], content=content_text)) mistral_messages.append(ChatMessage(role="user", content=message_text)) history.append({"role": "user", "content": message_text}) history.append({"role": "assistant", "content": ""}) response_text = "" for chunk in client.chat_stream( model="mistral-tiny", messages=mistral_messages, max_tokens=500, temperature=0.7 ): token = chunk.choices[0].delta.content if token: response_text += token history[-1]["content"] = response_text yield "", history except Exception as e: print(f"Error: {str(e)}") error_str = str(e).lower() if "429" in error_str or "rate limit" in error_str or "quota" in error_str or "too many" in error_str: pesan = "⚠️ Layanan sedang sibuk atau kuota habis. Silakan coba beberapa saat lagi." elif "401" in error_str or "unauthorized" in error_str or "api key" in error_str: pesan = "🔑 API Key tidak valid atau sudah kadaluarsa. Hubungi administrator." elif "timeout" in error_str or "connection" in error_str: pesan = "🌐 Koneksi bermasalah. Periksa internet dan coba lagi." else: pesan = "❌ Terjadi kesalahan. Silakan coba lagi." if history and history[-1]["content"] == "": history[-1]["content"] = pesan else: history.append({"role": "assistant", "content": pesan}) yield "", history css = """ footer { display: none !important; } /* Ganti loading dots dengan teks Thinking... */ .progress-text span, .eta-bar, .generating span { display: none !important; } .generating::after { content: 'Thinking...' !important; color: #94a3b8 !important; font-style: italic !important; font-size: 0.9rem !important; } body, .gradio-container { background-color: #0a0e27 !important; } .main, .wrap { background-color: #0a0e27 !important; } .chatbot { background-color: #131729 !important; border: 1px solid rgba(148, 163, 184, 0.15) !important; border-radius: 12px !important; } .message-wrap { background-color: #131729 !important; } .user .message { background: linear-gradient(135deg, #00d9ff, #7c3aed) !important; color: white !important; border-radius: 12px !important; } .bot .message { background-color: #1a1f3a !important; color: #e2e8f0 !important; border-radius: 12px !important; border: 1px solid rgba(148, 163, 184, 0.1) !important; } textarea, input[type="text"] { background-color: #131729 !important; color: #e2e8f0 !important; border: 1px solid rgba(148, 163, 184, 0.2) !important; border-radius: 10px !important; } textarea:focus, input[type="text"]:focus { border-color: #00d9ff !important; box-shadow: 0 0 0 2px rgba(0, 217, 255, 0.2) !important; } label, .label-wrap span { color: #94a3b8 !important; } .primary { background: linear-gradient(135deg, #00d9ff, #7c3aed) !important; border: none !important; color: white !important; border-radius: 25px !important; font-weight: 600 !important; } .primary:hover { opacity: 0.9 !important; transform: translateY(-1px) !important; } .secondary { background-color: #1a1f3a !important; border: 1px solid rgba(148, 163, 184, 0.2) !important; color: #94a3b8 !important; border-radius: 25px !important; } .secondary:hover { border-color: #00d9ff !important; color: #00d9ff !important; } .panel, .block, .form { background-color: #131729 !important; border-color: rgba(148, 163, 184, 0.1) !important; } """ with gr.Blocks(title="DentoAI", css=css) as demo: chatbot = gr.Chatbot(height=500, label="Chat dengan DentoAI") msg = gr.Textbox( label="Ketik pertanyaan tentang kesehatan gigi...", placeholder="Contoh: Bagaimana cara menyikat gigi yang benar?", lines=2 ) with gr.Row(): submit_btn = gr.Button("Send", variant="primary") clear_btn = gr.Button("Clear Chat", variant="secondary") msg.submit(dental_chat, [msg, chatbot], [msg, chatbot]) submit_btn.click(dental_chat, [msg, chatbot], [msg, chatbot]) clear_btn.click(lambda: [], None, chatbot, queue=False) if __name__ == "__main__": demo.launch()