diff --git a/Dockerfile b/Dockerfile index b7780e0..c7ebd3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ COPY README.md ./ COPY app.py ./ COPY asgi_app.py ./ COPY mcp_server_main.py ./ +COPY rest_api.py ./ # Copy MCP modules and shared packages COPY anayasa_mcp_module ./anayasa_mcp_module @@ -51,4 +52,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD python -c "import httpx; httpx.get('http://localhost:8000/health', timeout=5)" || exit 1 # Run the ASGI application -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "rest_api:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/rest_api.py b/rest_api.py new file mode 100644 index 0000000..88578ff --- /dev/null +++ b/rest_api.py @@ -0,0 +1,232 @@ +# 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"}