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>
This commit is contained in:
bilalgursen
2026-10-07 23:16:34 +03:00
parent 4d30dd1305
commit 15f4c4f4e1
7 changed files with 439 additions and 30 deletions

View File

@@ -7,16 +7,24 @@
* 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ı. Model davranışını ÖLÇMEZ; onu eval:sohbet ölçer.
* 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 };
type Senaryo = { ad: string; beklenen: Beklenen; adimlar: ({ metin: string } | { araclar: { name: string; arguments: string }[] })[] };
const B = (vaatNotu: boolean, kayitNotu: boolean, reddedilenArac = 0): Beklenen => ({ vaatNotu, kayitNotu, reddedilenArac });
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." }] },
@@ -25,6 +33,19 @@ const SENARYOLAR: Senaryo[] = [
{ 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;
@@ -61,6 +82,9 @@ async function main() {
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;
@@ -76,6 +100,7 @@ async function main() {
system: "test",
mesajlar: [{ role: "user", content: "test" }],
aracBaglami: { userId, sira: 100000, tur: "say", profil: {} },
rehberler,
onTamam: async (m) => {
tam = m;
},
@@ -91,7 +116,9 @@ async function main() {
if (v.type === "tool-output-available") araclar.push(JSON.stringify(v.output).slice(0, 70));
}
const d = denetim as Beklenen | null;
const tuttu = d != null && d.vaatNotu === s.beklenen.vaatNotu && d.kayitNotu === s.beklenen.kayitNotu && d.reddedilenArac === s.beklenen.reddedilenArac;
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, " ⏎ ")}`);
}