Preserve full draft sections and disclose missing requested content
This commit is contained in:
@@ -451,3 +451,13 @@ Model yanıtında boş/geçersiz metin, hiç atıf olmaması veya modele sunulma
|
||||
Sınır: Atıf numarasının geçerliliği semantik doğruluk kanıtı değildir. Terim tabanlı ilk sekiz parçanın seçimi başka bölümdeki ek/çelişen koşulu kaçırabilir. Tam doküman/ortak kaynak modeli ve gerçek sağlayıcı kabulü halen açık.
|
||||
|
||||
Doğrulama: uzun tek satır ve kısa son madde, Unicode sınırları, geçersiz boyut, sahte/eksik kaynak numarası ve yalnız gerçek atıfların tam içeriğini döndürme için dört test. Tüm 95 test ve üretim derlemesi başarılı. Mevcut büyük paket uyarıları sürüyor. Canlıya yayın yapılmadı.
|
||||
|
||||
### D06 — Üretilen taslağın bölüm bütünlüğü
|
||||
|
||||
Taslak üretimi önceki kodda istenen bölüm sayısını kontrol etmiyor, tek geçerli bölüm varsa başarılı yanıt dönüyor ve her bölümün içeriğini 12.000 karakterde sessizce kesiyordu. Yeni `qualifyOfferDraft` istenen başlıkları tekil metin eşleşmesiyle sıralar. Eksik/boş başlıklar için açık DOLDURUN alanları ve `partial` durumu döndürür. Eşleşmeyen ek içerik kaybolmaz fakat istenen bölümün yerine sayılmaz. Yinelenen başlık ve nesne gibi geçersiz alan türleri hata verir. Uzun bölümün son koşulları korunur.
|
||||
|
||||
Ekran, modele göre tamamlanmayan bölümleri ve eşleşmeyen ek bölümleri listeler. Tam bölüm kümesi bile yalnız `draft` durumudur; resmi hazır/onaylı durumu üretilmez. Kullanıcı düzenlediğinde ilk üretim kapsamı uyarısı otomatik doğrulanmış sayılmaz.
|
||||
|
||||
Doğrulama: eksik bölüm, doğru sıra, 12.000 karakterden uzun içerik ve son koşul, fazladan başlık, yinelenen/bozuk bölüm için dört yeni test. Tam test ve build sonuçları aşağıdaki commit öncesi kontrolle doğrulandı. Gerçek sağlayıcı bölüm üretimi ve tarayıcı kabulü halen açık; ortak gereklilik kaynağının kapsam sınırları bu değişiklikle kapanmış sayılmaz.
|
||||
|
||||
Sonuç: 99 test geçti; üretim derlemesi başarılı. Büyük paket uyarıları sürüyor. Canlı yayın yapılmadı.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { qualifyOfferDraft } from './offer-draft.mjs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { questionChunks, qualifyQuestionAnswer } from './document-question.mjs';
|
||||
import { qualifyOfferAudit } from './offer-audit.mjs';
|
||||
@@ -117,11 +118,7 @@ export async function teklifTaslak({ ikn, tur, tender, firma, belgeler, fiyat, g
|
||||
Array.isArray(belgeler) && belgeler.length ? 'DEPODAKİ BELGELER:\n' + belgeler.slice(0, 40).map((b) => `- ${b.ad || b}${b.kategori ? ' [' + b.kategori + ']' : ''}`).join('\n') : null,
|
||||
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.`;
|
||||
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 bolumler = (Array.isArray(out?.result?.bolumler) ? out.result.bolumler : [])
|
||||
.map((b) => ({ baslik: String(b?.baslik ?? '').slice(0, 200), icerik: String(b?.icerik ?? '').slice(0, 12000) }))
|
||||
.filter((b) => b.baslik && b.icerik);
|
||||
if (!bolumler.length) throw new Error('Model bölüm üretmedi; tekrar deneyin.');
|
||||
return { bolumler };
|
||||
return qualifyOfferDraft(out?.result, istenen);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const normalize = value => value.normalize('NFC').replace(/\s+/g, ' ').trim();
|
||||
|
||||
/** Preserve useful draft text, while making missing requested sections explicit. */
|
||||
export function qualifyOfferDraft(result, requested) {
|
||||
if (!Array.isArray(result?.bolumler) || !result.bolumler.length)
|
||||
throw new Error('Model bölüm üretmedi; tekrar deneyin.');
|
||||
const byTitle = new Map();
|
||||
for (const row of result.bolumler) {
|
||||
if (typeof row?.baslik !== 'string' || typeof row?.icerik !== 'string' || !row.baslik.trim())
|
||||
throw new Error('Model geçersiz bir taslak bölümü döndürdü. Yeniden deneyin.');
|
||||
const key = normalize(row.baslik);
|
||||
if (byTitle.has(key)) throw new Error('Model aynı taslak bölümünü birden fazla kez döndürdü. Yeniden deneyin.');
|
||||
byTitle.set(key, {baslik:row.baslik.trim(), icerik:row.icerik.trim()});
|
||||
}
|
||||
const missingSections = [];
|
||||
const bolumler = requested.map(baslik => {
|
||||
const key = normalize(baslik), found = byTitle.get(key);
|
||||
byTitle.delete(key);
|
||||
if (found?.icerik) return {...found, baslik};
|
||||
missingSections.push(baslik);
|
||||
return {baslik, icerik:`[DOLDURUN: ${baslik} bölümü model tarafından tamamlanmadı. Kaynak belgelerle hazırlayın.]`};
|
||||
});
|
||||
// Keep unexpected useful content; it cannot substitute for a missing section.
|
||||
const extraSections = [...byTitle.values()].filter(row => row.icerik);
|
||||
return {bolumler:[...bolumler, ...extraSections], draftReview:{
|
||||
status:missingSections.length || extraSections.length ? 'partial' : 'draft',
|
||||
requestedSections:requested, missingSections, extraSections:extraSections.map(row=>row.baslik),
|
||||
note:'Bu çıktı düzenlenebilir bir taslaktır. Bölümlerin bulunması, bilgilerin doğru veya resmi teklif dosyasının hazır olduğunu göstermez. Kaynak, firma bilgileri, fiyatlar ve zorunlu belgeler ayrıca kontrol edilmelidir.',
|
||||
}};
|
||||
}
|
||||
@@ -38,6 +38,7 @@ const OfferAssistantSession = ({ tender }) => {
|
||||
const [denetim, setDenetim] = useState(null);
|
||||
const [tur, setTur] = useState('tam');
|
||||
const [bolumler, setBolumler] = useState([]);
|
||||
const [draftReview, setDraftReview] = useState(null);
|
||||
const [mesgul, setMesgul] = useState(false);
|
||||
const [hata, setHata] = useState(null);
|
||||
|
||||
@@ -58,7 +59,7 @@ const OfferAssistantSession = ({ tender }) => {
|
||||
});
|
||||
const uret = () => calistir(async () => {
|
||||
const result = await post('/api/ai/teklif-taslak', {ikn:tender.ikn, tur, tender, text:metin || undefined}, listDocuments);
|
||||
if (current()) setBolumler(result.bolumler || []);
|
||||
if (current()) { setBolumler(result.bolumler || []); setDraftReview(result.draftReview || null); }
|
||||
});
|
||||
const kopyala = (t) => { if (current()) navigator.clipboard?.writeText(t).catch(() => {}); };
|
||||
const dosyaOku = (f) => {
|
||||
@@ -154,6 +155,12 @@ const OfferAssistantSession = ({ tender }) => {
|
||||
</button>
|
||||
{bolumler.length > 0 && <button type="button" onClick={() => kopyala(bolumler.map((b) => `${b.baslik}\n\n${b.icerik}`).join('\n\n\n'))} className="text-xs text-slate-600 border border-slate-200 rounded-lg px-2 py-1.5 flex items-center gap-1 hover:bg-slate-50"><Copy className="w-3.5 h-3.5" /> Tümünü kopyala</button>}
|
||||
</div>
|
||||
{draftReview && <div role="status" className="rounded-lg border border-amber-200 bg-amber-50 p-3 text-xs text-amber-900">
|
||||
<p className="font-semibold">{draftReview.status === 'partial' ? 'Kısmi taslak — bölüm kontrolü gerekli' : 'Taslak — kullanıcı kontrolü gerekli'}</p>
|
||||
<p>{draftReview.note}</p>
|
||||
{draftReview.missingSections?.length > 0 && <p>Modelin tamamlamadığı bölümler: {draftReview.missingSections.join(', ')}. Aşağıdaki DOLDURUN alanlarını düzenleyin.</p>}
|
||||
{draftReview.extraSections?.length > 0 && <p>İstenen başlıklara eşleşmeyen ek bölümler: {draftReview.extraSections.join(', ')}.</p>}
|
||||
</div>}
|
||||
{bolumler.map((b, i) => (
|
||||
<div key={i} className="space-y-1">
|
||||
<input value={b.baslik} onChange={(e) => setBolumler((p) => p.map((x, k) => (k === i ? { ...x, baslik: e.target.value } : x)))} className="w-full border border-slate-200 rounded-lg px-2 py-1 text-sm font-bold" />
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import {test} from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {qualifyOfferDraft} from '../server/offer-draft.mjs';
|
||||
const requested=['Kapak','Teknik','Ekler'];
|
||||
test('missing sections are explicit editable placeholders, not a complete dossier',()=>{
|
||||
const out=qualifyOfferDraft({bolumler:[{baslik:'Kapak',icerik:'Kapak metni'}]},requested);
|
||||
assert.equal(out.draftReview.status,'partial');
|
||||
assert.deepEqual(out.draftReview.missingSections,['Teknik','Ekler']);
|
||||
assert.match(out.bolumler[1].icerik,/DOLDURUN/);assert.equal(out.bolumler[0].icerik,'Kapak metni');
|
||||
});
|
||||
test('long sections retain their last conditions and requested section order',()=>{
|
||||
const text='Teknik şart '.repeat(1500)+'SON KOŞUL: Numune zorunludur.';
|
||||
const out=qualifyOfferDraft({bolumler:[{baslik:'Ekler',icerik:'Ekler metni'},{baslik:'Teknik',icerik:text},{baslik:'Kapak',icerik:'Kapak'}]},requested);
|
||||
assert.deepEqual(out.bolumler.map(row=>row.baslik),requested);
|
||||
assert.equal(out.bolumler[1].icerik,text);assert.equal(out.draftReview.status,'draft');
|
||||
});
|
||||
test('unexpected content is preserved but never substitutes for a requested section',()=>{
|
||||
const out=qualifyOfferDraft({bolumler:[{baslik:'Başka bölüm',icerik:'Ek içerik'}]},requested);
|
||||
assert.equal(out.draftReview.status,'partial');assert.equal(out.draftReview.missingSections.length,3);
|
||||
assert.equal(out.bolumler.at(-1).icerik,'Ek içerik');
|
||||
});
|
||||
test('malformed or repeated sections cannot silently overwrite user-visible content',()=>{
|
||||
for(const bolumler of [[],[null],[{baslik:'Kapak',icerik:{text:'invalid'}}],[{baslik:'Kapak',icerik:'a'},{baslik:'Kapak',icerik:'b'}]])
|
||||
assert.throws(()=>qualifyOfferDraft({bolumler},requested));
|
||||
});
|
||||
Reference in New Issue
Block a user