diff --git a/src/lib/ai/araclar.ts b/src/lib/ai/araclar.ts index 7b76b46..94ff573 100644 --- a/src/lib/ai/araclar.ts +++ b/src/lib/ai/araclar.ts @@ -101,7 +101,7 @@ export const ARACLAR: AracTanimi[] = [ enum: ["Burslu", "%50 İndirimli", "%25 İndirimli", "Ücretli"], description: "Vakıf burs oranı", }, - ucret_ust: { type: "integer", description: "Yıllık ücret üst sınırı (TL); devlet programları her zaman geçer" }, + ucret_ust: { type: "integer", minimum: 1, description: "Yıllık ücret üst sınırı (TL); devlet programları her zaman geçer" }, dilim: { type: "string", enum: ["hayal", "dengeli", "guvenli"], @@ -127,7 +127,7 @@ export const ARACLAR: AracTanimi[] = [ parameters: { type: "object", properties: { - program_id: { type: "string", description: "program_ara sonucundaki id (kılavuz kodu)" }, + program_id: { type: ["string", "integer"], description: "program_ara sonucundaki id (kılavuz kodu)" }, }, required: ["program_id"], additionalProperties: false, @@ -143,7 +143,7 @@ export const ARACLAR: AracTanimi[] = [ parameters: { type: "object", properties: { - program_id: { type: "string", description: "program_ara sonucundaki id (kılavuz kodu)" }, + program_id: { type: ["string", "integer"], description: "program_ara sonucundaki id (kılavuz kodu)" }, }, required: ["program_id"], additionalProperties: false, @@ -160,7 +160,7 @@ export const ARACLAR: AracTanimi[] = [ properties: { program_idler: { type: "array", - items: { type: "string" }, + items: { type: ["string", "integer"] }, minItems: 2, maxItems: KARSILASTIRMA_UST, description: "program_ara sonucundaki id'ler", @@ -279,7 +279,7 @@ export const ARACLAR: AracTanimi[] = [ properties: { alan: { type: "string", enum: PROFIL_ALANLARI }, deger: { - type: ["string", "null"], + type: ["string", "number", "null"], description: "evIl: il adı; maxKm: sayı (km); ucretUst: sayı (TL, yıllık); " + `burs: ${Object.keys(PROFIL_SECENEKLERI.burs).join("|")}; ` + @@ -768,7 +768,8 @@ function sayiParse(v: unknown): number | null { if (typeof v === "number") return v; if (typeof v !== "string") return null; const t = v.toLocaleLowerCase("tr-TR").replace(/\s/g, ""); - const m = t.match(/^([\d.,]+)(bin|k|milyon|m)?/); + // Birim eki sayının parçası değil: "300 km"deki "k" bin sayılmasın + const m = t.match(/^([\d.,]+)(bin|k|milyon|m)?(?:km|kilometre|tl|₺|lira)?$/); if (!m) return null; let n = Number.parseFloat(m[1].replace(/\./g, "").replace(",", ".")); if (!Number.isFinite(n)) return null; @@ -786,7 +787,7 @@ async function profilGuncelle(b: AracBaglami, a: { alan?: string; deger?: unknow if (deger !== null && deger !== undefined) { if (alan === "maxKm" || alan === "ucretUst") { deger = sayiParse(deger); - if (deger == null) return { hata: "Sayı okunamadı." }; + if (deger == null) return { hata: `Sayı okunamadı (${alan}): yalnız sayı yaz, ör. ${alan === "maxKm" ? "300" : "400000"}.` }; } else if (alan === "evIl") { const il = ilAdiNormalize(String(deger)); if (!il) return { hata: `İl tanınmadı: ${String(deger)}` }; @@ -808,10 +809,101 @@ async function profilGuncelle(b: AracBaglami, a: { alan?: string; deger?: unknow } } -/** Route'un araç döngüsüne verdiği çalıştırıcı. Bilinmeyen araç hata döner. */ +// ── Argüman doğrulama ──────────────────────────────────────────────────── + +type AlanSemasi = { + type?: string | string[]; + enum?: readonly unknown[]; + items?: AlanSemasi; + minimum?: number; +}; +type ArgumanSemasi = { properties?: Record; required?: string[] }; + +const TIP_ADI: Record = { + string: "metin", + integer: "tam sayı", + number: "sayı", + array: "liste", + null: "null", +}; + +function tipUyuyor(tip: string, v: unknown): boolean { + switch (tip) { + case "string": + return typeof v === "string"; + case "integer": + return Number.isInteger(v); + case "number": + return typeof v === "number" && Number.isFinite(v); + case "array": + return Array.isArray(v); + case "null": + return v === null; + default: + return true; + } +} + +function alanHatasi(ad: string, s: AlanSemasi, v: unknown): string | null { + const gelen = (JSON.stringify(v) ?? String(v)).slice(0, 60); + const tipler = s.type == null ? [] : Array.isArray(s.type) ? s.type : [s.type]; + if (tipler.length && !tipler.some((t) => tipUyuyor(t, v))) { + return `${ad}: ${tipler.map((t) => TIP_ADI[t] ?? t).join(" ya da ")} olmalı (gelen: ${gelen})`; + } + if (s.enum && !s.enum.includes(v)) { + return `${ad}: geçersiz seçenek (gelen: ${gelen}); şunlardan biri olmalı: ${s.enum.join(", ")}`; + } + if (typeof v === "number" && s.minimum != null && v < s.minimum) { + return `${ad}: en az ${s.minimum} olmalı (gelen: ${gelen})`; + } + if (Array.isArray(v) && s.items) { + for (const [i, x] of v.entries()) { + const h = alanHatasi(`${ad}[${i}]`, s.items, x); + if (h) return h; + } + } + return null; +} + +/** + * Argümanı aracın ARACLAR'daki şemasına göre denetler: bilinmeyen alan, + * yanlış tip, geçersiz seçenek, alt sınır, eksik zorunlu alan. Hata varsa + * hangi alan ve neden olduğunu söyleyen metin döner (model düzeltip yeniden + * çağırabilsin); bozuk argüman sessizce yanlış süzgece dönüşmez. Liste + * uzunluğu burada denetlenmez, gövdeler kendi mesajıyla ele alır. + */ +function argumanHatasi(sema: ArgumanSemasi, a: Record): string | null { + const alanlar = sema.properties ?? {}; + for (const [ad, v] of Object.entries(a)) { + if (!Object.hasOwn(alanlar, ad)) { + return `bilinmeyen alan: ${ad.slice(0, 40)}; geçerli alanlar: ${Object.keys(alanlar).join(", ") || "(yok)"}`; + } + // Zorunlu olmayan alanda null "verilmedi" demektir (modeller boş alanı böyle yollayabiliyor) + if (v === null && !sema.required?.includes(ad)) continue; + const h = alanHatasi(ad, alanlar[ad], v); + if (h) return h; + } + const eksik = (sema.required ?? []).find((ad) => a[ad] === undefined); + return eksik ? `${eksik}: zorunlu alan eksik` : null; +} + +const ARAC_SEMALARI = new Map( + ARACLAR.flatMap((a) => + a.type === "function" ? [[a.function.name, (a.function.parameters ?? {}) as ArgumanSemasi] as const] : [], + ), +); + +/** Route'un araç döngüsüne verdiği çalıştırıcı. Bilinmeyen araç ve şemaya uymayan argüman hata döner. */ export function aracCalistirici(b: AracBaglami) { return async (ad: string, arg: unknown): Promise => { - const a = (arg && typeof arg === "object" ? arg : {}) as Record; + const sema = ARAC_SEMALARI.get(ad); + if (!sema) return { hata: `Bilinmeyen araç: ${ad}` }; + if (arg != null && (typeof arg !== "object" || Array.isArray(arg))) { + return { hata: "Geçersiz argüman: alan adlarıyla bir nesne gönder, düz metin ya da liste değil." }; + } + const a = (arg ?? {}) as Record; + const hata = argumanHatasi(sema, a); + if (hata) return { hata: `Geçersiz argüman — ${hata}. Düzeltip yeniden çağır.` }; switch (ad) { case "program_ara": return programAra(b, a as ProgramAraArg);