fix(danisman): liste revizyonu koda bağlandı — model artık revizyon başlatamaz (QA B1)
Revizyon sohbet modelinin gönüllü liste_revize araç seçimine bağlıydı (8 Eki eval: 8 istekte 5 çağrı, 3 sahte beyan; tool_choice required OpenRouter'da 80-112 sn). Şimdi: - Niyet kodda: revizyonNiyetiVar (regex) + paketli → lib/ai/revizyon-niyeti.ts yapılandırılmış çıkarım (yapilandirilmisUret, json_schema; eski liste_revize parametre şeması ve revizyonKuraliKur aynen yeniden kullanılır, kopya yok). Model niyetModeli() (OPENROUTER_NIYET_MODEL, yoksa rapor modeli), 20 sn, OpenInference bu çağrıda atlanır (11-12,5 sn ölçüldü). - Dallanma kodda: kural geçerli → listeRevizeKoddan (paket/kilit/kredi kapıları aynı); kural eksik → soru formu koddan (modelin formu kod doğrular, yoksa varsayılan form); niyet soru/diger → normal sohbet; çıkarım başarısız → koddan "okuyamadım" cevabı + kredi iadesi (normal sohbete düşünce model uydurma adlarla "kurdum" yazıyordu). - Cevap da kodda: revizyon/form turunda model hiç çağrılmaz; sayı ve program adları sunucu sonucundan şablonla (sabitCevap). Model sonuç önündeyken dağılımı 8'de 2 yanlış yazdı. Ekrana eski araç parçaları (tool-liste_revize / tool-soru_sor) aynen gider; istemci ve kayıt değişmez. Başarısız revizyon artık ekranda "kuramadım" hapı (output-error). - liste_revize modelin araç listesinden çıktı; prompt revizyon bölümü "kurdum deme, net istek iste ya da soru_sor" diye sadeleşti. - Güvenlik ağı: beyan regex'i kaldı; beyan var + revizyon yok → not + mesajın sohbet kredisi iade (grantCredits refund, refId=mesajId, idempotent), not bunu söyler. liste-revizyon iade refId'si "<mesajId>:revizyon" oldu: (reason, refId) tekilliği sohbet iadesiyle çakışıyordu. - Geçmiş penceresi: "[Araç kaydı: …]" etiketi modelce kopyalanıyordu; yerine doğal tek cümle. - Kaldırıldı: ilkAdimAracZorunlu / SOHBET_REVIZYON_ARAC_ZORUNLU. Ölçüm (deepseek-v4-flash @ openrouter, pnpm eval:revizyon): koddan revizyon 8/8, kural doğru 8/8, sahte beyan 0, 10/10; revizyon turu 1 küçük çağrı (ort. 1-3 sn, 0,00005 $) + liste üretimi; sohbet çağrısı yok. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,16 @@
|
||||
*
|
||||
* Araç çağrıları akışta ayrı parça olarak görünür ("Programlara
|
||||
* bakıyorum…"); kayda yalnız METİN gider (chat_messages.content düz metin).
|
||||
*
|
||||
* Koddan işlem (9 Eki 2026): liste revizyonu ve belirsiz istekteki soru
|
||||
* formu modelin kararına bırakılmaz. Route, adayın mesajındaki niyeti
|
||||
* kodla + yapılandırılmış çıkarımla okur (revizyon-niyeti.ts) ve `islem`
|
||||
* verir; akış işlemi çalıştırır, ekrana eski araç parçasıyla aynı biçimde
|
||||
* yazar (tool-liste_revize / tool-soru_sor; istemci ve kayıt değişmez) ve
|
||||
* cevabı KODDAN şablonla yazar — bu turda model hiç çağrılmaz (9 Eki eval:
|
||||
* sonuç önünde dururken model dağılım sayılarını 8 cevabın 2'sinde yanlış
|
||||
* yazdı; sayı ve ad şablondan basılır, ilke: deterministik olan LLM'e
|
||||
* bırakılmaz). `liste_revize` modelin araç listesinde yok.
|
||||
* Kayıt ve kredi iadesi çağıranın işidir: cevap tamamlanınca `onTamam`,
|
||||
* yarıda kalınca `onYarim` çağrılır (ikisi de biriken metinle).
|
||||
*
|
||||
@@ -28,6 +38,7 @@ import { kaliciDislananSaglayicilar, sohbetDilModeli } from "./client";
|
||||
import {
|
||||
KAYIT_DUZELTME_NOTU,
|
||||
REVIZE_DUZELTME_NOTU,
|
||||
REVIZE_DUZELTME_NOTU_IADELI,
|
||||
kayitBeyaniVar,
|
||||
revizeBeyaniVar,
|
||||
semaSizintisiVar,
|
||||
@@ -49,12 +60,27 @@ const CEVAP_BASINA_ARAC = 8;
|
||||
const HATA_METNI = "Danışman şu anda yanıt veremiyor.";
|
||||
const ARAC_ADLARI = ARACLAR.flatMap((a) => (a.type === "function" ? [a.function.name] : []));
|
||||
/**
|
||||
* liste_revize liste üretimini çalıştırır (iki model denemesine kadar, ~20-60
|
||||
* sn); sohbetin 120 sn'lik toplam süresi ona yetmeyebilir. Araç başlayınca
|
||||
* zaman aşımı bu kadar ileri alınır (features/rapor/liste-revizyon.ts).
|
||||
* Koddan liste revizyonu liste üretimini çalıştırır (iki model denemesine
|
||||
* kadar, ~20-60 sn); sohbetin 120 sn'lik toplam süresi ona yetmeyebilir.
|
||||
* İşlem başlayınca zaman aşımı bu kadar ileri alınır (features/rapor/liste-revizyon.ts).
|
||||
*/
|
||||
const REVIZYON_EK_SURE_MS = 150_000;
|
||||
|
||||
/**
|
||||
* Modele gitmeden önce KODDAN çalıştırılan işlem. Ekranda ve kayıtta eski
|
||||
* aracın parçası olarak görünür (`tool-<ad>`; arac-pilleri.tsx etiketleri,
|
||||
* sohbet-client.tsx liste yenileme ve form çizimi aynen çalışır).
|
||||
*/
|
||||
export type KoddanIslem = {
|
||||
ad: "liste_revize" | "soru_sor";
|
||||
/** Ekrana giden girdi (ör. çıkarılan kural); modele gitmez. */
|
||||
girdi: unknown;
|
||||
/** İşlemin gövdesi; dönen nesne ekrana araç sonucu olarak gider ve cevap şablonuna girer. */
|
||||
calistir: () => Promise<unknown>;
|
||||
/** Revizyon cevabının şablon girdisi: kural özeti (lib/revizyon-kurali kuralOzeti) ve "ilk N" kuralı var mı. */
|
||||
anlatim?: { ozet: string[]; ilkN: boolean };
|
||||
};
|
||||
|
||||
export type CiktiDenetimi = {
|
||||
/** Jev'in ihlal olasılığı; denetim yapılamadıysa null. */
|
||||
jevP: number | null;
|
||||
@@ -66,7 +92,7 @@ export type CiktiDenetimi = {
|
||||
reddedilenArac: number;
|
||||
/** "Kaydettim" dendi ama başarılı profil_guncelle yoktu; düzeltme notu eklendi. */
|
||||
kayitNotu: boolean;
|
||||
/** "Yeniden kurdum" dendi ama başarılı liste_revize yoktu; düzeltme notu eklendi. */
|
||||
/** "Yeniden kurdum" dendi ama bu turda başarılı (koddan) revizyon yoktu; düzeltme notu eklendi. */
|
||||
revizeNotu: boolean;
|
||||
/** Cevapta araç adları / şema anahtarları var (yalnız gözlem). */
|
||||
semaSizintisi: boolean;
|
||||
@@ -74,6 +100,46 @@ export type CiktiDenetimi = {
|
||||
baglantiDuzeltme: number;
|
||||
};
|
||||
|
||||
/** Kısaltma olarak kalacak büyük harfli sözcükler (başlık yazımına çevrilmez). */
|
||||
const KISALTMA = new Set(["KKTC", "TED", "ODTÜ", "İTÜ", "YTÜ", "TOBB", "MEF", "İZTECH", "ABD", "AB"]);
|
||||
|
||||
/**
|
||||
* Veritabanı biçimindeki BÜYÜK HARFLİ sözcükleri başlık yazımına çevirir;
|
||||
* küçük harfli metne dokunmaz: "Bilgisayar Mühendisliği — EGE ÜNİVERSİTESİ
|
||||
* (İZMİR)" → "… — Ege Üniversitesi (İzmir)", "iller: KOCAELİ eklendi" →
|
||||
* "iller: Kocaeli eklendi".
|
||||
*/
|
||||
function baslikYazimi(metin: string): string {
|
||||
return metin.replace(/\p{Lu}[\p{Lu}'’.]+/gu, (soz) => {
|
||||
if (KISALTMA.has(soz)) return soz;
|
||||
const kucuk = soz.toLocaleLowerCase("tr-TR");
|
||||
return kucuk.charAt(0).toLocaleUpperCase("tr-TR") + kucuk.slice(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Koddan işlemin cevabı — model çağrılmaz. Soru formu: sorular ekranda
|
||||
* (model "ne istediğini yaz" diye formu baltalıyordu — 9 Eki eval r09).
|
||||
* Başarısız revizyon: neden zaten adaya yönelik metin. Başarılı revizyon:
|
||||
* sayı ve adlar sunucu sonucundan şablonla (model aynı sonuç önündeyken
|
||||
* dağılımı 8'de 2 yanlış yazdı, adları eksik/sıra dışı aktardı).
|
||||
*/
|
||||
export function sabitCevap(islem: Pick<KoddanIslem, "ad" | "anlatim">, cikti: unknown): string {
|
||||
const nesne = cikti != null && typeof cikti === "object" ? (cikti as Record<string, unknown>) : {};
|
||||
if (islem.ad === "soru_sor") {
|
||||
return nesne.soruldu === true
|
||||
? "Birkaç kısa soru hazırladım; cevaplarsan listeni ona göre yeniden kurarım."
|
||||
: "Soru formunu açamadım; ne değişsin istediğini bir cümleyle yazarsan listeni ona göre yeniden kurarım.";
|
||||
}
|
||||
if (nesne.ok !== true) return `Listeni yeniden kuramadım: ${String(nesne.hata ?? "bilinmeyen bir sorun oluştu.")}`;
|
||||
const d = (nesne.dagilim ?? {}) as Record<string, number>;
|
||||
const ilk = Array.isArray(nesne.ilkTercihler) ? (nesne.ilkTercihler as string[]) : [];
|
||||
const ozet = islem.anlatim?.ozet.length ? ` — ${islem.anlatim.ozet.map(baslikYazimi).join("; ")}` : "";
|
||||
const satirlar = ilk.map((x, i) => `${i + 1}. ${baslikYazimi(x)}`).join("\n");
|
||||
const ilkN = islem.anlatim?.ilkN ? ' "İlk N" kuralında listenin başı o ölçütle dolar, dilim sırası bilerek bozulur.' : "";
|
||||
return `Listeni yeniden kurdum${ozet}. ${nesne.toplam} tercih: ${d.hayal ?? 0} hayal / ${d.dengeli ?? 0} dengeli / ${d.garanti ?? 0} güvenli.\n\nİlk ${ilk.length}:\n${satirlar}\n\nEkrandaki liste yenilendi.${ilkN} Bir programı sormak ya da başka bir şey değiştirmek istersen yaz.`;
|
||||
}
|
||||
|
||||
export function danismanAkisi(opts: {
|
||||
system: string;
|
||||
mesajlar: ModelMessage[];
|
||||
@@ -87,8 +153,21 @@ export function danismanAkisi(opts: {
|
||||
/** Çıktı denetiminin sonucu (gözlem/eval için; cevabı etkilemez). */
|
||||
onDenetim?: (d: CiktiDenetimi) => void;
|
||||
/**
|
||||
* Her başarılı araç sonucu (ad, çıktı). Route bununla soru formunu ve
|
||||
* revizyon sonucunu kayda (chat_messages.meta) yazar.
|
||||
* Model "yeniden kurdum" dedi ama bu turda başarılı revizyon yok: düzeltme
|
||||
* notundan önce çağrılır; `true` dönerse not "kredin iade edildi" der.
|
||||
* Route burada mesajın sohbet kredisini iade eder (idempotent; bedelsiz
|
||||
* mesajda false döner). Verilmezse iadesiz not.
|
||||
*/
|
||||
onRevizeBeyani?: () => Promise<boolean>;
|
||||
/**
|
||||
* Koddan işlem (revizyon ya da soru formu). Verilirse model bu turda HİÇ
|
||||
* çağrılmaz: işlem çalışır, sonucu ekrana araç parçası olarak gider, cevap
|
||||
* şablondan yazılır (sabitCevap), kayıt ve kredi akışı aynen (onTamam).
|
||||
*/
|
||||
islem?: KoddanIslem;
|
||||
/**
|
||||
* Her başarılı araç sonucu (ad, çıktı) — koddan işlem dâhil. Route bununla
|
||||
* soru formunu ve revizyon sonucunu kayda (chat_messages.meta) yazar.
|
||||
*/
|
||||
onAracSonucu?: (ad: string, cikti: unknown) => void;
|
||||
/**
|
||||
@@ -98,18 +177,6 @@ export function danismanAkisi(opts: {
|
||||
* eski davranışı korur).
|
||||
*/
|
||||
rehberler?: readonly RehberKaydi[];
|
||||
/**
|
||||
* İlk adımda model araç çağırmak ZORUNDA (toolChoice "required"; hangi araç
|
||||
* olduğunu model seçer). Route bunu adayın mesajı liste değişikliği
|
||||
* istediğinde (sohbet-gecmisi.revizyonNiyetiVar) ve aday paketliyken verir:
|
||||
* model araçsız "listeni yeniden kurdum" yazamaz, ya liste_revize ya
|
||||
* soru_sor çağırır (QA B1, 8 Eki 2026). Kredi: değişmez; ek adım yok,
|
||||
* yalnız ilk adımın metin yerine araç olması zorunlu. Ölçüm (8 Eki 2026):
|
||||
* liste_revize çağrı oranı 1/8 → 7/8, sahte beyan 5 → 0; ama OpenRouter
|
||||
* "required" parametresini destekleyen sağlayıcıya (OpenInference) geçti,
|
||||
* gecikme 1-7 sn → 80-112 sn. Bu yüzden route'ta env bayrağıyla, varsayılan kapalı.
|
||||
*/
|
||||
ilkAdimAracZorunlu?: boolean;
|
||||
}): ReadableStream<UIMessageChunk> {
|
||||
return createUIMessageStream<UIMessage>({
|
||||
execute: async ({ writer }) => {
|
||||
@@ -136,31 +203,66 @@ export function danismanAkisi(opts: {
|
||||
let baglantiDuzeltme = 0;
|
||||
const duzeltilenSluglar: string[] = [];
|
||||
try {
|
||||
// Koddan işlem: ekrana araç parçası olarak, cevap şablondan; model bu
|
||||
// turda çağrılmaz. Başarısız revizyon da ekrana gider ("Listeni
|
||||
// yeniden kuramadım" hapı); gövde fırlatırsa aynı yol.
|
||||
writer.write({ type: "start" });
|
||||
if (opts.islem) {
|
||||
const { ad, girdi } = opts.islem;
|
||||
const toolCallId = `kod-${crypto.randomUUID()}`;
|
||||
console.info("[soru] koddan işlem:", ad);
|
||||
writer.write({ type: "tool-input-start", toolCallId, toolName: ad });
|
||||
writer.write({ type: "tool-input-available", toolCallId, toolName: ad, input: girdi });
|
||||
if (ad === "liste_revize") {
|
||||
clearTimeout(zamanlayici);
|
||||
sure += REVIZYON_EK_SURE_MS;
|
||||
zamanlayici = setTimeout(() => kesici.abort(), sure);
|
||||
}
|
||||
let cikti: unknown;
|
||||
try {
|
||||
cikti = await opts.islem.calistir();
|
||||
} catch (hata) {
|
||||
console.error("[soru] koddan işlem hatası:", ad, hata instanceof Error ? hata.message : String(hata));
|
||||
cikti = { hata: ad === "liste_revize" ? "Liste yeniden kurulurken bir sorun oluştu." : "Soru formu açılamadı." };
|
||||
}
|
||||
const nesne = cikti != null && typeof cikti === "object" ? (cikti as Record<string, unknown>) : null;
|
||||
if (ad === "liste_revize" && nesne?.ok === true) basariliRevizyon++;
|
||||
if (ad === "soru_sor" && nesne?.soruldu === true) formAcildi = true;
|
||||
if (nesne && "hata" in nesne) {
|
||||
writer.write({ type: "tool-output-error", toolCallId, errorText: String(nesne.hata) });
|
||||
} else {
|
||||
writer.write({ type: "tool-output-available", toolCallId, output: cikti });
|
||||
if (nesne) opts.onAracSonucu?.(ad, cikti);
|
||||
}
|
||||
// Cevap koddan (model çağrısı yok → gecikme, maliyet ve "yeniden
|
||||
// kurdum" riski sıfır; sayılar sunucu sonucundan). Kredi: mesajın
|
||||
// kredisi düşmüş kalır (form cevabı bedelsiz).
|
||||
const sabit = sabitCevap(opts.islem, cikti);
|
||||
const id = crypto.randomUUID();
|
||||
metin = sabit;
|
||||
writer.write({ type: "text-start", id });
|
||||
writer.write({ type: "text-delta", id, delta: sabit });
|
||||
writer.write({ type: "text-end", id });
|
||||
opts.onDenetim?.({ jevP: null, vaatRegex: [], vaatNotu: false, reddedilenArac: 0, kayitNotu: false, revizeNotu: false, semaSizintisi: false, baglantiDuzeltme: 0 });
|
||||
await opts.onTamam(metin);
|
||||
tamamlandi = true;
|
||||
writer.write({ type: "finish" });
|
||||
return;
|
||||
}
|
||||
const result = streamText({
|
||||
model: sohbetDilModeli(),
|
||||
instructions: opts.system,
|
||||
messages: opts.mesajlar,
|
||||
tools: aracSeti(
|
||||
opts.aracBaglami,
|
||||
(ad) => {
|
||||
console.info("[soru] araç:", ad);
|
||||
if (ad === "liste_revize") {
|
||||
clearTimeout(zamanlayici);
|
||||
sure += REVIZYON_EK_SURE_MS;
|
||||
zamanlayici = setTimeout(() => kesici.abort(), sure);
|
||||
}
|
||||
},
|
||||
(ad) => {
|
||||
(ad) => console.info("[soru] araç:", ad),
|
||||
() => {
|
||||
adimdakiArac++;
|
||||
toplamArac++;
|
||||
if (formAcildi) {
|
||||
reddedilenArac++;
|
||||
return "Soru formu adayda; cevap gelene kadar başka araç çağrılmaz. Bu çağrı ÇALIŞTIRILMADI. Tek kısa cümleyle bitir.";
|
||||
}
|
||||
if (ad === "liste_revize" && basariliRevizyon > 0) {
|
||||
reddedilenArac++;
|
||||
return "Liste bu cevapta zaten yeniden kuruldu; ikinci revizyon ÇALIŞTIRILMADI. Sonucu adaya anlat.";
|
||||
}
|
||||
if (adimdakiArac <= ADIM_BASINA_ARAC && toplamArac <= CEVAP_BASINA_ARAC) return null;
|
||||
reddedilenArac++;
|
||||
return `Araç çağrısı sınırı aşıldı (bir adımda en fazla ${ADIM_BASINA_ARAC}, bir cevapta en fazla ${CEVAP_BASINA_ARAC}). Bu çağrı ÇALIŞTIRILMADI. Yeni araç çağırma; elindeki sonuçlarla cevap ver, eksik kalan kısmı adaya açıkça söyle ve sorusunu daraltmasını iste. Çalışmayan çağrı için sayı uydurma.`;
|
||||
@@ -169,7 +271,6 @@ export function danismanAkisi(opts: {
|
||||
const nesne = cikti != null && typeof cikti === "object" ? (cikti as Record<string, unknown>) : null;
|
||||
// Başarılı kayıt = profil_guncelle "kaydedildi" döndü (hata dönerse sayılmaz)
|
||||
if (ad === "profil_guncelle" && nesne && "kaydedildi" in nesne) basariliKayit++;
|
||||
if (ad === "liste_revize" && nesne?.ok === true) basariliRevizyon++;
|
||||
if (ad === "soru_sor" && nesne?.soruldu === true) formAcildi = true;
|
||||
if (nesne && !("hata" in nesne)) opts.onAracSonucu?.(ad, cikti);
|
||||
},
|
||||
@@ -179,10 +280,9 @@ export function danismanAkisi(opts: {
|
||||
stopWhen: isStepCount(ARAC_TUR_SINIRI),
|
||||
prepareStep: ({ stepNumber }) => {
|
||||
adimdakiArac = 0;
|
||||
// Form açıldıysa model cevabı bitirmek zorunda (araç kapalı)
|
||||
// Form açıldıysa (modelin soru_sor'u ya da koddan form) model
|
||||
// cevabı bitirmek zorunda (araç kapalı)
|
||||
if (stepNumber >= ARAC_TUR_SINIRI - 1 || formAcildi) return { toolChoice: "none" };
|
||||
// Revizyon niyeti: ilk adım araçsız bitemez
|
||||
if (stepNumber === 0 && opts.ilkAdimAracZorunlu) return { toolChoice: "required" };
|
||||
return undefined;
|
||||
},
|
||||
maxOutputTokens: 1024,
|
||||
@@ -204,6 +304,8 @@ export function danismanAkisi(opts: {
|
||||
// Bitiş parçasını biz yazarız: araya çıktı denetimi notu girebilir.
|
||||
const okuyucu = toUIMessageStream({
|
||||
stream: result.stream,
|
||||
// Başlangıç parçası yukarıda yazıldı (koddan işlem ondan sonra gelir)
|
||||
sendStart: false,
|
||||
sendFinish: false,
|
||||
onError: () => HATA_METNI,
|
||||
}).getReader();
|
||||
@@ -278,14 +380,19 @@ export function danismanAkisi(opts: {
|
||||
writer.write({ type: "text-end", id });
|
||||
}
|
||||
// Revizyon beyanı denetimi: "yeniden kurdum" ama bu turda başarılı
|
||||
// liste_revize yok → düzeltme notu (kayıt denetimiyle aynı mantık).
|
||||
// koddan revizyon yok → düzeltme notu (kayıt denetimiyle aynı mantık).
|
||||
// Revizyonu artık model başlatamadığı için bu yalnız güvenlik ağıdır;
|
||||
// tetiklenirse mesajın kredisi iade edilir (route, onRevizeBeyani) ve
|
||||
// not bunu söyler. Kredi: iade idempotent, çift iade yok.
|
||||
const revizeNotu = basariliRevizyon === 0 && revizeBeyaniVar(metin);
|
||||
if (revizeNotu) {
|
||||
console.info("[soru] revizyon beyanı var, başarılı liste_revize yok → not eklendi");
|
||||
const iade = (await opts.onRevizeBeyani?.().catch(() => false)) ?? false;
|
||||
console.info("[soru] revizyon beyanı var, bu turda revizyon yok → not eklendi", iade ? "(kredi iade edildi)" : "");
|
||||
const not = iade ? REVIZE_DUZELTME_NOTU_IADELI : REVIZE_DUZELTME_NOTU;
|
||||
const id = crypto.randomUUID();
|
||||
metin += `\n\n${REVIZE_DUZELTME_NOTU}`;
|
||||
metin += `\n\n${not}`;
|
||||
writer.write({ type: "text-start", id });
|
||||
writer.write({ type: "text-delta", id, delta: REVIZE_DUZELTME_NOTU });
|
||||
writer.write({ type: "text-delta", id, delta: not });
|
||||
writer.write({ type: "text-end", id });
|
||||
}
|
||||
// İç şema sızıntısı: yalnız gözlem (cevap geri alınamaz). Metin loglanmaz.
|
||||
|
||||
Reference in New Issue
Block a user