Wrapping every multi-word phrase in exact-phrase quotes (added for the "uyuşturucu madde ticareti" false-positive problem) was too strict for independent keyword queries like "bıçak yaralaması beraat" or the AI query-optimizer's extracted keywords — those words rarely appear verbatim adjacent to each other, so exact-phrase returned 0 results. Now each word is prefixed with + instead (AND semantics: all words must appear somewhere in the decision, not necessarily adjacent). This still fixes the original "madde" noise problem while no longer breaking loose multi-keyword searches.
244 lines
8.9 KiB
Python
244 lines
8.9 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'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"}
|