feat(danisman): araç kullanımı — program_ara/program_detay/universite_profili sorgu araçları + kapalı listeli profil (profil_guncelle)

- src/lib/ai/araclar.ts: OpenAI uyumlu 4 araç; sorgu araçları yalnız okur
  (yokatlas.db + program_ozellik + YÖK İstatistik + URAP), sayı uydurma yok.
  profil_guncelle yalnız 7 tanımlı alanı yazar (evIl, maxKm, ucretUst, burs,
  dil, ogretimTuru, kampus); sağlık/din/gelir/cinsiyet şemada yok (KVKK §8).
  Profil süzgeci sonucu sıfırlarsa profilsiz tekrar denenip modele söylenir.
- src/lib/ai/cagri.ts: sohbetAkisiAraclarla — stream'de tool_calls biriktirme,
  en fazla 4 tur, son tur araç kapalı; sağlayıcı reddederse düz sohbete düşer.
- src/lib/danisman-profil.ts (saf şema + metinler), danisman-profil-db.ts,
  drizzle 0005 danisman_profil tablosu, /api/soru/profil GET+DELETE.
- /api/soru: profil prompt'a girer, araç kuralları eklendi.
- Sohbet başlığı altında 'Kaydettiklerim' çipleri (tek tıkla sil); listem
  sayfası profili sunucudan indirir. Analitiğe yalnız alan adı gider.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-25 01:13:58 +03:00
parent c23910cc79
commit aa2051732d
15 changed files with 1996 additions and 8 deletions

42
src/lib/mesafe.ts Normal file
View File

@@ -0,0 +1,42 @@
// İl merkezleri arası kuş uçuşu mesafe (km). Profildeki "evden en fazla N km"
// filtresi il düzeyinde çalışır: adres/koordinat tutulmaz (KVKK, §8).
import { IL_MERKEZ, type LatLng } from "./uni-koordinatlar";
const DUNYA_YARICAP_KM = 6371;
export function kmMesafe(a: LatLng, b: LatLng): number {
const rad = (d: number) => (d * Math.PI) / 180;
const dLat = rad(b[0] - a[0]);
const dLng = rad(b[1] - a[1]);
const h =
Math.sin(dLat / 2) ** 2 +
Math.cos(rad(a[0])) * Math.cos(rad(b[0])) * Math.sin(dLng / 2) ** 2;
return Math.round(2 * DUNYA_YARICAP_KM * Math.asin(Math.sqrt(h)));
}
/** DB il adı biçimine (büyük harf, Türkçe) çevirir; bilinmiyorsa null. */
export function ilAdiNormalize(ham: string): string | null {
const aday = ham.trim().toLocaleUpperCase("tr-TR");
if (aday in IL_MERKEZ) return aday;
// "Afyon" → AFYONKARAHİSAR gibi öneki eşleşen tek il varsa onu al
const adaylar = Object.keys(IL_MERKEZ).filter((il) => il.startsWith(aday));
return adaylar.length === 1 ? adaylar[0] : null;
}
/** Ev iline en fazla `km` uzaktaki iller (evin kendisi dahil), yakından uzağa. */
export function yakinIller(evIl: string, km: number): string[] {
const ev = IL_MERKEZ[evIl];
if (!ev) return [];
return Object.entries(IL_MERKEZ)
.map(([il, k]) => ({ il, km: kmMesafe(ev, k) }))
.filter((x) => x.km <= km)
.sort((a, b) => a.km - b.km)
.map((x) => x.il);
}
/** İki il arası km; biri bilinmiyorsa null. */
export function illerArasiKm(a: string, b: string): number | null {
const ka = IL_MERKEZ[a];
const kb = IL_MERKEZ[b];
return ka && kb ? kmMesafe(ka, kb) : null;
}