Audit all supplied requirements across complete long drafts

This commit is contained in:
2026-09-06 10:20:37 +03:00
parent c3ec8c8ba8
commit 673d6c1bc8
7 changed files with 89 additions and 9 deletions
+12 -9
View File
@@ -1,7 +1,7 @@
import { auditOfferInParts } from './offer-audit-parts.mjs';
import { qualifyOfferDraft } from './offer-draft.mjs';
import { createHash } from 'node:crypto';
import { questionChunks, qualifyQuestionAnswer } from './document-question.mjs';
import { qualifyOfferAudit } from './offer-audit.mjs';
import { documentContent } from './document-content.mjs';
// Teklif asistanı — İhaleAB'den taşınan üç AI özelliği, ihaleist'in mevcut
// analyzeSpec zinciri (yedekli sağlayıcılar) üzerinde:
@@ -73,12 +73,12 @@ export async function belgeyeSor({ ikn, soru, text, tender, chain }) {
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.'};
}
function gereklilikMetni(tender, gereklilikler, metin) {
export function gereklilikMetni(tender, gereklilikler, metin, allItems = false) {
const g = [];
for (const x of Array.isArray(gereklilikler) ? gereklilikler : []) if (x) g.push(typeof x === 'string' ? x : x.belge || x.item || x.ad || JSON.stringify(x));
for (const y of Array.isArray(tender?.yeterlikKriterleri) ? tender.yeterlikKriterleri : []) if (y) g.push(String(y));
for (const i of Array.isArray(tender?.items) ? tender.items.slice(0, 30) : []) g.push(`Kalem: ${i.ad} ${i.miktar ?? ''} ${i.birim ?? ''}`.trim());
if (tender?.ikn && kalemStore.available()) for (const k of ((kalemStore.kalemOku(tender.ikn) || {}).kalemler || []).slice(0, 30)) g.push(`Kalem: ${k.ad} ${k.miktar ?? ''} ${k.birim ?? ''}`.trim());
for (const i of Array.isArray(tender?.items) ? tender.items.slice(0, allItems ? undefined : 30) : []) g.push(`Kalem: ${i.ad} ${i.miktar ?? ''} ${i.birim ?? ''}`.trim());
if (tender?.ikn && kalemStore.available()) for (const k of ((kalemStore.kalemOku(tender.ikn) || {}).kalemler || []).slice(0, allItems ? undefined : 30)) g.push(`Kalem: ${k.ad} ${k.miktar ?? ''} ${k.birim ?? ''}`.trim());
if (metin) {
// Önce anahtar sözcüğü geçen SATIR (madde) alınır; bütün metni yapıştırmak okunmaz oluyordu.
const satirlar = String(metin).split(/\r?\n+/).map((x) => x.trim()).filter((x) => x.length > 15);
@@ -89,18 +89,21 @@ function gereklilikMetni(tender, gereklilikler, metin) {
if (p) g.push(`${s}: ${p.icerik.slice(0, 300).replace(/\s+/g, ' ')}`);
}
}
return [...new Set(g)].slice(0, 60);
return [...new Set(g)].slice(0, allItems ? undefined : 60);
}
export async function teklifDenetle({ ikn, taslak, gereklilikler, tender, chain, text }) {
const t = String(taslak ?? '').trim().slice(0, 60000);
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);
const liste = gereklilikMetni(tender, gereklilikler, metin);
const liste = gereklilikMetni(tender, gereklilikler, metin, true);
if (!liste.length) throw new Error('Gereklilik listesi yok — önce derin analiz çalıştırın veya dokümanı yükleyin.');
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.`;
const out = await analyzeSpec({ text: `GEREKLİLİKLER:\n${liste.map((g, i) => `${i + 1}. ${g}`).join('\n')}\n\nTEKLİF TASLAĞI:\n${t}\n\nYalnızca JSON döndür.`, tender, chain, systemOverride: sistem, mode: 'derin' });
return qualifyOfferAudit(out?.result, liste, t);
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});
return out?.result;
});
}
const BOLUMLER = {
+34
View File
@@ -0,0 +1,34 @@
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) {
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.');
if(current.length && size+requirement.length>8000){groups.push(current);current=[];size=0;}
current.push(requirement);size+=requirement.length+10;
}
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=>{
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)});}
}));
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) {
const matching=results.filter(result=>result.g===g);
const rows=matching.flatMap(result=>result.review?.maddeler.filter(row=>row.gereklilik===requirement) || []);
const verified=rows.find(row=>row.durum==='karsilaniyor');
const failed=matching.some(result=>result.error);
merged.push(failed ? {gereklilik:requirement,durum:'belirsiz',oneri:'Taslağın bazı bölümleri denetlenemedi. Yeniden deneyin.'}
: verified || (rows.length===parts.length && rows.every(row=>row.durum==='eksik')
? {gereklilik:requirement,durum:'eksik'} : {gereklilik:requirement,durum:'belirsiz'}));
}
const review=qualifyOfferAudit({maddeler:merged},requirements,draft);
return {...review,auditReview:{draftParts:parts.length,requirementGroups:groups.length,failedCalls:results.filter(row=>row.error).length},
kapsam:review.kapsam+' Uzun taslak bölümler halinde kontrol edilir; bölümler arasına yayılan bir kanıt elle kontrol gerektirebilir.'};
}