fix(danisman): M4+M5 (8) — araç argümanları şemayla doğrulansın, "300 km" 300 okunsun

- sayiParse: birim eki (km, TL) sayının parçası değil; "300 km" artık 300.000 değil 300.
- aracCalistirici argümanı ARACLAR şemasına göre denetler (bilinmeyen alan, yanlış
  tip, geçersiz seçenek, alt sınır, eksik zorunlu alan) ve modele hangi alanın neden
  reddedildiğini söyler; geçersiz dilim / negatif ucret_ust sessizce süzmez.
- Şema, gövdelerin zaten kabul ettiğini söyler: program id'si ve profil değeri sayı
  da olabilir; ucret_ust en az 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 01:08:24 +03:00
parent d9badeedfa
commit 72b673bb94

View File

@@ -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<string, AlanSemasi>; required?: string[] };
const TIP_ADI: Record<string, string> = {
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, unknown>): 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<string, ArgumanSemasi>(
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<unknown> => {
const a = (arg && typeof arg === "object" ? arg : {}) as Record<string, unknown>;
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<string, unknown>;
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);