# Ders5 — Tool Calling / Function Calling Demosu # # Tek soru, üç panel: # A. Güçlü model + araçlar açık -> doğru ve güncel cevap # B. Güçlü model + araçlar kapalı -> güncel veriyi bilemez # C. Zayıf model + araçlar açık -> araç çağırmayı dener ama bozuk üretir # # A/B karşılaştırması tool calling'in kattığını gösterir; A/C karşılaştırması # model kapasitesinin (araç olsa bile) belirleyici olduğunu gösterir. # # Çalıştırma: python3 app.py (OPENROUTER_API_KEY .env veya ortam değişkeninden okunur) import json import os import queue import threading import time import gradio as gr import httpx from openai import OpenAI # Bu uygulama gerçek GPU işi yapmıyor (sadece OpenRouter API + CPU üzerinde küçük # hesaplar), ama hesabın ücretsiz Hugging Face Spaces hakkı yalnızca ZeroGPU # donanımında geçerli. ZeroGPU çalışma zamanı, en az bir @spaces.GPU fonksiyonu # görmeden Space'i başlatmayı reddediyor; bu yüzden zararsız bir dummy fonksiyon # tanımlıyoruz. Yerelde `spaces` paketi kurulu değilse sessizce no-op'a düşer. try: import spaces _gpu_dekoratoru = spaces.GPU except ImportError: def _gpu_dekoratoru(fn): return fn @_gpu_dekoratoru def _zero_gpu_kontrolu(): """ZeroGPU çalışma zamanının aradığı işaret fonksiyon; gerçek iş yapmaz.""" return True # -------------------------------------------------------------------------------------- # Ortam # -------------------------------------------------------------------------------------- def _env_yukle(yol=".env"): if not os.path.exists(yol): return with open(yol, "r", encoding="utf-8") as f: for satir in f: satir = satir.strip() if not satir or satir.startswith("#") or "=" not in satir: continue k, _, v = satir.partition("=") k, v = k.strip(), v.strip().strip("'\"") if v and not os.environ.get(k): os.environ[k] = v _env_yukle() OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "").strip() OPENROUTER_BASE_URL = os.environ.get("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1").strip() MODEL_GUCLU = os.environ.get("OPENROUTER_MODEL", "google/gemma-4-26b-a4b-it:nitro").strip() # Küçük bir açık model: tool endpoint'i destekliyor (404 vermiyor) ama çok adımlı # zincirlerde gerçek bir tool_calls üretmek yerine düz metin içine sahte # {...} etiketleri ve UYDURMA sayılar yazıyor — ölçülmüş, # tekrarlanabilir bir başarısızlık modu. MODEL_ZAYIF = "meta-llama/llama-3.1-8b-instruct" _istemci = None def istemci(): global _istemci if _istemci is None: if not OPENROUTER_API_KEY: raise RuntimeError( "OPENROUTER_API_KEY tanımlı değil. Yerelde .env dosyasına, " "HF Spaces'te Settings > Secrets kısmına ekleyin." ) _istemci = OpenAI(api_key=OPENROUTER_API_KEY, base_url=OPENROUTER_BASE_URL) return _istemci # -------------------------------------------------------------------------------------- # Araçlar (3 adet: 2 canlı API + 1 zincirleme hesap) # -------------------------------------------------------------------------------------- HAVA_KODLARI = { 0: "açık", 1: "az bulutlu", 2: "parçalı bulutlu", 3: "kapalı", 45: "puslu", 51: "hafif çisenti", 61: "hafif yağmur", 63: "yağmur", 65: "şiddetli yağmur", 71: "kar", 80: "sağanak", 95: "gök gürültülü fırtına", } def hava_durumu(sehir): """Bir şehrin anlık hava durumunu döner (Open-Meteo, anahtarsız).""" try: geo = httpx.get( "https://geocoding-api.open-meteo.com/v1/search", params={"name": sehir, "count": 1, "language": "tr"}, timeout=15, ).json() sonuclar = geo.get("results") or [] if not sonuclar: return {"hata": f"'{sehir}' için konum bulunamadı."} konum = sonuclar[0] veri = httpx.get( "https://api.open-meteo.com/v1/forecast", params={ "latitude": konum["latitude"], "longitude": konum["longitude"], "current": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code", "timezone": "auto", }, timeout=15, ).json() anlik = veri.get("current", {}) return { "sehir": f"{konum['name']}, {konum.get('country', '')}".strip(", "), "sicaklik_c": anlik.get("temperature_2m"), "nem_yuzde": anlik.get("relative_humidity_2m"), "ruzgar_kmh": anlik.get("wind_speed_10m"), "durum": HAVA_KODLARI.get(anlik.get("weather_code"), "bilinmiyor"), "kaynak": "Open-Meteo", } except Exception as e: return {"hata": f"Hava durumu alınamadı: {e}"} def doviz_cevir(miktar, kaynak_birim, hedef_birim): """Güncel ECB kuruyla para birimi çevirir (Frankfurter, anahtarsız).""" try: kaynak_birim, hedef_birim = kaynak_birim.strip().upper(), hedef_birim.strip().upper() miktar = float(miktar) if kaynak_birim == hedef_birim: return {"miktar": miktar, "sonuc": miktar, "kaynak_birim": kaynak_birim, "hedef_birim": hedef_birim} veri = httpx.get( "https://api.frankfurter.dev/v1/latest", params={"base": kaynak_birim, "symbols": hedef_birim, "amount": miktar}, timeout=15, ).json() oranlar = veri.get("rates", {}) if hedef_birim not in oranlar: return {"hata": f"{kaynak_birim} -> {hedef_birim} kuru bulunamadı."} return { "miktar": miktar, "kaynak_birim": kaynak_birim, "hedef_birim": hedef_birim, "sonuc": round(oranlar[hedef_birim], 4), "kur_tarihi": veri.get("date"), "kaynak": "Frankfurter / ECB", } except Exception as e: return {"hata": f"Döviz çevrilemedi: {e}"} def sicaklik_cevir(deger, kaynak_birim, hedef_birim): """C/F/K arası sıcaklık çevirir (yerel hesap, API'ye gitmez).""" try: k, h = kaynak_birim.strip().lower()[0], hedef_birim.strip().lower()[0] deger = float(deger) celsius = {"c": deger, "f": (deger - 32) / 1.8, "k": deger - 273.15}.get(k) if celsius is None or h not in "cfk": return {"hata": f"'{kaynak_birim}' -> '{hedef_birim}' desteklenmiyor. Sadece C/F/K."} sonuc = {"c": celsius, "f": celsius * 1.8 + 32, "k": celsius + 273.15}[h] return { "girdi": deger, "kaynak_birim": kaynak_birim, "hedef_birim": hedef_birim, "sonuc": round(sonuc, 2), "kaynak": "yerel hesap", } except Exception as e: return {"hata": f"Sıcaklık çevrilemedi: {e}"} KRIPTO_TAKMA_ADLARI = { "btc": "bitcoin", "bitcoin": "bitcoin", "eth": "ethereum", "ethereum": "ethereum", "sol": "solana", "solana": "solana", "doge": "dogecoin", "dogecoin": "dogecoin", "xrp": "ripple", "ripple": "ripple", } def kripto_fiyat(kripto, para_birimi="usd"): """Bir kripto paranın güncel fiyatını döner (CoinGecko, anahtarsız).""" try: kimlik = KRIPTO_TAKMA_ADLARI.get(kripto.strip().lower(), kripto.strip().lower()) para_birimi = para_birimi.strip().lower() veri = httpx.get( "https://api.coingecko.com/api/v3/simple/price", params={"ids": kimlik, "vs_currencies": para_birimi, "include_24hr_change": "true"}, timeout=15, ).json() if kimlik not in veri: return {"hata": f"'{kripto}' CoinGecko'da bulunamadı."} kayit = veri[kimlik] degisim = kayit.get(f"{para_birimi}_24h_change") return { "kripto": kimlik, "para_birimi": para_birimi.upper(), "fiyat": kayit.get(para_birimi), "degisim_24s_yuzde": round(degisim, 2) if degisim is not None else None, "kaynak": "CoinGecko", } except Exception as e: return {"hata": f"Kripto fiyatı alınamadı: {e}"} WIKI_BASLIKLARI = {"User-Agent": "magibu-ders5-tool-calling/1.0 (egitim odevi; iletisim: salih12dede@gmail.com)"} def wikipedia_ara(sorgu, dil="tr"): """Wikipedia'da arayıp en alakalı maddenin özetini döner (anahtarsız).""" try: dil = dil.strip().lower() or "tr" arama = httpx.get( f"https://{dil}.wikipedia.org/w/api.php", params={"action": "query", "list": "search", "srsearch": sorgu, "srlimit": 1, "format": "json"}, headers=WIKI_BASLIKLARI, timeout=15, ).json() bulunanlar = arama.get("query", {}).get("search", []) if not bulunanlar: return {"hata": f"'{sorgu}' için Wikipedia'da sonuç bulunamadı."} baslik = bulunanlar[0]["title"] ozet = httpx.get( f"https://{dil}.wikipedia.org/w/api.php", params={"action": "query", "prop": "extracts", "exintro": 1, "explaintext": 1, "titles": baslik, "format": "json", "redirects": 1}, headers=WIKI_BASLIKLARI, timeout=15, ).json() sayfalar = ozet.get("query", {}).get("pages", {}) metin = next(iter(sayfalar.values()), {}).get("extract", "").strip() if len(metin) > 800: metin = metin[:800].rsplit(" ", 1)[0] + "..." return {"baslik": baslik, "ozet": metin or "Özet bulunamadı.", "kaynak": f"Wikipedia ({dil})"} except Exception as e: return {"hata": f"Wikipedia sorgusu başarısız: {e}"} def son_depremler(min_buyukluk=4.5, son_kac_gun=1): """Dünya genelinde son depremleri büyüklüğe göre listeler (USGS, anahtarsız).""" try: import datetime as dt veri = httpx.get( "https://earthquake.usgs.gov/fdsnws/event/1/query", params={ "format": "geojson", "minmagnitude": float(min_buyukluk), "starttime": f"now-{max(1, min(int(son_kac_gun), 30))}days", "orderby": "magnitude", "limit": 5, }, timeout=15, ).json() depremler = [] for oge in veri.get("features", []): ozellik = oge.get("properties", {}) zaman_ms = ozellik.get("time") depremler.append({ "buyukluk": ozellik.get("mag"), "yer": ozellik.get("place"), "zaman_utc": ( dt.datetime.fromtimestamp(zaman_ms / 1000, dt.timezone.utc).strftime("%Y-%m-%d %H:%M") if zaman_ms else None ), }) return {"kriter": f"son {son_kac_gun} gün, büyüklük >= {min_buyukluk}", "toplam": len(depremler), "depremler": depremler, "kaynak": "USGS"} except Exception as e: return {"hata": f"Deprem verisi alınamadı: {e}"} ARAC_FONKSIYONLARI = { "hava_durumu": hava_durumu, "doviz_cevir": doviz_cevir, "sicaklik_cevir": sicaklik_cevir, "kripto_fiyat": kripto_fiyat, "wikipedia_ara": wikipedia_ara, "son_depremler": son_depremler, } ARAC_UST_VERI = { "hava_durumu": {"etiket": "WX", "saglayici": "Open-Meteo", "aciklama": "Bir şehrin anlık sıcaklık, nem, rüzgar ve hava durumunu getirir."}, "doviz_cevir": {"etiket": "FX", "saglayici": "Frankfurter / ECB", "aciklama": "Güncel kurla iki para birimi arasında çevrim yapar."}, "sicaklik_cevir":{"etiket": "TEMP", "saglayici": "yerel hesap", "aciklama": "Sıcaklığı Celsius, Fahrenheit ve Kelvin arasında çevirir."}, "kripto_fiyat": {"etiket": "BTC", "saglayici": "CoinGecko", "aciklama": "Bir kripto paranın güncel fiyatını ve 24 saatlik değişimini getirir."}, "wikipedia_ara": {"etiket": "WIKI", "saglayici": "Wikipedia", "aciklama": "Wikipedia'da arayıp en alakalı maddenin özetini getirir."}, "son_depremler": {"etiket": "EQ", "saglayici": "USGS", "aciklama": "Son depremleri büyüklüğe göre sıralı listeler."}, } ARAC_SEMALARI = [ { "type": "function", "function": { "name": "hava_durumu", "description": "Bir şehrin anlık sıcaklık (Celsius), nem, rüzgar ve hava durumunu verir.", "parameters": { "type": "object", "properties": {"sehir": {"type": "string", "description": "Şehir adı, örn: Ankara"}}, "required": ["sehir"], }, }, }, { "type": "function", "function": { "name": "doviz_cevir", "description": "Güncel kurla bir para biriminden diğerine çevirir. Örn: 250 EUR kaç TL.", "parameters": { "type": "object", "properties": { "miktar": {"type": "number", "description": "Çevrilecek miktar"}, "kaynak_birim": {"type": "string", "description": "Kaynak para birimi kodu, örn: USD"}, "hedef_birim": {"type": "string", "description": "Hedef para birimi kodu, örn: TRY"}, }, "required": ["miktar", "kaynak_birim", "hedef_birim"], }, }, }, { "type": "function", "function": { "name": "sicaklik_cevir", "description": ( "Sıcaklığı Celsius/Fahrenheit/Kelvin arasında çevirir. " "hava_durumu her zaman Celsius döner; kullanıcı başka birim istiyorsa " "dönen değeri bu araca ikinci adım olarak ver." ), "parameters": { "type": "object", "properties": { "deger": {"type": "number", "description": "Çevrilecek sıcaklık değeri"}, "kaynak_birim": {"type": "string", "description": "C, F veya K"}, "hedef_birim": {"type": "string", "description": "C, F veya K"}, }, "required": ["deger", "kaynak_birim", "hedef_birim"], }, }, }, { "type": "function", "function": { "name": "kripto_fiyat", "description": "Bir kripto paranın güncel fiyatını ve 24 saatlik yüzde değişimini döner.", "parameters": { "type": "object", "properties": { "kripto": {"type": "string", "description": "Kripto adı/sembolü, örn: bitcoin, ETH"}, "para_birimi": {"type": "string", "description": "Fiyatın gösterileceği para birimi, örn: usd. Varsayılan usd."}, }, "required": ["kripto"], }, }, }, { "type": "function", "function": { "name": "wikipedia_ara", "description": "Wikipedia'da arama yapıp en alakalı maddenin özetini döner. Kişi, yer veya kavram sorularında kullan.", "parameters": { "type": "object", "properties": { "sorgu": {"type": "string", "description": "Aranacak konu"}, "dil": {"type": "string", "description": "Wikipedia dil kodu: tr veya en. Varsayılan tr."}, }, "required": ["sorgu"], }, }, }, { "type": "function", "function": { "name": "son_depremler", "description": "USGS kataloğundan son depremleri büyüklüğe göre sıralı listeler.", "parameters": { "type": "object", "properties": { "min_buyukluk": {"type": "number", "description": "Minimum deprem büyüklüğü. Varsayılan 4.5."}, "son_kac_gun": {"type": "integer", "description": "Kaç günlük geçmişe bakılacağı (1-30). Varsayılan 1."}, }, "required": [], }, }, }, ] def araci_calistir(ad, argumanlar): fonksiyon = ARAC_FONKSIYONLARI.get(ad) if fonksiyon is None: return {"hata": f"'{ad}' adında bir araç yok."} try: return fonksiyon(**(argumanlar or {})) except TypeError as e: return {"hata": f"'{ad}' hatalı argümanlarla çağrıldı: {e}"} # -------------------------------------------------------------------------------------- # Ajan döngüsü — generator, her adımda olay üretir # -------------------------------------------------------------------------------------- SISTEM_PROMPTU = ( "Türkçe konuşan bir asistansın. Hava durumu, döviz kuru, kripto fiyatı, deprem " "ve ansiklopedik bilgi gibi GÜNCEL/OLGUSAL sorularda tahmin yürütme; elindeki " "araçları çağır. Bir soru birim çevrimi de gerektiriyorsa (örn. Fahrenheit) önce " "veriyi al, sonra çevirme aracını ayrı bir adımda çağır. Araç hata döndürürse " "kullanıcıya dürüstçe söyle, veri uydurma. Araç gerektirmeyen sorularda araç " "çağırmadan doğrudan yanıtla." ) TOOLSUZ_PROMPTU = ( "Türkçe konuşan bir asistansın. Hiçbir dış araca/internete erişimin yok. Güncel " "hava durumu, döviz kuru, kripto fiyatı, deprem gibi anlık bilgi istenirse bunu " "bilemeyeceğini açıkça söyle; uydurma sayı verme." ) MAKS_TUR = 4 MAKS_DENEME = 2 def _hata_kisalt(e): """Sağlayıcı hatalarının uzun ham JSON'ını UI'da okunabilir tek satıra indirger.""" metin = str(e) for ayrac in ('"message":', "'message':"): if ayrac in metin: parca = metin.split(ayrac, 1)[1].strip().strip("'\"") return parca.split('", "code"')[0].split("', 'code'")[0][:180] return metin[:180] def _arguman_coz(cagri): try: cozulen = json.loads(cagri.function.arguments or "{}") return cozulen if isinstance(cozulen, dict) else {} except json.JSONDecodeError: return {"_cozulemeyen": cagri.function.arguments} def ajan_calistir(soru, model, araclar_acik): """Tool-calling döngüsünü çalıştırır; olayları generator olarak üretir.""" mesajlar = [ {"role": "system", "content": SISTEM_PROMPTU if araclar_acik else TOOLSUZ_PROMPTU}, {"role": "user", "content": soru}, ] semalar = ARAC_SEMALARI if araclar_acik else [] for tur in range(1, MAKS_TUR + 1): yield {"tip": "tur", "tur": tur} mesaj, hata = None, None for deneme in range(1, MAKS_DENEME + 1): try: istek = {"model": model, "messages": mesajlar, "temperature": 0.2 if deneme == 1 else 0.6} if semalar: istek["tools"] = semalar istek["tool_choice"] = "auto" yanit = istemci().chat.completions.create(**istek) except Exception as e: hata = _hata_kisalt(e) yield {"tip": "deneme_hata", "deneme": deneme, "detay": hata} continue secenekler = getattr(yanit, "choices", None) or [] if not secenekler: hata = str(getattr(yanit, "error", None) or "model boş yanıt döndü") yield {"tip": "deneme_hata", "deneme": deneme, "detay": hata} continue aday = secenekler[0] var_arac = bool(getattr(aday.message, "tool_calls", None)) var_icerik = bool((aday.message.content or "").strip()) if var_arac or var_icerik: mesaj = aday.message hata = None break hata = f"finish_reason={aday.finish_reason}, native={getattr(aday, 'native_finish_reason', None)}" yield {"tip": "deneme_hata", "deneme": deneme, "detay": hata} if mesaj is None: yield {"tip": "hata", "mesaj": f"Model geçerli bir yanıt üretemedi ({hata})."} return arac_cagrilari = list(getattr(mesaj, "tool_calls", None) or []) if not arac_cagrilari: yield {"tip": "final", "metin": (mesaj.content or "").strip() or "(boş yanıt)", "tur": tur} return mesajlar.append({ "role": "assistant", "content": mesaj.content or "", "tool_calls": [ {"id": c.id, "type": "function", "function": {"name": c.function.name, "arguments": c.function.arguments}} for c in arac_cagrilari ], }) for cagri in arac_cagrilari: argumanlar = _arguman_coz(cagri) yield {"tip": "arac_basladi", "id": cagri.id, "arac": cagri.function.name, "argumanlar": argumanlar} baslangic = time.time() sonuc = araci_calistir(cagri.function.name, argumanlar) sure = time.time() - baslangic yield { "tip": "arac_bitti", "id": cagri.id, "arac": cagri.function.name, "argumanlar": argumanlar, "sonuc": sonuc, "sure": sure, "basarili": not (isinstance(sonuc, dict) and "hata" in sonuc), } mesajlar.append({ "role": "tool", "tool_call_id": cagri.id, "name": cagri.function.name, "content": json.dumps(sonuc, ensure_ascii=False), }) yield {"tip": "hata", "mesaj": f"{MAKS_TUR} tur sonunda nihai cevaba ulaşılamadı."} # -------------------------------------------------------------------------------------- # Arayüz — 3 panel, aynı soruya paralel canlı akış # -------------------------------------------------------------------------------------- PANEL_AYARLARI = [ {"anahtar": "A", "baslik": "Güçlü model", "mod": "araçlar açık", "model": MODEL_GUCLU, "araclar": True}, {"anahtar": "B", "baslik": "Güçlü model", "mod": "araçlar kapalı", "model": MODEL_GUCLU, "araclar": False}, {"anahtar": "C", "baslik": "Küçük model", "mod": "araçlar açık", "model": MODEL_ZAYIF, "araclar": True}, ] # -------------------------------------------------------------------------------------- # Tasarım tokenleri — sade, tek aileden 3 renk: # 1) vurgu — marka/etkileşim rengi (bağlantı, aktif pil, gönder düğmesi, tooltip) # 2) basarili — araç çağrısı başarılı # 3) tehlike — araç/model hatası (yeniden deneme uyarıları da bu tona düşer) # Geri kalan her şey Gradio'nun kendi nötr tema değişkenlerinden gelir; böylece # açık/koyu tema otomatik uyum sağlar. Vurgu rengi teknik/ölçüm hissi için sıcak # bir bakır tonu — jenerik indigo/mor değil. # -------------------------------------------------------------------------------------- CSS = """ :root, .gradio-container { --vurgu: #a35a1d; --vurgu-zayif: rgba(163,90,29,.10); --basarili: #187a3c; --tehlike: #c22626; --basarili-zemin: rgba(24,122,60,.10); --tehlike-zemin: rgba(194,38,38,.10); --font-metin: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; --font-veri: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", Menlo, Consolas, monospace; } .dark, .dark .gradio-container { --vurgu: #d99a52; --vurgu-zayif: rgba(217,154,82,.16); --basarili: #4ade80; --tehlike: #f87171; --basarili-zemin: rgba(74,222,128,.12); --tehlike-zemin: rgba(248,113,113,.12); } .gecit-baslik { display:flex; align-items:baseline; gap:10px; margin: 0; } .gecit-baslik h1 { font-family: var(--font-metin); font-size: 1.22em; font-weight: 700; margin: 0; text-wrap: balance; } .gecit-tez { font-family: var(--font-metin); font-size: 0.84em; opacity: .68; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.4; margin: 3px 0 12px; } .eyebrow { font-family: var(--font-metin); font-size: 0.68em; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; opacity: .5; margin: 0 0 6px; } .arac-kayit { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-bottom: 12px; } .arac-etiket { position: relative; display: flex; align-items: center; gap: 7px; border: 1px solid var(--border-color-primary); border-radius: 7px; padding: 6px 10px; font-family: var(--font-metin); cursor: default; overflow: hidden; } .arac-etiket .rozet { font-family: var(--font-veri); font-size: 0.68em; font-weight: 700; color: var(--vurgu); background: var(--vurgu-zayif); border-radius: 4px; padding: 2px 6px; letter-spacing: .02em; flex-shrink: 0; } .arac-etiket .ad { font-weight: 600; font-size: 0.82em; flex-shrink: 0; } .arac-etiket .aciklama { font-size: 0.78em; opacity: .55; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .arac-etiket .ipucu { position: absolute; left: 12px; bottom: calc(100% + 8px); width: max-content; max-width: 240px; background: var(--body-text-color); color: var(--background-fill-primary); padding: 7px 10px; border-radius: 7px; font-size: 0.82em; line-height: 1.4; text-align: left; opacity: 0; pointer-events: none; transition: opacity .12s ease, transform .12s ease; z-index: 30; transform: translateY(4px); box-shadow: 0 6px 18px rgba(0,0,0,.18); } .arac-etiket .ipucu b { display: block; margin-bottom: 2px; } .arac-etiket:hover .ipucu { opacity: .96; transform: translateY(0); } @media (max-width: 900px) { .arac-kayit { grid-template-columns: repeat(2, 1fr); } } .panel-kutu { border: 1px solid var(--border-color-primary); border-radius: 10px; padding: 12px 14px; /* HF Space'in üst menüsü iframe'in kullanabildiği yüksekliği azaltır. Paneli doğrudan vh'nin yarısı yapmak composer'ı ekranın altına itiyordu. Başlık, araçlar, örnekler ve composer için 540px ayır; büyük ekranlarda da panelin gereksiz yere uzamasını engelle. */ height: clamp(250px, calc(100vh - 540px), 460px); height: clamp(250px, calc(100dvh - 540px), 460px); min-height: 0; display: flex; flex-direction: column; font-family: var(--font-metin); overflow: hidden; } .panel-ust { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; padding-bottom: 8px; margin-bottom: 2px; border-bottom: 1px solid var(--border-color-primary); flex-shrink: 0; } .panel-ad { font-weight: 700; font-size: 0.92em; } .panel-model { font-family: var(--font-veri); font-size: 0.7em; opacity: .6; margin-top: 1px; } .pil { font-size: 0.64em; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; border-radius: 999px; padding: 2px 8px; white-space: nowrap; } .pil.acik { background: var(--vurgu-zayif); color: var(--vurgu); } .pil.kapali { background: var(--background-fill-secondary); opacity: .6; } .panel-govde { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-top: 6px; scroll-behavior: smooth; } .panel-bos { opacity: .4; font-size: 0.8em; padding-top: 4px; } .panel-alt-bilgi { font-size: 0.68em; opacity: .5; margin-top: 4px; padding-top: 4px; flex-shrink: 0; border-top: 1px solid var(--border-color-primary); font-variant-numeric: tabular-nums; } .tur-etiketi { font-size: 0.72em; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; opacity: .45; margin: 14px 0 6px; } .tur-etiketi:first-child { margin-top: 0; } .arac-kart { border: 1px solid var(--border-color-primary); border-radius: 8px; padding: 10px 11px; margin-bottom: 8px; } .arac-kart.tamam { border-color: color-mix(in srgb, var(--basarili) 35%, var(--border-color-primary)); } .arac-kart.hata { border-color: color-mix(in srgb, var(--tehlike) 35%, var(--border-color-primary)); } .arac-kart-ust { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .arac-kart-ad { display: flex; align-items: center; gap: 7px; font-size: 0.86em; } .arac-kart-ad .rozet { font-family: var(--font-veri); font-size: 0.7em; font-weight: 700; color: var(--vurgu); background: var(--vurgu-zayif); border-radius: 4px; padding: 1px 5px; } .arac-kart-ad .saglayici { opacity: .55; font-size: 0.85em; } .durum-pil { font-size: 0.66em; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; border-radius: 5px; padding: 2px 7px; font-variant-numeric: tabular-nums; } .durum-pil.calisiyor { background: var(--background-fill-secondary); opacity: .7; } .durum-pil.tamam { background: var(--basarili-zemin); color: var(--basarili); } .durum-pil.hata { background: var(--tehlike-zemin); color: var(--tehlike); } .veri-blok { margin-top: 8px; } .veri-etiket { font-size: 0.66em; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; opacity: .45; margin-bottom: 3px; } .veri-satirlar { font-family: var(--font-veri); font-size: 0.78em; background: var(--background-fill-secondary); border-radius: 6px; padding: 7px 9px; line-height: 1.55; word-break: break-word; font-variant-numeric: tabular-nums; } .veri-satirlar .anahtar { opacity: .55; } .veri-satirlar .hata-satir { color: var(--tehlike); } .kaynak-notu { font-size: 0.72em; opacity: .5; margin-top: 4px; font-family: var(--font-metin); } .deneme-satir { font-size: 0.78em; background: var(--background-fill-secondary); border-radius: 6px; padding: 5px 9px; margin-bottom: 8px; font-family: var(--font-metin); } .deneme-satir b { color: var(--tehlike); } .deneme-satir .veri-satirlar { margin-top: 4px; background: transparent; padding: 0; opacity: .75; } .sonuc-blok { padding-top: 10px; } .sonuc-kutu { border-radius: 8px; padding: 11px 12px; font-size: 0.9em; line-height: 1.55; white-space: pre-wrap; background: var(--vurgu-zayif); border: 1px solid var(--vurgu-zayif); } .hata-kutu { border-radius: 8px; padding: 11px 12px; font-size: 0.86em; line-height: 1.5; background: var(--tehlike-zemin); color: var(--tehlike); } .ornek-baslik { font-family: var(--font-metin); font-size: 0.68em; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; opacity: .5; margin: 2px 0 6px; } #ornek-sorular { display: grid !important; grid-template-columns: repeat(6, 1fr); gap: 6px; flex-wrap: unset; } #ornek-sorular > * { min-width: 0; } #ornek-sorular button { font-size: 0.74em !important; white-space: normal !important; text-align: left !important; justify-content: flex-start !important; line-height: 1.25 !important; min-height: 40px; padding: 7px 9px !important; height: auto !important; } @media (max-width: 1100px) { #ornek-sorular { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 640px) { #ornek-sorular { grid-template-columns: repeat(2, 1fr); } } /* Paneller sabit yükseklikte iç kaydırmalı olduğu için toplam sayfa yüksekliği sorulara göre değişmiyor; composer bu nedenle normal akışta, sayfanın en altında sade bir çubuk olarak durabiliyor (fixed/sticky konumlandırma Gradio'nun kendi transform'lu sarmalayıcısıyla çakışıp bozuk konumlanmaya yol açıyordu). */ #composer-disi { border-top: 1px solid var(--border-color-primary); margin-top: 10px; padding-top: 8px; } #composer { gap: 8px; } /* Gradio'nun bloklar arası varsayılan dikey boşluğunu sıkılaştırır. */ .gradio-container .block { margin-bottom: 0 !important; } .gradio-container > .main, .gradio-container .contain { gap: 6px !important; padding-top: 8px !important; } /* Not: body/html'i flex ile dikeyde ortalamayı denemiştik ama Hugging Face Spaces sayfayı bir iframe içine gömüyor ve Gradio, iframe yüksekliğini body'nin doğal scrollHeight'ına göre otomatik ayarlıyor. body'yi flex+height:100% yapmak bu otomatik yükseklik hesabını bozup composer'ın iframe dışında kalmasına yol açtı. Bu yüzden doğal doküman akışına dönüyoruz; içerik zaten kompakt olduğundan (paneller iç kaydırmalı, sabit-olmayan sade bir sayfa) ekstra ortalamaya ihtiyaç yok. Sadece genişliği makul bir üst sınırla tutuyoruz. */ .gradio-container { max-width: 1400px; margin: 0 auto !important; } """ AUTO_KAYDIR_JS = """ """ def _kacir(metin): return str(metin).replace("&", "&").replace("<", "<").replace(">", ">") def _deger_yaz(v): if isinstance(v, float): return f"{v:,.2f}".rstrip("0").rstrip(".") if v != int(v) else f"{int(v):,}" if isinstance(v, list): if v and isinstance(v[0], dict): return f"[{len(v)} kayıt]" return ", ".join(str(x) for x in v) if v else "—" return str(v) def _veri_satirlari(d, haric=()): satirlar = [] for k, v in d.items(): if k in haric or v is None: continue satirlar.append(f'{_kacir(k)}: {_kacir(_deger_yaz(v))}') return "
".join(satirlar) if satirlar else "—" def _arac_kart_render(blok): ust = ARAC_UST_VERI.get(blok["arac"], {"etiket": "?", "saglayici": "—"}) durum = blok["durum"] durum_metni = {"calisiyor": "çalışıyor", "tamam": "tamamlandı", "hata": "hata"}[durum] sure_metni = f'{blok["sure"]:.2f}s' if blok.get("sure") is not None else "" parcalar = [ f'
' f'
' f'{ust["etiket"]}{_kacir(blok["arac"])}' f'{_kacir(ust["saglayici"])}' f'{durum_metni}{" · " + sure_metni if sure_metni else ""}' f'
' f'
girdi
' f'
{_veri_satirlari(blok["argumanlar"]) if blok["argumanlar"] else "—"}
' ] if durum == "tamam": sonuc = blok["sonuc"] or {} if isinstance(sonuc, dict) and "hata" in sonuc: parcalar.append( f'
çıktı
' f'
{_kacir(sonuc["hata"])}
' ) else: kaynak = sonuc.get("kaynak") if isinstance(sonuc, dict) else None parcalar.append( f'
çıktı
' f'
{_veri_satirlari(sonuc, haric=("kaynak",))}
' + (f'
kaynak: {_kacir(kaynak)}
' if kaynak else "") + '
' ) elif durum == "hata": parcalar.append( f'
çıktı
' f'
{_kacir((blok.get("sonuc") or {}).get("hata", "bilinmeyen hata"))}
' ) parcalar.append("
") return "".join(parcalar) def _durum_baslangic(): return {"bloklar": [], "id_indeks": {}, "final": None, "hata": None, "sure": None} def _durum_guncelle(durum, olay): tip = olay["tip"] if tip == "tur": durum["bloklar"].append({"tip": "tur", "tur": olay["tur"]}) elif tip == "arac_basladi": durum["id_indeks"][olay["id"]] = len(durum["bloklar"]) durum["bloklar"].append({ "tip": "arac", "arac": olay["arac"], "argumanlar": olay["argumanlar"], "durum": "calisiyor", "sonuc": None, "sure": None, }) elif tip == "arac_bitti": i = durum["id_indeks"].get(olay["id"]) if i is not None: blok = durum["bloklar"][i] blok.update(durum="tamam" if olay["basarili"] else "hata", sonuc=olay["sonuc"], sure=olay["sure"]) elif tip == "deneme_hata": durum["bloklar"].append({"tip": "deneme_hata", "deneme": olay["deneme"], "detay": olay["detay"]}) elif tip == "final": durum["final"] = olay["metin"] elif tip == "hata": durum["hata"] = olay["mesaj"] def _panel_render(ayar, durum): pil_sinif = "acik" if ayar["araclar"] else "kapali" parcalar = [ '
' f'
{_kacir(ayar["baslik"])}
' f'
{_kacir(ayar["model"])}
' f'{_kacir(ayar["mod"])}' '
' f'
' ] if not durum["bloklar"] and not durum["final"] and not durum["hata"]: parcalar.append('
Bir soru sorduğunuzda burada akacak.
') for blok in durum["bloklar"]: if blok["tip"] == "tur": parcalar.append(f'
Tur {blok["tur"]}
') elif blok["tip"] == "arac": parcalar.append(_arac_kart_render(blok)) elif blok["tip"] == "deneme_hata": parcalar.append( f'
Deneme {blok["deneme"]} geçersiz yanıt üretti' f'
{_kacir(blok["detay"])}
' ) if durum["final"] or durum["hata"]: parcalar.append('
') if durum["final"]: parcalar.append(f'
{_kacir(durum["final"])}
') if durum["hata"]: parcalar.append(f'
{_kacir(durum["hata"])}
') parcalar.append("
") parcalar.append("
") # .panel-govde kapanışı if durum["sure"] is not None: arac_sayisi = sum(1 for b in durum["bloklar"] if b["tip"] == "arac") parcalar.append(f'
{durum["sure"]:.1f}s · {arac_sayisi} araç çağrısı
') parcalar.append("
") # .panel-kutu kapanışı return "".join(parcalar) def _panel_calistir(anahtar, soru, model, araclar_acik, kuyruk): baslangic = time.time() try: for olay in ajan_calistir(soru, model, araclar_acik): kuyruk.put((anahtar, olay, time.time() - baslangic)) except Exception as e: kuyruk.put((anahtar, {"tip": "hata", "mesaj": f"Beklenmeyen hata: {e}"}, time.time() - baslangic)) kuyruk.put((anahtar, {"tip": "bitti"}, time.time() - baslangic)) def uc_paneli_calistir(soru): """Panelleri render eder; son çıktı olarak soru kutusunu da yönetir. İlk yield'de kutu boşaltılır (kullanıcı gönderir göndermez temizlenir); sonraki yield'lerde kutuya dokunulmaz, böylece kullanıcı akış sürerken sıradaki soruyu yazmaya başlarsa üzerine yazılmaz. """ soru = (soru or "").strip() if not soru: bos = [_panel_render(a, _durum_baslangic()) for a in PANEL_AYARLARI] yield (*bos, gr.update()) return kuyruk = queue.Queue() durumlar = {a["anahtar"]: _durum_baslangic() for a in PANEL_AYARLARI} threadler = [ threading.Thread(target=_panel_calistir, args=(a["anahtar"], soru, a["model"], a["araclar"], kuyruk), daemon=True) for a in PANEL_AYARLARI ] for th in threadler: th.start() bitenler = set() ilk_yield = True while len(bitenler) < len(PANEL_AYARLARI): anahtar, olay, gecen_sure = kuyruk.get() durum = durumlar[anahtar] if olay["tip"] == "bitti": bitenler.add(anahtar) durum["sure"] = gecen_sure else: _durum_guncelle(durum, olay) panel_htmlleri = tuple(_panel_render(a, durumlar[a["anahtar"]]) for a in PANEL_AYARLARI) yield (*panel_htmlleri, ("" if ilk_yield else gr.update())) ilk_yield = False ORNEK_SORULAR = [ "Ankara mı daha sıcak Londra mı, bu değerler Fahrenheit olarak kaç eder?", "İstanbul'da hava kaç derece, Kelvin olarak da söyler misin?", "250 Euro kaç TL eder?", "Bitcoin şu an kaç dolar, 24 saatte ne kadar değişti?", "Mimar Sinan kimdir kısaca anlatır mısın?", "Son 3 günde 5'ten büyük deprem oldu mu?", ] with gr.Blocks(title="Tool Calling Karşılaştırması") as demo: gr.HTML( '

Tool Calling Karşılaştırması

' '

Aynı soru üçüne birden gider: güçlü model + araçlar, aynı model araçsız, ' 'küçük model + araçlar — fark, tool calling ve model kapasitesinin gerçek katkısını gösterir.

' '
Kullanılabilir araçlar
' '
' + "".join( f'
{u["etiket"]}' f'{ad}' f'{u["aciklama"]}' f'{u["saglayici"]}{u["aciklama"]}
' for ad, u in ARAC_UST_VERI.items() ) + '
' ) with gr.Row(): paneller = [gr.HTML(_panel_render(a, _durum_baslangic())) for a in PANEL_AYARLARI] gr.HTML('
Örnek sorular
') with gr.Row(elem_id="ornek-sorular"): ornek_butonlar = [gr.Button(s, size="sm") for s in ORNEK_SORULAR] with gr.Group(elem_id="composer-disi"): with gr.Row(elem_id="composer"): soru_kutusu = gr.Textbox( placeholder="Bir soru sorun — örn. Ankara mı daha sıcak Londra mı...", scale=6, show_label=False, container=False, ) gonder = gr.Button("Gönder", variant="primary", scale=1) for buton, s in zip(ornek_butonlar, ORNEK_SORULAR): buton.click(lambda s=s: s, outputs=soru_kutusu) cikislar = paneller + [soru_kutusu] gonder.click(uc_paneli_calistir, inputs=soru_kutusu, outputs=cikislar) soru_kutusu.submit(uc_paneli_calistir, inputs=soru_kutusu, outputs=cikislar) if __name__ == "__main__": demo.queue().launch(css=CSS, head=AUTO_KAYDIR_JS)