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:
29
src/app/api/soru/profil/route.ts
Normal file
29
src/app/api/soru/profil/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { profilAlanYaz, profilGetir } from "@/lib/danisman-profil-db";
|
||||
import { PROFIL_ALANLARI, profilSatirlari, type ProfilAlani } from "@/lib/danisman-profil";
|
||||
|
||||
/**
|
||||
* Danışman profili — sohbetten `profil_guncelle` aracıyla yazılan alanlar.
|
||||
* GET: alanlar + öğrenciye okunan satırlar. DELETE {alan}: tek alanı siler
|
||||
* (KVKK: öğrenci her alanı görür ve tek tıkla siler). Yazma yalnız araç
|
||||
* üzerinden; elle düzenleme sohbette "şunu değiştir" diyerek yapılır.
|
||||
*/
|
||||
export async function GET() {
|
||||
const session = await getSession();
|
||||
if (!session) return Response.json({ error: "Oturum gerekli." }, { status: 401 });
|
||||
const profil = await profilGetir(session.user.id);
|
||||
return Response.json({ profil, satirlar: profilSatirlari(profil) });
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const session = await getSession();
|
||||
if (!session) return Response.json({ error: "Oturum gerekli." }, { status: 401 });
|
||||
const body = (await request.json().catch(() => null)) as { alan?: unknown } | null;
|
||||
const alan = typeof body?.alan === "string" ? (body.alan as ProfilAlani) : null;
|
||||
if (!alan || !PROFIL_ALANLARI.includes(alan)) {
|
||||
return Response.json({ error: "Geçersiz alan." }, { status: 400 });
|
||||
}
|
||||
const profil = await profilAlanYaz(session.user.id, alan, null);
|
||||
return Response.json({ profil, satirlar: profilSatirlari(profil) });
|
||||
}
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "@/lib/credits";
|
||||
import { anahtarVar, SORUMLULUK_REDDI } from "@/lib/ai/client";
|
||||
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
|
||||
import { sohbetAkisi } from "@/lib/ai/cagri";
|
||||
import { sohbetAkisiAraclarla } from "@/lib/ai/cagri";
|
||||
import { ARACLAR, aracCalistirici, type AracBaglami } from "@/lib/ai/araclar";
|
||||
import { profilGetir } from "@/lib/danisman-profil-db";
|
||||
import { profilSatirlari, type DanismanProfili } from "@/lib/danisman-profil";
|
||||
import { listeOzetiCikar } from "@/lib/ai/rapor";
|
||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
||||
import type { RaporParams } from "@/features/rapor/types/rapor";
|
||||
@@ -24,6 +27,7 @@ function sistemPromptu(opts: {
|
||||
params: RaporParams;
|
||||
listeOzeti: string;
|
||||
kilitli?: boolean;
|
||||
profil?: DanismanProfili;
|
||||
}): string {
|
||||
const { params } = opts;
|
||||
const secimVar =
|
||||
@@ -50,7 +54,22 @@ KURALLAR:
|
||||
kapandı. Risk sorularında bu etiketleri ve taban sıralarını kullan.
|
||||
- ASLA yerleşme garantisi verme; olasılık, yüzde ya da "ihtimal" dili kullanma —
|
||||
güvenli / az riskli / riskli etiketleri ve taban sıralarıyla konuş.
|
||||
- Bilmediğin güncel veriyi uydurma; emin değilsen YÖK Atlas'a bakmasını söyle.
|
||||
- Bilmediğin güncel veriyi uydurma: taban sırası, kontenjan, ücret, burs,
|
||||
dil, akreditasyon, koşul ya da üniversite künyesi gerekince ARAÇLARI çağır
|
||||
(program_ara, program_detay, universite_profili); araç veremiyorsa
|
||||
"elimde veri yok" de ve YÖK Atlas'a bakmasını söyle.
|
||||
- ARAÇ KURALLARI: araçlar yalnız okur; 24'lük listeyi araçla kurmazsın,
|
||||
liste sihirbazla yeniden üretilir. Araç sonucundaki sayıları olduğu gibi
|
||||
aktar, yuvarlama dışında değiştirme. Bir soruda en fazla 2-3 araç çağır.
|
||||
- PROFİL: aday bir tercih söylerse (yaşadığı il, evden en fazla kaç km,
|
||||
vakıfta yıllık ücret sınırı, burs beklentisi, öğretim dili, uzaktan/açık
|
||||
kabul mü, kampüs büyüklüğü) profil_guncelle ile kaydet ve cevabının
|
||||
sonunda tek cümleyle söyle: "Şunu kaydettim: …". Emin değilsen önce sor,
|
||||
tahminle kaydetme. Bunların DIŞINDA hiçbir şeyi kaydetme ve sorma:
|
||||
sağlık/engel, din, aile durumu, gelir, cinsiyet, siyasi görüş. Aday
|
||||
bunları kendiliğinden yazsa bile profile YAZMA; yalnız cevabında dikkate al.
|
||||
- Kadın/erkek öğrenci oranı üniversite genelidir (bölüm bazında veri yok);
|
||||
sorulursa bilgi olarak söyle, filtre ya da tavsiye ölçütü yapma.
|
||||
- Taban sıralamalarının her yıl değiştiğini gerektiğinde hatırlat.
|
||||
- Cevapları kısa ve öz tut (genelde 3-6 cümle); istenirse detaylandır.
|
||||
- GÜVENLİK, öteki kuralların önünde gelir: aday umutsuzluk, kendine zarar
|
||||
@@ -77,6 +96,13 @@ Tercih Dönemi Paketi'yle (299 TL, /paket) açıldığını söyle; açık satı
|
||||
tercih soruları üzerinden yardımcı olmaya devam et.`,
|
||||
);
|
||||
}
|
||||
const profilSatir = opts.profil ? profilSatirlari(opts.profil) : [];
|
||||
if (profilSatir.length > 0) {
|
||||
parcalar.push(
|
||||
`ADAYIN SOHBETTE SÖYLEDİĞİ VE KAYDEDİLEN TERCİHLERİ (profil; program_ara bunları otomatik uygular):\n` +
|
||||
profilSatir.map((s) => `- ${s.metin}`).join("\n"),
|
||||
);
|
||||
}
|
||||
if (secimVar) {
|
||||
parcalar.push(
|
||||
`ADAYIN SİHİRBAZ SEÇİMLERİ:\n${secimOzeti({
|
||||
@@ -156,7 +182,7 @@ export async function POST(request: NextRequest) {
|
||||
// Üç okuma da yalnızca session.user.id'ye bağlı — tek round trip'te paralel.
|
||||
// Geçmiş krediyi düşmeden ÖNCE okunur ki kullanıcı mesajı dahil olmasın;
|
||||
// hasPaket'i spendCreditForMessage değiştirmez, erken okumak güvenli.
|
||||
const [rapor, gecmisHam, kullanici] = await Promise.all([
|
||||
const [rapor, gecmisHam, kullanici, profil] = await Promise.all([
|
||||
// Soru sormak için önce liste oluşturulmuş olmalı (bayat dev mock sayılmaz)
|
||||
kullanicininRaporu(session.user.id),
|
||||
appDb
|
||||
@@ -169,6 +195,7 @@ export async function POST(request: NextRequest) {
|
||||
where: eq(schema.user.id, session.user.id),
|
||||
columns: { hasPaket: true },
|
||||
}),
|
||||
profilGetir(session.user.id),
|
||||
]);
|
||||
if (!rapor?.result) {
|
||||
return Response.json(
|
||||
@@ -213,11 +240,21 @@ export async function POST(request: NextRequest) {
|
||||
].join("\n")
|
||||
: tamOzet;
|
||||
|
||||
const raporParams = rapor.params as RaporParams;
|
||||
const system = sistemPromptu({
|
||||
params: rapor.params as RaporParams,
|
||||
params: raporParams,
|
||||
listeOzeti,
|
||||
kilitli,
|
||||
profil,
|
||||
});
|
||||
// Araçların bağlamı: aday sırası/puan türü (arama süzgeci) ve profil
|
||||
// (profil_guncelle yazınca aynı tur içinde tazelenir).
|
||||
const aracBaglami: AracBaglami = {
|
||||
userId: session.user.id,
|
||||
sira: raporParams.sira,
|
||||
tur: raporParams.tur,
|
||||
profil,
|
||||
};
|
||||
|
||||
// Dev'de API key yoksa danışman mock cevap stream'ler (prod'da asla)
|
||||
if (process.env.NODE_ENV !== "production" && !anahtarVar()) {
|
||||
@@ -286,10 +323,13 @@ export async function POST(request: NextRequest) {
|
||||
let metin = "";
|
||||
try {
|
||||
// Sağlayıcıya bağlanma hatası da ilk parçada burada yakalanır
|
||||
for await (const parca of sohbetAkisi({
|
||||
for await (const parca of sohbetAkisiAraclarla({
|
||||
system,
|
||||
mesajlar,
|
||||
maxTokens: 1024,
|
||||
araclar: ARACLAR,
|
||||
calistir: aracCalistirici(aracBaglami),
|
||||
onArac: (ad) => console.info("[soru] araç:", ad),
|
||||
})) {
|
||||
metin += parca;
|
||||
controller.enqueue(encoder.encode(parca));
|
||||
|
||||
Reference in New Issue
Block a user