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"],
|
enum: ["Burslu", "%50 İndirimli", "%25 İndirimli", "Ücretli"],
|
||||||
description: "Vakıf burs oranı",
|
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: {
|
dilim: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["hayal", "dengeli", "guvenli"],
|
enum: ["hayal", "dengeli", "guvenli"],
|
||||||
@@ -127,7 +127,7 @@ export const ARACLAR: AracTanimi[] = [
|
|||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
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"],
|
required: ["program_id"],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -143,7 +143,7 @@ export const ARACLAR: AracTanimi[] = [
|
|||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
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"],
|
required: ["program_id"],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -160,7 +160,7 @@ export const ARACLAR: AracTanimi[] = [
|
|||||||
properties: {
|
properties: {
|
||||||
program_idler: {
|
program_idler: {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: { type: "string" },
|
items: { type: ["string", "integer"] },
|
||||||
minItems: 2,
|
minItems: 2,
|
||||||
maxItems: KARSILASTIRMA_UST,
|
maxItems: KARSILASTIRMA_UST,
|
||||||
description: "program_ara sonucundaki id'ler",
|
description: "program_ara sonucundaki id'ler",
|
||||||
@@ -279,7 +279,7 @@ export const ARACLAR: AracTanimi[] = [
|
|||||||
properties: {
|
properties: {
|
||||||
alan: { type: "string", enum: PROFIL_ALANLARI },
|
alan: { type: "string", enum: PROFIL_ALANLARI },
|
||||||
deger: {
|
deger: {
|
||||||
type: ["string", "null"],
|
type: ["string", "number", "null"],
|
||||||
description:
|
description:
|
||||||
"evIl: il adı; maxKm: sayı (km); ucretUst: sayı (TL, yıllık); " +
|
"evIl: il adı; maxKm: sayı (km); ucretUst: sayı (TL, yıllık); " +
|
||||||
`burs: ${Object.keys(PROFIL_SECENEKLERI.burs).join("|")}; ` +
|
`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 === "number") return v;
|
||||||
if (typeof v !== "string") return null;
|
if (typeof v !== "string") return null;
|
||||||
const t = v.toLocaleLowerCase("tr-TR").replace(/\s/g, "");
|
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;
|
if (!m) return null;
|
||||||
let n = Number.parseFloat(m[1].replace(/\./g, "").replace(",", "."));
|
let n = Number.parseFloat(m[1].replace(/\./g, "").replace(",", "."));
|
||||||
if (!Number.isFinite(n)) return null;
|
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 (deger !== null && deger !== undefined) {
|
||||||
if (alan === "maxKm" || alan === "ucretUst") {
|
if (alan === "maxKm" || alan === "ucretUst") {
|
||||||
deger = sayiParse(deger);
|
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") {
|
} else if (alan === "evIl") {
|
||||||
const il = ilAdiNormalize(String(deger));
|
const il = ilAdiNormalize(String(deger));
|
||||||
if (!il) return { hata: `İl tanınmadı: ${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) {
|
export function aracCalistirici(b: AracBaglami) {
|
||||||
return async (ad: string, arg: unknown): Promise<unknown> => {
|
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) {
|
switch (ad) {
|
||||||
case "program_ara":
|
case "program_ara":
|
||||||
return programAra(b, a as ProgramAraArg);
|
return programAra(b, a as ProgramAraArg);
|
||||||
|
|||||||
Reference in New Issue
Block a user