/** * Danışman cevabının UI mesaj akışı (AI SDK). api/soru/route.ts bunu * createUIMessageStreamResponse ile döner; istemci useChat ile okur. * * 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). * 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). * * Saf modül: DB'ye ve oturuma dokunmaz, Next dışından da çalışır. */ import { createUIMessageStream, isStepCount, streamText, toUIMessageStream, type ModelMessage, type UIMessage, type UIMessageChunk, } from "ai"; import { aracSeti, type AracBaglami } from "./araclar"; import { sohbetDilModeli } from "./client"; import { cevapIhlalOlasiligi, JEV_DUZELTME_NOTU, JEV_IHLAL_ESIGI } from "./jev"; /** Bir cevapta en fazla model adımı (araç turları + son cevap). */ const ARAC_TUR_SINIRI = 4; const HATA_METNI = "Danışman şu anda yanıt veremiyor."; export function danismanAkisi(opts: { system: string; mesajlar: ModelMessage[]; aracBaglami: AracBaglami; /** Cevap tamamlandı (çıktı denetimi notu dahil). Fırlatırsa akış hata verir. */ onTamam: (metin: string) => Promise; /** Cevap tamamlanamadı; `metin` o ana kadar üretilen kısım (boş olabilir). */ onYarim: (metin: string) => Promise; }): ReadableStream { return createUIMessageStream({ execute: async ({ writer }) => { let tamamlandi = false; let metin = ""; // Model/sağlayıcı hatası akışa "error" parçası olarak düşer let akisHatasi: string | null = null; try { const result = streamText({ model: sohbetDilModeli(), instructions: opts.system, messages: opts.mesajlar, tools: aracSeti(opts.aracBaglami, (ad) => console.info("[soru] araç:", ad)), // En fazla ARAC_TUR_SINIRI adım; son adımda araç kapalı (cevap // vermek zorunda). stopWhen: isStepCount(ARAC_TUR_SINIRI), prepareStep: ({ stepNumber }) => stepNumber >= ARAC_TUR_SINIRI - 1 ? { toolChoice: "none" } : undefined, maxOutputTokens: 1024, abortSignal: AbortSignal.timeout(120_000), providerOptions: { openaiCompatible: { reasoningEffort: "none" } }, onError: ({ error }) => { // Hata nesnesinin tamamı istek gövdesini ve cevap başlıklarını // taşır; loga yalnız durum kodu ve mesaj gider. const e = error as { statusCode?: number; message?: string }; console.error("[soru] model hatası:", e?.statusCode ?? "", e?.message ?? String(error)); }, }); // Bitiş parçasını biz yazarız: araya çıktı denetimi notu girebilir. const okuyucu = toUIMessageStream({ stream: result.stream, sendFinish: false, onError: () => HATA_METNI, }).getReader(); for (;;) { const { done, value: parca } = await okuyucu.read(); if (done) break; if (parca.type === "text-start" && metin && !metin.endsWith("\n")) { // Araç turundan sonra yeni metin parçası: kayıtta yapışmasın metin += "\n\n"; } else if (parca.type === "text-delta") { metin += parca.delta; } else if (parca.type === "error") { akisHatasi = parca.errorText; } writer.write(parca); } if (akisHatasi != null) throw new Error(akisHatasi); if (!metin.trim()) throw new Error("Boş cevap"); // Çıktı denetimi (jev.ts): cevap garanti/olasılık dili içeriyorsa // sonuna düzeltme notu eklenir. Akış zaten gittiği için cevap geri // alınamaz; not hem ekrana hem kayda yazılır. Denetim yapılamazsa // (anahtar yok, zaman aşımı) cevap olduğu gibi kalır. const ihlalP = await cevapIhlalOlasiligi(metin); if (ihlalP != null) { console.info("[soru] jev ihlal olasılığı:", ihlalP.toFixed(2)); if (ihlalP >= JEV_IHLAL_ESIGI) { const id = crypto.randomUUID(); metin += `\n\n${JEV_DUZELTME_NOTU}`; writer.write({ type: "text-start", id }); writer.write({ type: "text-delta", id, delta: JEV_DUZELTME_NOTU }); writer.write({ type: "text-end", id }); } } await opts.onTamam(metin); tamamlandi = true; writer.write({ type: "finish" }); } catch (err) { if (!tamamlandi) await opts.onYarim(metin).catch(() => {}); console.error("[soru] stream hatası:", err); // Model hatası akışa zaten "error" parçası olarak yazıldı; yazılmadıysa // (boş cevap, kayıt hatası) istemci hatayı buradan görür. if (akisHatasi == null) throw err; } }, onError: () => HATA_METNI, }); }