feat: verify analysis citations against source text
This commit is contained in:
+1
-1
@@ -336,7 +336,7 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid
|
||||
// Derin analiz hem daha çok metin okur hem daha uzun JSON üretir; çıktı
|
||||
// kesilirse JSON parse edilemez ve zincir boşuna tüketilir.
|
||||
const userPrompt = buildUserPrompt({ text, fileName, tender, limit: derin ? 90000 : 60000 });
|
||||
const sistem = systemOverride || (derin ? DERIN_SYSTEM_PROMPT : SYSTEM_PROMPT);
|
||||
const sistem = (systemOverride || (derin ? DERIN_SYSTEM_PROMPT : SYSTEM_PROMPT)) + '\nYanıta citations dizisi ekle: her kritik bulgu, kalem ve belge için {"path":"itemsAndSpecs.0","quote":"kaynak metinden aynen alınan 12-1500 karakterlik dayanak"}. path ilgili çıktı dizisinin sıfır tabanlı indeksidir. Alıntı metnini değiştirme, sayfa veya madde uydurma. Kaynakta dayanak yoksa alıntı üretme.';
|
||||
const maxTokens = derin ? 12000 : 4000;
|
||||
const timeoutMs = derin ? 150000 : 90000;
|
||||
const attempts = [];
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
const fields=new Set(['itemsAndSpecs','documentChecklist','qualificationCriteria','criticalRequirements','risksAndPenalties','criticalDetails','disqualificationRisks','deadlines','roadmap']);
|
||||
export function verifyAnalysisCitations(result,source) {
|
||||
const evidence=[];
|
||||
for(const citation of Array.isArray(result.citations)?result.citations:[]) {
|
||||
const match=/^([A-Za-z]+)\.(\d+)$/.exec(String(citation?.path || ''));
|
||||
if(!match || !fields.has(match[1]) || !Array.isArray(result[match[1]]) || Number(match[2])>=result[match[1]].length)continue;
|
||||
const quote=String(citation.quote || '').trim();
|
||||
if(quote.length<12 || quote.length>1500)continue;
|
||||
const start=source.indexOf(quote);
|
||||
evidence.push({path:citation.path,quote,status:start<0?'not_found':source.indexOf(quote,start+1)>=0?'ambiguous':'located',
|
||||
start:start<0?null:start,end:start<0?null:start+quote.length});
|
||||
}
|
||||
const missing=[];
|
||||
for(const field of fields)for(let i=0;i<(Array.isArray(result[field])?result[field].length:0);i++)
|
||||
if(!evidence.some(e=>e.path===`${field}.${i}` && e.status==='located'))missing.push(`${field}.${i}`);
|
||||
return {evidence,missing};
|
||||
}
|
||||
@@ -24,16 +24,23 @@ export async function analyzeInParts(input,runPart) {
|
||||
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.`);
|
||||
const pathMap=new Map();
|
||||
for(const field of arrayFields) {
|
||||
if(field==='itemsAndSpecs') {
|
||||
// Identical rows may be separate official items; retain them with origin.
|
||||
merged[field]=results.flatMap(({part,out})=>(out.result[field] || []).map(row=>({...row,sourcePart:part.index+1,sourceRange:{start:part.start,end:part.end}})));
|
||||
continue;
|
||||
}
|
||||
merged[field]=[];
|
||||
const entries=new Map();
|
||||
for(const {out} of results) for(const value of out.result[field] || [])entries.set(JSON.stringify(value),value);
|
||||
merged[field]=[...entries.values()];
|
||||
for(const {part,out} of results) for(const [i,value] of (out.result[field] || []).entries()) {
|
||||
const key=JSON.stringify(value);
|
||||
let index=field==='itemsAndSpecs'?undefined:entries.get(key);
|
||||
if(index===undefined) {
|
||||
index=merged[field].length;entries.set(key,index);
|
||||
merged[field].push(field==='itemsAndSpecs'?{...value,sourcePart:part.index+1,sourceRange:{start:part.start,end:part.end}}:value);
|
||||
}
|
||||
pathMap.set(`${part.index}:${field}.${i}`,`${field}.${index}`);
|
||||
}
|
||||
}
|
||||
merged.citations=results.flatMap(({part,out})=>(out.result.citations || []).flatMap(c=>{
|
||||
const path=pathMap.get(`${part.index}:${c.path}`);return path?[{path,quote:c.quote}]:[];
|
||||
}));
|
||||
for(const field of ['summary','scopeOfWork'])merged[field]=results.map(({part,out})=>out.result[field] ? `Bölüm ${part.index+1}: ${out.result[field]}` : '').filter(Boolean).join('\n\n');
|
||||
// Different part-level prices/strategies cannot be treated as a global bid.
|
||||
merged.bidAnalysis=null;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { verifyAnalysisCitations } from './analysis-citations.mjs';
|
||||
import {createHash} from 'node:crypto';
|
||||
export function qualifyAnalysis(result,{text='',limit=60000,fileName='',expectedItems=0,repaired=false}={}) {
|
||||
if(!result || typeof result !== 'object' || Array.isArray(result)) throw new Error('Analiz nesne biçiminde değil.');
|
||||
const clean={...result};delete clean._outputRepaired;
|
||||
if(clean.citations != null && !Array.isArray(clean.citations)) throw new Error('Kaynak alıntıları liste biçiminde değil.');
|
||||
const sections={};
|
||||
for(const key of ['itemsAndSpecs','documentChecklist','qualificationCriteria','risksAndPenalties','roadmap']) {
|
||||
if(clean[key] != null && !Array.isArray(clean[key])) throw new Error(`${key} liste biçiminde değil.`);
|
||||
@@ -11,8 +13,9 @@ export function qualifyAnalysis(result,{text='',limit=60000,fileName='',expected
|
||||
if(source.length>limit)issues.push('Kaynak metnin tamamı analiz edilmedi.');
|
||||
if(repaired)issues.push('Analiz yanıtı kesildi; yalnız kurtarılabilen alanlar gösteriliyor.');
|
||||
if(expectedItems>0 && (clean.itemsAndSpecs?.length || 0)<expectedItems)issues.push(`Kaynakta ${expectedItems} kalem var; analiz listesi daha kısa.`);
|
||||
const citations=verifyAnalysisCitations(clean,source.slice(0,limit));
|
||||
const version=createHash('sha256').update(source).digest('hex');
|
||||
return {...clean,documentVersion:version,sourceReview:{status:issues.length?'partial':'review_required',
|
||||
return {...clean,sourceEvidence:citations.evidence,documentVersion:version,sourceReview:{status:issues.length?'partial':'review_required',
|
||||
fileName,sourceCharacters:source.length,analyzedCharacters:Math.min(source.length,limit),
|
||||
outputRepaired:repaired,sections,issues}};
|
||||
outputRepaired:repaired,sections,issues,unverifiedFindings:citations.missing}};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user