feat: derin analiz & yol haritasi + ihale dosyasi hazirlama; docx/xlsx/zip okunuyor
Doküman panelindeki "Tum Evraki Derin Analiz Et" dugmesi EKAP ilan metnini ve
ihaleye yuklenmis butun belgeleri TEK metinde birlestirip gonderiyor; model
sartnameyi ilanla ve eklerle capraz okuyabiliyor.
Derin analiz (mode='derin', ayri sistem istemi + 8000 token):
- Teklifi gecersiz kilabilecek noktalar (kirmizi uyari, en ustte)
- Gozden kacan ince detaylar (Kritik/Yuksek/Orta + madde no + neden onemli)
- Alinacak kalemler: aranan evsaflar, miktar, sartnamede geciyorsa fiyat
(modele "TAHMIN URETME" talimati verildi, yoksa null)
- Belge kontrol listesi, kritik tarihler, maliyet kalemleri
- Teklife kadar yol haritasi (faz / kim / ne zaman / cikti)
- Idareye sorulacak belirsizlikler
Standart hizli ozet modu aynen duruyor.
Ihale Dosyasi Hazirla (rapor sonunda, acilir bolum):
- Eksik firma bilgileri kullaniciya sorulur, profilden gelenler sorulmaz,
girilenler tarayicida saklanir
- Kalem fiyatlandirma tablosu, canli toplam + yaziyla karsiligi
- Teklif Mektubu, Birim Fiyat Teklif Cetveli, Evrak Kontrol Listesi
- Belgeler yazdirilabilir HTML: jsPDF'in gomulu fontlari WinAnsi, s/g/i/I
tasimiyor (bkz. services/pdf.js ASCII basliklari) — resmi teklif mektubunda
bozuk Turkce kabul edilemez
docx/xlsx/zip metin cikarimi (server/office-text.mjs):
- ihaleab projesindeki officeText.ts'ten tasindi, harici bagimlilik yok
- Sunucuda: tarayicida cozucu yok, baytlar zaten BELGE_DIR'de
- Tablolar " | " ile korunuyor, ic ice zip aciliyor, ~$ gecici dosyalari atlanir
- Yeni uc: GET /api/me/documents/text
- Yukleme kabul listesi .doc/.rar cikarildi (karsiligi yok), .xlsx eklendi
fix: yuklenen belgelerdeki "AI analiz" dugmesi bozuktu — extractText nesne
donduruyor ({text,pageCount,usedPages}), kod onu duz metin sanip gonderiyordu;
sunucuya "[object Object]" gidiyor ve analiz reddediliyordu. Uzunluk kontrolu
de nesne uzerinde undefined verdigi icin hatayi yutuyordu.
fix: analiz dokuman panelinden baslatildiginda modal kendi isAnalyzing'ini
bilmiyordu, kullanici 1-2 dakika bos modal goruyordu — ortak aiAnalyzing sinyali
This commit is contained in:
+92
-18
@@ -100,7 +100,75 @@ YALNIZCA geçerli JSON döndür, başka hiçbir metin yazma. Şema:
|
||||
}
|
||||
Metinde olmayan bilgiyi UYDURMA — bilinmiyorsa null veya boş dizi kullan.`;
|
||||
|
||||
function buildUserPrompt({ text, fileName, tender }) {
|
||||
/**
|
||||
* Derin analiz — teknik şartname + idari şartname + sözleşme tasarısı için.
|
||||
* Standart analizden farkı: gözden kaçan "ince detayları", teklifi geçersiz
|
||||
* kılan sebepleri, belge kontrol listesini ve teklife kadarki yol haritasını
|
||||
* ayrı ayrı üretir.
|
||||
*/
|
||||
export const DERIN_SYSTEM_PROMPT = `Sen Türkiye kamu ihale mevzuatı (4734 sayılı Kamu İhale Kanunu, 4735 sayılı Kamu İhale Sözleşmeleri Kanunu ve ikincil mevzuat) konusunda çok deneyimli bir ihale danışmanısın.
|
||||
Sana verilen teknik şartname / idari şartname / sözleşme tasarısı / ihale ilanı metnini madde madde incele.
|
||||
İki şey üret: (1) derinlemesine analiz, (2) teklif anına kadar izlenecek somut yol haritası.
|
||||
|
||||
Özellikle şunları ara — ihaleyi kaybettiren ama gözden kaçan detaylar bunlardır:
|
||||
- Teklif zarfında eksikse doğrudan değerlendirme dışı bırakılma sebepleri
|
||||
- Belgelerin tarih/süre koşulları (son X ay içinde alınmış, ihale tarihinden önceki Y gün, geçerlilik süresi)
|
||||
- Geçici/kesin teminat oranı, süresi, kabul edilen teminat türleri
|
||||
- Numune / demo / katalog / teknik doküman sunma zorunluluğu, teslim yeri ve anı
|
||||
- Yerli malı belgesi ve fiyat avantajı oranı
|
||||
- İş deneyim belgesi oranı, benzer iş tanımı, iş ortaklığında pilot ortak payı
|
||||
- Birim fiyat mı anahtar teslim götürü bedel mi; aritmetik hatanın sonucu
|
||||
- Kısmi teklif verilebilir mi, yeterlik kısım bazında mı aranıyor
|
||||
- Teslim süresi, montaj / eğitim / garanti / yedek parça yükümlülükleri
|
||||
- Gecikme cezası oranı, fesih ve yasaklama sonuçları
|
||||
- Fiyat farkı verilip verilmeyeceği
|
||||
- Zeyilname / açıklama talebi ve şikâyet başvurusu süreleri
|
||||
- Alınacak ürün/hizmetin belirleyici teknik evsafları, miktarları ve varsa birim fiyatları
|
||||
|
||||
YALNIZCA geçerli JSON döndür, başka hiçbir metin yazma. Şema:
|
||||
{
|
||||
"summary": "3-5 cümlelik yönetici özeti",
|
||||
"scopeOfWork": "işin kapsamı: ne alınıyor/yaptırılıyor, miktar, süre, yer",
|
||||
"disqualificationRisks": ["teklifin doğrudan değerlendirme dışı kalmasına yol açacak durumlar (en fazla 8)"],
|
||||
"criticalDetails": [
|
||||
{"level":"Kritik|Yüksek|Orta","title":"kısa başlık","where":"madde no veya bölüm","detail":"ince detayın kendisi","whyItMatters":"gözden kaçarsa ne olur"}
|
||||
],
|
||||
"itemsAndSpecs": [
|
||||
{"name":"kalem/ürün adı","quantity":"miktar ve birim (ör. 3 adet)","keySpecs":["teklifi belirleyen zorunlu teknik evsaf"],"estimatedUnitPrice":sayı_veya_null,"estimatedTotal":sayı_veya_null,"note":"marka/eşdeğer, garanti, montaj gibi kaleme özel not"}
|
||||
],
|
||||
"qualificationCriteria": ["istenen yeterlik belgeleri, madde madde (en fazla 15)"],
|
||||
"criticalRequirements": ["dikkat edilmesi gereken kritik teknik/idari şartlar (en fazla 12)"],
|
||||
"documentChecklist": [
|
||||
{"document":"belge adı","mandatory":true,"note":"nereden/nasıl temin edilir, tarih veya geçerlilik koşulu"}
|
||||
],
|
||||
"deadlines": [{"label":"neyin süresi","value":"tarih veya süre","note":"kaçırılırsa sonucu"}],
|
||||
"costDrivers": ["maliyeti asıl belirleyen kalemler (en fazla 8)"],
|
||||
"risksAndPenalties": [{"level":"Yüksek|Orta|Düşük","clause":"madde no veya bölüm","text":"risk açıklaması"}],
|
||||
"roadmap": [
|
||||
{"step":1,"phase":"Hazırlık|Belge Toplama|Teknik|Fiyatlandırma|Teklif|Sözleşme","action":"yapılacak somut iş","owner":"kim yapmalı","timing":"ne zaman / hangi tarihe kadar","output":"ortaya çıkacak çıktı"}
|
||||
],
|
||||
"questionsForAdministration": ["idareye sorulması / zeyilname istenmesi gereken belirsizlikler (en fazla 8)"],
|
||||
"bidAnalysis": {
|
||||
"estimatedCost": sayı_veya_null,
|
||||
"recommendedBid": sayı_veya_null,
|
||||
"minBidLimit": sayı_veya_null,
|
||||
"maxBidLimit": sayı_veya_null,
|
||||
"winProbability": 0-100_arası_sayı_veya_null,
|
||||
"competitionLevel": "rekabet seviyesi yorumu",
|
||||
"notes": "teklif stratejisi notu"
|
||||
}
|
||||
}
|
||||
|
||||
Kurallar:
|
||||
- criticalDetails en fazla 12, documentChecklist en fazla 18, deadlines en fazla 10, roadmap en fazla 10, itemsAndSpecs en fazla 20 madde olsun.
|
||||
- itemsAndSpecs'i teknik şartnamedeki kalem/miktar tablolarından çıkar; keySpecs'e yalnızca teklifi bağlayan zorunlu evsafları yaz (ör. "en az 17x17 inç dedektör", "IP54"), tanıtım cümlelerini yazma.
|
||||
- estimatedUnitPrice / estimatedTotal yalnızca metinde geçen bir yaklaşık maliyet, tahmini bedel veya birim fiyat varsa doldurulur; TAHMİN ÜRETME, yoksa null bırak. Sayıları yalnızca rakam olarak yaz (para birimi simgesi, nokta/virgül ayracı koyma).
|
||||
- roadmap adımlarını mantıklı sırayla ve step alanını 1'den başlayarak numaralandır.
|
||||
- Metinde olmayan bilgiyi UYDURMA — bilinmiyorsa null veya boş dizi kullan.
|
||||
- Madde/bölüm numarası metinde geçiyorsa "where" ve "clause" alanlarında mutlaka yaz.
|
||||
- Tüm metinleri Türkçe yaz.`;
|
||||
|
||||
function buildUserPrompt({ text, fileName, tender, limit = 60000 }) {
|
||||
const parts = [];
|
||||
if (fileName) parts.push(`Dosya: ${fileName}`);
|
||||
if (tender) {
|
||||
@@ -113,12 +181,12 @@ function buildUserPrompt({ text, fileName, tender }) {
|
||||
parts.push('Kalemler: ' + tender.items.map((i) => `${i.ad} (${i.miktar} ${i.birim})`).join('; '));
|
||||
}
|
||||
}
|
||||
parts.push('\n--- ŞARTNAME / İLAN METNİ ---\n' + String(text || '').slice(0, 60000));
|
||||
parts.push('\n--- ŞARTNAME / İLAN METNİ ---\n' + String(text || '').slice(0, limit));
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
// --- OpenRouter (OpenAI uyumlu) ---
|
||||
async function callOpenRouter(model, key, userPrompt, signal, sistem = SYSTEM_PROMPT) {
|
||||
async function callOpenRouter(model, key, userPrompt, signal, sistem = SYSTEM_PROMPT, maxTokens = 4000) {
|
||||
const res = await fetch(OPENROUTER_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -135,7 +203,7 @@ async function callOpenRouter(model, key, userPrompt, signal, sistem = SYSTEM_PR
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
temperature: 0.2,
|
||||
max_tokens: 4000,
|
||||
max_tokens: maxTokens,
|
||||
response_format: { type: 'json_object' },
|
||||
}),
|
||||
signal,
|
||||
@@ -146,7 +214,7 @@ async function callOpenRouter(model, key, userPrompt, signal, sistem = SYSTEM_PR
|
||||
return data?.choices?.[0]?.message?.content || '';
|
||||
}
|
||||
|
||||
async function callOpenAiStyle(p, key, model, userPrompt, signal, sistem = SYSTEM_PROMPT) {
|
||||
async function callOpenAiStyle(p, key, model, userPrompt, signal, sistem = SYSTEM_PROMPT, maxTokens = 4000) {
|
||||
const res = await fetch(p.url, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
||||
@@ -157,7 +225,7 @@ async function callOpenAiStyle(p, key, model, userPrompt, signal, sistem = SYSTE
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
temperature: 0.2,
|
||||
max_tokens: 4000,
|
||||
max_tokens: maxTokens,
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
@@ -166,7 +234,7 @@ async function callOpenAiStyle(p, key, model, userPrompt, signal, sistem = SYSTE
|
||||
return data?.choices?.[0]?.message?.content || '';
|
||||
}
|
||||
|
||||
async function callGemini(p, key, model, userPrompt, signal, sistem = SYSTEM_PROMPT) {
|
||||
async function callGemini(p, key, model, userPrompt, signal, sistem = SYSTEM_PROMPT, maxTokens = 4000) {
|
||||
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(key)}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
@@ -174,7 +242,7 @@ async function callGemini(p, key, model, userPrompt, signal, sistem = SYSTEM_PRO
|
||||
body: JSON.stringify({
|
||||
systemInstruction: { parts: [{ text: sistem }] },
|
||||
contents: [{ role: 'user', parts: [{ text: userPrompt }] }],
|
||||
generationConfig: { temperature: 0.2, maxOutputTokens: 4000, responseMimeType: 'application/json' },
|
||||
generationConfig: { temperature: 0.2, maxOutputTokens: maxTokens, responseMimeType: 'application/json' },
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
@@ -183,13 +251,13 @@ async function callGemini(p, key, model, userPrompt, signal, sistem = SYSTEM_PRO
|
||||
return data?.candidates?.[0]?.content?.parts?.map((x) => x.text).join('') || '';
|
||||
}
|
||||
|
||||
async function callAnthropic(p, key, model, userPrompt, signal, sistem = SYSTEM_PROMPT) {
|
||||
async function callAnthropic(p, key, model, userPrompt, signal, sistem = SYSTEM_PROMPT, maxTokens = 4000) {
|
||||
const res = await fetch(p.url, {
|
||||
method: 'POST',
|
||||
headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
max_tokens: 4000,
|
||||
max_tokens: maxTokens,
|
||||
system: sistem,
|
||||
messages: [{ role: 'user', content: userPrompt }],
|
||||
}),
|
||||
@@ -212,11 +280,16 @@ function extractJson(raw) {
|
||||
}
|
||||
|
||||
/** Zinciri sırayla dener; ilk başarılı sonucu döndürür. */
|
||||
export async function analyzeSpec({ text, fileName, tender, chain, systemOverride }) {
|
||||
export async function analyzeSpec({ text, fileName, tender, chain, systemOverride, mode }) {
|
||||
const derin = mode === 'derin';
|
||||
const order = (Array.isArray(chain) && chain.length ? chain : ['glmflash', 'gemini', 'deepseek', 'grok', 'gpt', 'claude'])
|
||||
.filter((id) => PROVIDERS[id]);
|
||||
const userPrompt = buildUserPrompt({ text, fileName, tender });
|
||||
const sistem = systemOverride || SYSTEM_PROMPT;
|
||||
// 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 maxTokens = derin ? 8000 : 4000;
|
||||
const timeoutMs = derin ? 150000 : 90000;
|
||||
const attempts = [];
|
||||
|
||||
for (const id of order) {
|
||||
@@ -227,22 +300,23 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid
|
||||
continue;
|
||||
}
|
||||
const ac = new AbortController();
|
||||
const timer = setTimeout(() => ac.abort(), 90000);
|
||||
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
||||
try {
|
||||
let raw;
|
||||
if (route.yol === 'openrouter') {
|
||||
raw = await callOpenRouter(route.model, route.key, userPrompt, ac.signal, sistem);
|
||||
raw = await callOpenRouter(route.model, route.key, userPrompt, ac.signal, sistem, maxTokens);
|
||||
} else if (p.style === 'gemini') {
|
||||
raw = await callGemini(p, route.key, route.model, userPrompt, ac.signal, sistem);
|
||||
raw = await callGemini(p, route.key, route.model, userPrompt, ac.signal, sistem, maxTokens);
|
||||
} else if (p.style === 'anthropic') {
|
||||
raw = await callAnthropic(p, route.key, route.model, userPrompt, ac.signal, sistem);
|
||||
raw = await callAnthropic(p, route.key, route.model, userPrompt, ac.signal, sistem, maxTokens);
|
||||
} else {
|
||||
raw = await callOpenAiStyle(p, route.key, route.model, userPrompt, ac.signal, sistem);
|
||||
raw = await callOpenAiStyle(p, route.key, route.model, userPrompt, ac.signal, sistem, maxTokens);
|
||||
}
|
||||
const result = extractJson(raw);
|
||||
attempts.push({ id, ok: true, yol: route.yol });
|
||||
return {
|
||||
result,
|
||||
mode: derin ? 'derin' : 'standart',
|
||||
model: { id, label: p.label, provider: p.provider, model: route.model, yol: route.yol },
|
||||
attempts,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user