Files
yargi-mcp/rest_api.py
T

233 lines
8.3 KiB
Python
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.
# 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"}