Spaces:
Sleeping
Sleeping
Muhammet Enes Nas commited on
Commit ·
1d006b3
1
Parent(s): c5549d8
space
Browse files- README.md +120 -0
- app.py +400 -0
- db.py +191 -0
- requirements.txt +6 -0
- screenshots/ornekCikti.jpg +0 -0
- screenshots/ornekcikti2.jpg +0 -0
- screenshots/siparis.jpg +0 -0
- screenshots/siparisLog.jpg +0 -0
- search_provider.py +125 -0
- seed_db.py +151 -0
- tools.py +233 -0
README.md
CHANGED
|
@@ -1,4 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
|
|
|
| 2 |
title: PharmacyWebsite
|
| 3 |
emoji: ⚡
|
| 4 |
colorFrom: purple
|
|
|
|
| 1 |
+
# 🏥 Eczane Sipariş & Prospektüs Asistanı
|
| 2 |
+
|
| 3 |
+
Tool-calling destekli, gerçek veritabanı okuma/yazma yapan, halüsinasyon üretmeyen bir eczane asistanı.
|
| 4 |
+
|
| 5 |
+
## 🎯 Senaryo Özeti
|
| 6 |
+
|
| 7 |
+
Kullanıcı bir ilaç hakkında bilgi ister (stok durumu, prospektüs özeti), sipariş oluşturur ve sipariş durumunu sorgular. Model hiçbir zaman ilaç bilgisini kendi hafızasından **uydurmaz** — her yanıt bir tool çağrısının döndürdüğü gerçek veriye dayanır.
|
| 8 |
+
|
| 9 |
+
## 🏗️ Mimari
|
| 10 |
+
|
| 11 |
+
```
|
| 12 |
+
Kullanıcı
|
| 13 |
+
│
|
| 14 |
+
▼
|
| 15 |
+
[Gradio Arayüzü]
|
| 16 |
+
│
|
| 17 |
+
▼
|
| 18 |
+
[Turn 1] Model → sistem promptuna göre JSON tool-call üretir
|
| 19 |
+
│
|
| 20 |
+
▼
|
| 21 |
+
[Tool Router] → hangi fonksiyon çağrılacak, argümanları parse eder
|
| 22 |
+
│
|
| 23 |
+
├─► get_drug_info(drug_name) ─┐
|
| 24 |
+
├─► create_order(drug_name, qty) ├─► SQLite DB (drugs, orders tabloları)
|
| 25 |
+
└─► check_order_status(order_id) ─┘
|
| 26 |
+
│
|
| 27 |
+
▼ (get_drug_info: DB'de ilaç yoksa)
|
| 28 |
+
[Web Arama API: Tavily / SerpApi] → prospektüs özeti çeker → DB'ye yazar
|
| 29 |
+
│
|
| 30 |
+
▼
|
| 31 |
+
[Turn 2] Model, tool sonucunu alır, kullanıcıya doğal dilde özetler
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
### Kullanılan Model
|
| 35 |
+
|
| 36 |
+
**`menesnas/gemma_4_pharmacy_merged`** — Gemma 4 tabanlı, Türkçe eczacılık QA verisiyle LoRA fine-tune edilmiş, merge edilmiş model.
|
| 37 |
+
|
| 38 |
+
## 📂 Dosya Yapısı
|
| 39 |
+
|
| 40 |
+
```
|
| 41 |
+
OrderDrug/
|
| 42 |
+
├── app.py # Gradio arayüzü + model yükleme + tool router
|
| 43 |
+
├── db.py # SQLite bağlantı, şema oluşturma, CRUD fonksiyonları
|
| 44 |
+
├── tools.py # get_drug_info, create_order, check_order_status
|
| 45 |
+
├── search_provider.py # Tavily/SerpApi soyutlaması
|
| 46 |
+
├── seed_db.py # İlk çalıştırmada örnek 10 ilaç ekler
|
| 47 |
+
├── requirements.txt # Bağımlılıklar
|
| 48 |
+
├── pharmacy.db # SQLite veritabanı (seed_db.py ile üretilir)
|
| 49 |
+
├── PROJECT_SPEC.md # Detaylı proje spesifikasyonu
|
| 50 |
+
└── README.md # Bu dosya
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## 🚀 Yerelde Çalıştırma
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
# 1. Bağımlılıkları kur
|
| 57 |
+
pip install -r requirements.txt
|
| 58 |
+
|
| 59 |
+
# 2. Veritabanını başlat (opsiyonel — app.py otomatik çağırır)
|
| 60 |
+
python seed_db.py
|
| 61 |
+
|
| 62 |
+
# 3. Uygulamayı başlat
|
| 63 |
+
python app.py
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### Ortam Değişkenleri
|
| 67 |
+
|
| 68 |
+
Web arama özelliği için (DB'de olmayan ilaçları aramak):
|
| 69 |
+
|
| 70 |
+
| Değişken | Açıklama |
|
| 71 |
+
|---|---|
|
| 72 |
+
| `TAVILY_API_KEY` | Tavily API anahtarı (önerilen) |
|
| 73 |
+
| `SERPAPI_API_KEY` | SerpApi API anahtarı (alternatif) |
|
| 74 |
+
|
| 75 |
+
> Her iki anahtar da yoksa, DB'de bulunmayan ilaçlar için "bulunamadı" hatası döner.
|
| 76 |
+
|
| 77 |
+
## 🧪 Örnek Kullanım Senaryoları
|
| 78 |
+
|
| 79 |
+
| # | Kullanıcı Girdisi | Tetiklenen Tool | Beklenen Davranış |
|
| 80 |
+
|---|---|---|---|
|
| 81 |
+
| 1 | "Parol stokta var mı?" | `get_drug_info` | DB'den stok/prospektüs döner |
|
| 82 |
+
| 2 | "Aspirin hakkında bilgi ver" | `get_drug_info` | Web araması → DB'ye kayıt → sonuç |
|
| 83 |
+
| 3 | "3 kutu Aferin sipariş et" | `create_order` | Stok azalır, sipariş oluşur |
|
| 84 |
+
| 4 | "500 kutu Parol sipariş et" | `create_order` | "Yetersiz stok" hatası |
|
| 85 |
+
| 5 | "5 numaralı sipariş ne durumda?" | `check_order_status` | Sipariş durumu döner |
|
| 86 |
+
| 6 | "Hava nasıl?" | — | Normal yanıt (tool çağrılmaz) |
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
## 📸 Örnek Çalıştırma Ekran Görüntüleri
|
| 90 |
+
|
| 91 |
+
Aşağıda, sistemin farklı senaryolarda tool-call mekanizmasını nasıl tetiklediğini gösteren örnek girdi/çıktılar yer almaktadır.
|
| 92 |
+
|
| 93 |
+
### 1. Semptoma Göre İlaç Sorgulama (Asistan Yanıtı)
|
| 94 |
+
|
| 95 |
+
**Kullanıcı girdisi:** `"Başım dönüyor. Hangi ilaç işe yarar? Prospektüsünü verebilir misin"`
|
| 96 |
+
|
| 97 |
+
Model `get_drug_info` fonksiyonunu çağırır, veritabanından gerçek stok ve prospektüs bilgisini çeker.
|
| 98 |
+
|
| 99 |
+

|
| 100 |
+
|
| 101 |
+

|
| 102 |
+
|
| 103 |
+
### 2. Sipariş Oluşturma
|
| 104 |
+
|
| 105 |
+
**Kullanıcı girdisi:** `"3 kutu Aferin sipariş et"`
|
| 106 |
+
|
| 107 |
+
`create_order` fonksiyonu çağrılır, stok düşürülür ve yeni bir sipariş kaydı oluşturulur.
|
| 108 |
+
|
| 109 |
+

|
| 110 |
+
|
| 111 |
+

|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
## ⚠️ Disclaimer
|
| 116 |
+
|
| 117 |
+
Bu sistem akademik bir projedir; stok/fiyat verileri simülasyondur, prospektüs özetleri gerçek tıbbi tavsiye yerine geçmez.
|
| 118 |
+
|
| 119 |
+
|
| 120 |
---
|
| 121 |
+
|
| 122 |
title: PharmacyWebsite
|
| 123 |
emoji: ⚡
|
| 124 |
colorFrom: purple
|
app.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Eczane Sipariş & Prospektüs Asistanı — Ana Uygulama
|
| 3 |
+
Gradio arayüzü + model yükleme + tool-calling loop.
|
| 4 |
+
|
| 5 |
+
Kullanılan model: menesnas/gemma_4_pharmacy_merged
|
| 6 |
+
(Gemma 4 tabanlı, Türkçe eczacılık QA verisiyle LoRA fine-tune, merge edilmiş)
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import re
|
| 11 |
+
import os
|
| 12 |
+
import time
|
| 13 |
+
|
| 14 |
+
import gradio as gr
|
| 15 |
+
import torch
|
| 16 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 17 |
+
|
| 18 |
+
from db import init_db, list_drugs
|
| 19 |
+
from seed_db import seed
|
| 20 |
+
from tools import TOOL_DEFINITIONS, route_tool_call
|
| 21 |
+
|
| 22 |
+
# ---------------------------------------------------------------------------
|
| 23 |
+
# Veritabanı başlat
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
init_db()
|
| 26 |
+
seed()
|
| 27 |
+
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
# Model Yükleme
|
| 30 |
+
# ---------------------------------------------------------------------------
|
| 31 |
+
MODEL_ID = "menesnas/gemma_4_pharmacy_merged"
|
| 32 |
+
|
| 33 |
+
print(f"[INFO] Model yukleniyor: {MODEL_ID}")
|
| 34 |
+
print(f"[INFO] CUDA mevcut mu: {torch.cuda.is_available()}")
|
| 35 |
+
if torch.cuda.is_available():
|
| 36 |
+
print(f"[INFO] GPU: {torch.cuda.get_device_name(0)}")
|
| 37 |
+
free_bytes, total_bytes = torch.cuda.mem_get_info(0)
|
| 38 |
+
print(f"[INFO] GPU bellek: {free_bytes / 1e9:.1f} GB bos / {total_bytes / 1e9:.1f} GB toplam")
|
| 39 |
+
else:
|
| 40 |
+
print("[UYARI] GPU bulunamadi — model CPU'da calisacak, bu COK YAVAS olur "
|
| 41 |
+
"(bir soru icin dakikalar surebilir). Colab'da Runtime > Change runtime "
|
| 42 |
+
"type > T4 GPU secili oldugundan emin olun.")
|
| 43 |
+
|
| 44 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 45 |
+
|
| 46 |
+
# NOT: device_map="auto" bazen (ozellikle Colab'da runtime yeniden baslatilmadan
|
| 47 |
+
# hucreler tekrar calistirildiginda GPU bellegi onceki yuklemelerle dolu gorunurse)
|
| 48 |
+
# modelin TAMAMINI CPU'ya atiyor — bu da uretimi ~100x yavaslatir. GPU varsa
|
| 49 |
+
# dogrudan tek GPU'ya zorluyoruz, "auto"nun yanlis offload karari vermesini onluyoruz.
|
| 50 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 51 |
+
MODEL_ID,
|
| 52 |
+
dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
|
| 53 |
+
device_map={"": 0} if torch.cuda.is_available() else "cpu",
|
| 54 |
+
)
|
| 55 |
+
print(f"[INFO] Model cihazi: {model.device}")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _resolve_eos_token_ids() -> list[int] | None:
|
| 59 |
+
"""
|
| 60 |
+
generate() icin durma token'larini belirler.
|
| 61 |
+
Gemma sohbet formati <end_of_turn> ile biter; modelin generation_config'i
|
| 62 |
+
bunu eos olarak tanimiyorsa, model her seferinde max_new_tokens'a kadar
|
| 63 |
+
(gereksiz yere COK yavas) uretim yapar. Bu yuzden hem <eos> hem
|
| 64 |
+
<end_of_turn> id'lerini elle topluyoruz.
|
| 65 |
+
"""
|
| 66 |
+
ids = set()
|
| 67 |
+
if tokenizer.eos_token_id is not None:
|
| 68 |
+
ids.add(tokenizer.eos_token_id)
|
| 69 |
+
end_of_turn_id = tokenizer.convert_tokens_to_ids("<end_of_turn>")
|
| 70 |
+
if end_of_turn_id is not None and end_of_turn_id != tokenizer.unk_token_id:
|
| 71 |
+
ids.add(end_of_turn_id)
|
| 72 |
+
return list(ids) if ids else None
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
EOS_TOKEN_IDS = _resolve_eos_token_ids()
|
| 76 |
+
PAD_TOKEN_ID = tokenizer.pad_token_id
|
| 77 |
+
if PAD_TOKEN_ID is None and EOS_TOKEN_IDS:
|
| 78 |
+
PAD_TOKEN_ID = EOS_TOKEN_IDS[0]
|
| 79 |
+
|
| 80 |
+
print(f"[INFO] EOS token id'leri: {EOS_TOKEN_IDS}")
|
| 81 |
+
print(f"[INFO] Tum ozel token'lar: {tokenizer.all_special_tokens}")
|
| 82 |
+
print(f"[INFO] '<end_of_turn>' -> id: {tokenizer.convert_tokens_to_ids('<end_of_turn>')} "
|
| 83 |
+
f"(unk_token_id: {tokenizer.unk_token_id})")
|
| 84 |
+
print("[OK] Model hazir!")
|
| 85 |
+
|
| 86 |
+
# ---------------------------------------------------------------------------
|
| 87 |
+
# System Prompt
|
| 88 |
+
# ---------------------------------------------------------------------------
|
| 89 |
+
SYSTEM_PROMPT = """Sen bir eczane sipariş asistanısın. Sana verilen 3 araçtan başka hiçbir bilgi kaynağın yok.
|
| 90 |
+
Kullanıcı ilaç bilgisi sorarsa, sipariş vermek isterse veya sipariş durumu sorarsa, SADECE aşağıdaki JSON formatlarından birini üret. Başka hiçbir şey yazma.
|
| 91 |
+
|
| 92 |
+
Araçların:
|
| 93 |
+
1. get_drug_info: İlacın stok ve prospektüs bilgisini getirir.
|
| 94 |
+
2. create_order: Sipariş oluşturur.
|
| 95 |
+
3. check_order_status: Sipariş durumunu sorgular.
|
| 96 |
+
|
| 97 |
+
Örnek 1:
|
| 98 |
+
Kullanıcı: "Parol var mı, stokta mı?"
|
| 99 |
+
Sen: {"name": "get_drug_info", "arguments": {"drug_name": "Parol"}}
|
| 100 |
+
|
| 101 |
+
Örnek 2:
|
| 102 |
+
Kullanıcı: "3 kutu Aferin sipariş etmek istiyorum"
|
| 103 |
+
Sen: {"name": "create_order", "arguments": {"drug_name": "Aferin", "quantity": 3}}
|
| 104 |
+
|
| 105 |
+
Örnek 3:
|
| 106 |
+
Kullanıcı: "12 numaralı siparişim nerede kaldı?"
|
| 107 |
+
Sen: {"name": "check_order_status", "arguments": {"order_id": 12}}
|
| 108 |
+
|
| 109 |
+
Örnek 4:
|
| 110 |
+
Kullanıcı: "Başım ağrıyor ne ilaç önerirsin?"
|
| 111 |
+
Sen: {"name": "get_drug_info", "arguments": {"drug_name": "baş ağrısı"}}
|
| 112 |
+
|
| 113 |
+
KURALLAR:
|
| 114 |
+
- İlaç/stok/sipariş ile ilgili her soruda SADECE yukarıdaki formatta JSON yaz, başka hiçbir şey yazma.
|
| 115 |
+
- İlaç bilgisi, stok, fiyat veya prospektüs bilgisini KENDİ BİLGİNDEN UYDURMA. Sadece tool sonucunda dönen veriyi kullan.
|
| 116 |
+
- Tool sonucu hata içeriyorsa, alternatif bilgi uydurma, sadece hatayı ilet.
|
| 117 |
+
- İlaç/eczane ile ilgili olmayan sorularda normal Türkçe yanıt verebilirsin.
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
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.
|
| 121 |
+
SADECE aşağıdaki verileri kullan, kendi bilginden ek bilgi EKLEME.
|
| 122 |
+
Eğer sonuçta hata varsa, hatayı kullanıcıya nazikçe ilet.
|
| 123 |
+
|
| 124 |
+
Araç sonucu:
|
| 125 |
+
{tool_result}
|
| 126 |
+
|
| 127 |
+
Kullanıcının sorusu: {user_message}
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
# ---------------------------------------------------------------------------
|
| 131 |
+
# Model çıkarım yardımcıları
|
| 132 |
+
# ---------------------------------------------------------------------------
|
| 133 |
+
|
| 134 |
+
def generate_response(prompt: str, max_new_tokens: int = 512) -> str:
|
| 135 |
+
"""Model'den yanıt üretir."""
|
| 136 |
+
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
|
| 137 |
+
start = time.time()
|
| 138 |
+
with torch.no_grad():
|
| 139 |
+
outputs = model.generate(
|
| 140 |
+
**inputs,
|
| 141 |
+
max_new_tokens=max_new_tokens,
|
| 142 |
+
do_sample=True,
|
| 143 |
+
temperature=0.3,
|
| 144 |
+
top_p=0.9,
|
| 145 |
+
repetition_penalty=1.15,
|
| 146 |
+
eos_token_id=EOS_TOKEN_IDS,
|
| 147 |
+
pad_token_id=PAD_TOKEN_ID,
|
| 148 |
+
)
|
| 149 |
+
elapsed = time.time() - start
|
| 150 |
+
generated = outputs[0][inputs["input_ids"].shape[1]:]
|
| 151 |
+
n_new = generated.shape[0]
|
| 152 |
+
print(f"[TIMING] generate(): {n_new} token, {elapsed:.1f}s ({n_new / elapsed:.2f} tok/s)")
|
| 153 |
+
# Sadece üretilen kısmı çöz
|
| 154 |
+
return tokenizer.decode(generated, skip_special_tokens=True).strip()
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def parse_tool_call(text: str) -> dict | None:
|
| 158 |
+
"""
|
| 159 |
+
Model çıktısından JSON tool call parse eder.
|
| 160 |
+
Başarısızsa None döner.
|
| 161 |
+
"""
|
| 162 |
+
# İlk olarak temiz JSON deniyoruz
|
| 163 |
+
cleaned = text.strip()
|
| 164 |
+
|
| 165 |
+
# Markdown code block kaldır
|
| 166 |
+
cleaned = re.sub(r"```(?:json)?\s*", "", cleaned)
|
| 167 |
+
cleaned = re.sub(r"```\s*$", "", cleaned)
|
| 168 |
+
cleaned = cleaned.strip()
|
| 169 |
+
|
| 170 |
+
# JSON objesini bul
|
| 171 |
+
match = re.search(r'\{[^{}]*"name"\s*:\s*"[^"]+"\s*,\s*"arguments"\s*:\s*\{[^}]*\}[^}]*\}', cleaned, re.DOTALL)
|
| 172 |
+
if match:
|
| 173 |
+
try:
|
| 174 |
+
parsed = json.loads(match.group())
|
| 175 |
+
if "name" in parsed and "arguments" in parsed:
|
| 176 |
+
return parsed
|
| 177 |
+
except json.JSONDecodeError:
|
| 178 |
+
pass
|
| 179 |
+
|
| 180 |
+
# Daha geniş JSON arama
|
| 181 |
+
for m in re.finditer(r'\{.*?\}', cleaned, re.DOTALL):
|
| 182 |
+
try:
|
| 183 |
+
parsed = json.loads(m.group())
|
| 184 |
+
if "name" in parsed and "arguments" in parsed:
|
| 185 |
+
return parsed
|
| 186 |
+
except (json.JSONDecodeError, KeyError):
|
| 187 |
+
continue
|
| 188 |
+
|
| 189 |
+
# Nested JSON için son deneme
|
| 190 |
+
try:
|
| 191 |
+
brace_start = cleaned.find("{")
|
| 192 |
+
brace_end = cleaned.rfind("}") + 1
|
| 193 |
+
if brace_start >= 0 and brace_end > brace_start:
|
| 194 |
+
candidate = cleaned[brace_start:brace_end]
|
| 195 |
+
parsed = json.loads(candidate)
|
| 196 |
+
if "name" in parsed and "arguments" in parsed:
|
| 197 |
+
return parsed
|
| 198 |
+
except (json.JSONDecodeError, KeyError):
|
| 199 |
+
pass
|
| 200 |
+
|
| 201 |
+
return None
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
# ---------------------------------------------------------------------------
|
| 205 |
+
# Ana işlem döngüsü (3-turn tool-calling)
|
| 206 |
+
# ---------------------------------------------------------------------------
|
| 207 |
+
|
| 208 |
+
def process_message(user_message: str) -> tuple[str, str]:
|
| 209 |
+
"""
|
| 210 |
+
Kullanıcı mesajını işler, tool-calling döngüsünü yürütür.
|
| 211 |
+
(final_response, log_text) döner.
|
| 212 |
+
"""
|
| 213 |
+
log_lines = []
|
| 214 |
+
|
| 215 |
+
# --- Turn 1: Model'e kullanıcı sorusunu ver, tool call üretmesini bekle ---
|
| 216 |
+
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"
|
| 217 |
+
|
| 218 |
+
log_lines.append("=" * 60)
|
| 219 |
+
log_lines.append("📝 TURN 1 — Model'e gönderilen kullanıcı mesajı:")
|
| 220 |
+
log_lines.append(f" \"{user_message}\"")
|
| 221 |
+
log_lines.append("")
|
| 222 |
+
|
| 223 |
+
raw_output = generate_response(turn1_prompt, max_new_tokens=256)
|
| 224 |
+
log_lines.append("🤖 Model çıktısı (Turn 1 — ham):")
|
| 225 |
+
log_lines.append(f" {raw_output}")
|
| 226 |
+
log_lines.append("")
|
| 227 |
+
|
| 228 |
+
# --- Tool call parse ---
|
| 229 |
+
tool_call = parse_tool_call(raw_output)
|
| 230 |
+
|
| 231 |
+
if tool_call is None:
|
| 232 |
+
# Tool call yok → model doğrudan yanıt veriyor (ilaçla ilgisi olmayan soru)
|
| 233 |
+
log_lines.append("ℹ️ Tool call tespit edilmedi — model doğrudan yanıt veriyor.")
|
| 234 |
+
return raw_output, "\n".join(log_lines)
|
| 235 |
+
|
| 236 |
+
tool_name = tool_call["name"]
|
| 237 |
+
tool_args = tool_call["arguments"]
|
| 238 |
+
|
| 239 |
+
# Tamsayı argümanları düzelt (model bazen string verebilir)
|
| 240 |
+
if "quantity" in tool_args and isinstance(tool_args["quantity"], str):
|
| 241 |
+
try:
|
| 242 |
+
tool_args["quantity"] = int(tool_args["quantity"])
|
| 243 |
+
except ValueError:
|
| 244 |
+
pass
|
| 245 |
+
if "order_id" in tool_args and isinstance(tool_args["order_id"], str):
|
| 246 |
+
try:
|
| 247 |
+
tool_args["order_id"] = int(tool_args["order_id"])
|
| 248 |
+
except ValueError:
|
| 249 |
+
pass
|
| 250 |
+
|
| 251 |
+
log_lines.append(f"🔧 Tool call tespit edildi:")
|
| 252 |
+
log_lines.append(f" Araç: {tool_name}")
|
| 253 |
+
log_lines.append(f" Argümanlar: {json.dumps(tool_args, ensure_ascii=False)}")
|
| 254 |
+
log_lines.append("")
|
| 255 |
+
|
| 256 |
+
# --- Tool çalıştır ---
|
| 257 |
+
tool_result = route_tool_call(tool_name, tool_args)
|
| 258 |
+
tool_result_json = json.dumps(tool_result, ensure_ascii=False, indent=2)
|
| 259 |
+
|
| 260 |
+
log_lines.append("📦 Tool sonucu:")
|
| 261 |
+
log_lines.append(f" {tool_result_json}")
|
| 262 |
+
log_lines.append("")
|
| 263 |
+
|
| 264 |
+
# --- Turn 2: Tool sonucunu modele ver, doğal dilde özetlemesini iste ---
|
| 265 |
+
result_prompt = RESULT_PROMPT_TEMPLATE.format(
|
| 266 |
+
tool_result=tool_result_json,
|
| 267 |
+
user_message=user_message,
|
| 268 |
+
)
|
| 269 |
+
turn2_prompt = (
|
| 270 |
+
f"<start_of_turn>system\n{SYSTEM_PROMPT}<end_of_turn>\n"
|
| 271 |
+
f"<start_of_turn>user\n{user_message}<end_of_turn>\n"
|
| 272 |
+
f"<start_of_turn>model\n{json.dumps(tool_call, ensure_ascii=False)}<end_of_turn>\n"
|
| 273 |
+
f"<start_of_turn>user\n{result_prompt}<end_of_turn>\n"
|
| 274 |
+
f"<start_of_turn>model\n"
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
final_response = generate_response(turn2_prompt, max_new_tokens=512)
|
| 278 |
+
|
| 279 |
+
log_lines.append("💬 TURN 2 — Model'in nihai yanıtı:")
|
| 280 |
+
log_lines.append(f" {final_response}")
|
| 281 |
+
log_lines.append("=" * 60)
|
| 282 |
+
|
| 283 |
+
return final_response, "\n".join(log_lines)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
# ---------------------------------------------------------------------------
|
| 287 |
+
# Gradio Arayüzü
|
| 288 |
+
# ---------------------------------------------------------------------------
|
| 289 |
+
|
| 290 |
+
def get_drugs_table():
|
| 291 |
+
"""Mevcut ilaçlar tablosunu DataFrame için döner."""
|
| 292 |
+
drugs = list_drugs()
|
| 293 |
+
if not drugs:
|
| 294 |
+
return [["Veri yok", "", ""]]
|
| 295 |
+
return [
|
| 296 |
+
[
|
| 297 |
+
d["display_name"],
|
| 298 |
+
str(d["stock"]),
|
| 299 |
+
f"{d['price']:.2f} ₺" if d["price"] else "—",
|
| 300 |
+
]
|
| 301 |
+
for d in drugs
|
| 302 |
+
]
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def chat_handler(user_message: str):
|
| 306 |
+
"""Gradio event handler."""
|
| 307 |
+
if not user_message.strip():
|
| 308 |
+
return "Lütfen bir mesaj yazın.", "", get_drugs_table()
|
| 309 |
+
|
| 310 |
+
final_response, log_text = process_message(user_message.strip())
|
| 311 |
+
drugs_table = get_drugs_table()
|
| 312 |
+
return final_response, log_text, drugs_table
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
# --- Arayüz ---
|
| 316 |
+
DISCLAIMER = (
|
| 317 |
+
"⚠️ **Disclaimer:** Bu sistem akademik bir projedir; stok/fiyat verileri simülasyondur, "
|
| 318 |
+
"prospektüs özetleri gerçek tıbbi tavsiye yerine geçmez."
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
CUSTOM_CSS = """
|
| 322 |
+
.disclaimer {
|
| 323 |
+
background: #fff3cd;
|
| 324 |
+
border: 1px solid #ffc107;
|
| 325 |
+
border-radius: 8px;
|
| 326 |
+
padding: 12px;
|
| 327 |
+
margin-top: 16px;
|
| 328 |
+
font-size: 14px;
|
| 329 |
+
}
|
| 330 |
+
.header-title {
|
| 331 |
+
text-align: center;
|
| 332 |
+
margin-bottom: 8px;
|
| 333 |
+
}
|
| 334 |
+
"""
|
| 335 |
+
|
| 336 |
+
CUSTOM_THEME = gr.themes.Soft(
|
| 337 |
+
primary_hue="teal",
|
| 338 |
+
secondary_hue="blue",
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
with gr.Blocks(title="Eczane Siparis Asistani") as demo:
|
| 342 |
+
|
| 343 |
+
gr.Markdown(
|
| 344 |
+
"""
|
| 345 |
+
# 🏥 Eczane Sipariş & Prospektüs Asistanı
|
| 346 |
+
**Model:** `menesnas/gemma_4_pharmacy_merged` (Gemma 4 — Türkçe Eczacılık Fine-Tune)
|
| 347 |
+
|
| 348 |
+
İlaç bilgisi sorgulayabilir, sipariş oluşturabilir ve sipariş durumunu takip edebilirsiniz.
|
| 349 |
+
""",
|
| 350 |
+
elem_classes=["header-title"],
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
with gr.Row():
|
| 354 |
+
with gr.Column(scale=2):
|
| 355 |
+
user_input = gr.Textbox(
|
| 356 |
+
label="💬 Mesajınız",
|
| 357 |
+
placeholder='Örn: "Parol stokta var mı?", "3 kutu Aferin sipariş et", "5 numaralı sipariş ne durumda?"',
|
| 358 |
+
lines=2,
|
| 359 |
+
)
|
| 360 |
+
send_btn = gr.Button("📤 Gönder", variant="primary", size="lg")
|
| 361 |
+
|
| 362 |
+
response_output = gr.Textbox(
|
| 363 |
+
label="🤖 Asistan Yanıtı",
|
| 364 |
+
lines=6,
|
| 365 |
+
interactive=False,
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
with gr.Column(scale=1):
|
| 369 |
+
drugs_table = gr.Dataframe(
|
| 370 |
+
headers=["İlaç Adı", "Stok", "Fiyat"],
|
| 371 |
+
label="📋 Mevcut İlaçlar",
|
| 372 |
+
value=get_drugs_table(),
|
| 373 |
+
interactive=False,
|
| 374 |
+
wrap=True,
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
with gr.Accordion("🔍 Tool Call İşlem Adımları (Log)", open=False):
|
| 378 |
+
log_output = gr.Textbox(
|
| 379 |
+
label="İşlem Günlüğü",
|
| 380 |
+
lines=15,
|
| 381 |
+
interactive=False,
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
gr.Markdown(DISCLAIMER, elem_classes=["disclaimer"])
|
| 385 |
+
|
| 386 |
+
# Event binding
|
| 387 |
+
send_btn.click(
|
| 388 |
+
fn=chat_handler,
|
| 389 |
+
inputs=[user_input],
|
| 390 |
+
outputs=[response_output, log_output, drugs_table],
|
| 391 |
+
)
|
| 392 |
+
user_input.submit(
|
| 393 |
+
fn=chat_handler,
|
| 394 |
+
inputs=[user_input],
|
| 395 |
+
outputs=[response_output, log_output, drugs_table],
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
if __name__ == "__main__":
|
| 400 |
+
demo.launch(share=True, theme=CUSTOM_THEME, css=CUSTOM_CSS)
|
db.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Eczane Sipariş Asistanı — Veritabanı Katmanı
|
| 3 |
+
SQLite bağlantı yönetimi, şema oluşturma ve CRUD fonksiyonları.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import sqlite3
|
| 7 |
+
import os
|
| 8 |
+
from contextlib import contextmanager
|
| 9 |
+
|
| 10 |
+
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pharmacy.db")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def get_connection() -> sqlite3.Connection:
|
| 14 |
+
"""Yeni bir SQLite bağlantısı oluşturur (row_factory aktif)."""
|
| 15 |
+
conn = sqlite3.connect(DB_PATH)
|
| 16 |
+
conn.row_factory = sqlite3.Row
|
| 17 |
+
conn.execute("PRAGMA journal_mode=WAL")
|
| 18 |
+
conn.execute("PRAGMA foreign_keys=ON")
|
| 19 |
+
return conn
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@contextmanager
|
| 23 |
+
def get_db():
|
| 24 |
+
"""Context manager: bağlantı açar, işlem bitince kapatır."""
|
| 25 |
+
conn = get_connection()
|
| 26 |
+
try:
|
| 27 |
+
yield conn
|
| 28 |
+
conn.commit()
|
| 29 |
+
except Exception:
|
| 30 |
+
conn.rollback()
|
| 31 |
+
raise
|
| 32 |
+
finally:
|
| 33 |
+
conn.close()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def init_db():
|
| 37 |
+
"""Veritabanı tablolarını oluşturur (yoksa)."""
|
| 38 |
+
with get_db() as conn:
|
| 39 |
+
conn.executescript("""
|
| 40 |
+
CREATE TABLE IF NOT EXISTS drugs (
|
| 41 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 42 |
+
name TEXT UNIQUE NOT NULL,
|
| 43 |
+
display_name TEXT,
|
| 44 |
+
stock INTEGER NOT NULL DEFAULT 0,
|
| 45 |
+
price REAL,
|
| 46 |
+
prospektus_summary TEXT,
|
| 47 |
+
source TEXT,
|
| 48 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 49 |
+
);
|
| 50 |
+
|
| 51 |
+
CREATE TABLE IF NOT EXISTS orders (
|
| 52 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 53 |
+
drug_id INTEGER REFERENCES drugs(id),
|
| 54 |
+
drug_name TEXT,
|
| 55 |
+
quantity INTEGER NOT NULL,
|
| 56 |
+
status TEXT DEFAULT 'Hazırlanıyor',
|
| 57 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 58 |
+
);
|
| 59 |
+
""")
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# ---------------------------------------------------------------------------
|
| 63 |
+
# CRUD — drugs
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
|
| 66 |
+
def find_drug(name: str) -> dict | None:
|
| 67 |
+
"""
|
| 68 |
+
İlaç adına göre arama yapar (case-insensitive, boşluk normalize edilmiş).
|
| 69 |
+
Bulursa dict döner, bulamazsa None.
|
| 70 |
+
"""
|
| 71 |
+
normalized = " ".join(name.strip().lower().split())
|
| 72 |
+
with get_db() as conn:
|
| 73 |
+
row = conn.execute(
|
| 74 |
+
"SELECT * FROM drugs WHERE LOWER(REPLACE(name, ' ', '')) = LOWER(REPLACE(?, ' ', ''))",
|
| 75 |
+
(normalized,),
|
| 76 |
+
).fetchone()
|
| 77 |
+
if row:
|
| 78 |
+
return dict(row)
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def insert_drug(
|
| 83 |
+
name: str,
|
| 84 |
+
display_name: str,
|
| 85 |
+
stock: int = 0,
|
| 86 |
+
price: float | None = None,
|
| 87 |
+
prospektus_summary: str | None = None,
|
| 88 |
+
source: str = "seed",
|
| 89 |
+
) -> int:
|
| 90 |
+
"""Yeni ilaç kaydı ekler ve id döner."""
|
| 91 |
+
normalized = " ".join(name.strip().lower().split())
|
| 92 |
+
with get_db() as conn:
|
| 93 |
+
cur = conn.execute(
|
| 94 |
+
"""INSERT INTO drugs (name, display_name, stock, price, prospektus_summary, source)
|
| 95 |
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
| 96 |
+
(normalized, display_name, stock, price, prospektus_summary, source),
|
| 97 |
+
)
|
| 98 |
+
return cur.lastrowid
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def update_drug_prospektus(drug_id: int, summary: str, source: str = "fetched"):
|
| 102 |
+
"""Mevcut ilacın prospektüs özetini günceller."""
|
| 103 |
+
with get_db() as conn:
|
| 104 |
+
conn.execute(
|
| 105 |
+
"UPDATE drugs SET prospektus_summary = ?, source = ? WHERE id = ?",
|
| 106 |
+
(summary, source, drug_id),
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def search_drugs_by_keyword(keyword: str) -> list[dict]:
|
| 111 |
+
"""
|
| 112 |
+
Semptom, şikayet veya kelimeye göre prospektüs özetinde ve ilaç adında arama yapar.
|
| 113 |
+
"""
|
| 114 |
+
term = keyword.strip().lower()
|
| 115 |
+
# Yaygın kelimeleri temizle (örn. 'ilacı', 'önerisi', 'için')
|
| 116 |
+
stop_words = {"ilaci", "ilaç", "ilacı", "onerisi", "önerisi", "icin", "için", "ne", "var", "mi", "mı", "tavsiye"}
|
| 117 |
+
words = [w for w in term.split() if w not in stop_words and len(w) > 2]
|
| 118 |
+
|
| 119 |
+
if not words:
|
| 120 |
+
words = [term]
|
| 121 |
+
|
| 122 |
+
with get_db() as conn:
|
| 123 |
+
results = []
|
| 124 |
+
seen_ids = set()
|
| 125 |
+
for word in words:
|
| 126 |
+
pattern = f"%{word}%"
|
| 127 |
+
rows = conn.execute(
|
| 128 |
+
"""SELECT id, name, display_name, stock, price, prospektus_summary, source
|
| 129 |
+
FROM drugs
|
| 130 |
+
WHERE LOWER(prospektus_summary) LIKE ? OR LOWER(display_name) LIKE ? OR LOWER(name) LIKE ?""",
|
| 131 |
+
(pattern, pattern, pattern),
|
| 132 |
+
).fetchall()
|
| 133 |
+
for r in rows:
|
| 134 |
+
d = dict(r)
|
| 135 |
+
if d["id"] not in seen_ids:
|
| 136 |
+
seen_ids.add(d["id"])
|
| 137 |
+
results.append(d)
|
| 138 |
+
return results
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def list_drugs() -> list[dict]:
|
| 142 |
+
"""Tüm ilaçları döner (arayüz tablosu için)."""
|
| 143 |
+
with get_db() as conn:
|
| 144 |
+
rows = conn.execute(
|
| 145 |
+
"SELECT display_name, stock, price FROM drugs ORDER BY display_name"
|
| 146 |
+
).fetchall()
|
| 147 |
+
return [dict(r) for r in rows]
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# ---------------------------------------------------------------------------
|
| 151 |
+
# CRUD — orders
|
| 152 |
+
# ---------------------------------------------------------------------------
|
| 153 |
+
|
| 154 |
+
def create_order_record(drug_id: int, drug_name: str, quantity: int) -> dict | None:
|
| 155 |
+
"""
|
| 156 |
+
Sipariş oluşturur. Atomik stok güncellemesi yapar (race condition önlemi).
|
| 157 |
+
Başarılıysa sipariş dict, değilse None döner.
|
| 158 |
+
"""
|
| 159 |
+
with get_db() as conn:
|
| 160 |
+
# Atomik stok azaltma — tek sorguda kontrol + güncelleme
|
| 161 |
+
cur = conn.execute(
|
| 162 |
+
"UPDATE drugs SET stock = stock - ? WHERE id = ? AND stock >= ?",
|
| 163 |
+
(quantity, drug_id, quantity),
|
| 164 |
+
)
|
| 165 |
+
if cur.rowcount == 0:
|
| 166 |
+
return None # Stok yetersiz veya ilaç bulunamadı
|
| 167 |
+
|
| 168 |
+
cur = conn.execute(
|
| 169 |
+
"""INSERT INTO orders (drug_id, drug_name, quantity, status)
|
| 170 |
+
VALUES (?, ?, ?, 'Hazırlanıyor')""",
|
| 171 |
+
(drug_id, drug_name, quantity),
|
| 172 |
+
)
|
| 173 |
+
order_id = cur.lastrowid
|
| 174 |
+
return {
|
| 175 |
+
"order_id": order_id,
|
| 176 |
+
"drug_name": drug_name,
|
| 177 |
+
"quantity": quantity,
|
| 178 |
+
"status": "Hazırlanıyor",
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def find_order(order_id: int) -> dict | None:
|
| 183 |
+
"""Sipariş ID'ye göre arama yapar."""
|
| 184 |
+
with get_db() as conn:
|
| 185 |
+
row = conn.execute(
|
| 186 |
+
"SELECT id AS order_id, drug_name, quantity, status, created_at FROM orders WHERE id = ?",
|
| 187 |
+
(order_id,),
|
| 188 |
+
).fetchone()
|
| 189 |
+
if row:
|
| 190 |
+
return dict(row)
|
| 191 |
+
return None
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers>=4.45.0
|
| 2 |
+
torch>=2.1.0
|
| 3 |
+
gradio>=4.0.0
|
| 4 |
+
requests>=2.31.0
|
| 5 |
+
tavily-python>=0.3.0
|
| 6 |
+
accelerate>=0.25.0
|
screenshots/ornekCikti.jpg
ADDED
|
screenshots/ornekcikti2.jpg
ADDED
|
screenshots/siparis.jpg
ADDED
|
screenshots/siparisLog.jpg
ADDED
|
search_provider.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Eczane Sipariş Asistanı — Harici Arama Sağlayıcı Soyutlaması
|
| 3 |
+
Tavily (varsayılan) veya SerpApi ile web araması yapar.
|
| 4 |
+
Ortam değişkenlerine göre otomatik seçim yapar.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _tavily_search(query: str) -> str | None:
|
| 12 |
+
"""Tavily API ile arama yapar. Özetlenmiş sonuç döner."""
|
| 13 |
+
api_key = os.environ.get("TAVILY_API_KEY")
|
| 14 |
+
if not api_key:
|
| 15 |
+
return None
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
from tavily import TavilyClient
|
| 19 |
+
|
| 20 |
+
client = TavilyClient(api_key=api_key)
|
| 21 |
+
response = client.search(
|
| 22 |
+
query=query,
|
| 23 |
+
search_depth="basic",
|
| 24 |
+
max_results=3,
|
| 25 |
+
include_answer=True,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Tavily'nin kendi özetini önce dene
|
| 29 |
+
if response.get("answer"):
|
| 30 |
+
return response["answer"]
|
| 31 |
+
|
| 32 |
+
# Yoksa sonuçlardan birleştir
|
| 33 |
+
snippets = []
|
| 34 |
+
for result in response.get("results", [])[:3]:
|
| 35 |
+
content = result.get("content", "")
|
| 36 |
+
if content:
|
| 37 |
+
snippets.append(content[:300])
|
| 38 |
+
|
| 39 |
+
if snippets:
|
| 40 |
+
return " ".join(snippets)[:800]
|
| 41 |
+
|
| 42 |
+
return None
|
| 43 |
+
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print(f"[Tavily Arama Hatası] {e}")
|
| 46 |
+
return None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _serpapi_search(query: str) -> str | None:
|
| 50 |
+
"""SerpApi ile arama yapar (yedek sağlayıcı)."""
|
| 51 |
+
api_key = os.environ.get("SERPAPI_API_KEY")
|
| 52 |
+
if not api_key:
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
import requests
|
| 57 |
+
|
| 58 |
+
params = {
|
| 59 |
+
"q": query,
|
| 60 |
+
"api_key": api_key,
|
| 61 |
+
"engine": "google",
|
| 62 |
+
"hl": "tr",
|
| 63 |
+
"gl": "tr",
|
| 64 |
+
"num": 3,
|
| 65 |
+
}
|
| 66 |
+
resp = requests.get(
|
| 67 |
+
"https://serpapi.com/search",
|
| 68 |
+
params=params,
|
| 69 |
+
timeout=10,
|
| 70 |
+
)
|
| 71 |
+
resp.raise_for_status()
|
| 72 |
+
data = resp.json()
|
| 73 |
+
|
| 74 |
+
# Featured snippet veya answer box
|
| 75 |
+
if "answer_box" in data:
|
| 76 |
+
ab = data["answer_box"]
|
| 77 |
+
snippet = ab.get("snippet") or ab.get("answer") or ""
|
| 78 |
+
if snippet:
|
| 79 |
+
return snippet[:800]
|
| 80 |
+
|
| 81 |
+
# Organik sonuçlar
|
| 82 |
+
snippets = []
|
| 83 |
+
for result in data.get("organic_results", [])[:3]:
|
| 84 |
+
s = result.get("snippet", "")
|
| 85 |
+
if s:
|
| 86 |
+
snippets.append(s)
|
| 87 |
+
|
| 88 |
+
if snippets:
|
| 89 |
+
return " ".join(snippets)[:800]
|
| 90 |
+
|
| 91 |
+
return None
|
| 92 |
+
|
| 93 |
+
except Exception as e:
|
| 94 |
+
print(f"[SerpApi Arama Hatası] {e}")
|
| 95 |
+
return None
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def search_drug_info(drug_name: str) -> str | None:
|
| 99 |
+
"""
|
| 100 |
+
İlaç bilgisi araması yapar. Önce Tavily, yoksa SerpApi dener.
|
| 101 |
+
Sonuç bulunamazsa None döner.
|
| 102 |
+
"""
|
| 103 |
+
query = f"{drug_name} prospektüs kullanma talimatı doz yan etki"
|
| 104 |
+
|
| 105 |
+
# Önce Tavily dene
|
| 106 |
+
result = _tavily_search(query)
|
| 107 |
+
if result:
|
| 108 |
+
return result
|
| 109 |
+
|
| 110 |
+
# Tavily yoksa SerpApi dene
|
| 111 |
+
result = _serpapi_search(query)
|
| 112 |
+
if result:
|
| 113 |
+
return result
|
| 114 |
+
|
| 115 |
+
return None
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def get_active_provider() -> str:
|
| 119 |
+
"""Aktif arama sağlayıcısını döner (bilgi amaçlı)."""
|
| 120 |
+
if os.environ.get("TAVILY_API_KEY"):
|
| 121 |
+
return "Tavily"
|
| 122 |
+
elif os.environ.get("SERPAPI_API_KEY"):
|
| 123 |
+
return "SerpApi"
|
| 124 |
+
else:
|
| 125 |
+
return "Yok (API anahtarı tanımlı değil)"
|
seed_db.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Eczane Sipariş Asistanı — Veritabanı Başlangıç Verileri
|
| 3 |
+
İlk çalıştırmada 10 örnek ilaç ekler (source='seed').
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from db import init_db, insert_drug, find_drug
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
SEED_DRUGS = [
|
| 10 |
+
{
|
| 11 |
+
"name": "parol",
|
| 12 |
+
"display_name": "Parol",
|
| 13 |
+
"stock": 120,
|
| 14 |
+
"price": 32.50,
|
| 15 |
+
"prospektus_summary": (
|
| 16 |
+
"Parol (Parasetamol 500 mg): Hafif-orta şiddetli ağrı ve ateşte kullanılır. "
|
| 17 |
+
"Yetişkinler günde en fazla 4 g (8 tablet) alabilir. Karaciğer hastalığı "
|
| 18 |
+
"olanlarda dikkatli kullanılmalıdır. Yaygın yan etki nadirdir."
|
| 19 |
+
),
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"name": "majezik",
|
| 23 |
+
"display_name": "Majezik",
|
| 24 |
+
"stock": 85,
|
| 25 |
+
"price": 48.00,
|
| 26 |
+
"prospektus_summary": (
|
| 27 |
+
"Majezik (Flurbiprofen 100 mg): Non-steroid antiinflamatuar ilaçtır. "
|
| 28 |
+
"Diş ağrısı, kas-iskelet ağrıları ve eklem iltihabında kullanılır. "
|
| 29 |
+
"Tok karnına alınmalıdır. Mide hassasiyeti olanlarda dikkat edilmelidir."
|
| 30 |
+
),
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"name": "augmentin",
|
| 34 |
+
"display_name": "Augmentin",
|
| 35 |
+
"stock": 40,
|
| 36 |
+
"price": 95.00,
|
| 37 |
+
"prospektus_summary": (
|
| 38 |
+
"Augmentin (Amoksisilin/Klavulanat): Geniş spektrumlu antibiyotiktir. "
|
| 39 |
+
"Üst ve alt solunum yolu, idrar yolu, deri enfeksiyonlarında kullanılır. "
|
| 40 |
+
"Reçetesiz kullanılmamalıdır. Tedavi süresi doktor tarafından belirlenir."
|
| 41 |
+
),
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"name": "aferin",
|
| 45 |
+
"display_name": "Aferin",
|
| 46 |
+
"stock": 200,
|
| 47 |
+
"price": 28.00,
|
| 48 |
+
"prospektus_summary": (
|
| 49 |
+
"Aferin Fort: Parasetamol, klorfeniramin ve pseudoefedrin içerir. "
|
| 50 |
+
"Grip ve soğuk algınlığı belirtilerini (ateş, burun akıntısı, tıkanıklık) "
|
| 51 |
+
"hafifletir. Uyku yapabilir; araç kullanırken dikkatli olunmalıdır."
|
| 52 |
+
),
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"name": "ventolin",
|
| 56 |
+
"display_name": "Ventolin",
|
| 57 |
+
"stock": 60,
|
| 58 |
+
"price": 42.50,
|
| 59 |
+
"prospektus_summary": (
|
| 60 |
+
"Ventolin (Salbutamol) İnhaler: Bronş genişleticidir. Astım ve KOAH'ta "
|
| 61 |
+
"nefes darlığı ataklarında kullanılır. Günde en fazla 8 puf önerilir. "
|
| 62 |
+
"Çarpıntı ve titreme yan etkisi görülebilir."
|
| 63 |
+
),
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"name": "nurofen",
|
| 67 |
+
"display_name": "Nurofen",
|
| 68 |
+
"stock": 150,
|
| 69 |
+
"price": 55.00,
|
| 70 |
+
"prospektus_summary": (
|
| 71 |
+
"Nurofen (İbuprofen 400 mg): Ağrı kesici ve ateş düşürücüdür. "
|
| 72 |
+
"Baş ağrısı, diş ağrısı, adet sancısı ve kas ağrılarında kullanılır. "
|
| 73 |
+
"Tok karnına alınmalıdır. Mide ülseri olanlarda kontrendikedir."
|
| 74 |
+
),
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"name": "calpol",
|
| 78 |
+
"display_name": "Calpol",
|
| 79 |
+
"stock": 90,
|
| 80 |
+
"price": 38.00,
|
| 81 |
+
"prospektus_summary": (
|
| 82 |
+
"Calpol (Parasetamol 120 mg/5 ml şurup): Çocuklarda ateş ve ağrıda "
|
| 83 |
+
"kullanılır. Doz çocuğun kilosuna göre ayarlanır. 2 ayın altındaki "
|
| 84 |
+
"bebeklerde doktor kontrolünde kullanılmalıdır."
|
| 85 |
+
),
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"name": "cipro",
|
| 89 |
+
"display_name": "Cipro",
|
| 90 |
+
"stock": 30,
|
| 91 |
+
"price": 72.00,
|
| 92 |
+
"prospektus_summary": (
|
| 93 |
+
"Cipro (Siprofloksasin 500 mg): Florokinolon grubu antibiyotiktir. "
|
| 94 |
+
"İdrar yolu, solunum yolu ve gastrointestinal enfeksiyonlarda kullanılır. "
|
| 95 |
+
"Tendon rüptürü riski nedeniyle yaşlılarda dikkatli kullanılmalıdır."
|
| 96 |
+
),
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"name": "voltaren",
|
| 100 |
+
"display_name": "Voltaren",
|
| 101 |
+
"stock": 110,
|
| 102 |
+
"price": 65.00,
|
| 103 |
+
"prospektus_summary": (
|
| 104 |
+
"Voltaren (Diklofenak 75 mg): Non-steroid antiinflamatuar ilaçtır. "
|
| 105 |
+
"Romatizmal ağrılar, bel ağrısı, travma sonrası şişlik ve ağrıda "
|
| 106 |
+
"kullanılır. Uzun süreli kullanımda mide koruyucu ile birlikte alınmalıdır."
|
| 107 |
+
),
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"name": "a-ferin",
|
| 111 |
+
"display_name": "A-Ferin",
|
| 112 |
+
"stock": 75,
|
| 113 |
+
"price": 35.00,
|
| 114 |
+
"prospektus_summary": (
|
| 115 |
+
"A-Ferin Plus: Parasetamol ve kafein kombinasyonudur. Baş ağrısı ve "
|
| 116 |
+
"grip belirtilerinde kullanılır. Kafein hassasiyeti olanlarda dikkatli "
|
| 117 |
+
"olunmalıdır. Günde 3-4 tabletten fazla alınmamalıdır."
|
| 118 |
+
),
|
| 119 |
+
},
|
| 120 |
+
]
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def seed():
|
| 124 |
+
"""Veritabanını oluşturur ve örnek ilaçları ekler (zaten varsa atlar)."""
|
| 125 |
+
init_db()
|
| 126 |
+
added = 0
|
| 127 |
+
skipped = 0
|
| 128 |
+
for drug in SEED_DRUGS:
|
| 129 |
+
existing = find_drug(drug["name"])
|
| 130 |
+
if existing:
|
| 131 |
+
skipped += 1
|
| 132 |
+
print(f" [SKIP] '{drug['display_name']}' zaten mevcut, atlaniyor.")
|
| 133 |
+
continue
|
| 134 |
+
insert_drug(
|
| 135 |
+
name=drug["name"],
|
| 136 |
+
display_name=drug["display_name"],
|
| 137 |
+
stock=drug["stock"],
|
| 138 |
+
price=drug["price"],
|
| 139 |
+
prospektus_summary=drug["prospektus_summary"],
|
| 140 |
+
source="seed",
|
| 141 |
+
)
|
| 142 |
+
added += 1
|
| 143 |
+
print(f" [OK] '{drug['display_name']}' eklendi.")
|
| 144 |
+
|
| 145 |
+
print(f"\nToplam: {added} eklendi, {skipped} atlandi.")
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
if __name__ == "__main__":
|
| 149 |
+
print("Eczane veritabani baslangic verileri yukleniyor...\n")
|
| 150 |
+
seed()
|
| 151 |
+
print("\n[OK] Tamamlandi!")
|
tools.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Eczane Sipariş Asistanı — Tool Fonksiyonları
|
| 3 |
+
get_drug_info, create_order, check_order_status
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
from db import find_drug, insert_drug, create_order_record, find_order, search_drugs_by_keyword
|
| 8 |
+
from search_provider import search_drug_info
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# ---------------------------------------------------------------------------
|
| 12 |
+
# Tool tanımları (model'e gösterilecek JSON şeması)
|
| 13 |
+
# ---------------------------------------------------------------------------
|
| 14 |
+
|
| 15 |
+
TOOL_DEFINITIONS = [
|
| 16 |
+
{
|
| 17 |
+
"type": "function",
|
| 18 |
+
"function": {
|
| 19 |
+
"name": "get_drug_info",
|
| 20 |
+
"description": "Bir ilacın veya semptomun/şikayetin (örn. baş ağrısı, ateş) stok durumunu ve prospektüs özetini getirir.",
|
| 21 |
+
"parameters": {
|
| 22 |
+
"type": "object",
|
| 23 |
+
"properties": {
|
| 24 |
+
"drug_name": {"type": "string", "description": "İlacın adı veya şikayet/semptom (örn. Parol, baş ağrısı)"}
|
| 25 |
+
},
|
| 26 |
+
"required": ["drug_name"],
|
| 27 |
+
},
|
| 28 |
+
},
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"type": "function",
|
| 32 |
+
"function": {
|
| 33 |
+
"name": "create_order",
|
| 34 |
+
"description": "Belirtilen ilaçtan belirtilen adette sipariş oluşturur.",
|
| 35 |
+
"parameters": {
|
| 36 |
+
"type": "object",
|
| 37 |
+
"properties": {
|
| 38 |
+
"drug_name": {"type": "string"},
|
| 39 |
+
"quantity": {"type": "integer"},
|
| 40 |
+
},
|
| 41 |
+
"required": ["drug_name", "quantity"],
|
| 42 |
+
},
|
| 43 |
+
},
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"type": "function",
|
| 47 |
+
"function": {
|
| 48 |
+
"name": "check_order_status",
|
| 49 |
+
"description": "Sipariş numarasına göre sipariş durumunu sorgular.",
|
| 50 |
+
"parameters": {
|
| 51 |
+
"type": "object",
|
| 52 |
+
"properties": {
|
| 53 |
+
"order_id": {"type": "integer"}
|
| 54 |
+
},
|
| 55 |
+
"required": ["order_id"],
|
| 56 |
+
},
|
| 57 |
+
},
|
| 58 |
+
},
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# ---------------------------------------------------------------------------
|
| 63 |
+
# Tool uygulamaları
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
|
| 66 |
+
def get_drug_info(drug_name: str) -> dict:
|
| 67 |
+
"""
|
| 68 |
+
Cache-then-fetch mantığı:
|
| 69 |
+
1. DB'de birebir veya semptom/kelime araması yapar.
|
| 70 |
+
2. DB'de varsa → stok/fiyat/prospektüs döner (web'e çıkmaz)
|
| 71 |
+
3. Yoksa → web araması → DB'ye kaydeder → sonucu döner
|
| 72 |
+
4. Hiçbir şey bulunamazsa → hata
|
| 73 |
+
"""
|
| 74 |
+
drug = find_drug(drug_name)
|
| 75 |
+
|
| 76 |
+
if drug:
|
| 77 |
+
return {
|
| 78 |
+
"drug_name": drug["display_name"],
|
| 79 |
+
"stock": drug["stock"],
|
| 80 |
+
"price": drug["price"],
|
| 81 |
+
"prospektus_summary": drug["prospektus_summary"],
|
| 82 |
+
"source": "db",
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
# DB'de birebir yoksa semptom / kelime araması yap
|
| 86 |
+
keyword_matches = search_drugs_by_keyword(drug_name)
|
| 87 |
+
if keyword_matches:
|
| 88 |
+
results = [
|
| 89 |
+
{
|
| 90 |
+
"drug_name": d["display_name"],
|
| 91 |
+
"stock": d["stock"],
|
| 92 |
+
"price": d["price"],
|
| 93 |
+
"prospektus_summary": d["prospektus_summary"],
|
| 94 |
+
}
|
| 95 |
+
for d in keyword_matches
|
| 96 |
+
]
|
| 97 |
+
return {
|
| 98 |
+
"search_query": drug_name,
|
| 99 |
+
"matching_drugs": results,
|
| 100 |
+
"source": "db_keyword_search",
|
| 101 |
+
"note": f"Veritabanında '{drug_name}' ile ilgili {len(results)} ilaç bulundu."
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
# DB'de hiç yok → web araması
|
| 105 |
+
raw_result = search_drug_info(drug_name)
|
| 106 |
+
|
| 107 |
+
if raw_result:
|
| 108 |
+
# Özetleme kuralı: ham metin doğrudan kopyalanmaz, kısaltılır
|
| 109 |
+
summary_text = raw_result[:500].strip()
|
| 110 |
+
if len(raw_result) > 500:
|
| 111 |
+
# Son cümle sınırında kes
|
| 112 |
+
last_dot = summary_text.rfind(".")
|
| 113 |
+
if last_dot > 100:
|
| 114 |
+
summary_text = summary_text[: last_dot + 1]
|
| 115 |
+
|
| 116 |
+
disclaimer = "Bu bilgi web aramasından derlenmiş bir özettir, resmi prospektüs yerine geçmez."
|
| 117 |
+
full_summary = f"{disclaimer}\n\n{summary_text}"
|
| 118 |
+
|
| 119 |
+
# DB'ye kaydet
|
| 120 |
+
display_name = drug_name.strip().title()
|
| 121 |
+
drug_id = insert_drug(
|
| 122 |
+
name=drug_name,
|
| 123 |
+
display_name=display_name,
|
| 124 |
+
stock=0,
|
| 125 |
+
price=None,
|
| 126 |
+
prospektus_summary=full_summary,
|
| 127 |
+
source="fetched",
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
return {
|
| 131 |
+
"drug_name": display_name,
|
| 132 |
+
"stock": 0,
|
| 133 |
+
"price": None,
|
| 134 |
+
"prospektus_summary": full_summary,
|
| 135 |
+
"source": "fetched_new",
|
| 136 |
+
"note": "Bu ilaç veritabanına yeni eklendi, stok bilgisi güncel değildir.",
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
# API de sonuç döndürmedi
|
| 140 |
+
return {"error": f"'{drug_name}' ile ilgili ilaç bulunamadı. Lütfen kontrol edin."}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def create_order(drug_name: str, quantity: int | None = None) -> dict:
|
| 144 |
+
"""
|
| 145 |
+
Sipariş oluşturur. Stok kontrolü yapar.
|
| 146 |
+
"""
|
| 147 |
+
if quantity is None:
|
| 148 |
+
return {"error": "Sipariş miktarı belirtilmedi. Lütfen kaç adet sipariş etmek istediğinizi yazın."}
|
| 149 |
+
|
| 150 |
+
try:
|
| 151 |
+
quantity = int(quantity)
|
| 152 |
+
except (ValueError, TypeError):
|
| 153 |
+
return {"error": "Geçersiz sipariş miktarı. Lütfen geçerli bir sayı girin."}
|
| 154 |
+
|
| 155 |
+
if quantity <= 0:
|
| 156 |
+
return {"error": "Sipariş miktarı 0'dan büyük olmalıdır."}
|
| 157 |
+
|
| 158 |
+
drug = find_drug(drug_name)
|
| 159 |
+
|
| 160 |
+
if not drug:
|
| 161 |
+
return {
|
| 162 |
+
"error": (
|
| 163 |
+
f"'{drug_name}' veritabanında kayıtlı değil. "
|
| 164 |
+
"Önce get_drug_info ile ilacı sorgulayın."
|
| 165 |
+
)
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
if drug["stock"] < quantity:
|
| 169 |
+
return {
|
| 170 |
+
"error": (
|
| 171 |
+
f"Yetersiz stok. '{drug['display_name']}' için mevcut stok: "
|
| 172 |
+
f"{drug['stock']}, istenen: {quantity}."
|
| 173 |
+
)
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
result = create_order_record(
|
| 177 |
+
drug_id=drug["id"],
|
| 178 |
+
drug_name=drug["display_name"],
|
| 179 |
+
quantity=quantity,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
if result is None:
|
| 183 |
+
return {"error": "Sipariş oluşturulamadı (stok yetersiz olabilir)."}
|
| 184 |
+
|
| 185 |
+
return result
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def check_order_status(order_id: int | None = None) -> dict:
|
| 189 |
+
"""
|
| 190 |
+
Sipariş durumunu sorgular.
|
| 191 |
+
"""
|
| 192 |
+
if order_id is None:
|
| 193 |
+
return {"error": "Sipariş numarası belirtilmedi."}
|
| 194 |
+
|
| 195 |
+
try:
|
| 196 |
+
order_id = int(order_id)
|
| 197 |
+
except (ValueError, TypeError):
|
| 198 |
+
return {"error": "Geçersiz sipariş numarası."}
|
| 199 |
+
|
| 200 |
+
order = find_order(order_id)
|
| 201 |
+
|
| 202 |
+
if not order:
|
| 203 |
+
return {"error": f"{order_id} numaralı sipariş bulunamadı."}
|
| 204 |
+
|
| 205 |
+
return order
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# ---------------------------------------------------------------------------
|
| 209 |
+
# Tool Router
|
| 210 |
+
# ---------------------------------------------------------------------------
|
| 211 |
+
|
| 212 |
+
TOOL_MAP = {
|
| 213 |
+
"get_drug_info": get_drug_info,
|
| 214 |
+
"create_order": create_order,
|
| 215 |
+
"check_order_status": check_order_status,
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def route_tool_call(tool_name: str, arguments: dict) -> dict:
|
| 220 |
+
"""
|
| 221 |
+
Tool adına göre ilgili fonksiyonu çağırır.
|
| 222 |
+
Tanınmayan tool isimlerinde hata döner.
|
| 223 |
+
"""
|
| 224 |
+
func = TOOL_MAP.get(tool_name)
|
| 225 |
+
if func is None:
|
| 226 |
+
return {"error": f"Tanınmayan araç: '{tool_name}'"}
|
| 227 |
+
|
| 228 |
+
try:
|
| 229 |
+
return func(**arguments)
|
| 230 |
+
except TypeError as e:
|
| 231 |
+
return {"error": f"Araç argüman hatası: {e}"}
|
| 232 |
+
except Exception as e:
|
| 233 |
+
return {"error": f"Araç çalıştırma hatası: {e}"}
|