Files
kolaytercih/src/lib/ai/sohbet-akisi.ts
bilalgursen b935158d3c Danışman sohbeti AI SDK'ya taşındı; dört yeni araç
- Sunucu: elle yazılmış araç döngüsü yerine streamText + UI mesaj akışı
  (src/lib/ai/sohbet-akisi.ts). Kredi düşme/iade, bedelsiz profil onayı,
  mesaj kaydı ve Jev çıktı denetimi aynı davranışla korunur.
- İstemci: sohbet-client useChat kullanır; araç çalışırken balonun altında
  durum satırı görünür. Kayıtlı geçmiş düz metin kalır.
- Sağlayıcı aynı tek kapı (AI_BASE_URL / AI_API_KEY), openai-compatible.
- Yeni araçlar: bolum_ozeti, il_ozeti, rehber_ara, tercih_takvimi.
- Kalkan davranış: sağlayıcı araç çağrısını reddedince düz sohbete düşme.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 00:36:27 +03:00

116 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<void>;
/** Cevap tamamlanamadı; `metin` o ana kadar üretilen kısım (boş olabilir). */
onYarim: (metin: string) => Promise<void>;
}): ReadableStream<UIMessageChunk> {
return createUIMessageStream<UIMessage>({
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,
});
}