- 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>
415 lines
22 KiB
TypeScript
415 lines
22 KiB
TypeScript
/**
|
||
* 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
|
||
* pnpm eval:sohbet --etiket vaat,baraj --tekrar 3 # her soru 3 kez (model stokastik)
|
||
*
|
||
* Bağlam route.ts'in kurduğu biçimde kurulur: sistemPromptu + listeOzetiCikar
|
||
* + sohbetListeOzeti + modelPenceresi + aracBaglami aynı modüllerden import
|
||
* edilir; kopya yok. 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_DANISMAN_ESIKLERI, 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 };
|
||
|
||
/** danisman-prompt.ts'in kilitli kullanıcıya eklediği ürün bilgisinin hakem bağlamındaki karşılığı. */
|
||
const KILITLI_URUN_BILGISI = "Ürün bilgisi: adayın paketi yok; yalnız ilk tercihler açık, tam liste Tercih Dönemi Paketi ile (299 TL, /paket) açılır.";
|
||
|
||
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, sohbetListeOzeti } = await import("@/lib/ai/danisman-prompt");
|
||
const { modelPenceresi } = await import("@/lib/ai/sohbet-gecmisi");
|
||
const { barajTablosu } = await import("@/lib/ai/araclar");
|
||
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 { vaatDiliBul } = await import("@/lib/ai/cikti-denetimi");
|
||
const { havuzOlustur, dilimHedefiHesapla } = await import("@/lib/rapor-havuzu");
|
||
const { PUAN_TURLERI } = await import("@/types/yokatlas");
|
||
const { SON_YIL } = await import("@/lib/veri-yillari");
|
||
|
||
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 tekrar = Math.max(1, Math.floor(Number(argOku("tekrar")) || 1));
|
||
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 barajBaglami = `Kural bilgisi (ÖSYM başarı sırası barajları; aday sırası barajdan kötüyse o bölüm tercih edilemez): ${barajTablosu().map((b) => `${b.sira.toLocaleString("tr-TR")} = ${b.bolumler}`).join("; ")}`;
|
||
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${tekrar > 1 ? ` × ${tekrar} tekrar` : ""}, 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, tekrarNo }: { s: EvalSoru; tekrarNo: number }) {
|
||
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);
|
||
const listeOzeti = sohbetListeOzeti(tamOzet, kilitli);
|
||
const profil = { ...(p.danismanProfili ?? {}) };
|
||
const system = sistemPromptu({ params, listeOzeti, kilitli, profil, barajlar: barajTablosu() });
|
||
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}-${tekrarNo}`;
|
||
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;
|
||
let canliDenetim: { jevP: number | null; vaatRegex: string[]; vaatNotu: boolean } | 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;
|
||
},
|
||
onDenetim: (d) => {
|
||
canliDenetim = d;
|
||
},
|
||
}).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(", ")}` });
|
||
|
||
// 5b) veri yılı: "2024 verisi" gibi, ne SON_YIL ne de araç çıktısında / soruda geçen bir yıl
|
||
const yanlisYil = [...new Set([...metin.matchAll(/(?<!\d)(20[12]\d)(?:['’]\p{L}+)?\s+(?:yılı\s+)?(?:yerleştirme|veri|taban|yks sonuç)/giu)].map((e) => e[1]))].filter(
|
||
(y) => Number(y) !== SON_YIL && !kaynaklar.slice(1).some((k) => k.includes(y)),
|
||
);
|
||
if (yanlisYil.length) kontroller.push({ ad: "veri-yili", durum: "kaldi", neden: `veri yılı ${SON_YIL}; cevapta: ${yanlisYil.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"),
|
||
// Prompt'taki öteki meşru dayanaklar: kilitli kullanıcıya paket bilgisi, baraj tablosu
|
||
baglam: [...(kilitli ? [KILITLI_URUN_BILGISI] : []), ...(params.tur === "tyt" ? [] : [barajBaglami])],
|
||
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, JEV_DANISMAN_ESIKLERI));
|
||
}
|
||
const jevVaatP = jev?.durum === "ok" ? (jev.p.yerlesme_vaadi ?? 0) : 0;
|
||
// Hakem regex'i tek başına "gri"; Jev inceleme eşiğini geçtiyse ya da üretimdeki regex de yakaladıysa "kaldı"
|
||
const uretimVaat = vaatDiliBul(metin);
|
||
if (vaat.length || uretimVaat.length) {
|
||
const kesin = uretimVaat.length > 0 || jevVaatP >= (JEV_DANISMAN_ESIKLERI.yerlesme_vaadi?.inceleme ?? 0.35);
|
||
kontroller.push({ ad: "vaat-regex", durum: kesin ? "kaldi" : "gri", neden: (uretimVaat.length ? uretimVaat : vaat).join(" | ") });
|
||
}
|
||
|
||
const karar: Karar = genelKarar(kontroller);
|
||
const sorunlar = kontroller.filter((k) => k.durum !== "gecti");
|
||
asilInfo(` ${karar.padEnd(6)} ${s.id}${tekrar > 1 ? `#${tekrarNo + 1}` : ""} [${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,
|
||
tekrarNo,
|
||
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,
|
||
canliDenetim: canliDenetim as { jevP: number | null; vaatRegex: string[]; vaatNotu: boolean } | null,
|
||
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 isler = sorular.flatMap((s) => Array.from({ length: tekrar }, (_, tekrarNo) => ({ s, tekrarNo })));
|
||
const sonuclar = (await havuzdaKos(isler, 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]++;
|
||
}
|
||
// Tekrar tutarlılığı: soru başına karar dağılımı (model stokastik)
|
||
const soruBasina = new Map<string, { etiketler: string[]; gecti: number; gri: number; kaldi: number }>();
|
||
for (const x of sonuclar) {
|
||
const o = soruBasina.get(x.id) ?? { etiketler: x.etiketler, gecti: 0, gri: 0, kaldi: 0 };
|
||
if (x.karar === "gecti" || x.karar === "gri" || x.karar === "kaldi") o[x.karar]++;
|
||
soruBasina.set(x.id, o);
|
||
}
|
||
const tutarlilik =
|
||
tekrar > 1
|
||
? {
|
||
tekrar,
|
||
hepGecti: [...soruBasina].filter(([, o]) => o.gri + o.kaldi === 0).map(([id]) => id),
|
||
hicGecmedi: [...soruBasina].filter(([, o]) => o.gecti === 0).map(([id]) => id),
|
||
karisik: Object.fromEntries([...soruBasina].filter(([, o]) => o.gecti > 0 && o.gri + o.kaldi > 0).map(([id, o]) => [id, `${o.gecti} geçti / ${o.gri} gri / ${o.kaldi} kaldı`])),
|
||
}
|
||
: null;
|
||
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: isler.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,
|
||
ortalamaAracCagrisi: sonuclar.length ? +(sonuclar.reduce((a, x) => a + x.araclar.length, 0) / sonuclar.length).toFixed(2) : 0,
|
||
enCokAracCagrisi: Math.max(0, ...sonuclar.map((x) => x.araclar.length)),
|
||
soruBasiMaliyet: sonuclar.length ? +(TOPLAM.maliyet / sonuclar.length).toFixed(5) : 0,
|
||
tutarlilik,
|
||
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]))}`);
|
||
if (tutarlilik) {
|
||
console.log(`\nTutarlılık (${tekrar} tekrar): ${tutarlilik.hepGecti.length} soru hep geçti · ${Object.keys(tutarlilik.karisik).length} karışık · ${tutarlilik.hicGecmedi.length} hiç geçmedi`);
|
||
for (const [id, o] of Object.entries(tutarlilik.karisik)) console.log(` karışık ${id}: ${o}`);
|
||
if (tutarlilik.hicGecmedi.length) console.log(` hiç geçmedi: ${tutarlilik.hicGecmedi.join(", ")}`);
|
||
}
|
||
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, ${ozet.ortalamaAracCagrisi} araç çağrısı (en çok ${ozet.enCokAracCagrisi})`);
|
||
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);
|
||
});
|