Preserve question source chunks and reject fabricated citations
This commit is contained in:
+9
-17
@@ -1,3 +1,5 @@
|
||||
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
|
||||
@@ -18,17 +20,7 @@ export function terimler(soru) {
|
||||
return [...new Set(norm(soru).split(' ').filter((k) => k && !DURAK.has(k) && (k.length >= 3 || /^\d+$/.test(k))))].slice(0, 12);
|
||||
}
|
||||
|
||||
/** Metni ~1200 karakterlik parçalara böler (paragraf sınırlarında). */
|
||||
export function parcala(metin, hedef = 1200) {
|
||||
const p = []; let tampon = '';
|
||||
for (const satir of String(metin ?? '').replace(/\r/g, '').split('\n')) {
|
||||
if (tampon.length + satir.length > hedef * 1.5 && tampon.trim()) { p.push(tampon.trim()); tampon = ''; }
|
||||
tampon += satir + '\n';
|
||||
if (tampon.length >= hedef && satir.trim() === '') { p.push(tampon.trim()); tampon = ''; }
|
||||
}
|
||||
if (tampon.trim().length >= 40) p.push(tampon.trim());
|
||||
return p;
|
||||
}
|
||||
export const parcala = questionChunks;
|
||||
|
||||
/** Terim kapsamasına göre en iyi k parça. */
|
||||
export function getir(parcalar, soru, k = 8) {
|
||||
@@ -68,13 +60,13 @@ export async function belgeyeSor({ ikn, soru, text, tender, chain }) {
|
||||
if (!metin) throw new Error('Bu ihale için doküman metni bulunamadı — PDF\'i "Yapay Zeka Asistanı"ndan yükleyip tekrar sorun.');
|
||||
const parcalar = parcala(metin);
|
||||
const kaynaklar = getir(parcalar, q, 8);
|
||||
if (!kaynaklar.length) return { cevap: 'Belgede bu soruyla eşleşen bir bölüm bulunamadı; belgedeki terimlerle (teminat, teslim, ceza, adet) yeniden deneyin.', kaynaklar: [], kaynak };
|
||||
const baglam = kaynaklar.map((k) => `[${k.n}]\n${k.icerik.slice(0, 1400)}`).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 "Belgede bu bilgi bulunamadı" de, 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":"..."}`;
|
||||
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 cevap = String(out?.result?.cevap ?? '').trim() || 'Yanıt üretilemedi.';
|
||||
const kullanilan = new Set([...cevap.matchAll(/\[(\d+)\]/g)].map((m) => Number(m[1])));
|
||||
return { cevap, kaynaklar: (kullanilan.size ? kaynaklar.filter((k) => kullanilan.has(k.n)) : kaynaklar.slice(0, 3)).map((k) => ({ n: k.n, alinti: k.icerik.slice(0, 1400) })), kaynak };
|
||||
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.'};
|
||||
}
|
||||
|
||||
function gereklilikMetni(tender, gereklilikler, metin) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/** Lossless bounded chunks, including a short last clause and long unbroken lines. */
|
||||
export function questionChunks(value, target = 1200) {
|
||||
if (!Number.isInteger(target) || target < 32 || target > 1400) throw new Error('Geçersiz kaynak parçası boyutu.');
|
||||
const text = String(value ?? '');
|
||||
const chunks = [];
|
||||
let offset = 0;
|
||||
while (offset < text.length) {
|
||||
let end = Math.min(offset + target, text.length);
|
||||
if (end < text.length) {
|
||||
const segment = text.slice(offset, end);
|
||||
const boundary = Math.max(segment.lastIndexOf('\n'), segment.lastIndexOf(' '));
|
||||
if (boundary >= target / 2) end = offset + boundary + 1;
|
||||
// A Unicode surrogate pair is one character, never two source fragments.
|
||||
if (/[\uD800-\uDBFF]/.test(text[end - 1]) && /[\uDC00-\uDFFF]/.test(text[end])) end--;
|
||||
}
|
||||
chunks.push(text.slice(offset, end));
|
||||
offset = end;
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/** Reject fabricated references rather than attaching unrelated fallback sources. */
|
||||
export function qualifyQuestionAnswer(result, selected) {
|
||||
if (typeof result?.cevap !== 'string' || !result.cevap.trim())
|
||||
throw new Error('Model geçerli bir yanıt üretmedi. Yeniden deneyin.');
|
||||
const cevap = result.cevap.trim();
|
||||
const cited = [...new Set([...cevap.matchAll(/\[(\d+)\]/g)].map(match => Number(match[1])))];
|
||||
if (!cited.length) throw new Error('Yanıt kaynak parçasına atıf içermiyor. Yeniden deneyin.');
|
||||
const available = new Map(selected.map(part => [part.n, part]));
|
||||
if (cited.some(id => !available.has(id)))
|
||||
throw new Error('Yanıttaki kaynak numarası incelenen parçalarla uyuşmuyor. Yeniden deneyin.');
|
||||
return {cevap, kaynaklar:cited.map(n => ({n, alinti:available.get(n).icerik}))};
|
||||
}
|
||||
Reference in New Issue
Block a user