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>
134 lines
7.3 KiB
TypeScript
134 lines
7.3 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|