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.
159 lines
5.7 KiB
TypeScript
159 lines
5.7 KiB
TypeScript
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 [];
|
||
}
|
||
}
|