initial
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
# emsal_mcp_module/client.py
|
||||
|
||||
import httpx
|
||||
# from bs4 import BeautifulSoup # Uncomment if needed for advanced HTML pre-processing
|
||||
from typing import Dict, Any, List, Optional
|
||||
import logging
|
||||
import html
|
||||
import re
|
||||
import tempfile
|
||||
import os
|
||||
from markitdown import MarkItDown
|
||||
|
||||
from .models import (
|
||||
EmsalSearchRequest,
|
||||
EmsalDetailedSearchRequestData,
|
||||
EmsalApiResponse,
|
||||
EmsalDocumentMarkdown
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if not logger.hasHandlers():
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
class EmsalApiClient:
|
||||
"""API Client for Emsal (UYAP Precedent Decision) search system."""
|
||||
BASE_URL = "https://emsal.uyap.gov.tr"
|
||||
DETAILED_SEARCH_ENDPOINT = "/aramadetaylist"
|
||||
DOCUMENT_ENDPOINT = "/getDokuman"
|
||||
|
||||
def __init__(self, request_timeout: float = 30.0):
|
||||
self.http_client = httpx.AsyncClient(
|
||||
base_url=self.BASE_URL,
|
||||
headers={
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
timeout=request_timeout,
|
||||
verify=False # As per user's original FastAPI code
|
||||
)
|
||||
|
||||
async def search_detailed_decisions(
|
||||
self,
|
||||
params: EmsalSearchRequest
|
||||
) -> EmsalApiResponse:
|
||||
"""Performs a detailed search for Emsal decisions."""
|
||||
|
||||
data_for_api_payload = EmsalDetailedSearchRequestData(
|
||||
arananKelime=params.keyword or "",
|
||||
Bam_Hukuk_Mahkemeleri=params.selected_bam_civil_court, # Uses alias "Bam Hukuk Mahkemeleri"
|
||||
Hukuk_Mahkemeleri=params.selected_civil_court, # Uses alias "Hukuk Mahkemeleri"
|
||||
birimHukukMah="+".join(params.selected_regional_civil_chambers) if params.selected_regional_civil_chambers else "",
|
||||
esasYil=params.case_year_esas or "",
|
||||
esasIlkSiraNo=params.case_start_seq_esas or "",
|
||||
esasSonSiraNo=params.case_end_seq_esas or "",
|
||||
kararYil=params.decision_year_karar or "",
|
||||
kararIlkSiraNo=params.decision_start_seq_karar or "",
|
||||
kararSonSiraNo=params.decision_end_seq_karar or "",
|
||||
baslangicTarihi=params.start_date or "",
|
||||
bitisTarihi=params.end_date or "",
|
||||
siralama=params.sort_criteria,
|
||||
siralamaDirection=params.sort_direction,
|
||||
pageSize=params.page_size,
|
||||
pageNumber=params.page_number
|
||||
)
|
||||
|
||||
final_payload = {"data": data_for_api_payload.model_dump(by_alias=True, exclude_none=True)}
|
||||
|
||||
logger.info(f"EmsalApiClient: Performing DETAILED search with payload: {final_payload}")
|
||||
return await self._execute_api_search(self.DETAILED_SEARCH_ENDPOINT, final_payload)
|
||||
|
||||
async def _execute_api_search(self, endpoint: str, payload: Dict) -> EmsalApiResponse:
|
||||
"""Helper method to execute search POST request and process response for Emsal."""
|
||||
try:
|
||||
response = await self.http_client.post(endpoint, json=payload)
|
||||
response.raise_for_status()
|
||||
response_json_data = response.json()
|
||||
logger.debug(f"EmsalApiClient: Raw API response from {endpoint}: {response_json_data}")
|
||||
|
||||
api_response_parsed = EmsalApiResponse(**response_json_data)
|
||||
|
||||
if api_response_parsed.data and api_response_parsed.data.data:
|
||||
for decision_item in api_response_parsed.data.data:
|
||||
if decision_item.id:
|
||||
decision_item.document_url = f"{self.BASE_URL}{self.DOCUMENT_ENDPOINT}?id={decision_item.id}"
|
||||
|
||||
return api_response_parsed
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"EmsalApiClient: HTTP request error during Emsal search to {endpoint}: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"EmsalApiClient: Error processing or validating Emsal search response from {endpoint}: {e}")
|
||||
raise
|
||||
|
||||
def _clean_html_and_convert_to_markdown_emsal(self, html_content_from_api_data_field: str) -> Optional[str]:
|
||||
"""
|
||||
Cleans HTML (from Emsal API 'data' field containing HTML string)
|
||||
and converts it to Markdown using MarkItDown.
|
||||
This assumes Emsal /getDokuman response is JSON with HTML in "data" field,
|
||||
similar to Yargitay and the last Emsal /getDokuman example.
|
||||
"""
|
||||
if not html_content_from_api_data_field:
|
||||
return None
|
||||
|
||||
# Basic HTML unescaping and fixing common escaped characters
|
||||
# Based on user's original fix_html_content in app/routers/emsal.py
|
||||
content = html.unescape(html_content_from_api_data_field)
|
||||
content = content.replace('\\"', '"')
|
||||
content = content.replace('\\r\\n', '\n')
|
||||
content = content.replace('\\n', '\n')
|
||||
content = content.replace('\\t', '\t')
|
||||
|
||||
# The HTML string from "data" field starts with "<html><head>..."
|
||||
html_input_for_markdown = content
|
||||
|
||||
markdown_text = None
|
||||
temp_file_path = None
|
||||
try:
|
||||
md_converter = MarkItDown(enable_plugins=False)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html", encoding="utf-8") as tmp_file:
|
||||
tmp_file.write(html_input_for_markdown)
|
||||
temp_file_path = tmp_file.name
|
||||
|
||||
conversion_result = md_converter.convert(temp_file_path)
|
||||
markdown_text = conversion_result.text_content
|
||||
logger.info("EmsalApiClient: HTML to Markdown conversion successful.")
|
||||
except Exception as e:
|
||||
logger.error(f"EmsalApiClient: Error during MarkItDown HTML to Markdown conversion for Emsal: {e}")
|
||||
finally:
|
||||
if temp_file_path and os.path.exists(temp_file_path):
|
||||
os.remove(temp_file_path)
|
||||
|
||||
return markdown_text
|
||||
|
||||
async def get_decision_document_as_markdown(self, document_id: str) -> EmsalDocumentMarkdown:
|
||||
"""
|
||||
Retrieves a specific Emsal decision by ID and returns its content as Markdown.
|
||||
Assumes Emsal /getDokuman endpoint returns JSON with HTML content in the 'data' field.
|
||||
"""
|
||||
document_api_url = f"{self.DOCUMENT_ENDPOINT}?id={document_id}"
|
||||
source_url = f"{self.BASE_URL}{document_api_url}"
|
||||
logger.info(f"EmsalApiClient: Fetching Emsal document for Markdown (ID: {document_id}) from {source_url}")
|
||||
|
||||
try:
|
||||
response = await self.http_client.get(document_api_url)
|
||||
response.raise_for_status()
|
||||
|
||||
# Emsal /getDokuman returns JSON with HTML in 'data' field (confirmed by user example)
|
||||
response_json = response.json()
|
||||
html_content_from_api = response_json.get("data")
|
||||
|
||||
if not isinstance(html_content_from_api, str) or not html_content_from_api.strip():
|
||||
logger.warning(f"EmsalApiClient: Received empty or non-string HTML in 'data' field for Emsal ID {document_id}.")
|
||||
return EmsalDocumentMarkdown(document_id=document_id, markdown_content=None, source_url=source_url)
|
||||
|
||||
markdown_content = self._clean_html_and_convert_to_markdown_emsal(html_content_from_api)
|
||||
|
||||
return EmsalDocumentMarkdown(
|
||||
document_id=document_id,
|
||||
markdown_content=markdown_content,
|
||||
source_url=source_url
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"EmsalApiClient: HTTP error fetching Emsal document (ID: {document_id}): {e}")
|
||||
raise
|
||||
except ValueError as e:
|
||||
logger.error(f"EmsalApiClient: ValueError processing Emsal document response (ID: {document_id}): {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"EmsalApiClient: General error processing Emsal document (ID: {document_id}): {e}")
|
||||
raise
|
||||
|
||||
async def close_client_session(self):
|
||||
"""Closes the HTTPX client session."""
|
||||
if self.http_client and not self.http_client.is_closed:
|
||||
await self.http_client.aclose()
|
||||
logger.info("EmsalApiClient: HTTP client session closed.")
|
||||
@@ -0,0 +1,104 @@
|
||||
# emsal_mcp_module/models.py
|
||||
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
class EmsalDetailedSearchRequestData(BaseModel):
|
||||
"""
|
||||
Internal model for the 'data' object in the Emsal detailed search payload.
|
||||
Field names use aliases to match the exact keys in the API payload
|
||||
(e.g., "Bam Hukuk Mahkemeleri" with spaces).
|
||||
The API expects empty strings for None/omitted optional fields.
|
||||
"""
|
||||
arananKelime: Optional[str] = ""
|
||||
|
||||
Bam_Hukuk_Mahkemeleri: Optional[str] = Field(None, alias="Bam Hukuk Mahkemeleri")
|
||||
Hukuk_Mahkemeleri: Optional[str] = Field(None, alias="Hukuk Mahkemeleri")
|
||||
# Add other specific court type fields from the form if they are separate keys in payload
|
||||
# E.g., "Ceza Mahkemeleri", "İdari Mahkemeler" etc.
|
||||
|
||||
birimHukukMah: Optional[str] = Field("", description="List of selected Regional Civil Chambers, '+' separated.")
|
||||
|
||||
esasYil: Optional[str] = ""
|
||||
esasIlkSiraNo: Optional[str] = ""
|
||||
esasSonSiraNo: Optional[str] = ""
|
||||
kararYil: Optional[str] = ""
|
||||
kararIlkSiraNo: Optional[str] = ""
|
||||
kararSonSiraNo: Optional[str] = ""
|
||||
baslangicTarihi: Optional[str] = ""
|
||||
bitisTarihi: Optional[str] = ""
|
||||
siralama: str # Mandatory in payload example
|
||||
siralamaDirection: str # Mandatory in payload example
|
||||
pageSize: int
|
||||
pageNumber: int
|
||||
|
||||
class Config:
|
||||
populate_by_name = True # Enables use of alias in serialization (when dumping to dict for payload)
|
||||
# anystr_strip_whitespace = True # Optional: strip whitespace from strings
|
||||
|
||||
class EmsalSearchRequest(BaseModel): # This is the model the MCP tool will accept
|
||||
"""Model for Emsal detailed search request, with user-friendly field names."""
|
||||
keyword: Optional[str] = Field(None, description="Keyword to search.")
|
||||
|
||||
selected_bam_civil_court: Optional[str] = Field(None, description="Selected BAM Civil Court (maps to 'Bam Hukuk Mahkemeleri' payload key).")
|
||||
selected_civil_court: Optional[str] = Field(None, description="Selected Civil Court (maps to 'Hukuk Mahkemeleri' payload key).")
|
||||
selected_regional_civil_chambers: Optional[List[str]] = Field(default_factory=list, description="Selected Regional Civil Chambers (for 'birimHukukMah', joined by '+').")
|
||||
|
||||
case_year_esas: Optional[str] = Field(None, description="Case year for 'Esas No'.")
|
||||
case_start_seq_esas: Optional[str] = Field(None, description="Starting sequence for 'Esas No'.")
|
||||
case_end_seq_esas: Optional[str] = Field(None, description="Ending sequence for 'Esas No'.")
|
||||
|
||||
decision_year_karar: Optional[str] = Field(None, description="Decision year for 'Karar No'.")
|
||||
decision_start_seq_karar: Optional[str] = Field(None, description="Starting sequence for 'Karar No'.")
|
||||
decision_end_seq_karar: Optional[str] = Field(None, description="Ending sequence for 'Karar No'.")
|
||||
|
||||
start_date: Optional[str] = Field(None, description="Start date for decision (DD.MM.YYYY).")
|
||||
end_date: Optional[str] = Field(None, description="End date for decision (DD.MM.YYYY).")
|
||||
|
||||
sort_criteria: str = Field("1", description="Sorting criteria (e.g., 1: Esas No).")
|
||||
sort_direction: str = Field("desc", description="Sorting direction ('asc' or 'desc').")
|
||||
|
||||
page_number: int = Field(default=1, ge=1)
|
||||
page_size: int = Field(default=10, ge=1, le=100)
|
||||
|
||||
|
||||
class EmsalApiDecisionEntry(BaseModel):
|
||||
"""Model for an individual decision entry from the Emsal API search response."""
|
||||
id: str
|
||||
daire: Optional[str] = Field(None, description="The chamber/court that made the decision.")
|
||||
esasNo: Optional[str] = Field(None)
|
||||
kararNo: Optional[str] = Field(None)
|
||||
kararTarihi: Optional[str] = Field(None)
|
||||
arananKelime: Optional[str] = Field(None, description="Matched keyword from the search.")
|
||||
durum: Optional[str] = Field(None, description="Status of the decision (e.g., 'KESİNLEŞMEDİ').")
|
||||
# index: Optional[int] = None # Present in Emsal response, can be added if tool needs it
|
||||
|
||||
document_url: Optional[HttpUrl] = Field(None, description="URL to the full document, constructed by the client.")
|
||||
|
||||
class Config:
|
||||
extra = 'ignore'
|
||||
|
||||
class EmsalApiResponseInnerData(BaseModel):
|
||||
"""Model for the inner 'data' object in the Emsal API search response."""
|
||||
data: List[EmsalApiDecisionEntry]
|
||||
recordsTotal: int
|
||||
recordsFiltered: int
|
||||
draw: Optional[int] = Field(None, description="Draw counter from API, usually for DataTables.")
|
||||
|
||||
class EmsalApiResponse(BaseModel):
|
||||
"""Model for the complete search response from the Emsal API."""
|
||||
data: EmsalApiResponseInnerData
|
||||
metadata: Optional[Dict[str, Any]] = Field(None, description="Optional metadata from API, if any.")
|
||||
|
||||
class EmsalDocumentMarkdown(BaseModel):
|
||||
"""Model for an Emsal decision document, containing only Markdown content."""
|
||||
document_id: str
|
||||
markdown_content: Optional[str] = Field(None, description="The decision content converted to Markdown.")
|
||||
source_url: HttpUrl
|
||||
|
||||
class CompactEmsalSearchResult(BaseModel):
|
||||
"""A compact search result model for the MCP tool to return."""
|
||||
decisions: List[EmsalApiDecisionEntry]
|
||||
total_records: int
|
||||
requested_page: int
|
||||
page_size: int
|
||||
Reference in New Issue
Block a user