feat: setup rest_api and prepare coolify deployment
This commit is contained in:
+2
-1
@@ -18,6 +18,7 @@ COPY README.md ./
|
|||||||
COPY app.py ./
|
COPY app.py ./
|
||||||
COPY asgi_app.py ./
|
COPY asgi_app.py ./
|
||||||
COPY mcp_server_main.py ./
|
COPY mcp_server_main.py ./
|
||||||
|
COPY rest_api.py ./
|
||||||
|
|
||||||
# Copy MCP modules and shared packages
|
# Copy MCP modules and shared packages
|
||||||
COPY anayasa_mcp_module ./anayasa_mcp_module
|
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
|
CMD python -c "import httpx; httpx.get('http://localhost:8000/health', timeout=5)" || exit 1
|
||||||
|
|
||||||
# Run the ASGI application
|
# 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"]
|
||||||
|
|||||||
+232
@@ -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"}
|
||||||
Reference in New Issue
Block a user