Validate assistant response contracts and bound AI request waiting

This commit is contained in:
2026-09-06 10:12:53 +03:00
parent 996cc15245
commit a1d70b9dcf
5 changed files with 75 additions and 5 deletions
+13 -5
View File
@@ -1,5 +1,6 @@
import { validateOfferResponse } from './offerResponse.js';
/** Bind the entire multi-step request to its starting account and mounted view. */
export async function requestOfferAssistant({url, body, readToken, isCurrent = () => true, listDocuments, fetcher = fetch}) {
export async function requestOfferAssistant({url, body, readToken, isCurrent = () => true, listDocuments, fetcher = fetch, timeoutMs = 120000}) {
const token = readToken();
const check = () => {
if (!token || readToken() !== token || !isCurrent())
@@ -17,9 +18,16 @@ export async function requestOfferAssistant({url, body, readToken, isCurrent = (
.map(b => ({ad:b.ad, kategori:b.kategori}))};
}
check();
const response = await fetcher(url, {method:'POST', headers:{'Content-Type':'application/json', Authorization:`Bearer ${token}`}, body:JSON.stringify(payload)});
const data = await response.json().catch(() => ({}));
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetcher(url, {signal:controller.signal, method:'POST', headers:{'Content-Type':'application/json', Authorization:`Bearer ${token}`}, body:JSON.stringify(payload)});
const data = await response.json().catch(() => { throw new Error('Sunucudan okunabilir JSON yanıtı alınamadı. Yeniden deneyin.'); });
check();
if (!response.ok || data.success === false) throw new Error(data.error || `HTTP ${response.status}`);
return data;
if (!response.ok || data?.success === false) throw new Error(typeof data?.error === 'string' ? data.error : `HTTP ${response.status}`);
return validateOfferResponse(url, data);
} catch (error) {
if (controller.signal.aborted) throw new Error('AI yanıtı zaman aşımına uğradı. Önceki çalışmanız korundu; yeniden deneyin.');
throw error;
} finally { clearTimeout(timer); }
}
+25
View File
@@ -0,0 +1,25 @@
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
const text = value => typeof value === 'string';
const texts = value => Array.isArray(value) && value.every(text);
const optionalText = value => value == null || text(value);
export function validateOfferResponse(url, data) {
let valid = object(data) && data.success === true;
if (valid && url === '/api/ai/belge-sor') {
valid = text(data.cevap) && Boolean(data.cevap.trim()) && Array.isArray(data.kaynaklar)
&& data.kaynaklar.every(row => object(row) && Number.isInteger(row.n) && row.n > 0 && text(row.alinti))
&& optionalText(data.kaynak) && optionalText(data.kapsam)
&& (data.sourceReview == null || (object(data.sourceReview) && Number.isInteger(data.sourceReview.totalChunks) && Number.isInteger(data.sourceReview.selectedChunks)));
} else if (valid && url === '/api/ai/teklif-denetim') {
valid = Array.isArray(data.maddeler) && data.maddeler.length > 0
&& data.maddeler.every(row => object(row) && text(row.gereklilik) && ['karsilaniyor','eksik','belirsiz'].includes(row.durum) && optionalText(row.kanit) && optionalText(row.oneri))
&& texts(data.eksikler) && Number.isFinite(data.uyum_yuzde) && data.uyum_yuzde >= 0 && data.uyum_yuzde <= 100
&& Number.isInteger(data.belirsizSayisi) && data.belirsizSayisi >= 0 && optionalText(data.ozet) && optionalText(data.kapsam);
} else if (valid && url === '/api/ai/teklif-taslak') {
valid = Array.isArray(data.bolumler) && data.bolumler.length > 0
&& data.bolumler.every(row => object(row) && text(row.baslik) && row.baslik.trim() && text(row.icerik) && row.icerik.trim())
&& object(data.draftReview) && ['partial','draft'].includes(data.draftReview.status)
&& texts(data.draftReview.requestedSections) && texts(data.draftReview.missingSections) && texts(data.draftReview.extraSections) && text(data.draftReview.note);
}
if (!valid) throw new Error('Sunucu yanıtı eksik veya beklenen biçimde değil. Önceki çalışmanız korundu; yeniden deneyin.');
return data;
}