Files
kolaytercih/scripts/eval/akis-denetim.ts
bilalgursen 15f4c4f4e1 feat(sohbet): rehber bağlantıları akışta gerçek slug'larla doğrulanıyor
Model araç çağırmadan ezberden /rehber/<slug> yazabiliyordu; istemci her
adresi "Rehber yazısı" hapı yapıp 404'e gönderiyordu. Yeni saf modül
lib/ai/baglanti-denetimi.ts: kayıtlı slug dokunulmaz, yakın slug (Dice ≥ 0,5
ve açık ara önde) kayıttaki başlıkla yeniden yazılır, uydurma slug düz metne
iner / çıplak adres silinir. BaglantiTamponu akış sırasında çalışır (cevap
bittikten sonra ekrandaki hap geri alınamaz); parça parça çıktı tek seferlik
sonuçla birebir aynı. danismanAkisi `rehberler` seçeneği alır (verilmezse
pasif); route gerçek content/rehber listesini verir; CiktiDenetimi'ne
baglantiDuzeltme eklendi. Prompt BAĞLANTI maddesi rehber_ara'yı teşvik eder.

Aynı dosyalardaki revizyon-hak metinleri (prompt revizyonBolumu, araç
açıklaması/HAK dalı, route'taki kalanHak/kalanRevizyon) de bu commit'te:
sınırın kaldırılması bir sonraki commit'te tamamlanır.

Test: scripts/eval/baglanti-denetim.ts (44/44) ve akis-denetim.ts'e
rehber-uydurma / rehber-gercek senaryoları (9/9).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 23:16:34 +03:00

134 lines
7.3 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 akışındaki sunucu denetimlerinin MEKANİK testi — parasız, anahtarsız.
*
* pnpm tsx --conditions=react-server scripts/eval/akis-denetim.ts
*
* Yerelde sahte bir OpenAI-uyumlu uç açılır; "model" senaryodaki metni ya da
* araç çağrılarını aynen döner. Gerçek danismanAkisi + gerçek araçlar koşar
* (profil_guncelle data/app.db'nin geçici kopyasına yazar). Doğrulananlar:
* vaat regex'i notu (Jev anahtarı yokken), "kaydettim" deyip kaydetmeme notu,
* araç çağrısı kotası, rehber bağlantısı doğrulama (uydurma slug akışta
* gerçek slug'a döner, kayıt da düzeltilmiş metni alır). Model davranışını
* ÖLÇMEZ; onu eval:sohbet ölçer.
* Beklenti tutmazsa çıkış kodu 1.
*/
import { createServer } from "node:http";
import { copyFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
type Beklenen = { vaatNotu: boolean; kayitNotu: boolean; reddedilenArac: number; baglantiDuzeltme: number };
type Senaryo = {
ad: string;
beklenen: Beklenen;
adimlar: ({ metin: string } | { araclar: { name: string; arguments: string }[] })[];
/** Kaydedilen metinde olması / olmaması gerekenler (rehber bağlantısı senaryoları). */
metinde?: { var?: string[]; yok?: string[] };
};
const B = (vaatNotu: boolean, kayitNotu: boolean, reddedilenArac = 0, baglantiDuzeltme = 0): Beklenen => ({ vaatNotu, kayitNotu, reddedilenArac, baglantiDuzeltme });
const SENARYOLAR: Senaryo[] = [
{ ad: "kaydettim-aracsiz", beklenen: B(false, true), adimlar: [{ metin: "Kaydettim: Vakıfta yıllık ücret üst sınırı 400.000 TL. Aramalar buna göre süzülecek." }] },
{ ad: "kaydettim-aracli", beklenen: B(false, false), adimlar: [{ araclar: [{ name: "profil_guncelle", arguments: JSON.stringify({ alan: "ucretUst", deger: 400000 }) }] }, { metin: "Kaydettim: yıllık ücret en fazla 400.000 TL." }] },
{ ad: "kaydettim-arac-hata", beklenen: B(false, true), adimlar: [{ araclar: [{ name: "profil_guncelle", arguments: JSON.stringify({ alan: "maxKm", deger: 30 }) }] }, { metin: "Kaydettim: evden en fazla 30 km." }] },
{ ad: "kaydetmedim-not-ettim", beklenen: B(false, false), adimlar: [{ metin: "Tamam, kaydetmedim. İzmir'de oturduğunu not ettim; listen zaten İzmir ağırlıklı." }] },
{ ad: "vaat", beklenen: B(true, false), adimlar: [{ metin: "Bu programa yerleşme olasılığın düşük ama yine de yazabilirsin." }] },
{ ad: "temiz", beklenen: B(false, false), adimlar: [{ metin: "Bu program son yerleştirmede 178.278. sırada kapandı; etiketi Riskli. Garanti verilemez." }] },
{ ad: "kota", beklenen: B(false, false, 2), adimlar: [{ araclar: Array.from({ length: 6 }, (_, i) => ({ name: "il_ozeti", arguments: JSON.stringify({ il: ["İzmir", "Ankara", "Bursa", "Konya", "Adana", "Samsun"][i] }) })) }, { metin: "İllere baktım." }] },
// Model araç çağırmadan ezberden iki yanlış rehber adresi yazar; ikisi de akışta gerçek slug'a döner
{
ad: "rehber-uydurma",
beklenen: B(false, false, 0, 2),
adimlar: [{ metin: "Şu yazılara bak: [OBP](/rehber/obp-nedir) ve [Yatay geçiş](/rehber/yatay-gecis-rehberi). İkisi de sana göre." }],
metinde: { var: ["/rehber/obp-nedir-nasil-hesaplanir", "/rehber/yatay-gecis-cap-yandal"], yok: ["/rehber/obp-nedir)", "/rehber/yatay-gecis-rehberi"] },
},
{
ad: "rehber-gercek",
beklenen: B(false, false, 0, 0),
adimlar: [{ metin: "Bak: [Devlet mi vakıf mı](/rehber/devlet-mi-vakif-mi) yazısı bunu anlatıyor." }],
metinde: { var: ["/rehber/devlet-mi-vakif-mi"] },
},
];
let aktif: Senaryo = SENARYOLAR[0];
let adimNo = 0;
const sunucu = createServer((req, res) => {
let govde = "";
req.on("data", (c) => (govde += c));
req.on("end", () => {
const adim = aktif.adimlar[Math.min(adimNo++, aktif.adimlar.length - 1)];
res.writeHead(200, { "Content-Type": "text/event-stream" });
const yaz = (delta: unknown, finish: string | null = null) =>
res.write(`data: ${JSON.stringify({ id: "x", object: "chat.completion.chunk", created: 1, model: "sahte", choices: [{ index: 0, delta, finish_reason: finish }] })}\n\n`);
if ("metin" in adim) {
yaz({ role: "assistant", content: adim.metin });
yaz({}, "stop");
} else {
yaz({ role: "assistant", content: null, tool_calls: adim.araclar.map((a, i) => ({ index: i, id: `c${adimNo}_${i}`, type: "function", function: a })) });
yaz({}, "tool_calls");
}
res.write("data: [DONE]\n\n");
res.end();
});
});
async function main() {
await new Promise<void>((r) => sunucu.listen(0, "127.0.0.1", r));
const port = (sunucu.address() as { port: number }).port;
process.env.AI_BASE_URL = `http://127.0.0.1:${port}/v1`;
process.env.AI_API_KEY = "sahte-anahtar";
delete process.env.TYPESAFE_API_KEY; // Jev'e gitme: yalnız regex yolu
const db = `${process.env.TMPDIR ?? "/tmp"}/kolaytercih-akis-dene-${process.pid}.db`;
copyFileSync("data/app.db", db);
process.env.APP_DB_URL = `file:${db}`;
execFileSync("node", ["scripts/db-goc.mjs"], { env: { ...process.env }, stdio: "ignore" });
const Database = (await import("better-sqlite3")).default;
const sqlite = new Database(db);
const { danismanAkisi } = await import("@/lib/ai/sohbet-akisi");
// Gerçek content/rehber listesi: bağlantı doğrulama tamponu aktif
const { getRehberAramaKayitlari } = await import("@/lib/rehber");
const rehberler = getRehberAramaKayitlari();
const info = console.info;
console.info = () => {};
let hata = 0;
for (const s of SENARYOLAR) {
aktif = s;
adimNo = 0;
const userId = `akis-dene-${s.ad}`;
sqlite.prepare("INSERT OR REPLACE INTO user (id, name, email, email_verified, credit_balance, has_paket, created_at, updated_at) VALUES (?, 'Dene', ?, 1, 0, 1, 1, 1)").run(userId, `${userId}@eval.invalid`);
let tam: string | null = null;
let denetim: Beklenen | null = null;
const araclar: string[] = [];
const okuyucu = danismanAkisi({
system: "test",
mesajlar: [{ role: "user", content: "test" }],
aracBaglami: { userId, sira: 100000, tur: "say", profil: {} },
rehberler,
onTamam: async (m) => {
tam = m;
},
onYarim: async () => {},
onDenetim: (d) => {
denetim = d;
},
}).getReader();
for (;;) {
const { done, value } = await okuyucu.read();
if (done) break;
const v = value as { type: string; output?: unknown };
if (v.type === "tool-output-available") araclar.push(JSON.stringify(v.output).slice(0, 70));
}
const d = denetim as Beklenen | null;
const kayit = String(tam);
const metinTuttu = (s.metinde?.var ?? []).every((m) => kayit.includes(m)) && (s.metinde?.yok ?? []).every((m) => !kayit.includes(m));
const tuttu = d != null && d.vaatNotu === s.beklenen.vaatNotu && d.kayitNotu === s.beklenen.kayitNotu && d.reddedilenArac === s.beklenen.reddedilenArac && d.baglantiDuzeltme === s.beklenen.baglantiDuzeltme && metinTuttu;
if (!tuttu) hata++;
info(`\n${tuttu ? "geçti" : "KALDI"} [${s.ad}]\n denetim: ${JSON.stringify(denetim)}\n araç çıktıları: ${araclar.length} → ${araclar.join(" | ")}\n son metin: ${String(tam).replace(/\n+/g, " ⏎ ")}`);
}
sqlite.close();
sunucu.close();
info(`\n${SENARYOLAR.length - hata}/${SENARYOLAR.length} senaryo geçti (gerçek modelle koşulmadı; mekanik test)`);
process.exit(hata ? 1 : 0);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});