Cancel disconnected offer analysis and stop remaining provider calls
This commit is contained in:
@@ -57,7 +57,7 @@ export async function ihaleMetni(ikn, verilen, ilan) {
|
||||
return { text: '', kaynak: null };
|
||||
}
|
||||
|
||||
export async function belgeyeSor({ ikn, soru, text, tender, chain }) {
|
||||
export async function belgeyeSor({ ikn, soru, text, tender, chain, signal }) {
|
||||
const q = String(soru ?? '').trim().slice(0, 500);
|
||||
if (q.length < 3) throw new Error('Soru en az 3 karakter olmalı.');
|
||||
const { text: metin, kaynak } = await ihaleMetni(ikn, text, tender?.aciklama);
|
||||
@@ -67,7 +67,7 @@ export async function belgeyeSor({ ikn, soru, text, tender, chain }) {
|
||||
if (!kaynaklar.length) return { cevap: 'Terim aramasında bu soruyla eşleşen bir bölüm bulunamadı; bu, şartın belgede bulunmadığını kanıtlamaz; belgedeki terimlerle (teminat, teslim, ceza, adet) yeniden deneyin.', kaynaklar: [], kaynak };
|
||||
const baglam = kaynaklar.map((k) => `[${k.n}]\n${k.icerik}`).join('\n\n');
|
||||
const sistem = `Sen Türkiye kamu ihalesi uzmanısın. Sana bir ihale dokümanının numaralı parçaları verilecek. YALNIZCA bu parçalardaki bilgiyle yanıt ver; parçalarda yoksa "Seçilen kaynak parçalarında bu bilgi bulunamadı" de ve kontrol edilen parçalara atıf ver, tahmin etme. Her iddianın sonuna dayandığı parça numarasını köşeli parantezle yaz: [1], [3]. Sayı, tarih, tutar, süre değerlerini aynen aktar. Kısa ve net Türkçe. Yalnızca JSON döndür: {"cevap":"..."}`;
|
||||
const out = await analyzeSpec({ text: `PARÇALAR:\n\n${baglam}\n\nSORU: ${q}\n\nYalnızca {"cevap":"..."} JSON'u döndür.`, tender, chain, systemOverride: sistem });
|
||||
const out = await analyzeSpec({ text: `PARÇALAR:\n\n${baglam}\n\nSORU: ${q}\n\nYalnızca {"cevap":"..."} JSON'u döndür.`, tender, chain, signal, systemOverride: sistem });
|
||||
return {...qualifyQuestionAnswer(out?.result, kaynaklar), kaynak,
|
||||
sourceReview:{sourceHash:createHash('sha256').update(metin).digest('hex'), totalChunks:parcalar.length, selectedChunks:kaynaklar.length},
|
||||
kapsam:'Yanıt yalnız soruyla eşleşen kaynak parçalarına dayanır. Kaynak numarası kontrolü, yanıtın anlam bakımından doğruluğunu tek başına kanıtlamaz; diğer bölümlerde ek veya çelişen şartlar bulunabilir.'};
|
||||
@@ -92,7 +92,7 @@ export function gereklilikMetni(tender, gereklilikler, metin, allItems = false)
|
||||
return [...new Set(g)].slice(0, allItems ? undefined : 60);
|
||||
}
|
||||
|
||||
export async function teklifDenetle({ ikn, taslak, gereklilikler, tender, chain, text }) {
|
||||
export async function teklifDenetle({ ikn, taslak, gereklilikler, tender, chain, text, signal }) {
|
||||
const t = String(taslak ?? '').trim();
|
||||
if (t.length < 200) throw new Error('Taslak en az 200 karakter olmalı.');
|
||||
const { text: metin } = await ihaleMetni(ikn, text, tender?.aciklama);
|
||||
@@ -101,9 +101,9 @@ export async function teklifDenetle({ ikn, taslak, gereklilikler, tender, chain,
|
||||
const sistem = `Sen kamu ihalelerinde teklif dosyası denetçisisin. Gereklilik listesi ve teklif TASLAĞI verilecek. Her gereklilik için taslağı kontrol et. "karsilaniyor" yalnızca taslakta açık karşılığı varsa (alıntıyı 'kanit'e aynen kopyala); değinilmiyorsa "eksik"; kısmen/belirsizse "belirsiz". Sayı üretme. Yalnızca JSON: {"ozet":"2-3 cümle","maddeler":[{"gereklilik":"","durum":"karsilaniyor"|"eksik"|"belirsiz","kanit":"veya null","oneri":"veya null"}]} — maddeleri verilen sırada döndür; gereklilik alanına verilen gereklilik metnini değiştirmeden aynen kopyala.`;
|
||||
return auditOfferInParts(liste,t,async (group,draftPart) => {
|
||||
const out = await analyzeSpec({text:`GEREKLİLİKLER:\n${group.map((g,i)=>`${i+1}. ${g}`).join('\n')}\n\nTEKLİF TASLAĞI BÖLÜMÜ:\n${draftPart}\n\nYalnızca JSON döndür.`,
|
||||
tender:tender ? {...tender,items:[]} : undefined,chain,systemOverride:sistem,mode:'derin',_singlePart:true});
|
||||
tender:tender ? {...tender,items:[]} : undefined,chain,systemOverride:sistem,mode:'derin',signal,_singlePart:true});
|
||||
return out?.result;
|
||||
});
|
||||
},{signal});
|
||||
}
|
||||
|
||||
const BOLUMLER = {
|
||||
@@ -113,7 +113,7 @@ const BOLUMLER = {
|
||||
tam: ['Teklif Mektubu (Kapak)', 'Firma Tanıtımı ve Yeterlilik', 'Teknik Teklif — Kapsam ve Yaklaşım', 'Ürün / Hizmet Uygunluğu (Şartname Maddeleriyle)', 'Teslim Planı ve Kalite Güvencesi', 'Ticari Teklif — Fiyat Özeti', 'Ödeme ve Teslim Koşulları', 'Teklif Geçerlilik Süresi ve Teminat', 'Ekler'],
|
||||
};
|
||||
|
||||
export async function teklifTaslak({ ikn, tur, tender, firma, belgeler, fiyat, gereklilikler, chain, text }) {
|
||||
export async function teklifTaslak({ ikn, tur, tender, firma, belgeler, fiyat, gereklilikler, chain, text, signal }) {
|
||||
const istenen = BOLUMLER[tur] || BOLUMLER.tam;
|
||||
const { text: metin } = await ihaleMetni(ikn, text, tender?.aciklama);
|
||||
const liste = gereklilikMetni(tender, gereklilikler, metin);
|
||||
@@ -125,6 +125,6 @@ export async function teklifTaslak({ ikn, tur, tender, firma, belgeler, fiyat, g
|
||||
fiyat ? `FİYAT VERİSİ: ${typeof fiyat === 'string' ? fiyat : JSON.stringify(fiyat).slice(0, 1500)}` : null,
|
||||
].filter(Boolean).join('\n\n');
|
||||
const sistem = `Sen Türkiye kamu ihalelerinde (4734) deneyimli bir teklif yazarısın. Verilen bilgilerle resmî, kısa ve somut bir teklif dosyası taslağı yaz. Verilmeyen bilgiyi UYDURMA: firma adı, yetkili, tarih, fiyat, süre boşluklarına [DOLDURUN: ...] yaz. Fiyat yalnızca FİYAT VERİSİ varsa yazılır. Ekler bölümünde istenen belgeleri madde madde ver. Üslup resmî; abartı yok. Yalnızca JSON: {"bolumler":[{"baslik":"","icerik":""}]} — istenen bölümleri aynı sırada ver; bölüm başlıklarını değiştirmeden aynen kopyala.`;
|
||||
const out = await analyzeSpec({ text: `BAĞLAM:\n${baglam}\n\nİSTENEN BÖLÜMLER:\n${istenen.map((x, i) => `${i + 1}. ${x}`).join('\n')}\n\nYalnızca JSON döndür.`, tender, chain, systemOverride: sistem, mode: 'derin' });
|
||||
const out = await analyzeSpec({ text: `BAĞLAM:\n${baglam}\n\nİSTENEN BÖLÜMLER:\n${istenen.map((x, i) => `${i + 1}. ${x}`).join('\n')}\n\nYalnızca JSON döndür.`, tender, chain, signal, systemOverride: sistem, mode: 'derin' });
|
||||
return qualifyOfferDraft(out?.result, istenen);
|
||||
}
|
||||
|
||||
+10
-4
@@ -317,11 +317,12 @@ function kesikJsonOnar(s) {
|
||||
}
|
||||
|
||||
/** Zinciri sırayla dener; ilk başarılı sonucu döndürür. */
|
||||
export async function analyzeSpec({ text, fileName, tender, chain, systemOverride, mode, extraction, _singlePart = false }) {
|
||||
export async function analyzeSpec({ text, fileName, tender, chain, systemOverride, mode, extraction, signal, _singlePart = false }) {
|
||||
signal?.throwIfAborted();
|
||||
if (!_singlePart && String(text || '').length > 24000) {
|
||||
return analyzeInParts({text,fileName,tender,mode,extraction}, part => analyzeSpec({
|
||||
return analyzeInParts({text,fileName,tender,mode,extraction,signal}, part => analyzeSpec({
|
||||
text:part.text,fileName:`${fileName || 'Doküman'} — bölüm ${part.index+1}`,
|
||||
tender:tender ? {...tender,items:[]} : undefined,chain,mode,_singlePart:true,
|
||||
tender:tender ? {...tender,items:[]} : undefined,chain,mode,signal,_singlePart:true,
|
||||
systemOverride:(systemOverride || (mode === 'derin' ? DERIN_SYSTEM_PROMPT : SYSTEM_PROMPT)) + '\nBu metin büyük dokümanın bir bölümüdür. Yalnız bu bölümde görülen koşulları çıkar; bu bölümde bulunmayan şartın bütün dokümanda bulunmadığı sonucunu çıkarma.',
|
||||
}));
|
||||
}
|
||||
@@ -342,6 +343,7 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid
|
||||
const attempts = [];
|
||||
|
||||
for (const id of order) {
|
||||
signal?.throwIfAborted();
|
||||
const p = PROVIDERS[id];
|
||||
const route = routeFor(id);
|
||||
if (!route) {
|
||||
@@ -349,6 +351,8 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid
|
||||
continue;
|
||||
}
|
||||
const ac = new AbortController();
|
||||
const cancel = () => ac.abort();
|
||||
signal?.addEventListener('abort',cancel,{once:true});
|
||||
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
||||
try {
|
||||
let raw;
|
||||
@@ -361,6 +365,7 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid
|
||||
} else {
|
||||
raw = await callOpenAiStyle(p, route.key, route.model, userPrompt, ac.signal, sistem, maxTokens);
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
const parsed = extractJson(raw);
|
||||
const result = qualifyAnalysis(parsed,{text,fileName,extraction,limit:derin ? 90000 : 60000,
|
||||
expectedItems:tender?.items?.length || 0,repaired:parsed._outputRepaired === true});
|
||||
@@ -372,9 +377,10 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid
|
||||
attempts,
|
||||
};
|
||||
} catch (e) {
|
||||
signal?.throwIfAborted();
|
||||
attempts.push({ id, ok: false, yol: route.yol, error: e.name === 'AbortError' ? 'zaman aşımı' : e.message });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
clearTimeout(timer);signal?.removeEventListener('abort',cancel);
|
||||
}
|
||||
}
|
||||
const err = new Error('Tüm AI sağlayıcıları başarısız oldu: ' + attempts.map((a) => `${a.id}(${a.error})`).join(', '));
|
||||
|
||||
@@ -17,6 +17,7 @@ export async function analyzeInParts(input,runPart) {
|
||||
const parts=splitAnalysisText(input.text),results=[],failures=[];
|
||||
// Two requests at a time bounds provider pressure while processing every part.
|
||||
for(let i=0;i<parts.length;i+=2) {
|
||||
input.signal?.throwIfAborted();
|
||||
await Promise.all(parts.slice(i,i+2).map(async part=>{
|
||||
try {
|
||||
const out=await runPart(part);
|
||||
@@ -26,6 +27,7 @@ export async function analyzeInParts(input,runPart) {
|
||||
catch(error){failures.push({part,error:String(error.message || error)});}
|
||||
}));
|
||||
}
|
||||
input.signal?.throwIfAborted();
|
||||
results.sort((a,b)=>a.part.index-b.part.index);
|
||||
if(!results.length)throw new Error('Dokümanın hiçbir parçası analiz edilemedi.');
|
||||
const merged={},issues=failures.map(f=>`${f.part.index+1}. parça analiz edilemedi.`);
|
||||
|
||||
+5
-2
@@ -1,3 +1,4 @@
|
||||
import { requestLifetime } from './request-lifetime.mjs';
|
||||
import {documentReview,saveDocumentReview,reviewedAnalysisSource} from './document-review.mjs';
|
||||
import { documentMetadataPatch, decodeDocumentUpload } from './document-metadata.mjs';
|
||||
import { deadlineInfo } from '../src/services/tenderCalendar.js';
|
||||
@@ -1457,9 +1458,10 @@ const server = http.createServer(async (req, res) => {
|
||||
const user = userFromAuthHeader(req.headers['authorization']);
|
||||
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
|
||||
if (!aiConfigured()) return json(res, 503, { success: false, error: 'AI hizmeti şu anda kullanılamıyor. Daha sonra yeniden deneyin; sorun sürerse uygulama yöneticinize bildirin.' });
|
||||
const lifetime = requestLifetime(req,res);
|
||||
try {
|
||||
const body = await readBody(req, 2 * 1024 * 1024);
|
||||
const ortak = { ikn: body.ikn, tender: body.tender, chain: body.chain, text: body.text };
|
||||
const ortak = { signal:lifetime.signal, ikn: body.ikn, tender: body.tender, chain: body.chain, text: body.text };
|
||||
const out = url.pathname === '/api/ai/belge-sor'
|
||||
? await belgeyeSor({ ...ortak, soru: body.soru })
|
||||
: url.pathname === '/api/ai/teklif-denetim'
|
||||
@@ -1467,8 +1469,9 @@ const server = http.createServer(async (req, res) => {
|
||||
: await teklifTaslak({ ...ortak, tur: body.tur, firma: body.firma, belgeler: body.belgeler, fiyat: body.fiyat, gereklilikler: body.gereklilikler });
|
||||
return json(res, 200, { success: true, ...out });
|
||||
} catch (e) {
|
||||
if (lifetime.signal.aborted) return;
|
||||
return json(res, e.attempts ? 502 : 400, { success: false, error: e.message });
|
||||
}
|
||||
} finally { lifetime.dispose(); }
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/tenders') {
|
||||
|
||||
@@ -2,7 +2,8 @@ import {splitAnalysisText} from './analysis-parts.mjs';
|
||||
import {qualifyOfferAudit} from './offer-audit.mjs';
|
||||
|
||||
/** Audit every supplied requirement against every draft part, without generic AI-schema merging. */
|
||||
export async function auditOfferInParts(requirements, draft, run) {
|
||||
export async function auditOfferInParts(requirements, draft, run, {signal} = {}) {
|
||||
signal?.throwIfAborted();
|
||||
const groups=[];let current=[],size=0;
|
||||
for(const requirement of requirements) {
|
||||
if(requirement.length>8000) throw new Error('Tek bir denetim gerekliliği çok uzun. Kaynak maddeyi ayrı gerekliliklere ayırın.');
|
||||
@@ -11,12 +12,13 @@ export async function auditOfferInParts(requirements, draft, run) {
|
||||
}
|
||||
if(current.length)groups.push(current);
|
||||
const parts=splitAnalysisText(draft,10000),jobs=groups.flatMap((group,g)=>parts.map(part=>({group,g,part}))),results=[];
|
||||
for(let i=0;i<jobs.length;i+=2) await Promise.all(jobs.slice(i,i+2).map(async job=>{
|
||||
for(let i=0;i<jobs.length;i+=2) { signal?.throwIfAborted(); await Promise.all(jobs.slice(i,i+2).map(async job=>{
|
||||
try {
|
||||
const output=await run(job.group,job.part.text);
|
||||
results.push({...job,review:qualifyOfferAudit(output,job.group,job.part.text)});
|
||||
} catch(error){results.push({...job,error:String(error.message || error)});}
|
||||
}));
|
||||
})); }
|
||||
signal?.throwIfAborted();
|
||||
if(!results.some(result=>result.review))throw new Error('Taslağın hiçbir bölümü denetlenemedi. Yeniden deneyin.');
|
||||
const merged=[];
|
||||
for(const [g,group] of groups.entries()) for(const requirement of group) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/** Request completion is normal; premature response closure cancels dependent work. */
|
||||
export function requestLifetime(req, res) {
|
||||
const controller = new AbortController();
|
||||
const abort = () => controller.abort();
|
||||
const closed = () => {if (!res.writableFinished) abort();};
|
||||
req.once('aborted',abort);res.once('close',closed);
|
||||
if(req.aborted || res.destroyed) abort();
|
||||
return {signal:controller.signal,dispose(){req.off('aborted',abort);res.off('close',closed);}};
|
||||
}
|
||||
Reference in New Issue
Block a user