233 lines
8.3 KiB
Python
233 lines
8.3 KiB
Python
# rest_api.py
|
||
#
|
||
# Bu dosya, yargi-mcp fork'unun kok dizinine eklenir (mcp_server_main.py ile ayni seviyede).
|
||
# Amac: FastMCP/MCP protokolunu (JSON-RPC) tamamen atlayip, mcp_server_main.py'deki
|
||
# gercek client nesnelerini (bedesten_client_instance vb.) dogrudan cagiran duz bir
|
||
# REST API sunmak. Boylece Node backend, MCP istemcisi konusmadan basit HTTP ile
|
||
# bu servise istek atabilir.
|
||
#
|
||
# Calistirma:
|
||
# uvicorn rest_api:app --host 0.0.0.0 --port 8001
|
||
#
|
||
# NOT: mcp_server_main.py'deki bedesten_client_instance nesnesini yeniden olusturmak
|
||
# yerine dogrudan ayni siniflari import edip burada kendi instance'imizi kuruyoruz.
|
||
# Boylece mcp_server_main.py'yi (FastMCP/app.tool decorator'lariyla) hic calistirmaya
|
||
# gerek kalmiyor, sadece alttaki client katmanini kullaniyoruz.
|
||
|
||
from fastapi import FastAPI, HTTPException, Query
|
||
from pydantic import BaseModel, Field
|
||
from typing import List, Optional
|
||
import logging
|
||
import re
|
||
|
||
from bedesten_mcp_module.client import BedestenApiClient, BedestenRateLimited
|
||
from bedesten_mcp_module.models import (
|
||
BedestenSearchRequest,
|
||
BedestenSearchData,
|
||
BedestenCourtTypeEnum,
|
||
)
|
||
from bedesten_mcp_module.enums import BirimAdiEnum
|
||
import httpx
|
||
|
||
logging.basicConfig(level=logging.INFO)
|
||
logger = logging.getLogger("rest_api")
|
||
|
||
app = FastAPI(title="LegalOS - Yargi Karar Arama REST API")
|
||
|
||
bedesten_client = BedestenApiClient()
|
||
|
||
|
||
# --- Request/Response semalari ---
|
||
|
||
class SearchRequest(BaseModel):
|
||
phrase: str = Field(..., description="Arama ifadesi (Turkce). Ornek: 'mulkiyet hakki'")
|
||
court_types: List[str] = Field(
|
||
default=["YARGITAYKARARI", "DANISTAYKARAR"],
|
||
description="YARGITAYKARARI, DANISTAYKARAR, YERELHUKUK, ISTINAFHUKUK, KYB",
|
||
)
|
||
page_number: int = Field(default=1, ge=1)
|
||
birim_adi: str = Field(default="ALL", description="Daire filtresi, orn. H1, C3, HGK")
|
||
karar_tarihi_start: Optional[str] = Field(default=None, description="YYYY-MM-DD")
|
||
karar_tarihi_end: Optional[str] = Field(default=None, description="YYYY-MM-DD")
|
||
exact_phrase: bool = Field(
|
||
default=True,
|
||
description=(
|
||
"True ise coklu kelimeli phrase otomatik tam ifade (\"...\") aramasina cevrilir. "
|
||
"Bedesten API'de tirnaksiz coklu kelime aramasi kelimeleri ayri ayri eslestirir "
|
||
"(orn. 'madde' gibi her kararda gecen ortak kelimeler alakasiz sonuclari one cikarir); "
|
||
"tam ifade araması bu gurultuyu onler. Kullanici zaten tirnak/AND/OR/NOT/+/- kullaniyorsa dokunulmaz."
|
||
),
|
||
)
|
||
|
||
|
||
class SearchResultItem(BaseModel):
|
||
document_id: str
|
||
raw: dict
|
||
|
||
|
||
class SearchResponse(BaseModel):
|
||
decisions: List[dict]
|
||
total_records: int
|
||
requested_page: int
|
||
page_size: int
|
||
searched_courts: List[str]
|
||
error: Optional[str] = None
|
||
retry_after: Optional[float] = None
|
||
|
||
|
||
class DocumentResponse(BaseModel):
|
||
document_id: str
|
||
markdown_content: Optional[str]
|
||
source_url: Optional[str]
|
||
mime_type: Optional[str]
|
||
|
||
|
||
# --- Yardimci fonksiyon: tarih formatlama (mcp_server_main.py'deki mantikla ayni) ---
|
||
|
||
def _format_date(value: Optional[str], end_of_day: bool = False) -> str:
|
||
if not value:
|
||
return ""
|
||
if value.endswith("Z"):
|
||
return value
|
||
if "T" not in value:
|
||
suffix = "T23:59:59.999Z" if end_of_day else "T00:00:00.000Z"
|
||
return f"{value}{suffix}"
|
||
return value
|
||
|
||
|
||
# --- Yardimci fonksiyon: coklu kelimeli aramalari tam ifadeye cevirme ---
|
||
#
|
||
# Bedesten API'de tirnaksiz coklu kelime aramasi kelimeleri ayri ayri eslestiriyor.
|
||
# Ornek: "uyusturucu madde ticareti" -> "madde" gibi her kararda gecen (kanun maddesi
|
||
# anlaminda) ortak bir kelime yuzunden alakasiz sonuclar (ic icra/iflas kararlari) one
|
||
# cikabiliyor. Kullanici zaten ozel operator/tirnak kullanmiyorsa, coklu kelimeli
|
||
# aramayi otomatik tam ifadeye ("...") ceviriyoruz.
|
||
_OPERATOR_PATTERN = re.compile(r'"|\bAND\b|\bOR\b|\bNOT\b|(?:^|\s)[+-]\S', re.IGNORECASE)
|
||
|
||
|
||
def _apply_exact_phrase(phrase: str, exact_phrase: bool) -> str:
|
||
stripped = phrase.strip()
|
||
if not exact_phrase or not stripped:
|
||
return phrase
|
||
if len(stripped.split()) < 2:
|
||
return phrase
|
||
if _OPERATOR_PATTERN.search(stripped):
|
||
return phrase
|
||
return f'"{stripped}"'
|
||
|
||
|
||
# --- Endpoint 1: Arama ---
|
||
|
||
@app.post("/search", response_model=SearchResponse)
|
||
async def search(req: SearchRequest):
|
||
"""
|
||
Yargitay / Danistay / yerel mahkeme / istinaf / KYB kararlarinda arama yapar.
|
||
mcp_server_main.py'deki search_bedesten_unified aracinin dogrudan REST karsiligi.
|
||
"""
|
||
karar_tarihi_start = _format_date(req.karar_tarihi_start)
|
||
karar_tarihi_end = _format_date(req.karar_tarihi_end, end_of_day=True)
|
||
phrase = _apply_exact_phrase(req.phrase, req.exact_phrase)
|
||
|
||
search_data = BedestenSearchData(
|
||
pageSize=10,
|
||
pageNumber=req.page_number,
|
||
itemTypeList=req.court_types,
|
||
phrase=phrase,
|
||
birimAdi=req.birim_adi,
|
||
kararTarihiStart=karar_tarihi_start,
|
||
kararTarihiEnd=karar_tarihi_end,
|
||
)
|
||
search_request = BedestenSearchRequest(data=search_data)
|
||
|
||
logger.info(f"search: phrase={req.phrase!r} -> sent={phrase!r} courts={req.court_types} page={req.page_number}")
|
||
|
||
try:
|
||
response = await bedesten_client.search_documents(search_request)
|
||
|
||
if response.data is None:
|
||
return SearchResponse(
|
||
decisions=[],
|
||
total_records=0,
|
||
requested_page=req.page_number,
|
||
page_size=10,
|
||
searched_courts=req.court_types,
|
||
error="no_data",
|
||
)
|
||
|
||
decisions = response.data.emsalKararList or []
|
||
total = response.data.total or 0
|
||
|
||
return SearchResponse(
|
||
decisions=[d.model_dump() for d in decisions],
|
||
total_records=total,
|
||
requested_page=req.page_number,
|
||
page_size=10,
|
||
searched_courts=req.court_types,
|
||
)
|
||
|
||
except BedestenRateLimited as e:
|
||
logger.warning(f"local rate limit hit, retry_after={e.retry_after}")
|
||
raise HTTPException(
|
||
status_code=429,
|
||
detail={"error": "rate_limit_exceeded", "retry_after": e.retry_after},
|
||
)
|
||
except httpx.HTTPStatusError as e:
|
||
if e.response.status_code == 429:
|
||
retry_after = e.response.headers.get("Retry-After", "")
|
||
raise HTTPException(
|
||
status_code=429,
|
||
detail={"error": "upstream_rate_limit", "retry_after": retry_after},
|
||
)
|
||
logger.exception("Bedesten search error")
|
||
raise HTTPException(status_code=502, detail="Bedesten API hatasi")
|
||
except Exception:
|
||
logger.exception("Beklenmeyen arama hatasi")
|
||
raise HTTPException(status_code=500, detail="Sunucu hatasi")
|
||
|
||
|
||
# --- Endpoint 2: Belge getirme (tam metin, Markdown) ---
|
||
|
||
@app.get("/document/{document_id}", response_model=DocumentResponse)
|
||
async def get_document(document_id: str):
|
||
"""
|
||
Bir kararin tam metnini Markdown formatinda getirir.
|
||
mcp_server_main.py'deki get_bedesten_document_markdown aracinin REST karsiligi.
|
||
"""
|
||
if not document_id.strip():
|
||
raise HTTPException(status_code=400, detail="document_id bos olamaz")
|
||
|
||
logger.info(f"get_document: id={document_id}")
|
||
|
||
try:
|
||
doc = await bedesten_client.get_document_as_markdown(document_id)
|
||
return DocumentResponse(
|
||
document_id=document_id,
|
||
markdown_content=doc.markdown_content,
|
||
source_url=doc.source_url,
|
||
mime_type=doc.mime_type,
|
||
)
|
||
except BedestenRateLimited as e:
|
||
raise HTTPException(
|
||
status_code=429,
|
||
detail={"error": "rate_limit_exceeded", "retry_after": e.retry_after},
|
||
)
|
||
except httpx.HTTPStatusError as e:
|
||
if e.response.status_code == 429:
|
||
retry_after = e.response.headers.get("Retry-After", "")
|
||
raise HTTPException(
|
||
status_code=429,
|
||
detail={"error": "upstream_rate_limit", "retry_after": retry_after},
|
||
)
|
||
logger.exception("Bedesten document fetch error")
|
||
raise HTTPException(status_code=502, detail="Bedesten API hatasi")
|
||
except Exception:
|
||
logger.exception("Beklenmeyen belge getirme hatasi")
|
||
raise HTTPException(status_code=500, detail="Sunucu hatasi")
|
||
|
||
|
||
# --- Endpoint 3: Health check (Coolify icin) ---
|
||
|
||
@app.get("/health")
|
||
async def health():
|
||
return {"status": "ok"}
|