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

View File

@@ -0,0 +1,56 @@
import "server-only";
import { eq } from "drizzle-orm";
import { appDb, schema } from "@/lib/appdb";
import {
PROFIL_SEMASI,
profilTemizle,
type DanismanProfili,
type ProfilAlani,
} from "@/lib/danisman-profil";
/** Kullanıcının danışman profili; satır yoksa boş nesne. Bozuk alanlar düşer. */
export async function profilGetir(userId: string): Promise<DanismanProfili> {
const satir = await appDb.query.danismanProfil.findFirst({
where: eq(schema.danismanProfil.userId, userId),
columns: { alanlar: true },
});
return profilTemizle(satir?.alanlar);
}
/**
* Tek alanı yazar (deger null → alan silinir). Değer şemadan geçmezse hata
* fırlatır; çağıran taraf (araç) bunu modele "kaydedilmedi" diye döner.
*/
export async function profilAlanYaz(
userId: string,
alan: ProfilAlani,
deger: unknown,
): Promise<DanismanProfili> {
const mevcut = await profilGetir(userId);
const yeni: Record<string, unknown> = { ...mevcut };
if (deger === null || deger === undefined) {
delete yeni[alan];
} else {
const dogrulama = PROFIL_SEMASI.shape[alan].safeParse(deger);
if (!dogrulama.success) {
throw new Error(`Geçersiz değer: ${alan}`);
}
yeni[alan] = dogrulama.data;
}
const alanlar = PROFIL_SEMASI.parse(yeni);
const now = new Date();
await appDb
.insert(schema.danismanProfil)
.values({ userId, alanlar, updatedAt: now })
.onConflictDoUpdate({
target: schema.danismanProfil.userId,
set: { alanlar, updatedAt: now },
});
return alanlar;
}
export async function profilSil(userId: string): Promise<void> {
await appDb
.delete(schema.danismanProfil)
.where(eq(schema.danismanProfil.userId, userId));
}