Files
mstfyldz 47ca4cc962 fix: replace exact-phrase wrap with AND-required-terms in /search
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.
2026-08-09 10:55:34 +03:00

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"}