Spaces:
Sleeping
Sleeping
| """ | |
| Eczane Sipariş & Prospektüs Asistanı — Ana Uygulama | |
| Hugging Face Space (ZeroGPU) Uyumlu | |
| """ | |
| # --- ZeroGPU Uyumluğu (torch'tan ÖNCE import edilmeli) --- | |
| try: | |
| import spaces | |
| HAS_SPACES = True | |
| except ImportError: | |
| HAS_SPACES = False | |
| class spaces: | |
| def GPU(func): | |
| return func | |
| import json | |
| import re | |
| import os | |
| import gradio as gr | |
| import torch | |
| from transformers import AutoTokenizer, AutoModelForCausalLM | |
| from db import init_db, list_drugs | |
| from seed_db import seed | |
| from tools import TOOL_DEFINITIONS, route_tool_call | |
| # Veritabanını ve başlangıç ilaçlarını yükle | |
| init_db() | |
| seed() | |
| MODEL_ID = "menesnas/gemma_4_pharmacy_merged" | |
| print(f"[INFO] Model yukleniyor: {MODEL_ID}") | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) | |
| try: | |
| from transformers import BitsAndBytesConfig | |
| bnb_config = BitsAndBytesConfig( | |
| load_in_4bit=True, | |
| bnb_4bit_compute_dtype=torch.float16, | |
| bnb_4bit_quant_type="nf4", | |
| ) | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MODEL_ID, | |
| quantization_config=bnb_config, | |
| device_map="auto", | |
| ) | |
| print("[OK] Model 4-bit quantization ile yuklendi!") | |
| except Exception as e: | |
| print(f"[WARN] 4-bit yukleme basarisiz ({e}), float16 deneniyor...") | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MODEL_ID, | |
| dtype=torch.float16 if torch.cuda.is_available() else torch.float32, | |
| device_map="auto", | |
| ) | |
| print("[OK] Model float16 ile yuklendi!") | |
| SYSTEM_PROMPT = """Sen bir eczane sipariş asistanısın. Sana verilen araçlardan başka hiçbir bilgi kaynağın yok. | |
| Kullanıcı ilaç bilgisi sorarsa, şikayet/semptom belirtip öneri isterse, sipariş vermek isterse veya sipariş durumu sorarsa, SADECE aşağıdaki JSON formatlarından birini üret. Başka hiçbir şey yazma. | |
| Araçların: | |
| 1. get_drug_info: Belirli bir ilacın (örn. Parol, Majezik) stok ve prospektüs bilgisini getirir. | |
| 2. search_by_symptom: Şikayet/semptoma (örn. baş dönmesi, ateş, mide yanması) uygun ilaçları arar. | |
| 3. create_order: Sipariş oluşturur. | |
| 4. check_order_status: Sipariş durumunu sorgular. | |
| Örnek 1: | |
| Kullanıcı: "Parol var mı, stokta mı?" | |
| Sen: {"name": "get_drug_info", "arguments": {"drug_name": "Parol"}} | |
| Örnek 2: | |
| Kullanıcı: "Baş dönmesine iyi gelen ilaç var mı? Tavsiyen var mı?" | |
| Sen: {"name": "search_by_symptom", "arguments": {"symptom": "baş dönmesi"}} | |
| Örnek 3: | |
| Kullanıcı: "3 kutu Aferin sipariş etmek istiyorum" | |
| Sen: {"name": "create_order", "arguments": {"drug_name": "Aferin", "quantity": 3}} | |
| Örnek 4: | |
| Kullanıcı: "12 numaralı siparişim nerede kaldı?" | |
| Sen: {"name": "check_order_status", "arguments": {"order_id": 12}} | |
| KURALLAR: | |
| - Kullanıcı spesifik bir ilaç adı söylemediyse (örn. baş dönmesi, ateş, mide yanması dediyse) KESİNLİKLE uydurma bir ilaç adı yazma, `search_by_symptom` aracını kullan. | |
| - İlaç/stok/sipariş ile ilgili her soruda SADECE yukarıdaki formatta JSON yaz, başka hiçbir şey yazma. | |
| - İlaç bilgisi, stok, fiyat veya prospektüs bilgisini KENDİ BİLGİNDEN UYDURMA. Sadece tool sonucunda dönen veriyi kullan. | |
| - İlaç/eczane ile ilgili olmayan sorularda normal Türkçe yanıt verebilirsin. | |
| """ | |
| RESULT_PROMPT_TEMPLATE = """Aşağıda bir araç çağrısının sonucu var. Bu sonucu kullanıcıya doğal, kibar Türkçe ile özetle. | |
| SADECE aşağıdaki verileri kullan, kendi bilginden ek bilgi EKLEME. | |
| Eğer sonuçta hata varsa, hatayı kullanıcıya nazikçe ilet. | |
| Araç sonucu: | |
| {tool_result} | |
| Kullanıcının sorusu: {user_message} | |
| """ | |
| def generate_response(prompt: str, max_new_tokens: int = 128) -> str: | |
| """Model'den hızlı ve kararlı yanıt üretir (HF ZeroGPU Uyumlu).""" | |
| inputs = tokenizer(prompt, return_tensors="pt").to(model.device) | |
| pad_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id | |
| eos_ids = [tokenizer.eos_token_id] | |
| end_of_turn_id = tokenizer.convert_tokens_to_ids("<end_of_turn>") | |
| if isinstance(end_of_turn_id, int) and end_of_turn_id != tokenizer.unk_token_id: | |
| eos_ids.append(end_of_turn_id) | |
| with torch.no_grad(): | |
| outputs = model.generate( | |
| **inputs, | |
| max_new_tokens=max_new_tokens, | |
| do_sample=False, | |
| pad_token_id=pad_id, | |
| eos_token_id=eos_ids, | |
| ) | |
| generated = outputs[0][inputs["input_ids"].shape[1]:] | |
| text = tokenizer.decode(generated, skip_special_tokens=False) | |
| if "<end_of_turn>" in text: | |
| text = text.split("<end_of_turn>")[0] | |
| if "<start_of_turn>" in text: | |
| text = text.split("<start_of_turn>")[0] | |
| return text.replace("<end_of_turn>", "").replace("<start_of_turn>", "").strip() | |
| def parse_tool_call(text: str) -> dict | None: | |
| cleaned = text.strip() | |
| cleaned = re.sub(r"```(?:json)?\s*", "", cleaned) | |
| cleaned = re.sub(r"```\s*$", "", cleaned) | |
| cleaned = cleaned.strip() | |
| match = re.search(r'\{[^{}]*"name"\s*:\s*"[^"]+"\s*,\s*"arguments"\s*:\s*\{[^}]*\}[^}]*\}', cleaned, re.DOTALL) | |
| if match: | |
| try: | |
| parsed = json.loads(match.group()) | |
| if "name" in parsed and "arguments" in parsed: | |
| return parsed | |
| except json.JSONDecodeError: | |
| pass | |
| for m in re.finditer(r'\{.*?\}', cleaned, re.DOTALL): | |
| try: | |
| parsed = json.loads(m.group()) | |
| if "name" in parsed and "arguments" in parsed: | |
| return parsed | |
| except (json.JSONDecodeError, KeyError): | |
| continue | |
| try: | |
| brace_start = cleaned.find("{") | |
| brace_end = cleaned.rfind("}") + 1 | |
| if brace_start >= 0 and brace_end > brace_start: | |
| candidate = cleaned[brace_start:brace_end] | |
| parsed = json.loads(candidate) | |
| if "name" in parsed and "arguments" in parsed: | |
| return parsed | |
| except (json.JSONDecodeError, KeyError): | |
| pass | |
| return None | |
| def process_message(user_message: str) -> tuple[str, str]: | |
| log_lines = [] | |
| turn1_prompt = f"<start_of_turn>system\n{SYSTEM_PROMPT}<end_of_turn>\n<start_of_turn>user\n{user_message}<end_of_turn>\n<start_of_turn>model\n" | |
| log_lines.append("=" * 60) | |
| log_lines.append("📝 TURN 1 — Model'e gönderilen kullanıcı mesajı:") | |
| log_lines.append(f" \"{user_message}\"") | |
| log_lines.append("") | |
| raw_output = generate_response(turn1_prompt, max_new_tokens=64) | |
| log_lines.append("🤖 Model çıktısı (Turn 1 — ham):") | |
| log_lines.append(f" {raw_output}") | |
| log_lines.append("") | |
| tool_call = parse_tool_call(raw_output) | |
| if tool_call is None: | |
| log_lines.append("ℹ️ Tool call tespit edilmedi — model doğrudan yanıt veriyor.") | |
| return raw_output, "\n".join(log_lines) | |
| tool_name = tool_call["name"] | |
| tool_args = tool_call["arguments"] | |
| if "quantity" in tool_args and isinstance(tool_args["quantity"], str): | |
| try: | |
| tool_args["quantity"] = int(tool_args["quantity"]) | |
| except ValueError: | |
| pass | |
| if "order_id" in tool_args and isinstance(tool_args["order_id"], str): | |
| try: | |
| tool_args["order_id"] = int(tool_args["order_id"]) | |
| except ValueError: | |
| pass | |
| # --- Akıllı Halüsinasyon Önleme & Rerouting --- | |
| if tool_name == "get_drug_info" and "drug_name" in tool_args: | |
| requested_drug = tool_args["drug_name"] | |
| from db import find_drug, search_drugs_by_keyword | |
| if not find_drug(requested_drug) and not search_drugs_by_keyword(requested_drug): | |
| symptom_matches = search_drugs_by_keyword(user_message) | |
| if symptom_matches: | |
| log_lines.append(f"⚠️ Model '{requested_drug}' adında olmayan bir ilaç üretti.") | |
| log_lines.append(f"🔄 Kullanıcı sorusundan veritabanı semptom aramasına yönlendiriliyor...") | |
| tool_name = "search_by_symptom" | |
| tool_args = {"symptom": user_message} | |
| log_lines.append(f"🔧 Tool call tespit edildi:") | |
| log_lines.append(f" Araç: {tool_name}") | |
| log_lines.append(f" Argümanlar: {json.dumps(tool_args, ensure_ascii=False)}") | |
| log_lines.append("") | |
| tool_result = route_tool_call(tool_name, tool_args) | |
| tool_result_json = json.dumps(tool_result, ensure_ascii=False, indent=2) | |
| log_lines.append("📦 Tool sonucu:") | |
| log_lines.append(f" {tool_result_json}") | |
| log_lines.append("") | |
| result_prompt = RESULT_PROMPT_TEMPLATE.format( | |
| tool_result=tool_result_json, | |
| user_message=user_message, | |
| ) | |
| turn2_prompt = ( | |
| f"<start_of_turn>system\n{SYSTEM_PROMPT}<end_of_turn>\n" | |
| f"<start_of_turn>user\n{user_message}<end_of_turn>\n" | |
| f"<start_of_turn>model\n{json.dumps(tool_call, ensure_ascii=False)}<end_of_turn>\n" | |
| f"<start_of_turn>user\n{result_prompt}<end_of_turn>\n" | |
| f"<start_of_turn>model\n" | |
| ) | |
| final_response = generate_response(turn2_prompt, max_new_tokens=256) | |
| log_lines.append("💬 TURN 2 — Model'in nihai yanıtı:") | |
| log_lines.append(f" {final_response}") | |
| log_lines.append("=" * 60) | |
| return final_response, "\n".join(log_lines) | |
| def get_drugs_table(): | |
| drugs = list_drugs() | |
| if not drugs: | |
| return [["Veri yok", "", ""]] | |
| return [ | |
| [ | |
| d["display_name"], | |
| str(d["stock"]), | |
| f"{d['price']:.2f} ₺" if d["price"] else "—", | |
| ] | |
| for d in drugs | |
| ] | |
| def chat_handler(user_message: str): | |
| if not user_message.strip(): | |
| return "Lütfen bir mesaj yazın.", "", get_drugs_table() | |
| final_response, log_text = process_message(user_message.strip()) | |
| drugs_table = get_drugs_table() | |
| return final_response, log_text, drugs_table | |
| DISCLAIMER = ( | |
| "⚠️ **Disclaimer:** Bu sistem akademik bir projedir; stok/fiyat verileri simülasyondur, " | |
| "prospektüs özetleri gerçek tıbbi tavsiye yerine geçmez." | |
| ) | |
| CUSTOM_CSS = """ | |
| .disclaimer { | |
| background: #fff3cd; | |
| border: 1px solid #ffc107; | |
| border-radius: 8px; | |
| padding: 12px; | |
| margin-top: 16px; | |
| font-size: 14px; | |
| } | |
| .header-title { | |
| text-align: center; | |
| margin-bottom: 8px; | |
| } | |
| """ | |
| CUSTOM_THEME = gr.themes.Soft( | |
| primary_hue="teal", | |
| secondary_hue="blue", | |
| ) | |
| with gr.Blocks(title="Eczane Siparis Asistani") as demo: | |
| gr.Markdown( | |
| """ | |
| # 🏥 Eczane Sipariş & Prospektüs Asistanı | |
| **Model:** `menesnas/gemma_4_pharmacy_merged` (Gemma 4 — Türkçe Eczacılık Fine-Tune) | |
| İlaç bilgisi sorgulayabilir, sipariş oluşturabilir ve sipariş durumunu takip edebilirsiniz. | |
| """, | |
| elem_classes=["header-title"], | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| user_input = gr.Textbox( | |
| label="💬 Mesajınız", | |
| placeholder='Örn: "Parol stokta var mı?", "3 kutu Aferin sipariş et", "5 numaralı sipariş ne durumda?"', | |
| lines=2, | |
| ) | |
| send_btn = gr.Button("📤 Gönder", variant="primary", size="lg") | |
| response_output = gr.Textbox( | |
| label="🤖 Asistan Yanıtı", | |
| lines=6, | |
| interactive=False, | |
| ) | |
| with gr.Column(scale=1): | |
| drugs_table = gr.Dataframe( | |
| headers=["İlaç Adı", "Stok", "Fiyat"], | |
| label="📋 Mevcut İlaçlar", | |
| value=get_drugs_table(), | |
| interactive=False, | |
| wrap=True, | |
| ) | |
| with gr.Accordion("🔍 Tool Call İşlem Adımları (Log)", open=False): | |
| log_output = gr.Textbox( | |
| label="İşlem Günlüğü", | |
| lines=15, | |
| interactive=False, | |
| ) | |
| gr.Markdown(DISCLAIMER, elem_classes=["disclaimer"]) | |
| send_btn.click( | |
| fn=chat_handler, | |
| inputs=[user_input], | |
| outputs=[response_output, log_output, drugs_table], | |
| ) | |
| user_input.submit( | |
| fn=chat_handler, | |
| inputs=[user_input], | |
| outputs=[response_output, log_output, drugs_table], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(theme=CUSTOM_THEME, css=CUSTOM_CSS) | |