| """VYS SQL Öğrenme Uygulaması — Hugging Face Spaces (Gradio). |
| |
| Akış: her konuda öğrenci önce kavramı okur, çalıştırılmış bir örnek görür, |
| sonra kendi görevini yazar. SQL'i gerçek bir SQLite veritabanında çalışır. |
| |
| Doğru/yanlış kararını `degerlendirme.py` verir — dil modeli değil. |
| Dil modelinin tek işi hatayı açıklamak ve cevabı söylemeden ipucu vermektir. |
| |
| Gerekli Space secret'ları: |
| HF_TOKEN — "Make calls to Inference Providers" + "Write access to |
| contents/settings of repos" izinli fine-grained token |
| APP_USERS — "ogr01:parola1,ogr02:parola2,..." |
| |
| İsteğe bağlı Space variable'ları: |
| ILERLEME_DEPOSU — "SSSAssist/vys-sql-ilerleme" gibi bir dataset adı. |
| Boş bırakılırsa ilerleme kaydedilmez. |
| MODEL (varsayılan: openai/gpt-oss-120b) |
| IPUCU_GUNLUK_LIMIT (varsayılan: 40) |
| IPUCU_TOPLAM_LIMIT (varsayılan: 2000) |
| """ |
|
|
| import datetime |
| import os |
| import threading |
|
|
| import gradio as gr |
| import pandas as pd |
| from huggingface_hub import InferenceClient |
|
|
| import degerlendirme |
| import ilerleme |
| import mufredat |
| import veritabani |
|
|
| MODEL = os.environ.get("MODEL", "openai/gpt-oss-120b") |
| IPUCU_GUNLUK_LIMIT = int(os.environ.get("IPUCU_GUNLUK_LIMIT", 40)) |
| IPUCU_TOPLAM_LIMIT = int(os.environ.get("IPUCU_TOPLAM_LIMIT", 2000)) |
|
|
| istemci = InferenceClient(api_key=os.environ.get("HF_TOKEN")) |
|
|
| OGRETMEN_TALIMATI = """Sen bir SQL öğretmenisin. Öğrencilerin lisans düzeyinde, |
| Bilgisayar ve Öğretim Teknolojileri Eğitimi (BÖTE) bölümünde okuyor ve SQL'i |
| yeni öğreniyorlar. |
| |
| Kuralların: |
| 1. ASLA doğru sorgunun tamamını yazma. Öğrenci cevabı kendisi bulmalı. |
| 2. Türkçe, sade ve kısa yaz. En fazla 4-5 cümle. |
| 3. Hata mesajı varsa önce onun ne anlama geldiğini gündelik dille açıkla. |
| 4. Öğrencinin yazdığı sorguda neyin eksik veya yanlış olduğunu göster, |
| ama düzeltilmiş hâlini verme. |
| 5. Tek bir SQL anahtar kelimesini (ORDER BY, HAVING gibi) ipucu olarak |
| söyleyebilirsin; tam ifadeyi değil. |
| 6. Öğrenciyi küçümseme, cesaretlendir.""" |
|
|
|
|
| def kullanicilari_yukle(): |
| """APP_USERS secret'ını {kullanıcı: parola} sözlüğüne çevirir.""" |
| ham = os.environ.get("APP_USERS", "").strip() |
| if not ham: |
| raise SystemExit("HATA: APP_USERS secret'ı tanımlı değil.") |
|
|
| kullanicilar = {} |
| for kayit in ham.split(","): |
| kullanici, _, parola = kayit.strip().partition(":") |
| if kullanici.strip() and parola.strip(): |
| kullanicilar[kullanici.strip()] = parola.strip() |
|
|
| if not kullanicilar: |
| raise SystemExit("HATA: APP_USERS içinde geçerli kullanıcı yok.") |
| return kullanicilar |
|
|
|
|
| KULLANICILAR = kullanicilari_yukle() |
|
|
|
|
| class IpucuKotasi: |
| """Yalnızca yapay zekâ çağrılarını sayar. |
| |
| SQL çalıştırmak ve görev kontrolü yerel SQLite'ta yapıldığı için |
| ücretsizdir ve kotadan düşmez. |
| |
| Sayaçlar bellektedir; Space yeniden başlarsa sıfırlanır. Asıl bütçe |
| koruması IPUCU_TOPLAM_LIMIT'tir. |
| """ |
|
|
| def __init__(self): |
| self._kilit = threading.Lock() |
| self._gun = datetime.date.today() |
| self._kisi_basi = {} |
| self._toplam = 0 |
|
|
| def dusur(self, kullanici): |
| with self._kilit: |
| bugun = datetime.date.today() |
| if bugun != self._gun: |
| self._gun = bugun |
| self._kisi_basi.clear() |
| self._toplam = 0 |
|
|
| if self._toplam >= IPUCU_TOPLAM_LIMIT: |
| return False, "Sınıfın günlük ipucu kotası doldu." |
|
|
| kullanilan = self._kisi_basi.get(kullanici, 0) |
| if kullanilan >= IPUCU_GUNLUK_LIMIT: |
| return False, ( |
| f"Günlük {IPUCU_GUNLUK_LIMIT} ipucu hakkınızı doldurdunuz. " |
| "SQL çalıştırmaya ve görev kontrolüne devam edebilirsiniz." |
| ) |
|
|
| self._kisi_basi[kullanici] = kullanilan + 1 |
| self._toplam += 1 |
| return True, IPUCU_GUNLUK_LIMIT - kullanilan - 1 |
|
|
|
|
| kota = IpucuKotasi() |
|
|
|
|
| def tabloya_cevir(sutunlar, satirlar): |
| """SQL sonucunu Gradio'nun gösterebileceği bir DataFrame'e çevirir.""" |
| if not sutunlar: |
| return pd.DataFrame() |
| return pd.DataFrame(satirlar, columns=sutunlar) |
|
|
|
|
| def ornek_sonucu(konu): |
| """Konudaki örnek sorguyu taze bir veritabanında çalıştırıp sonucunu verir.""" |
| conn = veritabani.yeni_baglanti() |
| try: |
| sutunlar, satirlar, hata = veritabani.calistir(conn, konu["ornek"]) |
| if hata: |
| return pd.DataFrame({"hata": [hata]}) |
| return tabloya_cevir(sutunlar, satirlar) |
| finally: |
| conn.close() |
|
|
|
|
| def ilerleme_metni(sira, durum): |
| """Sol paneldeki konu listesi.""" |
| tamamlanan = set(durum["tamamlanan"]) |
| satirlar = [f"### İlerleme: {len(tamamlanan)} / {mufredat.TOPLAM}", ""] |
|
|
| if not ilerleme.etkin(): |
| satirlar.append("> ⚠️ Kalıcı kayıt kapalı — ilerlemeniz saklanmıyor.\n") |
|
|
| for indeks, konu in enumerate(mufredat.KONULAR): |
| if konu["id"] in tamamlanan: |
| isaret = "✅" |
| elif indeks == sira: |
| isaret = "▶️" |
| else: |
| isaret = "⬜" |
| etiket = f"{indeks + 1}. {konu['baslik']}" |
| satirlar.append(f"{isaret} **{etiket}**" if indeks == sira |
| else f"{isaret} {etiket}") |
| return "\n\n".join(satirlar) |
|
|
|
|
| def konu_yukle(sira, durum): |
| """Bir konuyu ekrana basar. Çıktı sırası `konu_ciktilari` ile aynıdır.""" |
| konu = mufredat.konu_getir(sira) |
|
|
| rozet = "⭐ GERİYAYILIM GÖREVİ" if konu.get("geriyayilim") else f"Konu {sira + 1}" |
| baslik = f"## {konu['baslik']}\n\n`{rozet}`\n{konu['konu']}" |
|
|
| gorev = f"### 📝 Göreviniz\n\n{konu['gorev']}" |
| if konu.get("sirali"): |
| gorev += "\n\n> ℹ️ Bu görevde **satır sırası da** kontrol edilir." |
| if konu["id"] in set(durum["tamamlanan"]): |
| gorev += "\n\n> ✅ Bu görevi daha önce tamamladınız." |
|
|
| return ( |
| baslik, |
| konu["ornek"], |
| f"*{konu['ornek_aciklama']}*", |
| ornek_sonucu(konu), |
| gorev, |
| "", |
| pd.DataFrame(), |
| "", |
| ilerleme_metni(sira, durum), |
| ) |
|
|
|
|
| def oturum_baslat(request: gr.Request): |
| """Giriş sonrası: kaydedilmiş ilerlemeyi yükle, kalınan konuyu aç.""" |
| kullanici = request.username |
| durum, uyari = ilerleme.yukle(kullanici) |
| sira = ilerleme.ilk_tamamlanmamis(durum) |
|
|
| cikti = list(konu_yukle(sira, durum)) |
|
|
| tamam = len(durum["tamamlanan"]) |
| if uyari: |
| karsilama = f"⚠️ {uyari}" |
| elif tamam: |
| karsilama = ( |
| f"👋 Tekrar hoş geldiniz, **{kullanici}**. " |
| f"{tamam} konu tamamlamışsınız — kaldığınız yerden devam ediyorsunuz." |
| ) |
| else: |
| karsilama = f"👋 Hoş geldiniz, **{kullanici}**. İlk konudan başlıyoruz." |
|
|
| cikti[7] = karsilama |
| return (sira, durum, *cikti) |
|
|
|
|
| def sql_calistir(sql, conn): |
| """'Çalıştır' butonu — serbest deneme. Kotadan düşmez, doğruluk kontrol etmez.""" |
| sutunlar, satirlar, hata = veritabani.calistir(conn, sql) |
| if hata: |
| return pd.DataFrame(), ( |
| f"❌ **SQL hatası:** `{hata}`\n\n" |
| "*Ne demek olduğunu anlamadıysanız `💡 İpucu İste` butonunu kullanın.*" |
| ) |
|
|
| if not sutunlar: |
| return pd.DataFrame(), ( |
| "✔️ İfade çalıştı ama geriye tablo döndürmedi. " |
| "`CREATE`, `INSERT`, `UPDATE`, `DELETE` böyle davranır — " |
| "sonucu görmek için ayrıca bir `SELECT` yazın." |
| ) |
|
|
| return tabloya_cevir(sutunlar, satirlar), f"✔️ {len(satirlar)} satır döndü." |
|
|
|
|
| def gorev_kontrol(sql, sira, durum): |
| """'Kontrol Et' — deterministik değerlendirme. Doğruysa ilerleme kaydedilir.""" |
| konu = mufredat.konu_getir(sira) |
| ilerleme.sayaci_artir(durum, konu["id"], "deneme") |
|
|
| dogru, mesaj = degerlendirme.degerlendir(konu, sql) |
|
|
| if not dogru: |
| return f"### ❌ Henüz değil\n\n{mesaj}", durum, ilerleme_metni(sira, durum) |
|
|
| yeni_tamamlama = konu["id"] not in set(durum["tamamlanan"]) |
| if yeni_tamamlama: |
| durum["tamamlanan"] = sorted(set(durum["tamamlanan"]) | {konu["id"]}) |
|
|
| if sira + 1 < mufredat.TOPLAM: |
| mesaj += "\n\n**Sonraki konuya geçebilirsiniz.**" |
| else: |
| mesaj += "\n\n🏁 **Bütün konuları tamamladınız. Tebrikler!**" |
|
|
| kayit_uyarisi = ilerleme.kaydet(durum) if yeni_tamamlama else None |
| if kayit_uyarisi: |
| mesaj += f"\n\n{kayit_uyarisi}" |
|
|
| return f"## ✅ {mesaj}", durum, ilerleme_metni(sira, durum) |
|
|
|
|
| def ipucu_iste(sql, sira, durum_mesaji, durum, request: gr.Request): |
| """'İpucu İste' — tek yapay zekâ çağrısı, kotadan düşer.""" |
| izin, bilgi = kota.dusur(request.username) |
| if not izin: |
| yield f"⛔ {bilgi}", durum |
| return |
|
|
| konu = mufredat.konu_getir(sira) |
| ilerleme.sayaci_artir(durum, konu["id"], "ipucu") |
|
|
| istek = f"""Öğrencinin çalıştığı konu: {konu['baslik']} |
| |
| Konu anlatımı: |
| {konu['konu']} |
| |
| Öğrenciye verilen görev: |
| {konu['gorev']} |
| |
| Öğrencinin yazdığı SQL: |
| ```sql |
| {sql.strip() or '(henüz bir şey yazmadı)'} |
| ``` |
| |
| Sistemin verdiği geri bildirim: |
| {durum_mesaji or '(öğrenci henüz kontrol etmedi, doğrudan ipucu istedi)'} |
| |
| Bu öğrenciye, cevabı vermeden, bir sonraki adımı bulmasını sağlayacak bir ipucu ver.""" |
|
|
| yanit = "" |
| try: |
| akis = istemci.chat.completions.create( |
| model=MODEL, |
| messages=[ |
| {"role": "system", "content": OGRETMEN_TALIMATI}, |
| {"role": "user", "content": istek}, |
| ], |
| max_tokens=500, |
| stream=True, |
| ) |
| for parca in akis: |
| icerik = parca.choices[0].delta.content |
| if icerik: |
| yanit += icerik |
| yield f"### 💡 İpucu\n\n{yanit}", durum |
| except Exception as hata: |
| yield ( |
| f"⚠️ İpucu alınamadı: {hata}\n\nSQL çalıştırmaya devam edebilirsiniz.", |
| durum, |
| ) |
| return |
|
|
| yield ( |
| f"### 💡 İpucu\n\n{yanit}\n\n---\n*Bugün kalan ipucu hakkınız: {bilgi}*", |
| durum, |
| ) |
|
|
|
|
| def konu_degistir(yon, sira, durum): |
| """Konu değiştirmek aynı zamanda bir kayıt noktasıdır (ipucu sayaçları için).""" |
| yeni = max(0, min(mufredat.TOPLAM - 1, sira + yon)) |
| if yeni != sira: |
| ilerleme.kaydet(durum) |
| return (yeni, durum, *konu_yukle(yeni, durum)) |
|
|
|
|
| with gr.Blocks(title="VYS SQL Öğrenme") as demo: |
| sira_st = gr.State(0) |
| durum_st = gr.State(lambda: ilerleme.bos_durum("")) |
| conn_st = gr.State(veritabani.yeni_baglanti) |
|
|
| gr.Markdown( |
| "# 🗃️ SQL Öğreniyorum\n" |
| "Her konuda önce **örneği** inceleyin, sonra **görevi** kendiniz yazın. " |
| "`Çalıştır` ile istediğiniz kadar deneyebilirsiniz — sınırsız ve ücretsizdir." |
| ) |
|
|
| with gr.Row(): |
| with gr.Column(scale=1, min_width=240): |
| ilerleme_c = gr.Markdown() |
| with gr.Accordion("📚 Veritabanı şeması", open=False): |
| gr.Markdown(""" |
| **ogrenciler**: `id`, `ad`, `soyad`, `sinif`, `sehir`, `dogum_yili` |
| |
| **dersler**: `id`, `ad`, `kredi`, `ogretmen` |
| |
| **notlar**: `id`, `ogrenci_id`, `ders_id`, `puan` |
| |
| `notlar.ogrenci_id` → `ogrenciler.id` |
| `notlar.ders_id` → `dersler.id` |
| """) |
|
|
| with gr.Column(scale=3): |
| konu_c = gr.Markdown() |
|
|
| with gr.Accordion("👀 Örnek", open=True): |
| ornek_kod_c = gr.Code(language="sql", interactive=False, |
| label="Örnek sorgu") |
| ornek_aciklama_c = gr.Markdown() |
| ornek_sonuc_c = gr.Dataframe(label="Örneğin sonucu", wrap=True) |
|
|
| gorev_c = gr.Markdown() |
|
|
| editor_c = gr.Code(language="sql", label="SQL'inizi buraya yazın", |
| lines=6) |
|
|
| with gr.Row(): |
| calistir_b = gr.Button("▶️ Çalıştır", variant="secondary") |
| kontrol_b = gr.Button("✓ Kontrol Et", variant="primary") |
| ipucu_b = gr.Button("💡 İpucu İste") |
|
|
| durum_c = gr.Markdown() |
| sonuc_c = gr.Dataframe(label="Sorgunuzun sonucu", wrap=True) |
|
|
| with gr.Row(): |
| onceki_b = gr.Button("← Önceki konu") |
| sonraki_b = gr.Button("Sonraki konu →") |
|
|
| konu_ciktilari = [ |
| konu_c, ornek_kod_c, ornek_aciklama_c, ornek_sonuc_c, |
| gorev_c, editor_c, sonuc_c, durum_c, ilerleme_c, |
| ] |
|
|
| demo.load(oturum_baslat, None, [sira_st, durum_st, *konu_ciktilari]) |
|
|
| calistir_b.click(sql_calistir, [editor_c, conn_st], [sonuc_c, durum_c]) |
| kontrol_b.click(gorev_kontrol, [editor_c, sira_st, durum_st], |
| [durum_c, durum_st, ilerleme_c]) |
| ipucu_b.click(ipucu_iste, [editor_c, sira_st, durum_c, durum_st], |
| [durum_c, durum_st]) |
|
|
| onceki_b.click(lambda s, d: konu_degistir(-1, s, d), |
| [sira_st, durum_st], [sira_st, durum_st, *konu_ciktilari]) |
| sonraki_b.click(lambda s, d: konu_degistir(1, s, d), |
| [sira_st, durum_st], [sira_st, durum_st, *konu_ciktilari]) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch(auth=lambda k, p: KULLANICILAR.get(k) == p) |
|
|