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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user