Files
kolaytercih/scripts/eval/sohbet.ts
bilalgursen 065a9c102d 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>
2026-10-05 01:34:11 +03:00

364 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});