From a5946eeb4aa2843f15d9e390c571b0a74acfd553 Mon Sep 17 00:00:00 2001 From: Mustafa Yildiz Date: Mon, 17 Aug 2026 00:46:26 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Dava=20=C3=96zeti,=20=C3=87eli=C5=9Fki?= =?UTF-8?q?=20Avc=C4=B1s=C4=B1=20ve=20Dilek=C3=A7e=20Tasla=C4=9F=C4=B1'na?= =?UTF-8?q?=20mevzuat=20entegrasyonu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mevzuat-etl'in doldurduğu Supabase mevzuat/mevzuat_madde tablolarından (aynı proje) ilgili kanun maddelerini bulup analiz/taslak üretiminden önce prompt'a ekliyor. Bulma: önce dava metnindeki açık atıfları regex ile yakala (TCK m.141, 5237 sayılı Kanun'un 141. maddesi gibi, tur-agnostik), boşsa precedent.controller.ts'deki AI-keyword-sıkıştırma + tam-metin arama desenine düşer. Hiçbir zaman exception fırlatmaz, mevzuat bulunamazsa analiz normal devam eder. Dilekçe Taslağı (generateDraft) farklı ele alınıyor — çıktısı JSON değil düz metin ve aynen dilekçe belgesi olarak kullanılıyor, bu yüzden referans bloğu ayrıca işaretlenip sistem promptuna "kopyalama, sadece atıf yap" talimatı eklendi. generateAngarya (Süre Tutum/Mazeret/Vekaletname) bilerek dışarıda bırakıldı — hukuki tartışma içermeyen idari dilekçeler. Tasarım: mevzuat-etl/docs/superpowers/specs/2026-08-17-laawos-mevzuat-entegrasyonu-design.md Not: drafts.used_legislation (jsonb) sütunu Supabase Studio'dan elle eklendi. --- src/controllers/case.controller.ts | 26 +++- src/controllers/drafting.controller.ts | 26 +++- src/lib/legislationLookup.ts | 159 +++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 src/lib/legislationLookup.ts diff --git a/src/controllers/case.controller.ts b/src/controllers/case.controller.ts index 9dff64d..afbd063 100644 --- a/src/controllers/case.controller.ts +++ b/src/controllers/case.controller.ts @@ -3,6 +3,19 @@ import { supabase } from '../lib/supabase'; import { AuthenticatedRequest } from '../middleware/auth'; import { runIddianameAnalysis, runCaseNarrativeAnalysis, runStatementAnalysis, runDeficiencyAnalysis, runStrategyAnalysis, runMediationAnalysis, runDigitalInternQa } from './document.controller'; import { findOrCreateCaseByTitle } from '../lib/caseLookup'; +import { findRelevantLegislation, LegislationMatch } from '../lib/legislationLookup'; + +function formatLegislationBlock(matches: LegislationMatch[]): string { + if (matches.length === 0) return ''; + const body = matches + .map((m) => `${m.kanun} madde ${m.maddeNo}${m.baslik ? ` - ${m.baslik}` : ''}: ${m.metin}`) + .join('\n\n'); + return `İLGİLİ MEVZUAT:\n${body}\n\n---\n\n`; +} + +function toTraceableLegislation(matches: LegislationMatch[]) { + return matches.map(({ maddeNo, baslik, kanun }) => ({ maddeNo, baslik, kanun })); +} export const createCase = async (req: AuthenticatedRequest, res: Response) => { try { @@ -89,15 +102,17 @@ export const summarizeCase = async (req: AuthenticatedRequest, res: Response) => .map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`) .join('\n\n'); - const analysisInput = iddianameJson + const legislationMatches = await findRelevantLegislation(combinedText); + + const analysisInput = formatLegislationBlock(legislationMatches) + (iddianameJson ? `İDDİANAME ÖN ANALİZİ (rapora bunu öncelikli bağlam olarak yansıt):\n${JSON.stringify(iddianameJson)}\n\n---\n\nDOSYADAKİ TÜM BELGELER:\n${combinedText}` - : combinedText; + : combinedText); const { summaryJson, aiModel } = await runCaseNarrativeAnalysis(analysisInput); const { data: analysisRecord, error: analysisError } = await supabase .from('analyses') - .insert([{ case_id: caseId, document_id: null, summary_json: summaryJson, model_used: aiModel }]) + .insert([{ case_id: caseId, document_id: null, summary_json: { ...summaryJson, kullanilan_mevzuat: toTraceableLegislation(legislationMatches) }, model_used: aiModel }]) .select() .single(); if (analysisError) throw analysisError; @@ -145,14 +160,15 @@ export const analyzeStatements = async (req: AuthenticatedRequest, res: Response .map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`) .join('\n\n'); - const analysisInput = `Aşağıdaki belgeler sadece ifade ve sorgu evraklarıdır:\n\n${combinedText}`; + const legislationMatches = await findRelevantLegislation(combinedText); + const analysisInput = formatLegislationBlock(legislationMatches) + `Aşağıdaki belgeler sadece ifade ve sorgu evraklarıdır:\n\n${combinedText}`; const { summaryJson, aiModel } = await runStatementAnalysis(analysisInput); // Çelişki analizi sonucunu 'analyses' tablosuna kaydet const { data: analysisRecord, error: analysisError } = await supabase .from('analyses') - .insert([{ case_id: caseId, document_id: null, summary_json: { statementAnalysis: summaryJson }, model_used: aiModel }]) + .insert([{ case_id: caseId, document_id: null, summary_json: { statementAnalysis: summaryJson, kullanilan_mevzuat: toTraceableLegislation(legislationMatches) }, model_used: aiModel }]) .select() .single(); if (analysisError) throw analysisError; diff --git a/src/controllers/drafting.controller.ts b/src/controllers/drafting.controller.ts index 538a1d5..afb1fb4 100644 --- a/src/controllers/drafting.controller.ts +++ b/src/controllers/drafting.controller.ts @@ -2,6 +2,7 @@ import { Response } from 'express'; import { supabase } from '../lib/supabase'; import { AuthenticatedRequest } from '../middleware/auth'; import { callAiModelRaw } from './document.controller'; +import { findRelevantLegislation, LegislationMatch } from '../lib/legislationLookup'; const DRAFT_SYSTEM_PROMPT = ` Sen uzman bir Türk avukatısın. Sana verilen dosya bilgilerine göre, istenen türde @@ -14,6 +15,10 @@ const DRAFT_SYSTEM_PROMPT = ` ESKİ dosyaya ait somut bilgileri ASLA kullanma veya kopyalama — onların yerine SADECE aşağıda verilen YENİ dosyanın gerçek bilgilerini koy. + EĞER sana "REFERANS MADDELER" verilmişse: bunları gerektiğinde "HUKUKİ SEBEPLER" + bölümünde doğal bir atıfla (örn. "TCK'nın 141. maddesi uyarınca...") kullan — + metinlerini olduğu gibi kopyalama, sadece atıf için referans al. + ÖNEMLİ — BİÇİMLENDİRME: Bu çıktı doğrudan bir dilekçe belgesine dönüştürülecek, o yüzden KESİNLİKLE Markdown biçimlendirmesi kullanma (**, ##, -, *, 1. gibi işaretler YASAK). Sadece düz metin yaz. Başlık/vurgu gerekiyorsa BÜYÜK HARF veya standart dilekçe @@ -23,6 +28,18 @@ const DRAFT_SYSTEM_PROMPT = ` Sadece dilekçe taslağının kendisini döndür — başlık, açıklama veya ek yorum ekleme. `; +function formatReferenceBlock(matches: LegislationMatch[]): string { + if (matches.length === 0) return ''; + const body = matches + .map((m) => `${m.kanun} madde ${m.maddeNo}${m.baslik ? ` - ${m.baslik}` : ''}: ${m.metin}`) + .join('\n\n'); + return `\n---\nREFERANS MADDELER (SADECE ATIF İÇİNDİR, DOĞRUDAN KOPYALAMA):\n${body}\n---\n`; +} + +function toTraceableLegislation(matches: LegislationMatch[]) { + return matches.map(({ maddeNo, baslik, kanun }) => ({ maddeNo, baslik, kanun })); +} + // Dilekçe Hazırlama ekranındaki "AI ile Taslak Oluştur" — dosyanın gerçek bilgilerini // (başlık, UYAP'tan gelen taraflar, varsa Dosya Özeti) ve kullanıcının notlarını, // isteğe bağlı olarak seçilen büro şablonunun DİLİNİ/FORMATINI referans alarak @@ -85,7 +102,11 @@ export const generateDraft = async (req: AuthenticatedRequest, res: Response) => userContent += `\n---\nBÜRO ŞABLONU "${template.name}" (SADECE dil/format/üslup referansı — içeriğini kopyalama):\n${template.extracted_text.slice(0, 6000)}\n`; } - const { aiContent, aiModel } = await callAiModelRaw(DRAFT_SYSTEM_PROMPT, userContent, 2500); + const legislationLookupText = `${caseRow.title}\n${partiesText}\n${notes || ''}\n${typeof ozet === 'string' ? ozet : ''}`; + const legislationMatches = await findRelevantLegislation(legislationLookupText); + userContent += formatReferenceBlock(legislationMatches); + + const { aiContent, aiModel } = await callAiModelRaw(DRAFT_SYSTEM_PROMPT, userContent, 2500, 180_000); // Taslağı kalıcı olarak kaydediyoruz — hem "Dilekçelerim" listesinde hem ilgili // dava dosyasının kendi "Dilekçeler" bölümünde görünsün, sayfadan çıkınca kaybolmasın. @@ -97,6 +118,7 @@ export const generateDraft = async (req: AuthenticatedRequest, res: Response) => petition_type, content: aiContent, template_id: template_id || null, + used_legislation: toTraceableLegislation(legislationMatches), }]) .select('id, created_at') .single(); @@ -104,7 +126,7 @@ export const generateDraft = async (req: AuthenticatedRequest, res: Response) => console.error('Save Draft Error:', draftError); } - res.json({ draft: aiContent, draftId: draftRecord?.id || null, model: aiModel, usedTemplate: !!template }); + res.json({ draft: aiContent, draftId: draftRecord?.id || null, model: aiModel, usedTemplate: !!template, usedLegislation: toTraceableLegislation(legislationMatches) }); } catch (error: any) { console.error('Generate Draft Error:', error); res.status(500).json({ error: error.message || 'Internal Server Error' }); diff --git a/src/lib/legislationLookup.ts b/src/lib/legislationLookup.ts new file mode 100644 index 0000000..18a6e39 --- /dev/null +++ b/src/lib/legislationLookup.ts @@ -0,0 +1,159 @@ +import { supabase } from './supabase'; +import { callOllama } from './aiClient'; + +export interface LegislationMatch { + maddeNo: string; + baslik: string | null; + metin: string; // 800 karaktere kesilmiş + kanun: string; // mevzuat.kisa_ad || mevzuat.ad +} + +const MAX_MADDE_LEN = 800; +const MAX_MATCHES = 5; + +// mevzuat.tur sütunuyla eşleşen snake_case değerler — mevzuat-etl/mevzuat_etl/config.py::MEVZUAT_TUR_MAP +// ile aynı 8 anahtar, burada bağımsız bir TS kopyası (Python paketine bağımlılık yok). +const TUR_LABEL_TO_DB: Record = { + 'Kanun': 'kanun', + 'KHK': 'khk', + 'Tüzük': 'tuzuk', + 'Cumhurbaşkanlığı Kararnamesi': 'cb_kararnamesi', + 'Cumhurbaşkanı Kararı': 'cb_karari', + 'Cumhurbaşkanlığı Yönetmeliği': 'cb_yonetmeligi', + 'Tebliğ': 'teblig', +}; + +// Kısaltma bazlı, iki sıra da olası — "TCK m.141" (m./madde sayıdan ÖNCE) ve +// "CMK 100. maddesi" (sayı m./madde'den ÖNCE) — ayrı ayrı test edildi (bkz. +// _verify_legislation_lookup.ts), tek regex'te birleştirmek grup-indeksini +// karmaşıklaştırdığı için iki ayrı desen daha güvenli. +const ABBREVIATION_BEFORE_RE = /\b(TCK|CMK|TMK|TBK|TTK|ANAYASA)\b[^\d\n]{0,10}\b(?:m\.|madde)\s*(\d+[/A-Za-z]*)/gi; +const ABBREVIATION_AFTER_RE = /\b(TCK|CMK|TMK|TBK|TTK|ANAYASA)\b[^\d\n]{0,15}(\d+[/A-Za-z]*)\.?\s*madde/gi; + +// Genel/tur-agnostik: "5237 sayılı Kanun'un 141. maddesi", "700 sayılı KHK'nın 3. maddesi". +const GENERIC_RE = /(\d+)\s+say[ıi]l[ıi]\s+(Kanun|KHK|Tüzük|Cumhurbaşkanlığı\s+Kararnamesi|Cumhurbaşkanı\s+Kararı|Cumhurbaşkanlığı\s+Yönetmeliği|Tebliğ)[^\d]{0,30}(\d+[/A-Za-z]*)\.?\s*madde/gi; + +interface Citation { + kisaAd?: string; + mevzuatNo?: string; + turHint?: string; // DB'nin snake_case değeri + maddeNo: string; +} + +function extractCitations(text: string): Citation[] { + const citations: Citation[] = []; + + for (const m of text.matchAll(ABBREVIATION_BEFORE_RE)) { + citations.push({ kisaAd: m[1].toUpperCase(), maddeNo: m[2] }); + } + for (const m of text.matchAll(ABBREVIATION_AFTER_RE)) { + citations.push({ kisaAd: m[1].toUpperCase(), maddeNo: m[2] }); + } + for (const m of text.matchAll(GENERIC_RE)) { + citations.push({ mevzuatNo: m[1], turHint: TUR_LABEL_TO_DB[m[2]], maddeNo: m[3] }); + } + + // Tekilleştir (kisaAd|mevzuatNo + maddeNo bazında). + const seen = new Set(); + return citations.filter((c) => { + const key = `${c.kisaAd || c.mevzuatNo}:${c.maddeNo}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +function truncate(text: string): string { + if (text.length <= MAX_MADDE_LEN) return text; + return text.slice(0, MAX_MADDE_LEN) + '…'; +} + +async function resolveMevzuatId(citation: Citation): Promise<{ id: string; kanun: string } | null> { + let query = supabase.from('mevzuat').select('id, ad, kisa_ad'); + if (citation.kisaAd) { + query = query.eq('kisa_ad', citation.kisaAd); + } else if (citation.mevzuatNo) { + query = query.eq('mevzuat_no', citation.mevzuatNo); + if (citation.turHint) query = query.eq('tur', citation.turHint); + } else { + return null; + } + const { data, error } = await query.maybeSingle(); + if (error || !data) return null; + return { id: data.id, kanun: data.kisa_ad || data.ad }; +} + +async function fetchByCitations(citations: Citation[]): Promise { + const matches: LegislationMatch[] = []; + for (const citation of citations) { + if (matches.length >= MAX_MATCHES) break; + const mevzuat = await resolveMevzuatId(citation); + if (!mevzuat) continue; + const { data, error } = await supabase + .from('mevzuat_madde') + .select('madde_no, baslik, metin') + .eq('mevzuat_id', mevzuat.id) + .eq('madde_no', citation.maddeNo) + .maybeSingle(); + if (error || !data) continue; + matches.push({ + maddeNo: data.madde_no, + baslik: data.baslik, + metin: truncate(data.metin), + kanun: mevzuat.kanun, + }); + } + return matches; +} + +const KEYWORD_PROMPT = `Sen uzman bir Türk avukatısın. Sana verilen dava metnini, ilgili kanun maddelerini bulmak için Türkçe tam-metin aramada kullanılacak 2 ila 4 temel hukuki terime çevir. +SADECE anahtar kelimeleri aralarına boşluk koyarak yaz. Noktalama işareti, açıklama veya ek kelime kullanma.`; + +async function fetchByFullTextSearch(caseText: string): Promise { + let keywords = ''; + try { + const aiContent = await callOllama( + [ + { role: 'system', content: KEYWORD_PROMPT }, + { role: 'user', content: caseText.slice(0, 2000) }, + ], + { numPredict: 40, timeoutMs: 20_000 } + ); + keywords = (aiContent || '').trim().replace(/["'\n]/g, ''); + } catch (err) { + console.error('Legislation keyword compression error:', err); + return []; + } + if (!keywords) return []; + + const { data, error } = await supabase + .from('mevzuat_madde') + .select('madde_no, baslik, metin, mevzuat:mevzuat_id(ad, kisa_ad)') + .textSearch('search_vector', keywords, { type: 'websearch', config: 'turkish' }) + .limit(MAX_MATCHES); + if (error || !data) return []; + + return data.map((row: any) => ({ + maddeNo: row.madde_no, + baslik: row.baslik, + metin: truncate(row.metin), + kanun: row.mevzuat?.kisa_ad || row.mevzuat?.ad || 'Bilinmeyen Mevzuat', + })); +} + +// Dava metninde geçen kanun maddesi atıflarını bulur ve o maddelerin metnini döner. +// Hiçbir zaman exception fırlatmaz — Supabase/Ollama erişilemezse veya eşleşme +// yoksa boş dizi döner, çağıran taraf bunu "mevzuat bağlamı yok" olarak yorumlar. +export async function findRelevantLegislation(caseText: string): Promise { + try { + const citations = extractCitations(caseText); + if (citations.length > 0) { + const matches = await fetchByCitations(citations); + if (matches.length > 0) return matches; + } + return await fetchByFullTextSearch(caseText); + } catch (err) { + console.error('findRelevantLegislation error:', err); + return []; + } +}