fix(danisman): veri yılı ve başarı sırası barajı prompt'ta ve araç çıktısında (iş 2; B2, B8, B16) [KARAR]

- Prompt: veri yılı veri-yillari.ts'ten (SON_YIL) türetilir; 'geçen yıl' kalktı, başka yıl uydurma yasağı.
- Prompt: baraj bölümü. Değerler program_ozellik.min_basari_sirasi'ndan (araclar.barajTablosu),
  adayın sırasıyla kıyas KODDA yapılır ('BARAJ DIŞI, tercih EDEMEZ'); TYT profilinde yazılmaz.
- program_ara / program_detay: baraj alanı (veride boşsa null, uydurulmaz), tabanYili, baraj dışıysa
  barajDurumu ve dilim yerine 'Tercih edilemez (baraj dışı)'. dilim süzgeci baraj dışını getirmez.
  program_detay'daki tercihBaraji alanının adı baraj oldu.
- program_ara: (KKTC Uyruklu) ve (M.T.O.K.) satırları varsayılan gizli (rapor-havuzu.ozelKontenjanSatiriMi);
  ozel_kontenjan=true ile açılır.
- Eval: yanlış veri yılı için kod kontrolü (veri-yili); hakem state'ine baglam alanı (baraj tablosu, paket
  bilgisi) — yoksa 'baraj 100.000' ve '299 TL' dayanaksız sayılıyordu; s103 araç şartı kalktı.
Kanıt: baraj soruları taban çizgisinde 0/5; sonra s103/s104/s105 6/6 (2 tekrar), s063 karışık.
Yanlış veri yılı: taban çizgisinde 9 cevap; aynı 8 soru × 2'de 0. Kilitli: s106 2/2.
Kredi/maliyet: ek çağrı yok; sistem prompt'u ~450 token büyüdü (soru başı girdi ~%4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 08:28:19 +03:00
parent 151e44780f
commit f5569394db
8 changed files with 2608 additions and 22 deletions

View File

@@ -11,7 +11,7 @@ import {
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
import { anahtarVar, SORUMLULUK_REDDI } from "@/lib/ai/client";
import { kullanicininRaporu, sohbetGecmisiGetir } from "@/features/rapor/rapor-queries";
import type { AracBaglami } from "@/lib/ai/araclar";
import { barajTablosu, type AracBaglami } from "@/lib/ai/araclar";
import { danismanAkisi } from "@/lib/ai/sohbet-akisi";
import { sistemPromptu, sohbetListeOzeti } from "@/lib/ai/danisman-prompt";
import { modelPenceresi, onayMesajiMi } from "@/lib/ai/sohbet-gecmisi";
@@ -151,6 +151,7 @@ export async function POST(request: NextRequest) {
listeOzeti,
kilitli,
profil,
barajlar: barajTablosu(),
});
// 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).

View File

