LawAgent-backend / src /generator.py
hllerdgn's picture
fix: Uvicorn PORT env var ile 7860'ta dinle (HF Spaces)
956221a
Raw
History Blame Contribute Delete
32.5 kB
"""
generator.py — LawAgent AI Backend (v5.8 - Düzeltilmiş İçtihat Talebi Sırası)
=======================================================================
Proje: TÜBİTAK 2209/A
YENİLİKLER (v5.8):
1. Aşama 2 (içtihat talebi) kontrolü, hukuki filtrenin ÖNÜNE alındı.
2. Tetikleyici ifade "emsal karar" olarak esnekleştirildi.
3. "Evet", "İstiyorum", "Bakalım" gibi kısa onaylar artık hukuki filtreye takılmaz.
"""
import os
import re
import time
import argparse
import logging
from typing import Optional, Dict, Any, List, Tuple
from contextlib import asynccontextmanager
from pathlib import Path
from collections import defaultdict
from datetime import datetime
from dotenv import load_dotenv
from groq import Groq, APIStatusError, APITimeoutError, RateLimitError
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from retriever import LegalRetriever
# ─── LOGGING ────────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s — %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger("LawAgent.Generator.v5.8")
# ─── ENV ────────────────────────────────────────────────────────────────────
_ENV_ADAYLARI = [
Path("/content/drive/MyDrive/lawagent/.env"),
Path(__file__).resolve().parent.parent.parent / ".env",
Path(__file__).resolve().parent.parent / ".env",
Path(__file__).resolve().parent / ".env",
]
for env_path in _ENV_ADAYLARI:
if env_path.exists():
load_dotenv(dotenv_path=env_path)
log.info(f".env yüklendi: {env_path}")
break
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
MODEL_NAME = "llama-3.3-70b-versatile"
if not GROQ_API_KEY:
log.warning("GROQ_API_KEY bulunamadı! .env dosyasını kontrol et.")
# ═══════════════════════════════════════════════════════════════════════════════
# 1. SESSION MEMORY
# ═══════════════════════════════════════════════════════════════════════════════
class ConversationMemory:
def __init__(self, max_memory: int = 4):
self.memory: Dict[str, List[Dict[str, str]]] = defaultdict(list)
self.last_chunks: Dict[str, List[Dict]] = defaultdict(list)
self.max_memory = max_memory
def add_exchange(self, session_id: str, user_msg: str, assistant_msg: str):
if session_id not in self.memory:
self.memory[session_id] = []
self.memory[session_id].append(
{
"role": "user",
"content": user_msg,
"timestamp": datetime.now().isoformat(),
}
)
self.memory[session_id].append(
{
"role": "assistant",
"content": assistant_msg,
"timestamp": datetime.now().isoformat(),
}
)
if len(self.memory[session_id]) > self.max_memory * 2:
self.memory[session_id] = self.memory[session_id][-(self.max_memory * 2) :]
def save_chunks(self, session_id: str, chunks: List[Dict]):
self.last_chunks[session_id] = chunks
def get_chunks(self, session_id: str) -> List[Dict]:
return self.last_chunks.get(session_id, [])
def get_history(self, session_id: str) -> List[Dict[str, str]]:
return self.memory.get(session_id, [])
def get_context_string(self, session_id: str) -> str:
history = self.get_history(session_id)
if not history:
return ""
context_lines = ["--- ÖNCEKI BAĞLAM ---"]
for msg in history[-4:]:
role = "Kullanıcı" if msg["role"] == "user" else "Asistan"
context_lines.append(f"{role}: {msg['content'][:300]}")
return "\n".join(context_lines) + "\n\n"
# ─── HUKUKI FİLTRE ──────────────────────────────────────────────────────────
_HUKUK_DISI = {
"hava",
"yemek",
"müzik",
"film",
"spor",
"oyun",
"minecraft",
"magazin",
"haber",
"gündem",
"sağlık",
"doktor",
"ilaç",
"matematik",
"fizik",
"kimya",
}
_HUKUKI_SINYALLER = {
"nedir",
"nasıl",
"hak",
"kanun",
"madde",
"dava",
"sözleşme",
"tazminat",
"kira",
"borç",
"alacak",
"fesih",
"temerrüt",
"cayma",
"garanti",
"tahliye",
"tbk",
"tkhk",
"ttk",
"6098",
"6502",
"6102",
"mahkeme",
"icra",
"ipotek",
"miras",
"velayet",
}
def is_legal_query(sorgu: str) -> bool:
s = sorgu.lower()
if any(hd in s.split() for hd in _HUKUK_DISI):
return False
return any(sig in s for sig in _HUKUKI_SINYALLER) or len(sorgu.split()) >= 3
# ─── İÇTİHAT TALEBİ KONTROLÜ (GÜNCELLENDİ) ─────────────────────────────────
_ICTIHAT_ISTEGI_KELIMELERI = {
"evet",
"isterim",
"istiyorum",
"göster",
"gösterin",
"bakalım",
"emsal",
"karar",
"içtihat",
"yargıtay",
"lütfen",
"tabii",
"tabi",
"olur",
"harika",
"güzel",
}
# ✅ Daha esnek tetikleyici: "emsal karar" - prompt'taki cümleyle birebir uyumlu
_ICTIHAT_SORUSU_TETIKLEYICI = "emsal karar"
def is_ictihat_request(sorgu: str, history: List[Dict]) -> bool:
if not history:
return False
last_msg = history[-1]
if last_msg.get("role") != "assistant":
return False
if _ICTIHAT_SORUSU_TETIKLEYICI not in last_msg.get("content", "").lower():
return False
sorgu_temiz = sorgu.lower().strip()
return any(kelime in sorgu_temiz for kelime in _ICTIHAT_ISTEGI_KELIMELERI)
# ═══════════════════════════════════════════════════════════════════════════════
# 2. QUERY INTENT ROUTER
# ═══════════════════════════════════════════════════════════════════════════════
class QueryIntentRouter:
INTENT_DEFINITIONS = {
"INFO_RETRIEVAL": {
"keywords": ["nedir", "ne", "neyin", "nasıl", "hangi", "kaç"],
"retrieval_k": 7,
},
"COMPARISON": {
"keywords": ["fark", "arasında", "farklı", "ne kadar", "vs", "karşılaştır"],
"retrieval_k": 10,
},
"PROCEDURE": {
"keywords": ["süre", "yapılır", "adım", "işlem", "başvuru", "başvur"],
"retrieval_k": 8,
},
"RIGHTS_OBLIGATION": {
"keywords": ["hak", "sorumluluk", "yükümlülük", "ödeme", "iade"],
"retrieval_k": 7,
},
"CONSEQUENCE": {
"keywords": ["sonuç", "ceza", "para", "tazminat", "zarar", "risiko"],
"retrieval_k": 6,
},
}
def __init__(self, client: Groq):
self.client = client
def detect_intent(self, sorgu: str) -> Tuple[str, int]:
sorgu_lower = sorgu.lower()
best_intent = "INFO_RETRIEVAL"
best_score = 0
for intent, config in self.INTENT_DEFINITIONS.items():
score = sum(1 for kw in config["keywords"] if kw in sorgu_lower)
if score > best_score:
best_score = score
best_intent = intent
recommended_k = self.INTENT_DEFINITIONS[best_intent]["retrieval_k"]
log.info(f"Intent Detection: {best_intent} (k={recommended_k})")
return best_intent, recommended_k
# ═══════════════════════════════════════════════════════════════════════════════
# 3. HALLÜSİNASYON KONTROLÜ
# ═══════════════════════════════════════════════════════════════════════════════
class HallucinationValidator:
_MADDE_REF_PATTERN = re.compile(r"m(?:adde)?\.?\s*(\d+)", re.IGNORECASE)
_KAPSAM_DISI_KANUNLAR = re.compile(
r"\b(TMK|CMK|HMK|TCK|İYUK|İş\s*K\.?|4857|4721)\b", re.IGNORECASE
)
def __init__(self, client: Groq):
self.client = client
def extract_article_refs(self, text: str) -> List[str]:
return [m.group(1) for m in self._MADDE_REF_PATTERN.finditer(text)]
def extract_source_articles(self, chunks: List[Dict]) -> List[str]:
return [str(c.get("article_no")).strip() for c in chunks if c.get("article_no")]
def validate_faithfulness(
self, answer: str, chunks: List[Dict]
) -> Tuple[bool, str, List[str]]:
kapsam_disi = self._KAPSAM_DISI_KANUNLAR.findall(answer)
if kapsam_disi:
kanunlar = ", ".join(sorted(set(k.upper() for k in kapsam_disi)))
return (
False,
f"⚠️ SİSTEM UYARISI: Yanıt, uzmanlık alanım dışındaki kanunlara ({kanunlar}) atıfta bulunuyor.",
[],
)
if not chunks:
return True, "", []
source_articles = self.extract_source_articles(chunks)
mentioned_articles = self.extract_article_refs(answer)
if not source_articles:
return True, "", mentioned_articles
source_blob = " ".join(source_articles)
for art in mentioned_articles:
if art not in source_blob:
return (
False,
f"⚠️ Uyarı: Yanıtta geçen madde numarası (m. {art}) veri tabanındaki kaynaklarda bulunamadı.",
[],
)
return True, "", mentioned_articles
# ─── QUERY REWRITE ──────────────────────────────────────────────────────────
_MADDE_REF_RE = re.compile(
r"\b(tbk|tkhk|ttk)\s*(?:m\.|madde)?\s*\d+\b|\b(6098|6502|6102)\b|\b(?:madde|m\.)\s*\d+\b",
re.IGNORECASE,
)
_REWRITE_SYSTEM = "Sen Türk hukuku uzmanısın. Kullanıcının sorusunu, anlamını bozmadan akademik hukuk terimleriyle yeniden yaz. Kanun kısaltmalarını (TBK, TKHK, TTK) koru. Sadece yeniden yazılmış soruyu döndür, açıklama ekleme."
def has_madde_ref(sorgu: str) -> bool:
return bool(_MADDE_REF_RE.search(sorgu))
def rewrite_query(client: Groq, sorgu: str) -> str:
if has_madde_ref(sorgu):
return sorgu
if len(sorgu.split()) < 4 or len(sorgu.split()) > 30:
return sorgu
try:
resp = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": _REWRITE_SYSTEM},
{"role": "user", "content": f"Soru: {sorgu}\n\nYeniden yazılmış hali:"},
],
temperature=0.0,
max_tokens=100,
)
yeni = resp.choices[0].message.content.strip()
return yeni if yeni and len(yeni) <= 300 else sorgu
except Exception as e:
log.warning(f"Query rewrite hatası: {e}")
return sorgu
# ─── SISTEM PROMPTLARI (v5.8) ───────────────────────────────────────────────
_SISTEM_PROMPT_TEMPLATE = """Sen sadece Türk Borçlar Kanunu (TBK), Türk Ticaret Kanunu (TTK) ve Tüketicinin Korunması Hakkında Kanun (TKHK) alanlarında uzmanlaşmış bir AI Hukuk Asistanısın.
BAĞLAM (SADECE BURADAKİ BİLGİLERİ KULLAN - Yargıtay kararı içermez):
{context}
GÖREVLERİN:
1. **İki Aşamalı Yanıt:** Kullanıcıya önce sadece 'Hukuki Değerlendirme' ve 'Dayanak Mevzuat' bölümlerini sun.
2. **Kapatış Sorusu:** Yanıtın en sonuna kesinlikle şu cümleyi ekle: "Bu konuyla ilgili daha fazla bilgi veya emsal karar görmek ister misiniz?" (Kapsam dışı durumda ekleme.)
3. **İçtihat Yasağı:** Bu yanıtta Yargıtay kararı, esas/karar numarası veya daire adı ASLA YAZMA.
4. **Madde Numarası Kullanımı (KRİTİK):**
- **Sadece BAĞLAM içinde geçen madde numaralarını kullan.** Bağlamda yoksa hiçbir madde numarası yazma.
- Her cümlende madde belirtmek zorunda değilsin. Sadece bir maddeye atıf yapacaksan, mutlaka bağlamda olmalı.
- Format: (TBK m. 117), (TKHK m. 11), (TTK m. 18).
5. **KAPSAM DIŞI DURUMU:** Eğer kullanıcının sorusu (boşanma, ceza, miras, velayet, iş hukuku gibi) TBK/TTK/TKHK dışındaysa, doğrudan şunu söyle: "Üzgünüm, veri tabanım sadece TBK, TTK ve TKHK konularını kapsamaktadır. Sorunuzdaki konu uzmanlık alanım dışındadır." **Bu durumda kapatış sorusunu ekleme.**
6. **ASLA UYDURMA:** Bağlamda kesinlikle yer almayan hiçbir madde numarasını yazma. Kanun adı (örneğin TMK, CMK, HMK) asla kullanma.
YANIT FORMATI:
**Hukuki Değerlendirme**
[Analiz]
**Dayanak Mevzuat**
- [Kanun] m.[No]: [Madde Özeti]
---
Bu konuyla ilgili daha fazla bilgi veya emsal karar görmek ister misiniz?
"""
_ICTIHAT_PROMPT_TEMPLATE = """Sen Türk Borçlar, Ticaret ve Tüketici Hukuku alanlarında uzmanlaşmış bir AI Hukuk Asistanısın.
BAĞLAM (SADECE BURADAKİ İÇTİHATLARI KULLAN):
{context}
GÖREVİN:
Yalnızca sana verilen bağlamdaki Yargıtay kararlarını aşağıdaki formatta özetle.
- Karar künyeleri (esas/karar no, daire) AYNEN koru.
- Her karardan çıkan hukuki ilkeyi 1-2 cümleyle açıkla.
- Eğer bağlamda içtihat yoksa "Bu konuya dair veri tabanımda emsal karar bulunmamaktadır." de.
YANIT FORMATI:
**Emsal Yargıtay Kararları**
### [Konu Başlığı]
- **Künye:** [Daire] — [Esas No] / [Karar No]
- **Hukuki İlke:** [Karardan çıkan temel kural]
"""
def build_context(chunks: list, source_filter: Optional[str] = None) -> str:
satirlar = []
for i, c in enumerate(chunks, 1):
source_type = str(c.get("source", "Mevzuat")).upper()
if source_filter and source_type != source_filter.upper():
continue
satirlar.append(
f"--- KAYNAK {i} ---\n"
f"KANUN: {c.get('law', '?')}\n"
f"MADDE: {c.get('article_no', '?')}\n"
f"METİN: {c.get('text', '')}"
)
return "\n\n".join(satirlar)
# ─── SINGLETON RETRIEVER ────────────────────────────────────────────────────
_retriever_instance: Optional[LegalRetriever] = None
def get_retriever() -> LegalRetriever:
global _retriever_instance
if _retriever_instance is None:
log.info("[Startup] Retriever yükleniyor...")
_retriever_instance = LegalRetriever()
log.info("[Startup] Retriever hazır.")
return _retriever_instance
# ═══════════════════════════════════════════════════════════════════════════════
# 4. LEGAL GENERATOR (v5.8) - DÜZELTİLMİŞ SIRA
# ═══════════════════════════════════════════════════════════════════════════════
class LegalGenerator:
def __init__(self, k: int = 7):
if not GROQ_API_KEY:
raise ValueError("GROQ_API_KEY bulunamadı.")
self.client = Groq(api_key=GROQ_API_KEY)
self.retriever = get_retriever()
self.default_k = k
self.memory = ConversationMemory(max_memory=4)
self.intent_router = QueryIntentRouter(self.client)
self.hallucination_validator = HallucinationValidator(self.client)
# ─── AŞAMA 2: İÇTİHAT + MEVZUAT KAYNAKLARI BİRLİKTE ─────────────────────
def _generate_ictihat_only(self, session_id: str) -> Dict[str, Any]:
t0 = time.time()
all_chunks = self.memory.get_chunks(session_id)
ictihat_chunks = [
c for c in all_chunks if str(c.get("source", "")).lower() == "yargitay"
]
mevzuat_chunks = [
c for c in all_chunks if str(c.get("source", "")).lower() != "yargitay"
]
if ictihat_chunks:
context = build_context(ictihat_chunks)
ictihat_prompt = _ICTIHAT_PROMPT_TEMPLATE.format(context=context)
try:
resp = self.client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": ictihat_prompt},
{
"role": "user",
"content": "Lütfen ilgili Yargıtay kararlarını özetle.",
},
],
temperature=0.1,
max_tokens=800,
)
yanit = resp.choices[0].message.content.strip()
except Exception as e:
log.error(f"İçtihat üretim hatası: {e}")
yanit = "İçtihat bilgilerini getirirken teknik bir sorun oluştu. Lütfen tekrar deneyin."
else:
yanit = "**Emsal Yargıtay Kararları**\n\nBu konuya dair veri tabanımda emsal karar bulunmamaktadır."
combined_sources = []
for c in mevzuat_chunks:
combined_sources.append(
{
"kanun": c.get("law") or "",
"madde": (
str(c.get("article_no"))
if c.get("article_no") is not None
else ""
),
"ozet": (c.get("text") or "")[:200] + "...",
"tip": "mevzuat",
}
)
for c in ictihat_chunks:
combined_sources.append(
{
"kanun": "Yargıtay",
"madde": c.get("decision_id", ""),
"ozet": (c.get("text") or "")[:200] + "...",
"tip": "ictihat",
}
)
self.memory.add_exchange(session_id, "[İçtihat talebi]", yanit)
return {
"answer": yanit,
"sources": combined_sources,
"intent": "ICTIHAT_DETAIL",
"sure_ms": int((time.time() - t0) * 1000),
"filtered": False,
}
# ─── ANA GENERATE (DÜZELTİLMİŞ SIRA: Selamlama → Aşama 2 → Hukuki Filtre) ──
def generate(
self, sorgu: str, session_id: str = "default", k: Optional[int] = None
) -> Dict[str, Any]:
t0 = time.time()
sorgu_temiz = sorgu.lower().strip()
history = self.memory.get_history(session_id)
# 1. SELAMLAMA KONTROLÜ
if sorgu_temiz in {"selam", "merhaba", "sa", "as", "günaydın", "iyi günler"}:
greeting = (
"Merhaba! Ben LawAgent AI. Türk Borçlar, Ticaret ve Tüketici Hukuku alanlarında size yardımcı olabilirim.\n\n"
"**Size nasıl yardımcı olabilirim? Örneğin şunları sorabilirsiniz:**\n"
"- 'Kira sözleşmemi nasıl feshedebilirim?'\n"
"- 'İnternetten aldığım ürünü iade edebilir miyim?'\n"
"- 'Borçlu temerrüdü nedir?'"
)
self.memory.add_exchange(session_id, sorgu, greeting)
return {
"answer": greeting,
"sources": [],
"filtered": False,
"intent": "GREETING",
"sure_ms": int((time.time() - t0) * 1000),
}
# 2. AŞAMA 2 KONTROLÜ (İçtihat talebi) – artık hukuki filtreden ÖNCE
if is_ictihat_request(sorgu, history):
log.info(f"[Aşama 2] İçtihat talebi yakalandı → session: {session_id}")
return self._generate_ictihat_only(session_id)
# 3. HUKUKİ FİLTRE (kısa onaylar buraya düşmez çünkü Aşama 2 onları yakalar)
if not is_legal_query(sorgu):
filtered = "Ben bir Türk hukuk asistanıyım. Lütfen Türk Borçlar, Ticaret veya Tüketici hukukuyla ilgili bir soru sorunuz."
self.memory.add_exchange(session_id, sorgu, filtered)
return {
"answer": filtered,
"sources": [],
"filtered": True,
"sure_ms": int((time.time() - t0) * 1000),
}
try:
# Intent ve K
intent, recommended_k = self.intent_router.detect_intent(sorgu)
k = k or recommended_k or self.default_k
# Query rewrite
yeni_sorgu = rewrite_query(self.client, sorgu)
# Retrieval (doğrudan madde sorgusunda history devre dışı)
history_context = self.memory.get_context_string(session_id)
direct_article_match = re.search(
r"(?:m\.|madde)?\s*\d+", sorgu, re.IGNORECASE
)
if direct_article_match:
retrieval_sorgu = sorgu
log.info(
"[Retrieval] Doğrudan madde sorgusu, history_context kullanılmadı."
)
else:
retrieval_sorgu = (
f"{history_context}{sorgu}".strip() if history_context else sorgu
)
chunks = self.retriever.retrieve(retrieval_sorgu, k=k)
# Fallback
if len(chunks) < 3 and yeni_sorgu != sorgu:
ek = self.retriever.retrieve(sorgu, k=k)
mevcut = {c["chunk_id"] for c in chunks}
for c in ek:
if c["chunk_id"] not in mevcut:
chunks.append(c)
chunks = chunks[:k]
# Alaka kontrolü (kapsam dışı kelimeler)
kapsam_disi_kelimeler = [
"boşan",
"nafaka",
"velayet",
"miras",
"hapis",
"suç",
"öldür",
"yarala",
]
if any(kelime in sorgu_temiz for kelime in kapsam_disi_kelimeler):
is_relevant = any(
any(
kw in str(c.get("text", "")).lower()
for kw in kapsam_disi_kelimeler
)
for c in chunks
)
if not is_relevant:
chunks = []
log.info(
f"[Alaka Kontrolü] Kapsam dışı sorgu: '{sorgu_temiz}' → chunks temizlendi."
)
# OUT_OF_SCOPE
if not chunks:
no_result = (
"Üzgünüm, bu konu (Aile Hukuku/Ceza Hukuku vb.) uzmanlık alanım olan "
"TBK, TTK ve TKHK dışında kalmaktadır. Veri tabanımda bu konuya dair "
"bir madde bulunmadığı için hukuki değerlendirme yapamam."
)
self.memory.add_exchange(session_id, sorgu, no_result)
return {
"answer": no_result,
"sources": [],
"intent": "OUT_OF_SCOPE",
"sure_ms": int((time.time() - t0) * 1000),
"filtered": False,
}
# Tüm chunk'ları belleğe kaydet (içtihat aşaması için)
self.memory.save_chunks(session_id, chunks)
# Mevzuat odaklı yanıt
context = build_context(chunks)
sistem_prompt = _SISTEM_PROMPT_TEMPLATE.format(context=context)
resp = self.client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": sistem_prompt},
{"role": "user", "content": f"SORU: {sorgu}"},
],
temperature=0.2,
max_tokens=1000,
)
yanit = resp.choices[0].message.content.strip()
# Hallüsinasyon kontrolü
is_faithful, validation_warning, _ = (
self.hallucination_validator.validate_faithfulness(yanit, chunks)
)
if not is_faithful:
yanit = yanit + f"\n\n{validation_warning}"
log.info(
f"[Aşama 1] Başarılı: intent={intent}, k={k}, faithful={is_faithful}, sources={len(chunks)}"
)
self.memory.add_exchange(session_id, sorgu, yanit)
# Kaynak listesi (sadece mevzuat)
sources = []
for c in chunks:
if str(c.get("source", "")).lower() != "yargitay":
sources.append(
{
"kanun": c.get("law") or "",
"madde": (
str(c.get("article_no"))
if c.get("article_no") is not None
else ""
),
"ozet": (c.get("text") or "")[:200] + "...",
}
)
return {
"answer": yanit,
"sources": sources,
"intent": intent,
"query_rewritten": yeni_sorgu if yeni_sorgu != sorgu else None,
"hallucination_check": {
"is_faithful": is_faithful,
"warning": validation_warning,
},
"sure_ms": int((time.time() - t0) * 1000),
"filtered": False,
}
except RateLimitError:
return {
"answer": "Şu an çok fazla istek alıyorum, lütfen birkaç saniye sonra tekrar deneyin.",
"sources": [],
"error": "rate_limit",
}
except APITimeoutError:
return {
"answer": "Sunucu yanıt vermedi, lütfen tekrar deneyin.",
"sources": [],
"error": "timeout",
}
except Exception as e:
log.exception(f"Kritik Hata: {e}")
return {
"answer": "Teknik bir aksaklık oluştu. Lütfen tekrar deneyin.",
"sources": [],
"error": str(e),
}
# ═══════════════════════════════════════════════════════════════════════════════
# 5. FASTAPI ENTEGRASYON
# ═══════════════════════════════════════════════════════════════════════════════
@asynccontextmanager
async def lifespan(app: FastAPI):
get_retriever()
log.info("[Startup] Uygulama başladı (v5.8)")
yield
global _retriever_instance
if _retriever_instance and hasattr(_retriever_instance, "qdrant"):
_retriever_instance.qdrant.close()
log.info("[Shutdown] Uygulama kapatıldı")
def create_app() -> FastAPI:
app = FastAPI(
title="LawAgent AI API",
version="5.8",
description="Türk Hukuku Asistanı (Düzeltilmiş İçtihat Sırası + Esnek Tetikleyici)",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def handle_options(request: Request, call_next):
if request.method == "OPTIONS":
return Response(
status_code=200,
headers={
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "*",
},
)
return await call_next(request)
class AskRequest(BaseModel):
query: str
k: int = 7
session_id: str = "default"
class AskResponse(BaseModel):
answer: str
sources: List[Dict[str, str]]
intent: Optional[str] = None
query_rewritten: Optional[str] = None
hallucination_check: Optional[Dict] = None
sure_ms: int = 0
filtered: bool = False
gen = LegalGenerator()
@app.post("/ask", response_model=AskResponse)
async def ask(req: AskRequest):
if not req.query.strip():
return JSONResponse(status_code=400, content={"detail": "Sorgu boş."})
result = gen.generate(req.query, session_id=req.session_id, k=req.k)
return result
@app.get("/health")
async def health():
return {
"status": "ok",
"version": "5.8",
"features": [
"Düzeltilmiş içtihat talebi sırası",
"Esnek tetikleyici (emsal karar)",
"Gelişmiş Madde Kontrolü",
"İki Aşamalı İçtihat",
"Alaka Kontrolü",
],
}
@app.get("/memory/{session_id}")
async def get_memory(session_id: str):
history = gen.memory.get_history(session_id)
return {
"session_id": session_id,
"message_count": len(history),
"history": history,
}
return app
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--api", action="store_true", help="FastAPI sunucusu başlat")
parser.add_argument("--interactive", action="store_true", help="İnteraktif CLI mod")
args = parser.parse_args()
if args.api:
_port = int(os.getenv("PORT", 7860))
print(f"FastAPI sunucusu başlatılıyor... (port={_port})")
import uvicorn
app = create_app()
uvicorn.run(app, host="0.0.0.0", port=_port, log_level="info")
elif args.interactive:
gen = LegalGenerator()
session = "cli_session"
print("\n" + "=" * 70)
print("LawAgent AI v5.8 - Düzeltilmiş İçtihat Sırası + Esnek Tetikleyici")
print("=" * 70)
print(
"Soru sor → Mevzuat gelir → 'Evet' de → İçtihat + Mevzuat kaynakları birlikte gelir."
)
print("'quit' ile çıkış.\n")
while True:
sorgu = input("Soru: ").strip()
if sorgu.lower() in {"quit", "q", "çık"}:
break
if not sorgu:
continue
result = gen.generate(sorgu, session_id=session)
print("\n" + "-" * 70)
print(f"[{result.get('intent', 'UNKNOWN')}] Yanıt:\n")
print(result["answer"])
if result.get("hallucination_check", {}).get("warning"):
print(f"\n⚠️ {result['hallucination_check']['warning']}")
if result.get("sources"):
print(f"\n📚 Kaynaklar ({len(result['sources'])} adet):")
for i, src in enumerate(result["sources"], 1):
print(f" {i}. {src.get('kanun', '')} {src.get('madde', '')}")
print(f"\n⏱️ İşlem Süresi: {result.get('sure_ms', 0)}ms\n")
else:
parser.print_help()