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:
114
src/lib/danisman-profil.ts
Normal file
114
src/lib/danisman-profil.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Danışman profili — sohbetten çıkarılan öğrenci tercihleri. KAPALI LİSTE:
|
||||
* yalnız burada tanımlı alanlar yazılabilir, serbest metin alanı yoktur.
|
||||
* Neden: model sohbetten din, sağlık, aile gibi özel nitelikli veriyi profile
|
||||
* taşıyamasın (KVKK md. 6); öğrenci her alanı görür ve tek tıkla siler
|
||||
* (docs/arastirma/2026-09-25-kisisellestirme/README.md §8).
|
||||
*
|
||||
* Sorulmayanlar: cinsiyet, sağlık/engel, gelir. Ücret üst sınırı sorulur, gelir
|
||||
* sorulmaz. Konum il düzeyindedir (adres/koordinat değil).
|
||||
*
|
||||
* Saf modül: client, server ve scripts aynı dosyayı okur.
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
export const PROFIL_SECENEKLERI = {
|
||||
burs: {
|
||||
"yalniz-burslu": "yalnız burslu",
|
||||
yuzde50: "%50 burslu da olur",
|
||||
"ucretli-olur": "ücretli de olur",
|
||||
},
|
||||
dil: {
|
||||
turkce: "Türkçe",
|
||||
ingilizce: "İngilizce",
|
||||
farketmez: "fark etmez",
|
||||
},
|
||||
ogretimTuru: {
|
||||
"yalniz-orgun": "yalnız örgün",
|
||||
"uzaktan-olur": "uzaktan/açık da olur",
|
||||
},
|
||||
kampus: {
|
||||
sakin: "sakin, küçük",
|
||||
kalabalik: "kalabalık, büyük",
|
||||
farketmez: "fark etmez",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const PROFIL_SEMASI = z
|
||||
.object({
|
||||
/** Yaşadığı il — DB'deki il adı biçimi (büyük harf, Türkçe). */
|
||||
evIl: z.string().min(2).max(40).optional(),
|
||||
/** Evden en fazla kaç km (il merkezleri arası kuş uçuşu). */
|
||||
maxKm: z.number().int().min(50).max(1500).optional(),
|
||||
/** Vakıfta yıllık ücret üst sınırı (TL). */
|
||||
ucretUst: z.number().int().min(50_000).max(5_000_000).optional(),
|
||||
burs: z.enum(["yalniz-burslu", "yuzde50", "ucretli-olur"]).optional(),
|
||||
dil: z.enum(["turkce", "ingilizce", "farketmez"]).optional(),
|
||||
ogretimTuru: z.enum(["yalniz-orgun", "uzaktan-olur"]).optional(),
|
||||
kampus: z.enum(["sakin", "kalabalik", "farketmez"]).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type DanismanProfili = z.infer<typeof PROFIL_SEMASI>;
|
||||
export type ProfilAlani = keyof DanismanProfili;
|
||||
|
||||
export const PROFIL_ALANLARI = Object.keys(PROFIL_SEMASI.shape) as ProfilAlani[];
|
||||
|
||||
/** Alanın öğrenciye görünen kısa açıklaması (çip ve araç şeması aynı metni kullanır). */
|
||||
export const PROFIL_ALAN_ACIKLAMA: Record<ProfilAlani, string> = {
|
||||
evIl: "Yaşadığı il (evden uzaklık için)",
|
||||
maxKm: "Evden en fazla kaç km uzakta okumak istediği",
|
||||
ucretUst: "Vakıfta yıllık ücret üst sınırı (TL)",
|
||||
burs: "Vakıfta burs beklentisi",
|
||||
dil: "Öğretim dili tercihi",
|
||||
ogretimTuru: "Uzaktan/açık öğretim kabul mü",
|
||||
kampus: "Kampüs büyüklüğü tercihi",
|
||||
};
|
||||
|
||||
/** Bilinmeyen anahtarları atarak profili güvenli okur; bozuk değer alanı düşürür. */
|
||||
export function profilTemizle(ham: unknown): DanismanProfili {
|
||||
if (!ham || typeof ham !== "object") return {};
|
||||
const sonuc: Record<string, unknown> = {};
|
||||
for (const alan of PROFIL_ALANLARI) {
|
||||
const deger = (ham as Record<string, unknown>)[alan];
|
||||
if (deger === undefined || deger === null) continue;
|
||||
const tek = PROFIL_SEMASI.shape[alan].safeParse(deger);
|
||||
if (tek.success) sonuc[alan] = tek.data;
|
||||
}
|
||||
return sonuc as DanismanProfili;
|
||||
}
|
||||
|
||||
/** Tek alanı öğrencinin okuyacağı cümleye çevirir: "Yıllık ücret en fazla 300.000 TL". */
|
||||
export function profilAlaniMetni(alan: ProfilAlani, deger: unknown): string {
|
||||
switch (alan) {
|
||||
case "evIl":
|
||||
return `Yaşadığı il: ${ilBaslik(String(deger))}`;
|
||||
case "maxKm":
|
||||
return `Evden en fazla ${Number(deger).toLocaleString("tr-TR")} km`;
|
||||
case "ucretUst":
|
||||
return `Yıllık ücret en fazla ${Number(deger).toLocaleString("tr-TR")} TL`;
|
||||
case "burs":
|
||||
return `Vakıfta ${PROFIL_SECENEKLERI.burs[deger as keyof typeof PROFIL_SECENEKLERI.burs]}`;
|
||||
case "dil":
|
||||
return `Öğretim dili: ${PROFIL_SECENEKLERI.dil[deger as keyof typeof PROFIL_SECENEKLERI.dil]}`;
|
||||
case "ogretimTuru":
|
||||
return deger === "yalniz-orgun" ? "Yalnız örgün öğretim" : "Uzaktan/açık öğretim de olur";
|
||||
case "kampus":
|
||||
return `Kampüs: ${PROFIL_SECENEKLERI.kampus[deger as keyof typeof PROFIL_SECENEKLERI.kampus]}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Profilin tamamı, dolu alanlar sırayla. */
|
||||
export function profilSatirlari(p: DanismanProfili): { alan: ProfilAlani; metin: string }[] {
|
||||
return PROFIL_ALANLARI.flatMap((alan) =>
|
||||
p[alan] === undefined ? [] : [{ alan, metin: profilAlaniMetni(alan, p[alan]) }]
|
||||
);
|
||||
}
|
||||
|
||||
function ilBaslik(il: string): string {
|
||||
return il
|
||||
.toLocaleLowerCase("tr-TR")
|
||||
.split(" ")
|
||||
.map((k) => k.charAt(0).toLocaleUpperCase("tr-TR") + k.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
Reference in New Issue
Block a user