# 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'deki her kelime otomatik olarak '+' ile zorunlu " "kilinir (orn. 'a b' -> '+a +b') — tum kelimeler karar icinde gecmeli ama yan yana " "olmalari gerekmez. Bedesten API'de tirnaksiz coklu kelime aramasi kelimeleri gevsek " "eslestirir (orn. 'madde' gibi her kararda gecen ortak kelimeler alakasiz sonuclari " "one cikarir); bu ayar 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 daha isabetli hale getirme --- # # Bedesten API'de tirnaksiz coklu kelime aramasi kelimeleri ayri ayri (gevsek) 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. # # Once tum ifadeyi tirnaklayip "tam bitisik ifade" aramasi denendi, ama bu cok kati # cikti: "bicak yaralamasi beraat" gibi bagimsiz anahtar kelimelerden olusan (AI'nin # uzun sorulardan cikardigi turden) aramalar, bu 3 kelime kararlarda hic yan yana/aynen # gecmedigi icin 0 sonuc donduruyordu. Bunun yerine her kelimeyi "+" ile ayri ayri # zorunlu kiliyoruz (AND semantigi): tum kelimeler karar icinde herhangi bir yerde # gecmeli ama bitisik/ayni sirada olmalari gerekmiyor. Bu hem orijinal "madde" sorununu # cozuyor (uyusturucu VE madde VE ticaret hepsi gecmeli) hem de bagimsiz anahtar kelime # aramalarini kirmiyor. _OPERATOR_PATTERN = re.compile(r'"|\bAND\b|\bOR\b|\bNOT\b|(?:^|\s)[+-]\S', re.IGNORECASE) def _apply_required_terms(phrase: str, exact_phrase: bool) -> str: stripped = phrase.strip() if not exact_phrase or not stripped: return phrase words = stripped.split() if len(words) < 2: return phrase if _OPERATOR_PATTERN.search(stripped): return phrase return " ".join(f"+{w}" for w in words) # --- 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_required_terms(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"}