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:
@@ -23,6 +23,7 @@ import {
|
||||
type UIMessageChunk,
|
||||
} from "ai";
|
||||
import { ARACLAR, aracSeti, type AracBaglami } from "./araclar";
|
||||
import { BaglantiTamponu, type RehberKaydi } from "./baglanti-denetimi";
|
||||
import { kaliciDislananSaglayicilar, sohbetDilModeli } from "./client";
|
||||
import {
|
||||
KAYIT_DUZELTME_NOTU,
|
||||
@@ -69,6 +70,8 @@ export type CiktiDenetimi = {
|
||||
revizeNotu: boolean;
|
||||
/** Cevapta araç adları / şema anahtarları var (yalnız gözlem). */
|
||||
semaSizintisi: boolean;
|
||||
/** Akış sırasında düzeltilen rehber bağlantısı sayısı (baglanti-denetimi.ts). */
|
||||
baglantiDuzeltme: number;
|
||||
};
|
||||
|
||||
export function danismanAkisi(opts: {
|
||||
@@ -88,6 +91,13 @@ export function danismanAkisi(opts: {
|
||||
* revizyon sonucunu kayda (chat_messages.meta) yazar.
|
||||
*/
|
||||
onAracSonucu?: (ad: string, cikti: unknown) => void;
|
||||
/**
|
||||
* Gerçek rehber yazıları (slug + başlık). Verilirse cevaptaki
|
||||
* `/rehber/<slug>` adresleri akış sırasında bu listeyle doğrulanır
|
||||
* (baglanti-denetimi.ts); verilmezse tampon pasiftir (betikler/evaller
|
||||
* eski davranışı korur).
|
||||
*/
|
||||
rehberler?: readonly RehberKaydi[];
|
||||
}): ReadableStream<UIMessageChunk> {
|
||||
return createUIMessageStream<UIMessage>({
|
||||
execute: async ({ writer }) => {
|
||||
@@ -109,6 +119,10 @@ export function danismanAkisi(opts: {
|
||||
let basariliRevizyon = 0;
|
||||
// soru_sor çağrıldı: form adayda; model aynı cevapta başka araç çağırmasın
|
||||
let formAcildi = false;
|
||||
// Rehber bağlantısı doğrulama: her metin parçası için ayrı tampon
|
||||
let tampon: BaglantiTamponu | null = null;
|
||||
let baglantiDuzeltme = 0;
|
||||
const duzeltilenSluglar: string[] = [];
|
||||
try {
|
||||
const result = streamText({
|
||||
model: sohbetDilModeli(),
|
||||
@@ -183,11 +197,30 @@ export function danismanAkisi(opts: {
|
||||
if (done) break;
|
||||
// Kesilme istemciye "abort" değil hata olarak gider (aşağıda fırlatılır)
|
||||
if (parca.type === "abort") continue;
|
||||
if (parca.type === "text-start" && metin && !metin.endsWith("\n")) {
|
||||
// Araç turundan sonra yeni metin parçası: kayıtta yapışmasın
|
||||
metin += "\n\n";
|
||||
if (parca.type === "text-start") {
|
||||
if (metin && !metin.endsWith("\n")) {
|
||||
// Araç turundan sonra yeni metin parçası: kayıtta yapışmasın
|
||||
metin += "\n\n";
|
||||
}
|
||||
if (opts.rehberler) tampon = new BaglantiTamponu(opts.rehberler);
|
||||
} else if (parca.type === "text-delta") {
|
||||
metin += parca.delta;
|
||||
// Tampon bir bağlantı adayı görünce metni tutar, aday kapanınca
|
||||
// doğrulanmış hâlini bırakır; ekran ve kayıt aynı metni alır.
|
||||
const cikti = tampon ? tampon.besle(parca.delta) : parca.delta;
|
||||
metin += cikti;
|
||||
if (cikti) writer.write({ ...parca, delta: cikti });
|
||||
continue;
|
||||
} else if (parca.type === "text-end") {
|
||||
const kalan = tampon?.bitir() ?? "";
|
||||
if (kalan) {
|
||||
metin += kalan;
|
||||
writer.write({ type: "text-delta", id: parca.id, delta: kalan });
|
||||
}
|
||||
if (tampon) {
|
||||
baglantiDuzeltme += tampon.duzeltmeler.length;
|
||||
for (const d of tampon.duzeltmeler) duzeltilenSluglar.push(d.yeniSlug ? `${d.slug}→${d.yeniSlug}` : `${d.slug}→(düz)`);
|
||||
tampon = null;
|
||||
}
|
||||
} else if (parca.type === "error") {
|
||||
akisHatasi = parca.errorText;
|
||||
}
|
||||
@@ -244,11 +277,15 @@ export function danismanAkisi(opts: {
|
||||
const semaSizintisi = semaSizintisiVar(metin, ARAC_ADLARI);
|
||||
if (semaSizintisi) console.warn("[soru] olası iç şema sızıntısı: cevapta araç adları / şema anahtarları var");
|
||||
if (reddedilenArac) console.info("[soru] araç sınırı:", reddedilenArac, "çağrı çalıştırılmadı");
|
||||
opts.onDenetim?.({ jevP, vaatRegex, vaatNotu, reddedilenArac, kayitNotu, revizeNotu, semaSizintisi });
|
||||
// Slug kişisel veri değildir; hangi uydurma adresin neye döndüğü loglanır
|
||||
if (baglantiDuzeltme) console.info("[soru] rehber bağlantısı düzeltildi:", baglantiDuzeltme, duzeltilenSluglar.join(", "));
|
||||
opts.onDenetim?.({ jevP, vaatRegex, vaatNotu, reddedilenArac, kayitNotu, revizeNotu, semaSizintisi, baglantiDuzeltme });
|
||||
await opts.onTamam(metin);
|
||||
tamamlandi = true;
|
||||
writer.write({ type: "finish" });
|
||||
} catch (err) {
|
||||
// Tamponda tutulan bağlantı adayı yarım kayda doğrulanmış hâliyle girsin
|
||||
if (tampon) metin += tampon.bitir();
|
||||
if (!tamamlandi) await opts.onYarim(metin).catch(() => {});
|
||||
// Tam hata nesnesi istek gövdesini ve cevap başlıklarını taşır; yalnız mesaj
|
||||
console.error("[soru] stream hatası:", err instanceof Error ? err.message : String(err));
|
||||
|
||||
Reference in New Issue
Block a user