Files
lagos-back/src/lib/legislationLookup.ts
T
Mustafa Yildiz b689b0f8fc feat: dinamik AI prompt yönetimi (ai_prompts tablosu + promptService)
20 AI system prompt'unu (chat/analiz/dilekçe/içtihat) koddan Supabase'e
taşıyan promptService.ts eklendi — artık bir prompt metnini değiştirmek
için redeploy gerekmiyor, admin panelden Supabase'e yazılıp 5dk içinde
devreye giriyor (bkz. laawos/docs/DINAMIK_PROMPT_MIMARISI_VE_YONETIMI.md,
laawos/sql/23_ai_prompts.sql — Supabase Studio'da elle çalıştırılmalı).

Canlı kullanıcı olduğu için davranış değişikliği YOK: promptService'in
fallback metinleri bugünkü hardcoded prompt'ların birebir kopyası
(programatik script ile karakter karakter doğrulandı), Supabase migration
çalıştırılmadan/erişilemezken sistem bugünkü gibi çalışmaya devam ediyor.
Bilinçli olarak dokunulmayanlar: model seçimi (tek global AI_MODEL env),
temperature (hiç gönderilmiyor), max_tokens (call-site'a göre değişiyor)
— hiçbiri gerçek AI çağrısına yansıtılmıyor, sadece prompt METNİ dinamik.

chat_intern iki farklı promptun (canlı sohbet vs. anlık soru-cevap) aynı
chat_mode='intern' etiketini paylaştığı görüldüğü için chat_intern_live
ve chat_intern_qa olarak ayrıştırıldı. Ayrıca kodda var olup dokümanda
listelenmeyen doc_strategy, doc_mediation, legislation_keywords, chat_title
ve chat_thread_general de tabloya eklendi.
2026-08-30 12:25:10 +03:00

159 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { supabase } from './supabase';
import { callOllama } from './aiClient';
import * as promptService from './promptService';
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<string, string> = {
'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<string>();
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<LegislationMatch[]> {
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;
}
async function fetchByFullTextSearch(caseText: string): Promise<LegislationMatch[]> {
let keywords = '';
try {
const keywordPrompt = await promptService.getPrompt('legislation_keywords');
const aiContent = await callOllama(
[
{ role: 'system', content: keywordPrompt },
{ 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<LegislationMatch[]> {
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 [];
}
}