SalihHub commited on
Commit
2dbf9a6
·
verified ·
1 Parent(s): 12bd82a

app.py eklendi

Browse files
Files changed (1) hide show
  1. app.py +943 -0
app.py ADDED
@@ -0,0 +1,943 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ders5 — Tool Calling / Function Calling Demosu
2
+ #
3
+ # Tek soru, üç panel:
4
+ # A. Güçlü model + araçlar açık -> doğru ve güncel cevap
5
+ # B. Güçlü model + araçlar kapalı -> güncel veriyi bilemez
6
+ # C. Zayıf model + araçlar açık -> araç çağırmayı dener ama bozuk üretir
7
+ #
8
+ # A/B karşılaştırması tool calling'in kattığını gösterir; A/C karşılaştırması
9
+ # model kapasitesinin (araç olsa bile) belirleyici olduğunu gösterir.
10
+ #
11
+ # Çalıştırma: python3 app.py (OPENROUTER_API_KEY .env veya ortam değişkeninden okunur)
12
+
13
+ import json
14
+ import os
15
+ import queue
16
+ import threading
17
+ import time
18
+
19
+ import gradio as gr
20
+ import httpx
21
+ from openai import OpenAI
22
+
23
+ # Bu uygulama gerçek GPU işi yapmıyor (sadece OpenRouter API + CPU üzerinde küçük
24
+ # hesaplar), ama hesabın ücretsiz Hugging Face Spaces hakkı yalnızca ZeroGPU
25
+ # donanımında geçerli. ZeroGPU çalışma zamanı, en az bir @spaces.GPU fonksiyonu
26
+ # görmeden Space'i başlatmayı reddediyor; bu yüzden zararsız bir dummy fonksiyon
27
+ # tanımlıyoruz. Yerelde `spaces` paketi kurulu değilse sessizce no-op'a düşer.
28
+ try:
29
+ import spaces
30
+ _gpu_dekoratoru = spaces.GPU
31
+ except ImportError:
32
+ def _gpu_dekoratoru(fn):
33
+ return fn
34
+
35
+
36
+ @_gpu_dekoratoru
37
+ def _zero_gpu_kontrolu():
38
+ """ZeroGPU çalışma zamanının aradığı işaret fonksiyon; gerçek iş yapmaz."""
39
+ return True
40
+
41
+
42
+ # --------------------------------------------------------------------------------------
43
+ # Ortam
44
+ # --------------------------------------------------------------------------------------
45
+
46
+ def _env_yukle(yol=".env"):
47
+ if not os.path.exists(yol):
48
+ return
49
+ with open(yol, "r", encoding="utf-8") as f:
50
+ for satir in f:
51
+ satir = satir.strip()
52
+ if not satir or satir.startswith("#") or "=" not in satir:
53
+ continue
54
+ k, _, v = satir.partition("=")
55
+ k, v = k.strip(), v.strip().strip("'\"")
56
+ if v and not os.environ.get(k):
57
+ os.environ[k] = v
58
+
59
+
60
+ _env_yukle()
61
+
62
+ OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "").strip()
63
+ OPENROUTER_BASE_URL = os.environ.get("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1").strip()
64
+
65
+ MODEL_GUCLU = os.environ.get("OPENROUTER_MODEL", "google/gemma-4-26b-a4b-it:nitro").strip()
66
+ # Küçük bir açık model: tool endpoint'i destekliyor (404 vermiyor) ama çok adımlı
67
+ # zincirlerde gerçek bir tool_calls üretmek yerine düz metin içine sahte
68
+ # <arac_adi>{...}</arac_adi> etiketleri ve UYDURMA sayılar yazıyor — ölçülmüş,
69
+ # tekrarlanabilir bir başarısızlık modu.
70
+ MODEL_ZAYIF = "meta-llama/llama-3.1-8b-instruct"
71
+
72
+ _istemci = None
73
+
74
+
75
+ def istemci():
76
+ global _istemci
77
+ if _istemci is None:
78
+ if not OPENROUTER_API_KEY:
79
+ raise RuntimeError(
80
+ "OPENROUTER_API_KEY tanımlı değil. Yerelde .env dosyasına, "
81
+ "HF Spaces'te Settings > Secrets kısmına ekleyin."
82
+ )
83
+ _istemci = OpenAI(api_key=OPENROUTER_API_KEY, base_url=OPENROUTER_BASE_URL)
84
+ return _istemci
85
+
86
+
87
+ # --------------------------------------------------------------------------------------
88
+ # Araçlar (3 adet: 2 canlı API + 1 zincirleme hesap)
89
+ # --------------------------------------------------------------------------------------
90
+
91
+ HAVA_KODLARI = {
92
+ 0: "açık", 1: "az bulutlu", 2: "parçalı bulutlu", 3: "kapalı",
93
+ 45: "puslu", 51: "hafif çisenti", 61: "hafif yağmur", 63: "yağmur",
94
+ 65: "şiddetli yağmur", 71: "kar", 80: "sağanak", 95: "gök gürültülü fırtına",
95
+ }
96
+
97
+
98
+ def hava_durumu(sehir):
99
+ """Bir şehrin anlık hava durumunu döner (Open-Meteo, anahtarsız)."""
100
+ try:
101
+ geo = httpx.get(
102
+ "https://geocoding-api.open-meteo.com/v1/search",
103
+ params={"name": sehir, "count": 1, "language": "tr"}, timeout=15,
104
+ ).json()
105
+ sonuclar = geo.get("results") or []
106
+ if not sonuclar:
107
+ return {"hata": f"'{sehir}' için konum bulunamadı."}
108
+ konum = sonuclar[0]
109
+
110
+ veri = httpx.get(
111
+ "https://api.open-meteo.com/v1/forecast",
112
+ params={
113
+ "latitude": konum["latitude"], "longitude": konum["longitude"],
114
+ "current": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code",
115
+ "timezone": "auto",
116
+ }, timeout=15,
117
+ ).json()
118
+ anlik = veri.get("current", {})
119
+
120
+ return {
121
+ "sehir": f"{konum['name']}, {konum.get('country', '')}".strip(", "),
122
+ "sicaklik_c": anlik.get("temperature_2m"),
123
+ "nem_yuzde": anlik.get("relative_humidity_2m"),
124
+ "ruzgar_kmh": anlik.get("wind_speed_10m"),
125
+ "durum": HAVA_KODLARI.get(anlik.get("weather_code"), "bilinmiyor"),
126
+ "kaynak": "Open-Meteo",
127
+ }
128
+ except Exception as e:
129
+ return {"hata": f"Hava durumu alınamadı: {e}"}
130
+
131
+
132
+ def doviz_cevir(miktar, kaynak_birim, hedef_birim):
133
+ """Güncel ECB kuruyla para birimi çevirir (Frankfurter, anahtarsız)."""
134
+ try:
135
+ kaynak_birim, hedef_birim = kaynak_birim.strip().upper(), hedef_birim.strip().upper()
136
+ miktar = float(miktar)
137
+ if kaynak_birim == hedef_birim:
138
+ return {"miktar": miktar, "sonuc": miktar, "kaynak_birim": kaynak_birim, "hedef_birim": hedef_birim}
139
+
140
+ veri = httpx.get(
141
+ "https://api.frankfurter.dev/v1/latest",
142
+ params={"base": kaynak_birim, "symbols": hedef_birim, "amount": miktar}, timeout=15,
143
+ ).json()
144
+ oranlar = veri.get("rates", {})
145
+ if hedef_birim not in oranlar:
146
+ return {"hata": f"{kaynak_birim} -> {hedef_birim} kuru bulunamadı."}
147
+
148
+ return {
149
+ "miktar": miktar, "kaynak_birim": kaynak_birim, "hedef_birim": hedef_birim,
150
+ "sonuc": round(oranlar[hedef_birim], 4), "kur_tarihi": veri.get("date"),
151
+ "kaynak": "Frankfurter / ECB",
152
+ }
153
+ except Exception as e:
154
+ return {"hata": f"Döviz çevrilemedi: {e}"}
155
+
156
+
157
+ def sicaklik_cevir(deger, kaynak_birim, hedef_birim):
158
+ """C/F/K arası sıcaklık çevirir (yerel hesap, API'ye gitmez)."""
159
+ try:
160
+ k, h = kaynak_birim.strip().lower()[0], hedef_birim.strip().lower()[0]
161
+ deger = float(deger)
162
+ celsius = {"c": deger, "f": (deger - 32) / 1.8, "k": deger - 273.15}.get(k)
163
+ if celsius is None or h not in "cfk":
164
+ return {"hata": f"'{kaynak_birim}' -> '{hedef_birim}' desteklenmiyor. Sadece C/F/K."}
165
+ sonuc = {"c": celsius, "f": celsius * 1.8 + 32, "k": celsius + 273.15}[h]
166
+ return {
167
+ "girdi": deger, "kaynak_birim": kaynak_birim, "hedef_birim": hedef_birim,
168
+ "sonuc": round(sonuc, 2), "kaynak": "yerel hesap",
169
+ }
170
+ except Exception as e:
171
+ return {"hata": f"Sıcaklık çevrilemedi: {e}"}
172
+
173
+
174
+ KRIPTO_TAKMA_ADLARI = {
175
+ "btc": "bitcoin", "bitcoin": "bitcoin", "eth": "ethereum", "ethereum": "ethereum",
176
+ "sol": "solana", "solana": "solana", "doge": "dogecoin", "dogecoin": "dogecoin",
177
+ "xrp": "ripple", "ripple": "ripple",
178
+ }
179
+
180
+
181
+ def kripto_fiyat(kripto, para_birimi="usd"):
182
+ """Bir kripto paranın güncel fiyatını döner (CoinGecko, anahtarsız)."""
183
+ try:
184
+ kimlik = KRIPTO_TAKMA_ADLARI.get(kripto.strip().lower(), kripto.strip().lower())
185
+ para_birimi = para_birimi.strip().lower()
186
+
187
+ veri = httpx.get(
188
+ "https://api.coingecko.com/api/v3/simple/price",
189
+ params={"ids": kimlik, "vs_currencies": para_birimi, "include_24hr_change": "true"},
190
+ timeout=15,
191
+ ).json()
192
+ if kimlik not in veri:
193
+ return {"hata": f"'{kripto}' CoinGecko'da bulunamadı."}
194
+
195
+ kayit = veri[kimlik]
196
+ degisim = kayit.get(f"{para_birimi}_24h_change")
197
+ return {
198
+ "kripto": kimlik, "para_birimi": para_birimi.upper(), "fiyat": kayit.get(para_birimi),
199
+ "degisim_24s_yuzde": round(degisim, 2) if degisim is not None else None,
200
+ "kaynak": "CoinGecko",
201
+ }
202
+ except Exception as e:
203
+ return {"hata": f"Kripto fiyatı alınamadı: {e}"}
204
+
205
+
206
+ WIKI_BASLIKLARI = {"User-Agent": "magibu-ders5-tool-calling/1.0 (egitim odevi; iletisim: salih12dede@gmail.com)"}
207
+
208
+
209
+ def wikipedia_ara(sorgu, dil="tr"):
210
+ """Wikipedia'da arayıp en alakalı maddenin özetini döner (anahtarsız)."""
211
+ try:
212
+ dil = dil.strip().lower() or "tr"
213
+ arama = httpx.get(
214
+ f"https://{dil}.wikipedia.org/w/api.php",
215
+ params={"action": "query", "list": "search", "srsearch": sorgu, "srlimit": 1, "format": "json"},
216
+ headers=WIKI_BASLIKLARI, timeout=15,
217
+ ).json()
218
+ bulunanlar = arama.get("query", {}).get("search", [])
219
+ if not bulunanlar:
220
+ return {"hata": f"'{sorgu}' için Wikipedia'da sonuç bulunamadı."}
221
+ baslik = bulunanlar[0]["title"]
222
+
223
+ ozet = httpx.get(
224
+ f"https://{dil}.wikipedia.org/w/api.php",
225
+ params={"action": "query", "prop": "extracts", "exintro": 1, "explaintext": 1,
226
+ "titles": baslik, "format": "json", "redirects": 1},
227
+ headers=WIKI_BASLIKLARI, timeout=15,
228
+ ).json()
229
+ sayfalar = ozet.get("query", {}).get("pages", {})
230
+ metin = next(iter(sayfalar.values()), {}).get("extract", "").strip()
231
+ if len(metin) > 800:
232
+ metin = metin[:800].rsplit(" ", 1)[0] + "..."
233
+
234
+ return {"baslik": baslik, "ozet": metin or "Özet bulunamadı.", "kaynak": f"Wikipedia ({dil})"}
235
+ except Exception as e:
236
+ return {"hata": f"Wikipedia sorgusu başarısız: {e}"}
237
+
238
+
239
+ def son_depremler(min_buyukluk=4.5, son_kac_gun=1):
240
+ """Dünya genelinde son depremleri büyüklüğe göre listeler (USGS, anahtarsız)."""
241
+ try:
242
+ import datetime as dt
243
+
244
+ veri = httpx.get(
245
+ "https://earthquake.usgs.gov/fdsnws/event/1/query",
246
+ params={
247
+ "format": "geojson", "minmagnitude": float(min_buyukluk),
248
+ "starttime": f"now-{max(1, min(int(son_kac_gun), 30))}days",
249
+ "orderby": "magnitude", "limit": 5,
250
+ }, timeout=15,
251
+ ).json()
252
+
253
+ depremler = []
254
+ for oge in veri.get("features", []):
255
+ ozellik = oge.get("properties", {})
256
+ zaman_ms = ozellik.get("time")
257
+ depremler.append({
258
+ "buyukluk": ozellik.get("mag"), "yer": ozellik.get("place"),
259
+ "zaman_utc": (
260
+ dt.datetime.fromtimestamp(zaman_ms / 1000, dt.timezone.utc).strftime("%Y-%m-%d %H:%M")
261
+ if zaman_ms else None
262
+ ),
263
+ })
264
+ return {"kriter": f"son {son_kac_gun} gün, büyüklük >= {min_buyukluk}",
265
+ "toplam": len(depremler), "depremler": depremler, "kaynak": "USGS"}
266
+ except Exception as e:
267
+ return {"hata": f"Deprem verisi alınamadı: {e}"}
268
+
269
+
270
+ ARAC_FONKSIYONLARI = {
271
+ "hava_durumu": hava_durumu,
272
+ "doviz_cevir": doviz_cevir,
273
+ "sicaklik_cevir": sicaklik_cevir,
274
+ "kripto_fiyat": kripto_fiyat,
275
+ "wikipedia_ara": wikipedia_ara,
276
+ "son_depremler": son_depremler,
277
+ }
278
+
279
+ ARAC_UST_VERI = {
280
+ "hava_durumu": {"etiket": "WX", "saglayici": "Open-Meteo",
281
+ "aciklama": "Bir şehrin anlık sıcaklık, nem, rüzgar ve hava durumunu getirir."},
282
+ "doviz_cevir": {"etiket": "FX", "saglayici": "Frankfurter / ECB",
283
+ "aciklama": "Güncel kurla iki para birimi arasında çevrim yapar."},
284
+ "sicaklik_cevir":{"etiket": "TEMP", "saglayici": "yerel hesap",
285
+ "aciklama": "Sıcaklığı Celsius, Fahrenheit ve Kelvin arasında çevirir."},
286
+ "kripto_fiyat": {"etiket": "BTC", "saglayici": "CoinGecko",
287
+ "aciklama": "Bir kripto paranın güncel fiyatını ve 24 saatlik değişimini getirir."},
288
+ "wikipedia_ara": {"etiket": "WIKI", "saglayici": "Wikipedia",
289
+ "aciklama": "Wikipedia'da arayıp en alakalı maddenin özetini getirir."},
290
+ "son_depremler": {"etiket": "EQ", "saglayici": "USGS",
291
+ "aciklama": "Son depremleri büyüklüğe göre sıralı listeler."},
292
+ }
293
+
294
+ ARAC_SEMALARI = [
295
+ {
296
+ "type": "function",
297
+ "function": {
298
+ "name": "hava_durumu",
299
+ "description": "Bir şehrin anlık sıcaklık (Celsius), nem, rüzgar ve hava durumunu verir.",
300
+ "parameters": {
301
+ "type": "object",
302
+ "properties": {"sehir": {"type": "string", "description": "Şehir adı, örn: Ankara"}},
303
+ "required": ["sehir"],
304
+ },
305
+ },
306
+ },
307
+ {
308
+ "type": "function",
309
+ "function": {
310
+ "name": "doviz_cevir",
311
+ "description": "Güncel kurla bir para biriminden diğerine çevirir. Örn: 250 EUR kaç TL.",
312
+ "parameters": {
313
+ "type": "object",
314
+ "properties": {
315
+ "miktar": {"type": "number", "description": "Çevrilecek miktar"},
316
+ "kaynak_birim": {"type": "string", "description": "Kaynak para birimi kodu, örn: USD"},
317
+ "hedef_birim": {"type": "string", "description": "Hedef para birimi kodu, örn: TRY"},
318
+ },
319
+ "required": ["miktar", "kaynak_birim", "hedef_birim"],
320
+ },
321
+ },
322
+ },
323
+ {
324
+ "type": "function",
325
+ "function": {
326
+ "name": "sicaklik_cevir",
327
+ "description": (
328
+ "Sıcaklığı Celsius/Fahrenheit/Kelvin arasında çevirir. "
329
+ "hava_durumu her zaman Celsius döner; kullanıcı başka birim istiyorsa "
330
+ "dönen değeri bu araca ikinci adım olarak ver."
331
+ ),
332
+ "parameters": {
333
+ "type": "object",
334
+ "properties": {
335
+ "deger": {"type": "number", "description": "Çevrilecek sıcaklık değeri"},
336
+ "kaynak_birim": {"type": "string", "description": "C, F veya K"},
337
+ "hedef_birim": {"type": "string", "description": "C, F veya K"},
338
+ },
339
+ "required": ["deger", "kaynak_birim", "hedef_birim"],
340
+ },
341
+ },
342
+ },
343
+ {
344
+ "type": "function",
345
+ "function": {
346
+ "name": "kripto_fiyat",
347
+ "description": "Bir kripto paranın güncel fiyatını ve 24 saatlik yüzde değişimini döner.",
348
+ "parameters": {
349
+ "type": "object",
350
+ "properties": {
351
+ "kripto": {"type": "string", "description": "Kripto adı/sembolü, örn: bitcoin, ETH"},
352
+ "para_birimi": {"type": "string", "description": "Fiyatın gösterileceği para birimi, örn: usd. Varsayılan usd."},
353
+ },
354
+ "required": ["kripto"],
355
+ },
356
+ },
357
+ },
358
+ {
359
+ "type": "function",
360
+ "function": {
361
+ "name": "wikipedia_ara",
362
+ "description": "Wikipedia'da arama yapıp en alakalı maddenin özetini döner. Kişi, yer veya kavram sorularında kullan.",
363
+ "parameters": {
364
+ "type": "object",
365
+ "properties": {
366
+ "sorgu": {"type": "string", "description": "Aranacak konu"},
367
+ "dil": {"type": "string", "description": "Wikipedia dil kodu: tr veya en. Varsayılan tr."},
368
+ },
369
+ "required": ["sorgu"],
370
+ },
371
+ },
372
+ },
373
+ {
374
+ "type": "function",
375
+ "function": {
376
+ "name": "son_depremler",
377
+ "description": "USGS kataloğundan son depremleri büyüklüğe göre sıralı listeler.",
378
+ "parameters": {
379
+ "type": "object",
380
+ "properties": {
381
+ "min_buyukluk": {"type": "number", "description": "Minimum deprem büyüklüğü. Varsayılan 4.5."},
382
+ "son_kac_gun": {"type": "integer", "description": "Kaç günlük geçmişe bakılacağı (1-30). Varsayılan 1."},
383
+ },
384
+ "required": [],
385
+ },
386
+ },
387
+ },
388
+ ]
389
+
390
+
391
+ def araci_calistir(ad, argumanlar):
392
+ fonksiyon = ARAC_FONKSIYONLARI.get(ad)
393
+ if fonksiyon is None:
394
+ return {"hata": f"'{ad}' adında bir araç yok."}
395
+ try:
396
+ return fonksiyon(**(argumanlar or {}))
397
+ except TypeError as e:
398
+ return {"hata": f"'{ad}' hatalı argümanlarla çağrıldı: {e}"}
399
+
400
+
401
+ # --------------------------------------------------------------------------------------
402
+ # Ajan döngüsü — generator, her adımda olay üretir
403
+ # --------------------------------------------------------------------------------------
404
+
405
+ SISTEM_PROMPTU = (
406
+ "Türkçe konuşan bir asistansın. Hava durumu, döviz kuru, kripto fiyatı, deprem "
407
+ "ve ansiklopedik bilgi gibi GÜNCEL/OLGUSAL sorularda tahmin yürütme; elindeki "
408
+ "araçları çağır. Bir soru birim çevrimi de gerektiriyorsa (örn. Fahrenheit) önce "
409
+ "veriyi al, sonra çevirme aracını ayrı bir adımda çağır. Araç hata döndürürse "
410
+ "kullanıcıya dürüstçe söyle, veri uydurma. Araç gerektirmeyen sorularda araç "
411
+ "çağırmadan doğrudan yanıtla."
412
+ )
413
+
414
+ TOOLSUZ_PROMPTU = (
415
+ "Türkçe konuşan bir asistansın. Hiçbir dış araca/internete erişimin yok. Güncel "
416
+ "hava durumu, döviz kuru, kripto fiyatı, deprem gibi anlık bilgi istenirse bunu "
417
+ "bilemeyeceğini açıkça söyle; uydurma sayı verme."
418
+ )
419
+
420
+ MAKS_TUR = 4
421
+ MAKS_DENEME = 2
422
+
423
+
424
+ def _hata_kisalt(e):
425
+ """Sağlayıcı hatalarının uzun ham JSON'ını UI'da okunabilir tek satıra indirger."""
426
+ metin = str(e)
427
+ for ayrac in ('"message":', "'message':"):
428
+ if ayrac in metin:
429
+ parca = metin.split(ayrac, 1)[1].strip().strip("'\"")
430
+ return parca.split('", "code"')[0].split("', 'code'")[0][:180]
431
+ return metin[:180]
432
+
433
+
434
+ def _arguman_coz(cagri):
435
+ try:
436
+ cozulen = json.loads(cagri.function.arguments or "{}")
437
+ return cozulen if isinstance(cozulen, dict) else {}
438
+ except json.JSONDecodeError:
439
+ return {"_cozulemeyen": cagri.function.arguments}
440
+
441
+
442
+ def ajan_calistir(soru, model, araclar_acik):
443
+ """Tool-calling döngüsünü çalıştırır; olayları generator olarak üretir."""
444
+ mesajlar = [
445
+ {"role": "system", "content": SISTEM_PROMPTU if araclar_acik else TOOLSUZ_PROMPTU},
446
+ {"role": "user", "content": soru},
447
+ ]
448
+ semalar = ARAC_SEMALARI if araclar_acik else []
449
+
450
+ for tur in range(1, MAKS_TUR + 1):
451
+ yield {"tip": "tur", "tur": tur}
452
+
453
+ mesaj, hata = None, None
454
+ for deneme in range(1, MAKS_DENEME + 1):
455
+ try:
456
+ istek = {"model": model, "messages": mesajlar, "temperature": 0.2 if deneme == 1 else 0.6}
457
+ if semalar:
458
+ istek["tools"] = semalar
459
+ istek["tool_choice"] = "auto"
460
+ yanit = istemci().chat.completions.create(**istek)
461
+ except Exception as e:
462
+ hata = _hata_kisalt(e)
463
+ yield {"tip": "deneme_hata", "deneme": deneme, "detay": hata}
464
+ continue
465
+
466
+ secenekler = getattr(yanit, "choices", None) or []
467
+ if not secenekler:
468
+ hata = str(getattr(yanit, "error", None) or "model boş yanıt döndü")
469
+ yield {"tip": "deneme_hata", "deneme": deneme, "detay": hata}
470
+ continue
471
+
472
+ aday = secenekler[0]
473
+ var_arac = bool(getattr(aday.message, "tool_calls", None))
474
+ var_icerik = bool((aday.message.content or "").strip())
475
+ if var_arac or var_icerik:
476
+ mesaj = aday.message
477
+ hata = None
478
+ break
479
+
480
+ hata = f"finish_reason={aday.finish_reason}, native={getattr(aday, 'native_finish_reason', None)}"
481
+ yield {"tip": "deneme_hata", "deneme": deneme, "detay": hata}
482
+
483
+ if mesaj is None:
484
+ yield {"tip": "hata", "mesaj": f"Model geçerli bir yanıt üretemedi ({hata})."}
485
+ return
486
+
487
+ arac_cagrilari = list(getattr(mesaj, "tool_calls", None) or [])
488
+ if not arac_cagrilari:
489
+ yield {"tip": "final", "metin": (mesaj.content or "").strip() or "(boş yanıt)", "tur": tur}
490
+ return
491
+
492
+ mesajlar.append({
493
+ "role": "assistant",
494
+ "content": mesaj.content or "",
495
+ "tool_calls": [
496
+ {"id": c.id, "type": "function",
497
+ "function": {"name": c.function.name, "arguments": c.function.arguments}}
498
+ for c in arac_cagrilari
499
+ ],
500
+ })
501
+
502
+ for cagri in arac_cagrilari:
503
+ argumanlar = _arguman_coz(cagri)
504
+ yield {"tip": "arac_basladi", "id": cagri.id, "arac": cagri.function.name, "argumanlar": argumanlar}
505
+
506
+ baslangic = time.time()
507
+ sonuc = araci_calistir(cagri.function.name, argumanlar)
508
+ sure = time.time() - baslangic
509
+
510
+ yield {
511
+ "tip": "arac_bitti", "id": cagri.id, "arac": cagri.function.name,
512
+ "argumanlar": argumanlar, "sonuc": sonuc, "sure": sure,
513
+ "basarili": not (isinstance(sonuc, dict) and "hata" in sonuc),
514
+ }
515
+ mesajlar.append({
516
+ "role": "tool", "tool_call_id": cagri.id, "name": cagri.function.name,
517
+ "content": json.dumps(sonuc, ensure_ascii=False),
518
+ })
519
+
520
+ yield {"tip": "hata", "mesaj": f"{MAKS_TUR} tur sonunda nihai cevaba ulaşılamadı."}
521
+
522
+
523
+ # --------------------------------------------------------------------------------------
524
+ # Arayüz — 3 panel, aynı soruya paralel canlı akış
525
+ # --------------------------------------------------------------------------------------
526
+
527
+ PANEL_AYARLARI = [
528
+ {"anahtar": "A", "baslik": "Güçlü model", "mod": "araçlar açık", "model": MODEL_GUCLU, "araclar": True},
529
+ {"anahtar": "B", "baslik": "Güçlü model", "mod": "araçlar kapalı", "model": MODEL_GUCLU, "araclar": False},
530
+ {"anahtar": "C", "baslik": "Küçük model", "mod": "araçlar açık", "model": MODEL_ZAYIF, "araclar": True},
531
+ ]
532
+
533
+ # --------------------------------------------------------------------------------------
534
+ # Tasarım tokenleri — sade, tek aileden 3 renk:
535
+ # 1) vurgu — marka/etkileşim rengi (bağlantı, aktif pil, gönder düğmesi, tooltip)
536
+ # 2) basarili — araç çağrısı başarılı
537
+ # 3) tehlike — araç/model hatası (yeniden deneme uyarıları da bu tona düşer)
538
+ # Geri kalan her şey Gradio'nun kendi nötr tema değişkenlerinden gelir; böylece
539
+ # açık/koyu tema otomatik uyum sağlar. Vurgu rengi teknik/ölçüm hissi için sıcak
540
+ # bir bakır tonu — jenerik indigo/mor değil.
541
+ # --------------------------------------------------------------------------------------
542
+
543
+ CSS = """
544
+ :root, .gradio-container {
545
+ --vurgu: #a35a1d; --vurgu-zayif: rgba(163,90,29,.10);
546
+ --basarili: #187a3c; --tehlike: #c22626;
547
+ --basarili-zemin: rgba(24,122,60,.10); --tehlike-zemin: rgba(194,38,38,.10);
548
+ --font-metin: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
549
+ --font-veri: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", Menlo, Consolas, monospace;
550
+ }
551
+ .dark, .dark .gradio-container {
552
+ --vurgu: #d99a52; --vurgu-zayif: rgba(217,154,82,.16);
553
+ --basarili: #4ade80; --tehlike: #f87171;
554
+ --basarili-zemin: rgba(74,222,128,.12); --tehlike-zemin: rgba(248,113,113,.12);
555
+ }
556
+
557
+ .gecit-baslik { display:flex; align-items:baseline; gap:10px; margin: 0; }
558
+ .gecit-baslik h1 { font-family: var(--font-metin); font-size: 1.22em; font-weight: 700; margin: 0; text-wrap: balance; }
559
+ .gecit-tez { font-family: var(--font-metin); font-size: 0.84em; opacity: .68; white-space: nowrap;
560
+ overflow: hidden; text-overflow: ellipsis; line-height: 1.4; margin: 3px 0 12px; }
561
+
562
+ .eyebrow { font-family: var(--font-metin); font-size: 0.68em; font-weight: 600; letter-spacing: .07em;
563
+ text-transform: uppercase; opacity: .5; margin: 0 0 6px; }
564
+ .arac-kayit { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-bottom: 12px; }
565
+ .arac-etiket { position: relative; display: flex; align-items: center; gap: 7px;
566
+ border: 1px solid var(--border-color-primary); border-radius: 7px; padding: 6px 10px;
567
+ font-family: var(--font-metin); cursor: default; overflow: hidden; }
568
+ .arac-etiket .rozet { font-family: var(--font-veri); font-size: 0.68em; font-weight: 700; color: var(--vurgu);
569
+ background: var(--vurgu-zayif); border-radius: 4px; padding: 2px 6px; letter-spacing: .02em;
570
+ flex-shrink: 0; }
571
+ .arac-etiket .ad { font-weight: 600; font-size: 0.82em; flex-shrink: 0; }
572
+ .arac-etiket .aciklama { font-size: 0.78em; opacity: .55; line-height: 1.3;
573
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
574
+ .arac-etiket .ipucu {
575
+ position: absolute; left: 12px; bottom: calc(100% + 8px);
576
+ width: max-content; max-width: 240px; background: var(--body-text-color); color: var(--background-fill-primary);
577
+ padding: 7px 10px; border-radius: 7px; font-size: 0.82em; line-height: 1.4; text-align: left;
578
+ opacity: 0; pointer-events: none; transition: opacity .12s ease, transform .12s ease; z-index: 30;
579
+ transform: translateY(4px); box-shadow: 0 6px 18px rgba(0,0,0,.18);
580
+ }
581
+ .arac-etiket .ipucu b { display: block; margin-bottom: 2px; }
582
+ .arac-etiket:hover .ipucu { opacity: .96; transform: translateY(0); }
583
+ @media (max-width: 900px) { .arac-kayit { grid-template-columns: repeat(2, 1fr); } }
584
+
585
+ .panel-kutu { border: 1px solid var(--border-color-primary); border-radius: 10px;
586
+ padding: 12px 14px; height: min(52vh, 620px); min-height: 230px;
587
+ display: flex; flex-direction: column; font-family: var(--font-metin); overflow: hidden; }
588
+ .panel-ust { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px;
589
+ padding-bottom: 8px; margin-bottom: 2px; border-bottom: 1px solid var(--border-color-primary);
590
+ flex-shrink: 0; }
591
+ .panel-ad { font-weight: 700; font-size: 0.92em; }
592
+ .panel-model { font-family: var(--font-veri); font-size: 0.7em; opacity: .6; margin-top: 1px; }
593
+ .pil { font-size: 0.64em; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
594
+ border-radius: 999px; padding: 2px 8px; white-space: nowrap; }
595
+ .pil.acik { background: var(--vurgu-zayif); color: var(--vurgu); }
596
+ .pil.kapali { background: var(--background-fill-secondary); opacity: .6; }
597
+
598
+ .panel-govde { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-top: 6px; scroll-behavior: smooth; }
599
+ .panel-bos { opacity: .4; font-size: 0.8em; padding-top: 4px; }
600
+ .panel-alt-bilgi { font-size: 0.68em; opacity: .5; margin-top: 4px; padding-top: 4px; flex-shrink: 0;
601
+ border-top: 1px solid var(--border-color-primary); font-variant-numeric: tabular-nums; }
602
+
603
+ .tur-etiketi { font-size: 0.72em; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
604
+ opacity: .45; margin: 14px 0 6px; }
605
+ .tur-etiketi:first-child { margin-top: 0; }
606
+
607
+ .arac-kart { border: 1px solid var(--border-color-primary); border-radius: 8px; padding: 10px 11px; margin-bottom: 8px; }
608
+ .arac-kart.tamam { border-color: color-mix(in srgb, var(--basarili) 35%, var(--border-color-primary)); }
609
+ .arac-kart.hata { border-color: color-mix(in srgb, var(--tehlike) 35%, var(--border-color-primary)); }
610
+ .arac-kart-ust { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
611
+ .arac-kart-ad { display: flex; align-items: center; gap: 7px; font-size: 0.86em; }
612
+ .arac-kart-ad .rozet { font-family: var(--font-veri); font-size: 0.7em; font-weight: 700; color: var(--vurgu);
613
+ background: var(--vurgu-zayif); border-radius: 4px; padding: 1px 5px; }
614
+ .arac-kart-ad .saglayici { opacity: .55; font-size: 0.85em; }
615
+ .durum-pil { font-size: 0.66em; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
616
+ border-radius: 5px; padding: 2px 7px; font-variant-numeric: tabular-nums; }
617
+ .durum-pil.calisiyor { background: var(--background-fill-secondary); opacity: .7; }
618
+ .durum-pil.tamam { background: var(--basarili-zemin); color: var(--basarili); }
619
+ .durum-pil.hata { background: var(--tehlike-zemin); color: var(--tehlike); }
620
+ .veri-blok { margin-top: 8px; }
621
+ .veri-etiket { font-size: 0.66em; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
622
+ opacity: .45; margin-bottom: 3px; }
623
+ .veri-satirlar { font-family: var(--font-veri); font-size: 0.78em; background: var(--background-fill-secondary);
624
+ border-radius: 6px; padding: 7px 9px; line-height: 1.55; word-break: break-word;
625
+ font-variant-numeric: tabular-nums; }
626
+ .veri-satirlar .anahtar { opacity: .55; }
627
+ .veri-satirlar .hata-satir { color: var(--tehlike); }
628
+ .kaynak-notu { font-size: 0.72em; opacity: .5; margin-top: 4px; font-family: var(--font-metin); }
629
+
630
+ .deneme-satir { font-size: 0.78em; background: var(--background-fill-secondary);
631
+ border-radius: 6px; padding: 5px 9px; margin-bottom: 8px; font-family: var(--font-metin); }
632
+ .deneme-satir b { color: var(--tehlike); }
633
+ .deneme-satir .veri-satirlar { margin-top: 4px; background: transparent; padding: 0; opacity: .75; }
634
+
635
+ .sonuc-blok { padding-top: 10px; }
636
+ .sonuc-kutu { border-radius: 8px; padding: 11px 12px; font-size: 0.9em; line-height: 1.55;
637
+ white-space: pre-wrap; background: var(--vurgu-zayif); border: 1px solid var(--vurgu-zayif); }
638
+ .hata-kutu { border-radius: 8px; padding: 11px 12px; font-size: 0.86em; line-height: 1.5;
639
+ background: var(--tehlike-zemin); color: var(--tehlike); }
640
+
641
+ .ornek-baslik { font-family: var(--font-metin); font-size: 0.68em; font-weight: 600; letter-spacing: .07em;
642
+ text-transform: uppercase; opacity: .5; margin: 2px 0 6px; }
643
+ #ornek-sorular { display: grid !important; grid-template-columns: repeat(6, 1fr); gap: 6px; flex-wrap: unset; }
644
+ #ornek-sorular > * { min-width: 0; }
645
+ #ornek-sorular button {
646
+ font-size: 0.74em !important; white-space: normal !important; text-align: left !important;
647
+ justify-content: flex-start !important; line-height: 1.25 !important; min-height: 40px;
648
+ padding: 7px 9px !important; height: auto !important;
649
+ }
650
+ @media (max-width: 1100px) { #ornek-sorular { grid-template-columns: repeat(3, 1fr); } }
651
+ @media (max-width: 640px) { #ornek-sorular { grid-template-columns: repeat(2, 1fr); } }
652
+
653
+ /* Paneller sabit yükseklikte iç kaydırmalı olduğu için toplam sayfa yüksekliği
654
+ sorulara göre değişmiyor; composer bu nedenle normal akışta, sayfanın en altında
655
+ sade bir çubuk olarak durabiliyor (fixed/sticky konumlandırma Gradio'nun kendi
656
+ transform'lu sarmalayıcısıyla çakışıp bozuk konumlanmaya yol açıyordu). */
657
+ #composer-disi { border-top: 1px solid var(--border-color-primary); margin-top: 10px; padding-top: 8px; }
658
+ #composer { gap: 8px; }
659
+
660
+ /* Gradio'nun bloklar arası varsayılan dikey boşluğunu sıkılaştırır. */
661
+ .gradio-container .block { margin-bottom: 0 !important; }
662
+ .gradio-container > .main, .gradio-container .contain { gap: 6px !important; padding-top: 8px !important; }
663
+
664
+ /* İçerik viewport'tan kısa olduğunda kalan boşluk hep alta yığılıyordu (gradio-app
665
+ body içinde flex-grow:1 ile tüm yüksekliği dolduruyor). Bunun yerine tüm
666
+ kompozisyonu ekranda hem yatayda hem dikeyde ortalıyoruz; genişliği de cihaza göre
667
+ makul bir üst sınırla tutuyoruz. */
668
+ html, body { height: 100%; margin: 0; }
669
+ body { display: flex !important; align-items: center !important; justify-content: center !important; }
670
+ gradio-app { flex-grow: 0 !important; width: 100%; max-width: 1400px; }
671
+ .gradio-container { height: auto !important; min-height: 0 !important; width: 100% !important; margin: 0 auto !important; }
672
+ """
673
+
674
+ AUTO_KAYDIR_JS = """
675
+ <script>
676
+ (function () {
677
+ // Panel içeriği her güncellemede yeniden çizildiği için DOM elemanı sıfırdan
678
+ // oluşuyor ve kaydırma konumu kayboluyor. Kullanıcı yukarı kaydırıp geçmişi
679
+ // okumak isterse "takip modu" kapanır ve otomatik kaydırma o panele dokunmaz;
680
+ // kullanıcı tekrar en alta inerse takip modu kendiliğinden açılır.
681
+ var takipEt = {};
682
+
683
+ function altaYakinMi(el) {
684
+ return el.scrollHeight - el.scrollTop - el.clientHeight < 24;
685
+ }
686
+
687
+ function elemanBul(hedef) {
688
+ return hedef && hedef.closest ? hedef.closest(".panel-govde") : null;
689
+ }
690
+
691
+ ["wheel", "touchmove"].forEach(function (tur) {
692
+ document.addEventListener(tur, function (e) {
693
+ var el = elemanBul(e.target);
694
+ if (!el) return;
695
+ takipEt[el.id] = altaYakinMi(el);
696
+ }, { passive: true });
697
+ });
698
+
699
+ setInterval(function () {
700
+ document.querySelectorAll(".panel-govde").forEach(function (el) {
701
+ if (takipEt[el.id] === false) return;
702
+ el.scrollTop = el.scrollHeight;
703
+ });
704
+ }, 350);
705
+ })();
706
+ </script>
707
+ """
708
+
709
+
710
+ def _kacir(metin):
711
+ return str(metin).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
712
+
713
+
714
+ def _deger_yaz(v):
715
+ if isinstance(v, float):
716
+ return f"{v:,.2f}".rstrip("0").rstrip(".") if v != int(v) else f"{int(v):,}"
717
+ if isinstance(v, list):
718
+ if v and isinstance(v[0], dict):
719
+ return f"[{len(v)} kayıt]"
720
+ return ", ".join(str(x) for x in v) if v else "—"
721
+ return str(v)
722
+
723
+
724
+ def _veri_satirlari(d, haric=()):
725
+ satirlar = []
726
+ for k, v in d.items():
727
+ if k in haric or v is None:
728
+ continue
729
+ satirlar.append(f'<span class="anahtar">{_kacir(k)}:</span> {_kacir(_deger_yaz(v))}')
730
+ return "<br>".join(satirlar) if satirlar else "—"
731
+
732
+
733
+ def _arac_kart_render(blok):
734
+ ust = ARAC_UST_VERI.get(blok["arac"], {"etiket": "?", "saglayici": "—"})
735
+ durum = blok["durum"]
736
+ durum_metni = {"calisiyor": "çalışıyor", "tamam": "tamamlandı", "hata": "hata"}[durum]
737
+ sure_metni = f'{blok["sure"]:.2f}s' if blok.get("sure") is not None else ""
738
+
739
+ parcalar = [
740
+ f'<div class="arac-kart {durum}">'
741
+ f'<div class="arac-kart-ust"><span class="arac-kart-ad">'
742
+ f'<span class="rozet">{ust["etiket"]}</span><b>{_kacir(blok["arac"])}</b>'
743
+ f'<span class="saglayici">{_kacir(ust["saglayici"])}</span></span>'
744
+ f'<span class="durum-pil {durum}">{durum_metni}{" · " + sure_metni if sure_metni else ""}</span>'
745
+ f'</div>'
746
+ f'<div class="veri-blok"><div class="veri-etiket">girdi</div>'
747
+ f'<div class="veri-satirlar">{_veri_satirlari(blok["argumanlar"]) if blok["argumanlar"] else "—"}</div></div>'
748
+ ]
749
+
750
+ if durum == "tamam":
751
+ sonuc = blok["sonuc"] or {}
752
+ if isinstance(sonuc, dict) and "hata" in sonuc:
753
+ parcalar.append(
754
+ f'<div class="veri-blok"><div class="veri-etiket">çıktı</div>'
755
+ f'<div class="veri-satirlar hata-satir">{_kacir(sonuc["hata"])}</div></div>'
756
+ )
757
+ else:
758
+ kaynak = sonuc.get("kaynak") if isinstance(sonuc, dict) else None
759
+ parcalar.append(
760
+ f'<div class="veri-blok"><div class="veri-etiket">çıktı</div>'
761
+ f'<div class="veri-satirlar">{_veri_satirlari(sonuc, haric=("kaynak",))}</div>'
762
+ + (f'<div class="kaynak-notu">kaynak: {_kacir(kaynak)}</div>' if kaynak else "")
763
+ + '</div>'
764
+ )
765
+ elif durum == "hata":
766
+ parcalar.append(
767
+ f'<div class="veri-blok"><div class="veri-etiket">çıktı</div>'
768
+ f'<div class="veri-satirlar hata-satir">{_kacir((blok.get("sonuc") or {}).get("hata", "bilinmeyen hata"))}</div></div>'
769
+ )
770
+
771
+ parcalar.append("</div>")
772
+ return "".join(parcalar)
773
+
774
+
775
+ def _durum_baslangic():
776
+ return {"bloklar": [], "id_indeks": {}, "final": None, "hata": None, "sure": None}
777
+
778
+
779
+ def _durum_guncelle(durum, olay):
780
+ tip = olay["tip"]
781
+ if tip == "tur":
782
+ durum["bloklar"].append({"tip": "tur", "tur": olay["tur"]})
783
+ elif tip == "arac_basladi":
784
+ durum["id_indeks"][olay["id"]] = len(durum["bloklar"])
785
+ durum["bloklar"].append({
786
+ "tip": "arac", "arac": olay["arac"], "argumanlar": olay["argumanlar"],
787
+ "durum": "calisiyor", "sonuc": None, "sure": None,
788
+ })
789
+ elif tip == "arac_bitti":
790
+ i = durum["id_indeks"].get(olay["id"])
791
+ if i is not None:
792
+ blok = durum["bloklar"][i]
793
+ blok.update(durum="tamam" if olay["basarili"] else "hata", sonuc=olay["sonuc"], sure=olay["sure"])
794
+ elif tip == "deneme_hata":
795
+ durum["bloklar"].append({"tip": "deneme_hata", "deneme": olay["deneme"], "detay": olay["detay"]})
796
+ elif tip == "final":
797
+ durum["final"] = olay["metin"]
798
+ elif tip == "hata":
799
+ durum["hata"] = olay["mesaj"]
800
+
801
+
802
+ def _panel_render(ayar, durum):
803
+ pil_sinif = "acik" if ayar["araclar"] else "kapali"
804
+ parcalar = [
805
+ '<div class="panel-kutu"><div class="panel-ust">'
806
+ f'<div><div class="panel-ad">{_kacir(ayar["baslik"])}</div>'
807
+ f'<div class="panel-model">{_kacir(ayar["model"])}</div></div>'
808
+ f'<span class="pil {pil_sinif}">{_kacir(ayar["mod"])}</span>'
809
+ '</div>'
810
+ f'<div class="panel-govde" id="panel-govde-{ayar["anahtar"]}">'
811
+ ]
812
+
813
+ if not durum["bloklar"] and not durum["final"] and not durum["hata"]:
814
+ parcalar.append('<div class="panel-bos">Bir soru sorduğunuzda burada akacak.</div>')
815
+
816
+ for blok in durum["bloklar"]:
817
+ if blok["tip"] == "tur":
818
+ parcalar.append(f'<div class="tur-etiketi">Tur {blok["tur"]}</div>')
819
+ elif blok["tip"] == "arac":
820
+ parcalar.append(_arac_kart_render(blok))
821
+ elif blok["tip"] == "deneme_hata":
822
+ parcalar.append(
823
+ f'<div class="deneme-satir"><b>Deneme {blok["deneme"]}</b> geçersiz yanıt üretti'
824
+ f'<div class="veri-satirlar">{_kacir(blok["detay"])}</div></div>'
825
+ )
826
+
827
+ if durum["final"] or durum["hata"]:
828
+ parcalar.append('<div class="sonuc-blok">')
829
+ if durum["final"]:
830
+ parcalar.append(f'<div class="sonuc-kutu">{_kacir(durum["final"])}</div>')
831
+ if durum["hata"]:
832
+ parcalar.append(f'<div class="hata-kutu">{_kacir(durum["hata"])}</div>')
833
+ parcalar.append("</div>")
834
+
835
+ parcalar.append("</div>") # .panel-govde kapanışı
836
+
837
+ if durum["sure"] is not None:
838
+ arac_sayisi = sum(1 for b in durum["bloklar"] if b["tip"] == "arac")
839
+ parcalar.append(f'<div class="panel-alt-bilgi">{durum["sure"]:.1f}s · {arac_sayisi} araç çağrısı</div>')
840
+
841
+ parcalar.append("</div>") # .panel-kutu kapanışı
842
+ return "".join(parcalar)
843
+
844
+
845
+ def _panel_calistir(anahtar, soru, model, araclar_acik, kuyruk):
846
+ baslangic = time.time()
847
+ try:
848
+ for olay in ajan_calistir(soru, model, araclar_acik):
849
+ kuyruk.put((anahtar, olay, time.time() - baslangic))
850
+ except Exception as e:
851
+ kuyruk.put((anahtar, {"tip": "hata", "mesaj": f"Beklenmeyen hata: {e}"}, time.time() - baslangic))
852
+ kuyruk.put((anahtar, {"tip": "bitti"}, time.time() - baslangic))
853
+
854
+
855
+ def uc_paneli_calistir(soru):
856
+ """Panelleri render eder; son çıktı olarak soru kutusunu da yönetir.
857
+
858
+ İlk yield'de kutu boşaltılır (kullanıcı gönderir göndermez temizlenir); sonraki
859
+ yield'lerde kutuya dokunulmaz, böylece kullanıcı akış sürerken sıradaki soruyu
860
+ yazmaya başlarsa üzerine yazılmaz.
861
+ """
862
+ soru = (soru or "").strip()
863
+ if not soru:
864
+ bos = [_panel_render(a, _durum_baslangic()) for a in PANEL_AYARLARI]
865
+ yield (*bos, gr.update())
866
+ return
867
+
868
+ kuyruk = queue.Queue()
869
+ durumlar = {a["anahtar"]: _durum_baslangic() for a in PANEL_AYARLARI}
870
+
871
+ threadler = [
872
+ threading.Thread(target=_panel_calistir, args=(a["anahtar"], soru, a["model"], a["araclar"], kuyruk), daemon=True)
873
+ for a in PANEL_AYARLARI
874
+ ]
875
+ for th in threadler:
876
+ th.start()
877
+
878
+ bitenler = set()
879
+ ilk_yield = True
880
+ while len(bitenler) < len(PANEL_AYARLARI):
881
+ anahtar, olay, gecen_sure = kuyruk.get()
882
+ durum = durumlar[anahtar]
883
+
884
+ if olay["tip"] == "bitti":
885
+ bitenler.add(anahtar)
886
+ durum["sure"] = gecen_sure
887
+ else:
888
+ _durum_guncelle(durum, olay)
889
+
890
+ panel_htmlleri = tuple(_panel_render(a, durumlar[a["anahtar"]]) for a in PANEL_AYARLARI)
891
+ yield (*panel_htmlleri, ("" if ilk_yield else gr.update()))
892
+ ilk_yield = False
893
+
894
+
895
+ ORNEK_SORULAR = [
896
+ "Ankara mı daha sıcak Londra mı, bu değerler Fahrenheit olarak kaç eder?",
897
+ "İstanbul'da hava kaç derece, Kelvin olarak da söyler misin?",
898
+ "250 Euro kaç TL eder?",
899
+ "Bitcoin şu an kaç dolar, 24 saatte ne kadar değişti?",
900
+ "Mimar Sinan kimdir kısaca anlatır mısın?",
901
+ "Son 3 günde 5'ten büyük deprem oldu mu?",
902
+ ]
903
+
904
+ with gr.Blocks(title="Tool Calling Karşılaştırması") as demo:
905
+ gr.HTML(
906
+ '<div class="gecit-baslik"><h1>Tool Calling Karşılaştırması</h1></div>'
907
+ '<p class="gecit-tez">Aynı soru üçüne birden gider: güçlü model + araçlar, aynı model araçsız, '
908
+ 'küçük model + araçlar — fark, tool calling ve model kapasitesinin gerçek katkısını gösterir.</p>'
909
+ '<div class="eyebrow">Kullanılabilir araçlar</div>'
910
+ '<div class="arac-kayit">' + "".join(
911
+ f'<div class="arac-etiket"><span class="rozet">{u["etiket"]}</span>'
912
+ f'<span class="ad">{ad}</span>'
913
+ f'<span class="aciklama">{u["aciklama"]}</span>'
914
+ f'<span class="ipucu"><b>{u["saglayici"]}</b>{u["aciklama"]}</span></div>'
915
+ for ad, u in ARAC_UST_VERI.items()
916
+ ) + '</div>'
917
+ )
918
+
919
+ with gr.Row():
920
+ paneller = [gr.HTML(_panel_render(a, _durum_baslangic())) for a in PANEL_AYARLARI]
921
+
922
+ gr.HTML('<div class="ornek-baslik">Örnek sorular</div>')
923
+ with gr.Row(elem_id="ornek-sorular"):
924
+ ornek_butonlar = [gr.Button(s, size="sm") for s in ORNEK_SORULAR]
925
+
926
+ with gr.Group(elem_id="composer-disi"):
927
+ with gr.Row(elem_id="composer"):
928
+ soru_kutusu = gr.Textbox(
929
+ placeholder="Bir soru sorun — örn. Ankara mı daha sıcak Londra mı...",
930
+ scale=6, show_label=False, container=False,
931
+ )
932
+ gonder = gr.Button("Gönder", variant="primary", scale=1)
933
+
934
+ for buton, s in zip(ornek_butonlar, ORNEK_SORULAR):
935
+ buton.click(lambda s=s: s, outputs=soru_kutusu)
936
+
937
+ cikislar = paneller + [soru_kutusu]
938
+ gonder.click(uc_paneli_calistir, inputs=soru_kutusu, outputs=cikislar)
939
+ soru_kutusu.submit(uc_paneli_calistir, inputs=soru_kutusu, outputs=cikislar)
940
+
941
+
942
+ if __name__ == "__main__":
943
+ demo.queue().launch(css=CSS, head=AUTO_KAYDIR_JS)