@@ -49,7 +49,8 @@ import {
import { DILIM_ETIKET } from "@/lib/risk";
import { trendOzetiMetni } from "@/lib/trend";
import { TUR_SAYFALARI } from "@/lib/unitur";
import { TERCIH_YILI, YILLAR } from "@/lib/veri-yillari";
import { ONCEKI_YIL, SON_YIL, TERCIH_YILI, YILLAR } from "@/lib/veri-yillari";
import { ozelKontenjanSatiriMi } from "@/lib/rapor-havuzu";
import { PUAN_TURLERI, type Program, type ProgramNetSatiri, type PuanTuruKey } from "@/types/yokatlas";
import {
PROFIL_ALAN_ACIKLAMA,
@@ -82,7 +83,7 @@ export const ARACLAR: AracTanimi[] = [
function: {
name: "program_ara",
description:
"Adayın puan türündeki programları veriden arar (YÖK Atlas, son yerleştirme). Bölüm adı, üniversite, il, devlet/vakıf, öğretim dili ve türü, burs ve yıllık ücret üst sınırına göre süzer; varsayılan olarak adayın sırasına en yakın programları döner. Sayı uydurmak yerine bunu çağır.",
`Adayın puan türündeki programları veriden arar (YÖK Atlas, ${SON_YIL} yerleştirmesi). Bölüm adı, üniversite, il, devlet/vakıf, öğretim dili ve türü, burs ve yıllık ücret üst sınırına göre süzer; varsayılan olarak adayın sırasına en yakın programları döner. Her sonuçta taban sırası, dilim ve varsa başarı sırası barajı (baraj) gelir; aday barajın dışındaysa dilim yerine "Tercih edilemez (baraj dışı)" yazar. 'X yazabilir miyim', 'tabanı kaç', 'hangi programlar var' sorularında sayı uydurmak yerine bunu çağır.`,
parameters: {
type: "object",
properties: {
@@ -107,6 +108,11 @@ export const ARACLAR: AracTanimi[] = [
enum: ["hayal", "dengeli", "guvenli"],
description: "Yalnız bu dilimdekiler: hayal = adayın sırasının üstünde kapananlar, dengeli = sırasının 1,4 katına kadar, guvenli = daha altı",
},
ozel_kontenjan: {
type: "boolean",
description:
"true = yalnız belirli adaylara ayrılmış kontenjan satırlarını da getir: '(KKTC Uyruklu)' ve '(M.T.O.K.)' (meslek lisesi mezunu). Varsayılan: gizli. Yalnız aday KKTC uyruklu ya da meslek lisesi mezunu olduğunu söylediyse kullan.",
},
sirala: {
type: "string",
enum: ["yakinlik", "taban", "ucret"],
@@ -123,7 +129,7 @@ export const ARACLAR: AracTanimi[] = [
function: {
name: "program_detay",
description:
"Tek programın ayrıntısı: yıllara göre taban sırası ve kontenjan, ücret/burs, dil, öğretim türü, akreditasyon, kadro, tercih barajı ve kılavuzdaki yerleşme koşulları (sağlık şartı, mülakat vb.).",
"Tek programın ayrıntısı: yıllara göre taban sırası ve kontenjan, ücret/burs, dil, öğretim türü, akreditasyon, kadro, başarı sırası barajı (baraj; aday dışındaysa barajDurumu 'tercih edilemez' der) ve kılavuzdaki yerleşme koşulları (sağlık şartı, mülakat vb.).",
parameters: {
type: "object",
properties: {
@@ -297,7 +303,16 @@ export const ARACLAR: AracTanimi[] = [
// ── Yardımcılar ──────────────────────────────────────────────────────────
function dilimAdi(sira: number, efektif: number | null): string {
/** Adayın sırası programın başarı sırası barajını geçemiyor mu (baraj boşsa: hayır). */
function barajDisi(sira: number, baraj: number | null | undefined): baraj is number {
return baraj != null && sira > baraj;
}
const BARAJ_DISI_ETIKET = "Tercih edilemez (baraj dışı)";
function dilimAdi(sira: number, efektif: number | null, baraj?: number | null): string {
// Baraj dışı programa Hayal/Dengeli denmez: aday o programı yazamaz (B2)
if (barajDisi(sira, baraj)) return BARAJ_DISI_ETIKET;
if (efektif == null) return "Veri yetersiz";
if (efektif < sira) return DILIM_ETIKET.hayal;
if (efektif <= dilimAraligi(sira, "dengeli").ust!) return DILIM_ETIKET.dengeli;
@@ -305,6 +320,8 @@ function dilimAdi(sira: number, efektif: number | null): string {
}
function programOzeti(p: Program, sira: number, o?: ProgramOzellik) {
// Baraj yalnız veride varsa yazılır; boş = barajsız ya da veride eksik (uydurulmaz)
const baraj = o?.minBasariSirasi ?? null;
return {
id: p.id,
program: p.isim,
@@ -312,7 +329,15 @@ function programOzeti(p: Program, sira: number, o?: ProgramOzellik) {
il: p.il,
tur: p.unitur,
tabanSira: p.efektifSira,
dilim: dilimAdi(sira, p.efektifSira),
// Efektif sıra son yılda boşsa bir önceki yıldan gelir (veri-yillari.ts)
tabanYili: p.efektifSira == null ? null : p.siraGecmisi.at(-1) != null ? SON_YIL : ONCEKI_YIL,
dilim: dilimAdi(sira, p.efektifSira, baraj),
baraj,
...(barajDisi(sira, baraj)
? {
barajDurumu: `BARAJ DIŞI — TERCİH EDİLEMEZ: bu programı yazabilmek için başarı sırası en fazla ${baraj.toLocaleString("tr-TR")} olmalı; adayın sırası ${sira.toLocaleString("tr-TR")}.`,
}
: {}),
kontenjan: p.kontenjanSon,
ozellik: o ? ozellikOzeti(o) : [],
};
@@ -337,7 +362,7 @@ const ayrikDegerler = new Map<string, { ham: string; katli: string }[]>();
* harf katlamayı bilmez ("isletme" İşletme'yi, "bogazici" Boğaziçi'ni bulmaz);
* ayrık değerler (birkaç bin) bir kez okunur, eşleşme JS'te yapılır.
*/
function katliEslesenler(kolon: "isim" | "universite", katliSorgu: string): string[] {
function ayrikListe(kolon: "isim" | "universite"): { ham: string; katli: string }[] {
let liste = ayrikDegerler.get(kolon);
if (!liste) {
liste = (getDb().prepare(`SELECT DISTINCT ${kolon} AS v FROM programs`).all() as { v: string | null }[])
@@ -345,7 +370,62 @@ function katliEslesenler(kolon: "isim" | "universite", katliSorgu: string): stri
.map((x) => ({ ham: x.v, katli: turkishSlugify(x.v) }));
ayrikDegerler.set(kolon, liste);
}
return liste.filter((x) => x.katli.includes(katliSorgu)).map((x) => x.ham);
return liste;
}
function katliEslesenler(kolon: "isim" | "universite", katliSorgu: string): string[] {
return ayrikListe(kolon).filter((x) => x.katli.includes(katliSorgu)).map((x) => x.ham);
}
/** Özel kontenjan satırlarının (KKTC Uyruklu, M.T.O.K.) ayrık program adları. */
function ozelKontenjanAdlari(): string[] {
return ayrikListe("isim").map((x) => x.ham).filter(ozelKontenjanSatiriMi);
}
type BarajSatiri = { sira: number; bolumler: string };
let barajOnbellek: BarajSatiri[] | null = null;
/**
* Verideki başarı sırası barajları ve hangi bölümlerde görüldükleri (sistem
* prompt'u için; danisman-prompt.ts adayın sırasıyla kıyaslar). Sabit tablo
* DEĞİL: program_ozellik.min_basari_sirasi'ndan türetilir, kılavuz değişip
* veri yenilenince kendiliğinden güncellenir. Az bölümlü barajda bölüm adları,
* çok bölümlüde ortak son sözcük ("… Mühendisliği") + iki örnek yazılır.
*/
export function barajTablosu(): BarajSatiri[] {
if (barajOnbellek) return barajOnbellek;
const satirlar = getDb()
.prepare(
`SELECT o.min_basari_sirasi AS baraj, p.isim AS isim, COUNT(*) AS n
FROM program_ozellik o JOIN programs p ON p.id = o.id
WHERE o.min_basari_sirasi IS NOT NULL GROUP BY 1, 2`,
)
.all() as { baraj: number; isim: string; n: number }[];
const gruplar = new Map<number, Map<string, number>>();
for (const r of satirlar) {
const ad = r.isim.replace(/\s*\([^)]*\)/g, "").trim();
const g = gruplar.get(r.baraj) ?? new Map<string, number>();
g.set(ad, (g.get(ad) ?? 0) + r.n);
gruplar.set(r.baraj, g);
}
barajOnbellek = [...gruplar]
.sort((x, y) => x[0] - y[0])
.map(([sira, g]) => {
const adlar = [...g].sort((x, y) => y[1] - x[1]);
const toplam = adlar.reduce((t, [, n]) => t + n, 0);
if (adlar.length <= 3) return { sira, bolumler: adlar.map(([ad]) => ad).join(", ") };
const sonSozcuk = new Map<string, number>();
for (const [ad, n] of adlar) {
const k = ad.split(" ").at(-1) ?? ad;
sonSozcuk.set(k, (sonSozcuk.get(k) ?? 0) + n);
}
const ekler = [...sonSozcuk].filter(([, n]) => n / toplam >= 0.1).sort((x, y) => y[1] - x[1]).map(([k]) => `"… ${k}"`);
return {
sira,
bolumler: `adı ${ekler.join(" ya da ")} ile biten bölümlerin çoğu (örn. ${adlar.slice(0, 2).map(([ad]) => ad).join(", ")}; ${adlar.length} ayrı bölüm adı — hepsi değil, programın "baraj" alanına bak)`,
};
});
return barajOnbellek;
}
function inKosulu(kolon: string, degerler: string[]): string {
@@ -364,6 +444,7 @@ type ProgramAraArg = {
burs?: string;
ucret_ust?: number;
dilim?: "hayal" | "dengeli" | "guvenli";
ozel_kontenjan?: boolean;
sirala?: "yakinlik" | "taban" | "ucret";
};
@@ -396,6 +477,17 @@ function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): Prog
args.push(...adlar);
uygulanan.push(`üniversite ~ "${a.universite}"`);
}
// Yalnız belirli adaylara ayrılmış kontenjan satırları varsayılan gizli (B8);
// kural tek yerde: rapor-havuzu.ozelKontenjanSatiriMi
if (!a.ozel_kontenjan) {
const ozel = ozelKontenjanAdlari();
if (ozel.length) {
kosullar.push(`isim NOT IN (${ozel.map(() => "?").join(",")})`);
args.push(...ozel);
}
} else {
uygulanan.push("özel kontenjan satırları (KKTC Uyruklu, M.T.O.K.) dahil");
}
// İl: parametre > profildeki km sınırı
if (a.il) {
const il = ilAdiNormalize(a.il);
@@ -459,6 +551,9 @@ function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): Prog
args.push(alt);
if (ust != null) args.push(ust);
uygulanan.push(`dilim = ${DILIM_ETIKET[anahtar]}`);
// Baraj dışı program hiçbir dilimde sayılmaz
kosullar.push("id NOT IN (SELECT id FROM program_ozellik WHERE min_basari_sirasi < ?)");
args.push(b.sira);
}
// İkincil anahtar hep sıraya yakınlık: eşit ücretlilerde (ör. tüm devletler) anlamlı kalsın
@@ -493,12 +588,18 @@ function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): Prog
};
}
const ozellikler = programOzellikleriGetir(programlar.map((p) => p.id));
const sonuclar = programlar.map((p) => programOzeti(p, b.sira, ozellikler[p.id]));
const barajDisiSayisi = sonuclar.filter((x) => "barajDurumu" in x).length;
return {
toplamEslesen: toplam,
gosterilen: programlar.length,
uygulananFiltreler: uygulanan,
sonuclar: programlar.map((p) => programOzeti(p, b.sira, ozellikler[p.id])),
not: "tabanSira son yerleştirme yılının taban başarı sırası; her yıl değişir.",
sonuclar,
not:
`tabanSira ${SON_YIL} yerleştirmesinin taban başarı sırasıdır (tabanYili ${ONCEKI_YIL} ise o program ${SON_YIL}'da dolmadı); her yıl değişir.` +
(barajDisiSayisi
? ` ${barajDisiSayisi} sonuç BARAJ DIŞI: aday bu programları tercih edemez; öneri olarak sunma, "Hayal olarak yaz" deme, barajı açıkça söyle.`
: ""),
};
}
@@ -533,7 +634,6 @@ function programDetay(b: AracBaglami, a: { program_id?: string }) {
kadro: o
? { prof: o.prof, docent: o.doc, drOgrUyesi: o.dou, arsGor: o.argor, ogretimUyesiToplam: ogretimUyesiToplami(o) }
: null,
tercihBaraji: o?.minBasariSirasi ?? null,
ilce: o?.ilce ?? null,
evdenKm: evKm,
kosullar: kosullar.map((k) => (k.metin.length > 240 ? `${k.metin.slice(0, 237)}…` : k.metin)),
@@ -841,6 +941,7 @@ const TIP_ADI: Record<string, string> = {
integer: "tam sayı",
number: "sayı",
array: "liste",
boolean: "true/false",
null: "null",
};
@@ -854,6 +955,8 @@ function tipUyuyor(tip: string, v: unknown): boolean {
return typeof v === "number" && Number.isFinite(v);
case "array":
return Array.isArray(v);
case "boolean":
return typeof v === "boolean";
case "null":
return v === null;
default:

View File

@@ -11,6 +11,7 @@ import type { RaporParams } from "@/features/rapor/types/rapor";
import { secimOzeti } from "@/features/sihirbaz/sihirbaz-sabitler";
import { PUAN_TURLERI } from "@/types/yokatlas";
import { ACIK_SATIR } from "@/lib/rapor-maske";
import { ONCEKI_YIL, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
/**
* Profil kaydı öncesi "kaydedeyim mi?" onayı (KVKK Üretken YZ Rehberi:
@@ -33,11 +34,39 @@ export function sohbetListeOzeti(tamOzet: string, kilitli: boolean): string {
].join("\n");
}
/**
* Başarı sırası barajı bölümü. Kıyas (adayın sırası barajın içinde mi) kodda
* yapılır; model yalnız aktarır. Baraj değerleri veriden gelir (sabit yazılmaz).
*/
function barajBolumu(sira: number, barajlar?: { sira: number; bolumler: string }[]): string | null {
if (!barajlar?.length) return null;
const sayi = (n: number) => n.toLocaleString("tr-TR");
const disinda = barajlar.filter((b) => sira > b.sira);
const satirlar = barajlar.map(
(b) => `- ${sayi(b.sira)}: ${b.bolumler} → aday ${sira > b.sira ? "BARAJ DIŞI, tercih EDEMEZ" : "barajın içinde, tercih edebilir"}`,
);
return `BAŞARI SIRASI BARAJI (ÖSYM kılavuz kuralı; değerler veriden): bazı bölümler
yalnız başarı sırası belli bir sıradan İYİ olan adaylarca tercih edilebilir.
Adayın sırası ${sayi(sira)}:
${satirlar.join("\n")}
${
disinda.length
? `Aday yukarıda "BARAJ DIŞI" yazan bölümleri TERCİH EDEMEZ; bu bir risk ya da olasılık meselesi değil, kuraldır. Bu bölümler sorulursa barajı sayısıyla söyle ("… için başarı sırası barajı ${sayi(disinda.at(-1)!.sira)}; senin sıran ${sayi(sira)}, bu yüzden tercih edemezsin"), "Hayal olarak yaz" deme ve bu bölümler için taban sırası kıyasına girme. Listede bu bölümlerin olmaması da bu yüzdendir.`
: "Aday bu barajların hepsinin içinde."
}
Barajın içinde olmak yerleşmek demek değildir. Tek bir programın barajı
program_ara / program_detay çıktısındaki "baraj" alanındadır; alan boşsa o
programda baraj yok ya da veride yok — baraj UYDURMA. Ön lisans (TYT)
programlarında başarı sırası barajı yoktur.`;
}
export function sistemPromptu(opts: {
params: RaporParams;
listeOzeti: string;
kilitli?: boolean;
profil?: DanismanProfili;
/** Verideki başarı sırası barajları (araclar.barajTablosu); verilmezse baraj bölümü yazılmaz. */
barajlar?: { sira: number; bolumler: string }[];
}): string {
const { params } = opts;
const secimVar =
@@ -60,8 +89,14 @@ KURALLAR:
(aday ya da eski mesajlar böyle dese bile sen "güvenli" diye düzelt).
- Listedeki her satırda dilim ve risk etiketi var: Güvenli (yeşil) = taban
sıralaması adayın belirgin altında, Az riskli (sarı) = adayın sıralaması
civarında, Riskli (kırmızı) = geçen yıl adayın sıralamasının üzerinde
kapandı. Risk sorularında bu etiketleri ve taban sıralarını kullan.
civarında, Riskli (kırmızı) = son yerleştirmede adayın sıralamasının
üzerinde kapandı. Risk sorularında bu etiketleri ve taban sıralarını kullan.
- VERİ YILI: listedeki ve araçlardaki taban sıraları ile kontenjanlar
${SON_YIL} yerleştirmesine aittir (${SON_YIL}'da dolmayan az sayıda programda
${ONCEKI_YIL}; araç bunu "tabanYili" alanında söyler). Yıl söylemen
gerekirse "${SON_YIL} yerleştirmesi" de; "${SON_YIL - 2} verisi", "${ONCEKI_YIL} verisi" gibi
başka bir yıl UYDURMA. Net, TÜİK ve URAP verisinin yılı araç çıktısında
yazar; onu aynen aktar. ${TERCIH_YILI} taban sıraları henüz yok, tahmin etme.
- OLASILIK VE GARANTİ YOK: yerleşme garantisi verme; yerleşmeyi olasılık,
yüzde, ihtimal ya da şans diliyle TAHMİN ETME. İki yönde de yasak: "yerleşme
olasılığın yüksek" de, "olasılığın düşük" de söylenmez. KolayTercih olasılık
@@ -125,8 +160,11 @@ KURALLAR:
konusuna dön (güvenlik kuralı bunun istisnasıdır).
- Kesin konuşmaktan kaçın: "${SORUMLULUK_REDDI}"`,
`ADAY BİLGİSİ: başarı sıralaması ${params.sira.toLocaleString("tr-TR")}, puan türü ${PUAN_TURLERI[params.tur]}.`,
`ADAYIN 24'LÜK LİSTESİ:\n${opts.listeOzeti}`,
`ADAYIN 24'LÜK LİSTESİ (taban sıraları ${SON_YIL} yerleştirmesinden):\n${opts.listeOzeti}`,
];
// TYT sırası lisans barajlarıyla kıyaslanmaz; ön lisansta baraj yok
const baraj = params.tur === "tyt" ? null : barajBolumu(params.sira, opts.barajlar);
if (baraj) parcalar.splice(2, 0, baraj);
if (opts.kilitli) {
parcalar.push(
`ÖNEMLİ: Adayın paketi yok; listenin yalnızca ilk ${ACIK_SATIR} tercihi açık.