Muhammet Enes Nas commited on
Commit
1d006b3
·
1 Parent(s): c5549d8
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
+ ![DB'de kayıtlı ilaç sorgusu](screenshots/ornekCikti.jpg)
100
+
101
+ ![](screenshots/ornekcikti2.jpg)
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
+ ![Sipariş oluşturma](screenshots/siparis.jpg)
110
+
111
+ ![](screenshots/siparisLog.jpg)
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}"}