Files
saidsurucuandClaude Opus 4.8 fadc3b0bc0 fix(aym,uyusmazlik): adapt to rebuilt AYM and Uyuşmazlık sites
Both sites were rebuilt and their old endpoints now 404:
- AYM moved to a single-page app backed by a JSON API
  (POST /api/core/public/search, kararTipi NormDenetimi/BireyselBasvuru;
  full text via {id, size:1} -> "icerik" HTML). Old /Ara, /ND/, /BB/ gone.
- Uyuşmazlık moved to ASP.NET WebForms (viewstate postback to /, GridView
  results, decisions served as /Uploads/{EsasNo}.pdf). Old /Arama/Search gone.

Changes:
- New anayasa_mcp_module/api_client.py: shared KBB JSON client, base64url
  document-id codec, HTML->Markdown + HTML text stripping helpers.
- Rewrite anayasa client/bireysel_client/unified_client over the new API,
  preserving public method names and response models.
- Rewrite uyusmazlik client for the postback flow + GridView parse + PDF
  document conversion; simplify request model to text + scope + paging.
- Trim search_anayasa_unified and search_uyusmazlik_decisions tool signatures
  to parameters the new APIs actually support; drop dead enums.

Verified live via FastMCP client: search + document retrieval work for AYM
norm/bireysel and Uyuşmazlık.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 19:22:49 +03:00

151 lines
5.9 KiB
Python

# anayasa_mcp_module/client.py
# Norm Denetimi client backed by the new KBB JSON API (see api_client.py).
#
# The Anayasa Mahkemesi sites were rebuilt as a single-page app; the old
# HTML-scraping endpoints on normkararlarbilgibankasi.anayasa.gov.tr/Ara now
# return HTTP 404. This client maps the rich legacy request model onto the new
# free-text "query" search and rebuilds the legacy response models from the JSON
# payload so existing tooling keeps working.
import logging
import math
from typing import List, Optional
from .api_client import (
AnayasaApiClient,
KARAR_TIPI_NORM,
DOCUMENT_MARKDOWN_CHUNK_SIZE,
build_document_url,
parse_document_url,
convert_icerik_to_markdown,
strip_html_text,
)
from .models import (
AnayasaNormDenetimiSearchRequest,
AnayasaDecisionSummary,
AnayasaSearchResult,
AnayasaDocumentMarkdown,
)
logger = logging.getLogger(__name__)
if not logger.hasHandlers():
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
def _build_query(params: AnayasaNormDenetimiSearchRequest) -> str:
"""Derive the free-text query string the new API expects from the legacy model.
The new endpoint only supports a single full-text "query" field, so the
keyword lists are flattened. Esas/Karar numbers are appended when no keyword
is provided so number-based lookups still return results.
"""
terms: List[str] = []
for bucket in (params.keywords_all, params.keywords_any):
if bucket:
terms.extend(t for t in bucket if t)
if not terms:
for value in (params.case_number_esas, params.decision_number_karar):
if value:
terms.append(value)
return " ".join(terms).strip()
class AnayasaMahkemesiApiClient:
"""Norm Denetimi search/document client over the KBB JSON API."""
def __init__(self, request_timeout: float = 60.0):
self.api = AnayasaApiClient(request_timeout)
async def search_norm_denetimi_decisions(
self,
params: AnayasaNormDenetimiSearchRequest,
) -> AnayasaSearchResult:
query = _build_query(params)
payload = await self.api.search(
karar_tipi=KARAR_TIPI_NORM,
query=query,
page=params.page_to_fetch,
size=params.results_per_page,
)
total_records = int(payload.get("total") or 0)
decisions: List[AnayasaDecisionSummary] = []
for item in payload.get("data") or []:
esas_no = item.get("esasNo") or ""
karar_no = item.get("kararNo") or ""
if esas_no and karar_no:
reference = f"E.{esas_no}, K.{karar_no}"
else:
reference = esas_no or karar_no or ""
decisions.append(AnayasaDecisionSummary(
decision_reference_no=reference,
decision_page_url=build_document_url(KARAR_TIPI_NORM, item.get("id", "")),
keywords_found_count=item.get("highlightCount") or 0,
application_type_summary=item.get("basvuruTuruLabel") or "",
applicant_summary=item.get("basvuranGenelLabel") or "",
decision_outcome_summary=strip_html_text(item.get("kararKonusu")),
decision_date_summary=item.get("kararTarihi") or "",
reviewed_norms=[],
))
return AnayasaSearchResult(
decisions=decisions,
total_records_found=total_records,
retrieved_page_number=params.page_to_fetch,
)
async def get_decision_document_as_markdown(
self,
document_url: str,
page_number: int = 1,
) -> AnayasaDocumentMarkdown:
karar_tipi, uuid = parse_document_url(document_url)
if karar_tipi is None:
karar_tipi = KARAR_TIPI_NORM
record = await self.api.get_decision(karar_tipi, uuid) if uuid else None
if not record:
logger.warning("AnayasaMahkemesiApiClient: No record for document_url %s", document_url)
return AnayasaDocumentMarkdown(
source_url=document_url, markdown_chunk=None,
current_page=page_number, total_pages=0, is_paginated=False,
)
esas_no = record.get("esasNo") or ""
karar_no = record.get("kararNo") or ""
reference = f"E.{esas_no}, K.{karar_no}" if (esas_no and karar_no) else (esas_no or karar_no or "")
rg_tarihi = record.get("resmiGazeteTarihi") or ""
rg_sayisi = record.get("resmiGazeteSayisi")
official_gazette = f"{rg_tarihi} / {rg_sayisi}".strip(" /") if (rg_tarihi or rg_sayisi) else ""
full_markdown = convert_icerik_to_markdown(record.get("icerik"))
if not full_markdown:
return AnayasaDocumentMarkdown(
source_url=document_url,
decision_reference_no_from_page=reference,
decision_date_from_page=record.get("kararTarihi") or "",
official_gazette_info_from_page=official_gazette,
markdown_chunk=None, current_page=page_number, total_pages=0, is_paginated=False,
)
total_pages = max(1, math.ceil(len(full_markdown) / DOCUMENT_MARKDOWN_CHUNK_SIZE))
current_page = max(1, min(page_number, total_pages))
start = (current_page - 1) * DOCUMENT_MARKDOWN_CHUNK_SIZE
chunk = full_markdown[start:start + DOCUMENT_MARKDOWN_CHUNK_SIZE]
return AnayasaDocumentMarkdown(
source_url=document_url,
decision_reference_no_from_page=reference,
decision_date_from_page=record.get("kararTarihi") or "",
official_gazette_info_from_page=official_gazette,
markdown_chunk=chunk,
current_page=current_page,
total_pages=total_pages,
is_paginated=(total_pages > 1),
)
async def close_client_session(self):
await self.api.close()
logger.info("AnayasaMahkemesiApiClient (Norm Denetimi): HTTP client session closed.")