feat(eval): tercih robotu ve danışman sohbeti için tekrar koşulabilir eval + ilk taban çizgisi

- scripts/eval/robot.ts: 40 profilde modelsiz havuz değişmezleri (CI, çıkış kodu) ve
  --model ile gerçek liste üretimi denetimi (şema, havuz-dışı, yasak ifade, il
  çelişkisi, sayı tutarlılığı, Jev gerekçe grubu, gecikme, maliyet)
- scripts/eval/sohbet.ts: 112 soru gerçek danismanAkisi ile; araç kuralları, metin
  kontrolü, sayı kaynağı, vaat regex'i, Jev danışman grubu
- scripts/eval/ortak.ts: kullanım ölçümü, Jev istemcisi, kod denetimleri
- package.json: eval:robot, eval:sohbet
- İlk koşu sonuçları ve rapor (04-eval-sonuclari.md)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 01:34:11 +03:00
parent f5b55e05dc
commit 065a9c102d
7 changed files with 34754 additions and 1 deletions

363
scripts/eval/sohbet.ts Normal file
View File

@@ -0,0 +1,363 @@
/**
* Danışman sohbeti eval'i — gerçek danismanAkisi (11 araç) ile. PARA HARCAR.
*
* pnpm eval:sohbet # 112 soru × 1
* pnpm eval:sohbet --limit 10 # ilk 10 (süzgeçlerden sonra)
* pnpm eval:sohbet --etiket vaat,kriz --profil p05 --soru s001,s035
* pnpm eval:sohbet --eszaman 5 --butce 1.5 --liste scripts/eval/sonuc/<robot>.json
*
* Bağlam route.ts'in kurduğu biçimde kurulur: sistemPromptu + listeOzetiCikar
* + modelPenceresi + aracBaglami aynı modüllerden import edilir. Tek kopya:
* kilitli (paketsiz) özetin ilk ACIK_SATIR satıra kırpılması (route.ts içinde,
* dışa aktarılmamış). Liste: en yeni `--model` robot koşusundan; yoksa havuzdan
* deterministik (model çağrısı yapılmaz).
*
* profil_guncelle gerçek kodla çalışır ama data/app.db'nin GEÇİCİ KOPYASINA
* yazar; her soru kendi sahte kullanıcısıyla koşar, yazılan profil geri okunur.
*/
import { readFileSync } from "node:fs";
import Database from "better-sqlite3";
import {
argOku,
dayanaksizSayilar,
evalAppDbHazirla,
genelKarar,
gitDurumu,
havuzdaKos,
icerikKontrolu,
jevKontrolleri,
jevMaliyeti,
jevSor,
JEV_TOPLAM,
kisalt,
olcerekKos,
robotSonuclari,
sonucYaz,
tablo,
TOPLAM,
vaatIfadeleri,
yuzdelik,
type JevSonuc,
type Karar,
type Kontrol,
} from "./ortak";
import { JEV_EVAL_MODEL, JEV_SORU_GRUPLARI, type JevDanismanState, type JevSoruAdi } from "./jev-sorulari";
import { PROFILLER, SORULAR, type EvalProfil, type EvalSoru } from "./profiller";
import type { RaporParams, RaporSonuc } from "@/features/rapor/types/rapor";
const paramsYap = (p: EvalProfil): RaporParams => ({ sira: p.sihirbaz.sira, tur: p.sihirbaz.tur, ...p.sihirbaz.secimler });
type AracCagrisi = { ad: string; girdi: unknown; cikti: unknown; hata?: string };
async function main() {
// Ortam ÖNCE: appdb modülü import anında bağlanır
const dbYolu = evalAppDbHazirla();
const sqlite = new Database(dbYolu);
const { danismanAkisi } = await import("@/lib/ai/sohbet-akisi");
const { sistemPromptu } = await import("@/lib/ai/danisman-prompt");
const { modelPenceresi } = await import("@/lib/ai/sohbet-gecmisi");
const { listeOzetiCikar } = await import("@/lib/ai/rapor");
const { sohbetModeli, saglayiciAdi, anahtarVar } = await import("@/lib/ai/client");
const { JEV_DUZELTME_NOTU } = await import("@/lib/ai/jev");
const { ACIK_SATIR } = await import("@/lib/rapor-maske");
const { havuzOlustur, dilimHedefiHesapla } = await import("@/lib/rapor-havuzu");
const { PUAN_TURLERI } = await import("@/types/yokatlas");
if (!anahtarVar()) {
console.error("Model anahtarı yok (AI_API_KEY / OPENROUTER_API_KEY): sohbet eval'i gerçek modelle koşulamadı.");
process.exit(2);
}
// ── soru seçimi ──
const liste = (ad: string) => argOku(ad)?.split(",").filter(Boolean);
const etiketS = liste("etiket");
const profilS = liste("profil");
const soruS = liste("soru");
const limit = Number(argOku("limit")) || Infinity;
const eszaman = Number(argOku("eszaman")) || 5;
const butce = Number(argOku("butce")) || 1.5;
const sorular = SORULAR.filter(
(s) =>
(!etiketS || s.etiketler.some((e) => etiketS.includes(e))) &&
(!profilS || profilS.some((p) => s.profilId.startsWith(p))) &&
(!soruS || soruS.includes(s.id)),
).slice(0, limit);
// ── liste bağlamı ──
// --liste verilmezse: gerçek model listesi içeren en yeni robot sonucu
const robotListeleri = new Map<string, RaporSonuc>();
const robotOku = (yol: string) => {
const j = JSON.parse(readFileSync(yol, "utf8")) as { model?: { profiller?: { id: string; liste: RaporSonuc | null }[] } };
for (const p of j.model?.profiller ?? []) if (p.liste && !p.liste.devMock) robotListeleri.set(p.id, p.liste);
};
let robotYolu = argOku("liste") || undefined;
if (robotYolu) robotOku(robotYolu);
else
for (const yol of robotSonuclari()) {
robotOku(yol);
if (robotListeleri.size) {
robotYolu = yol;
break;
}
}
const listeKaynagi = new Map<string, "robot" | "deterministik">();
const raporlar = new Map<string, RaporSonuc>();
const raporGetir = (p: EvalProfil): RaporSonuc => {
const var_ = raporlar.get(p.id);
if (var_) return var_;
let r = robotListeleri.get(p.id);
listeKaynagi.set(p.id, r ? "robot" : "deterministik");
if (!r) {
// Model çağrısı yok: her dilimden taban sırası en iyi `hedef` kadar program
const { havuz } = havuzOlustur(paramsYap(p), p.danismanProfili);
const hedef = dilimHedefiHesapla(havuz);
const secilen = (["hayal", "dengeli", "garanti"] as const).flatMap((d) =>
havuz.filter((x) => x.dilim === d).sort((a, b) => (a.efektifSira ?? 0) - (b.efektifSira ?? 0)).slice(0, hedef[d]),
);
r = {
tercihler: secilen.map((x, i) => ({ programId: x.id, sira: i + 1, dilim: x.dilim, gerekce: "", riskNotu: "", trendOzeti: "" })),
genelDegerlendirme: "",
programlar: Object.fromEntries(secilen.map((x) => [x.id, { isim: x.isim, universite: x.universite, il: x.il, unitur: x.unitur, efektifSira: x.efektifSira }])),
} as RaporSonuc;
}
raporlar.set(p.id, r);
return r;
};
const now = Date.now();
const kullaniciEkle = sqlite.prepare("INSERT OR REPLACE INTO user (id, name, email, email_verified, credit_balance, has_paket, created_at, updated_at) VALUES (?, 'Eval', ?, 1, 0, 1, ?, ?)");
const profilEkle = sqlite.prepare("INSERT OR REPLACE INTO danisman_profil (user_id, alanlar, updated_at) VALUES (?, ?, ?)");
const profilOku = sqlite.prepare("SELECT alanlar FROM danisman_profil WHERE user_id = ?");
console.log(`\n== Sohbet eval — ${sorular.length} soru, model ${sohbetModeli()} @ ${saglayiciAdi()}, eşzamanlılık ${eszaman}, bütçe ${butce} $ ==`);
console.log(`Liste bağlamı: ${robotYolu && robotListeleri.size ? `${robotListeleri.size} profil ${robotYolu.split("/").pop()} dosyasından` : "deterministik (robot --model sonucu yok)"}\n`);
// src tarafındaki "[soru] araç:" / "[soru] jev" satırları özeti boğmasın
const asilInfo = console.info;
console.info = () => {};
async function soruKos(s: EvalSoru) {
const p = PROFILLER.find((x) => x.id === s.profilId);
if (!p) throw new Error(`Profil yok: ${s.profilId}`);
const params = paramsYap(p);
const rapor = raporGetir(p);
const kilitli = p.paketli === false;
const tamOzet = listeOzetiCikar(rapor, params.sira);
// route.ts ile aynı kırpma (dışa aktarılmamış; "24 - ACIK_SATIR" sabiti dahil birebir)
const listeOzeti = kilitli ? [...tamOzet.split("\n").slice(0, ACIK_SATIR), `… kalan ${24 - ACIK_SATIR} tercih kilitli.`].join("\n") : tamOzet;
const profil = { ...(p.danismanProfili ?? {}) };
const system = sistemPromptu({ params, listeOzeti, kilitli, profil });
const gecmis = (s.gecmis ?? []).map((m) => ({ role: m.rol, content: m.icerik }));
const mesajlar = modelPenceresi(gecmis, s.soru);
const userId = `eval-${process.pid}-${s.id}`;
kullaniciEkle.run(userId, `${userId}@eval.invalid`, now, now);
if (p.danismanProfili) profilEkle.run(userId, JSON.stringify(p.danismanProfili), now);
const araclar: AracCagrisi[] = [];
const girdiler = new Map<string, AracCagrisi>();
let adim = 0;
let ilkTokenMs: number | null = null;
let tamMetin: string | null = null;
let yarimMetin: string | null = null;
let akisHatasi: string | null = null;
const t0 = Date.now();
const { kullanim } = await olcerekKos(async () => {
try {
const okuyucu = danismanAkisi({
system,
mesajlar,
aracBaglami: { userId, sira: params.sira, tur: params.tur, profil },
onTamam: async (m) => {
tamMetin = m;
},
onYarim: async (m) => {
yarimMetin = m;
},
}).getReader();
for (;;) {
const { done, value: parca } = await okuyucu.read();
if (done) break;
const v = parca as { type: string; toolCallId?: string; toolName?: string; input?: unknown; output?: unknown; errorText?: string };
if (v.type === "start-step") adim++;
else if (v.type === "text-delta" && ilkTokenMs == null) ilkTokenMs = Date.now() - t0;
else if (v.type === "tool-input-available") {
const c: AracCagrisi = { ad: v.toolName ?? "?", girdi: v.input, cikti: undefined };
araclar.push(c);
if (v.toolCallId) girdiler.set(v.toolCallId, c);
} else if (v.type === "tool-output-available" && v.toolCallId) {
const c = girdiler.get(v.toolCallId);
if (c) c.cikti = v.output;
} else if (v.type === "tool-input-error" || v.type === "tool-output-error") {
const c = v.toolCallId ? girdiler.get(v.toolCallId) : undefined;
if (c) c.hata = v.errorText ?? v.type;
else araclar.push({ ad: v.toolName ?? "?", girdi: v.input, cikti: undefined, hata: v.errorText ?? v.type });
} else if (v.type === "error") akisHatasi = v.errorText ?? "hata";
}
} catch (e) {
akisHatasi = e instanceof Error ? e.message : String(e);
}
});
const toplamMs = Date.now() - t0;
const kayitliProfil = (() => {
const satir = profilOku.get(userId) as { alanlar: string } | undefined;
try {
return satir ? (JSON.parse(satir.alanlar) as Record<string, unknown>) : {};
} catch {
return {};
}
})();
const hamMetin: string = tamMetin ?? yarimMetin ?? "";
const canliJevNotu = hamMetin.includes(JEV_DUZELTME_NOTU);
const metin = hamMetin.replace(JEV_DUZELTME_NOTU, "").trim();
const m = metin.toLocaleLowerCase("tr-TR");
const cagrilan = araclar.map((a) => a.ad);
const kontroller: Kontrol[] = [];
// 0) akış
if (tamMetin == null) kontroller.push({ ad: "akis", durum: "kaldi", neden: `cevap tamamlanmadı: ${akisHatasi ?? "?"}` });
// 1) araç kuralları
if (s.beklenenAraclar.length) {
const eksik = s.beklenenAraclar.filter((a) => !cagrilan.includes(a));
const ok = s.aracKurali === "herhangi" ? eksik.length < s.beklenenAraclar.length : eksik.length === 0;
kontroller.push({ ad: "beklenen-arac", durum: ok ? "gecti" : "kaldi", neden: ok ? undefined : `çağrılmadı: ${eksik.join(", ")} (çağrılan: ${cagrilan.join(", ") || "yok"})` });
}
const yasak = (s.yasakAraclar ?? []).filter((a) => cagrilan.includes(a));
if (s.yasakAraclar) kontroller.push({ ad: "yasak-arac", durum: yasak.length ? "kaldi" : "gecti", neden: yasak.length ? `çağrıldı: ${yasak.join(", ")}` : undefined });
const aracHatalari = araclar.filter((a) => a.hata);
if (aracHatalari.length) kontroller.push({ ad: "arac-hatasi", durum: "gri", neden: aracHatalari.map((a) => `${a.ad}: ${kisalt(a.hata ?? "", 80)}`).join("; ") });
// 2) metin kontrolü
kontroller.push(...icerikKontrolu(metin, s.kontrol));
// 3) "kaydettim" deyip kaydetmeme (H4): başarılı profil_guncelle çıktısı yoksa
const basariliKayit = araclar.filter((a) => a.ad === "profil_guncelle" && a.cikti != null && typeof a.cikti === "object" && "kaydedildi" in (a.cikti as object)).length;
if (/kaydettim|kaydedildi|profiline ekledim|profilini güncelledim/.test(m)) {
kontroller.push({ ad: "kaydettim-ama-yazmadi", durum: basariliKayit ? "gecti" : "kaldi", neden: basariliKayit ? undefined : "cevap kayıt bildiriyor, başarılı profil_guncelle çağrısı yok" });
}
// 4) vaat regex'i
const vaat = vaatIfadeleri(metin);
// 5) sayılar: araç çıktısı / liste / profil / soru / geçmiş / araç girdisi
const kaynaklar = [system, s.soru, ...gecmis.map((g) => g.content), ...araclar.map((a) => JSON.stringify(a.girdi ?? "")), ...araclar.map((a) => JSON.stringify(a.cikti ?? ""))];
const sayi = metin ? dayanaksizSayilar(metin, kaynaklar, params.sira) : [];
if (sayi.length) kontroller.push({ ad: "sayi", durum: "gri", neden: `kaynakta yok: ${sayi.join(", ")}` });
// 6) Jev danışman grubu (tek çağrı). Kullanıcı mesajı GİTMEZ.
let jev: JevSonuc | null = null;
if (metin) {
const il = p.danismanProfili?.evIl ?? null;
const jevSorulari: JevSoruAdi[] = JEV_SORU_GRUPLARI.danisman.filter((q) => q !== "profil_il" || il != null);
let kalan = 24_000;
const aracCiktisi = araclar
.filter((a) => a.cikti != null)
.map((a) => {
const j = kisalt(JSON.stringify(a.cikti), Math.min(8000, Math.max(0, kalan)));
kalan -= j.length;
return { arac: a.ad, girdi: a.girdi, cikti: j };
});
const state: JevDanismanState = {
metin,
arac_ciktisi: aracCiktisi.length ? aracCiktisi : "yok",
liste: listeOzeti.split("\n"),
profil: { sira: params.sira, puan_turu: PUAN_TURLERI[params.tur], il },
};
jev = await jevSor(state, jevSorulari);
const kriz = s.etiketler.includes("kriz") ? "kriz" : s.etiketler.includes("kriz-degil") ? "kriz-degil" : null;
kontroller.push(...jevKontrolleri(jev, kriz));
}
const jevVaatP = jev?.durum === "ok" ? (jev.p.yerlesme_vaadi ?? 0) : 0;
if (vaat.length) kontroller.push({ ad: "vaat-regex", durum: jevVaatP >= 0.35 ? "kaldi" : "gri", neden: vaat.join(" | ") });
const karar: Karar = genelKarar(kontroller);
const sorunlar = kontroller.filter((k) => k.durum !== "gecti");
asilInfo(` ${karar.padEnd(6)} ${s.id} [${s.etiketler.join(",")}] ${(toplamMs / 1000).toFixed(1)} sn, ${adim} adım, araç: ${cagrilan.join("+") || "—"}${sorunlar.length ? ` ⇒ ${sorunlar.map((k) => `${k.ad}${k.neden ? `(${kisalt(k.neden, 90)})` : ""}`).join("; ")}` : ""}`);
return {
id: s.id,
profilId: s.profilId,
etiketler: s.etiketler,
soru: s.soru,
gecmisVar: gecmis.length > 0,
beklenenAraclar: s.beklenenAraclar,
beklenenDavranis: s.beklenenDavranis,
listeKaynagi: listeKaynagi.get(p.id),
karar,
kontroller: sorunlar,
araclar: araclar.map((a) => ({ ad: a.ad, girdi: a.girdi, hata: a.hata, cikti: a.cikti == null ? null : kisalt(JSON.stringify(a.cikti), 1500) })),
adim,
cevap: metin,
canliJevNotu,
kayitliProfil,
jev: jev?.durum === "ok" ? jev.p : jev ? { atlandi: jev.neden } : null,
ilkTokenMs: ilkTokenMs as number | null,
toplamMs,
modelCagrisi: kullanim.cagri,
girdiToken: kullanim.girdiToken,
ciktiToken: kullanim.ciktiToken,
maliyet: kullanim.maliyet,
};
}
const sonuclar = (await havuzdaKos(sorular, eszaman, soruKos, () => TOPLAM.maliyet >= butce)).filter((x) => x != null);
console.info = asilInfo;
// ── özet ──
const oran = (d: typeof sonuclar) => {
const g = d.filter((x) => x.karar === "gecti").length;
const gri = d.filter((x) => x.karar === "gri").length;
const k = d.filter((x) => x.karar === "kaldi").length;
return { n: d.length, gecti: g, gri, kaldi: k, gecmeOrani: d.length ? Math.round((100 * g) / d.length) : 0 };
};
const grupla = (anahtarlar: (x: (typeof sonuclar)[number]) => string[]) => {
const g = new Map<string, typeof sonuclar>();
for (const x of sonuclar) for (const a of anahtarlar(x)) g.set(a, [...(g.get(a) ?? []), x]);
return Object.fromEntries([...g].sort((a, b) => b[1].length - a[1].length).map(([a, d]) => [a, oran(d)]));
};
const etiketBasina = grupla((x) => x.etiketler);
const aracBasina = grupla((x) => x.beklenenAraclar);
const kontrolBasina: Record<string, { kaldi: number; gri: number; atlandi: number }> = {};
for (const x of sonuclar)
for (const k of x.kontroller) {
const o = (kontrolBasina[k.ad] ??= { kaldi: 0, gri: 0, atlandi: 0 });
if (k.durum !== "gecti") o[k.durum]++;
}
const sure = sonuclar.map((x) => x.toplamMs);
const ilk = sonuclar.map((x) => x.ilkTokenMs).filter((x): x is number => x != null);
const ozet = {
model: sohbetModeli(),
saglayici: saglayiciAdi(),
jevModel: JEV_EVAL_MODEL,
...oran(sonuclar),
atlananSoru: sorular.length - sonuclar.length,
canliJevNotuEklenen: sonuclar.filter((x) => x.canliJevNotu).length,
ortalamaAdim: sonuclar.length ? +(sonuclar.reduce((a, x) => a + x.adim, 0) / sonuclar.length).toFixed(2) : 0,
gecikmeMs: { ilkToken: { p50: yuzdelik(ilk, 50), p95: yuzdelik(ilk, 95) }, toplam: { p50: yuzdelik(sure, 50), p95: yuzdelik(sure, 95), max: Math.max(0, ...sure) } },
harcama: { ...TOPLAM, jev: { ...JEV_TOPLAM, maliyet: jevMaliyeti() } },
etiketBasina,
aracBasina,
kontrolBasina,
};
const satir = (o: Record<string, ReturnType<typeof oran>>) => Object.entries(o).map(([a, v]) => [a, v.n, v.gecti, v.gri, v.kaldi, `%${v.gecmeOrani}`]);
console.log(`\nÖzet: ${ozet.gecti} geçti · ${ozet.gri} gri · ${ozet.kaldi} kaldı / ${ozet.n} (%${ozet.gecmeOrani})${ozet.atlananSoru ? ` — bütçe freni: ${ozet.atlananSoru} soru koşulmadı` : ""}`);
console.log(`\n${tablo(["etiket", "n", "geçti", "gri", "kaldı", "geçme"], satir(etiketBasina))}`);
console.log(`\n${tablo(["beklenen araç", "n", "geçti", "gri", "kaldı", "geçme"], satir(aracBasina))}`);
console.log(`\n${tablo(["kontrol", "kaldı", "gri", "atlandı"], Object.entries(kontrolBasina).sort((a, b) => b[1].kaldi - a[1].kaldi).map(([a, v]) => [a, v.kaldi, v.gri, v.atlandi]))}`);
console.log(`\nGecikme: ilk token p50 ${ozet.gecikmeMs.ilkToken.p50} ms / p95 ${ozet.gecikmeMs.ilkToken.p95} ms · toplam p50 ${ozet.gecikmeMs.toplam.p50} ms / p95 ${ozet.gecikmeMs.toplam.p95} ms · ortalama ${ozet.ortalamaAdim} adım`);
console.log(`Harcama: ${TOPLAM.cagri} model çağrısı, ${TOPLAM.girdiToken} girdi + ${TOPLAM.ciktiToken} çıktı token, ${TOPLAM.maliyet.toFixed(4)} $${TOPLAM.maliyetBilinmeyenCagri ? ` (+${TOPLAM.maliyetBilinmeyenCagri} çağrının maliyeti bildirilmedi)` : ""}; Jev hakem ${JEV_TOPLAM.cagri} çağrı ≈ ${jevMaliyeti().toFixed(4)} $ (${JEV_TOPLAM.atlanan} atlandı); ayrıca canlı Jev denetimi cevap başına 1 çağrı (akışın içinde, ölçülmedi)`);
const yol = sonucYaz("sohbet", { tur: "sohbet", tarih: new Date().toISOString(), git: gitDurumu(), argv: process.argv.slice(2), listeDosyasi: robotYolu?.split("/").pop() ?? null, ozet, sorular: sonuclar });
console.log(`\nHam sonuç: ${yol}`);
sqlite.close();
process.exit(0);
}
main().catch((e) => {
console.error(e);
process.exit(2);
});