Compare commits
19
Commits
install.bat
..
kik
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
180c29e82e | ||
|
|
957df46b7d | ||
|
|
9df6eba37c | ||
|
|
94553c3b78 | ||
|
|
539b0c7041 | ||
|
|
4a2af434ab | ||
|
|
c1352c868d | ||
|
|
2271423c91 | ||
|
|
d59b498be7 | ||
|
|
3c10913680 | ||
|
|
32c4d06eb6 | ||
|
|
f892d430cb | ||
|
|
b19ccc60d7 | ||
|
|
f6e95140cf | ||
|
|
4d86833fd6 | ||
|
|
81d82369da | ||
|
|
ae9bdebdad | ||
|
|
f82dc9f493 | ||
|
|
970dd65bce |
+3
-1
@@ -160,4 +160,6 @@ cython_debug/
|
||||
#.idea/
|
||||
.DS_Store
|
||||
hello.py
|
||||
*.toml
|
||||
|
||||
*.html
|
||||
test_kik_client.py
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://www.star-history.com/#saidsurucu/yargi-mcp&Date)
|
||||
|
||||
Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kararlar, Uyuşmazlık Mahkemesi ve Anayasa Mahkemesi - Norm Denetimi ile Bireysel Başvuru Kararları) erişimi kolaylaştıran bir [FastMCP](https://gofastmcp.com/) sunucusu oluşturur. Bu sayede, bu kaynaklardan veri arama ve belge getirme işlemleri, Model Context Protocol (MCP) destekleyen LLM (Büyük Dil Modeli) uygulamaları (örneğin Claude Desktop) ve diğer istemciler tarafından araç (tool) olarak kullanılabilir hale gelir.
|
||||
Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kararlar, Uyuşmazlık Mahkemesi, Anayasa Mahkemesi - Norm Denetimi ile Bireysel Başvuru Kararları ve Kamu İhale Kurulu Kararları) erişimi kolaylaştıran bir [FastMCP](https://gofastmcp.com/) sunucusu oluşturur. Bu sayede, bu kaynaklardan veri arama ve belge getirme işlemleri, Model Context Protocol (MCP) destekleyen LLM (Büyük Dil Modeli) uygulamaları (örneğin Claude Desktop) ve diğer istemciler tarafından araç (tool) olarak kullanılabilir hale gelir.
|
||||
|
||||

|
||||
|
||||
@@ -16,13 +16,103 @@ Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kara
|
||||
* **Uyuşmazlık Mahkemesi:** Form tabanlı kriterlerle karar arama ve karar metinlerini (URL ile erişilen) Markdown formatında getirme.
|
||||
* **Anayasa Mahkemesi (Norm Denetimi):** Kapsamlı kriterlerle norm denetimi kararlarını arama; uzun karar metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme.
|
||||
* **Anayasa Mahkemesi (Bireysel Başvuru):** Kapsamlı kriterlerle bireysel başvuru "Karar Arama Raporu" oluşturma ve listedeki kararların metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme.
|
||||
* **KİK (Kamu İhale Kurulu):** Çeşitli kriterlerle Kurul kararlarını arama; uzun karar metinlerini (varsayılan 5.000 karakterlik) sayfalanmış Markdown formatında getirme (Playwright ile tarayıcı otomasyonu kullanılır).
|
||||
* Karar metinlerinin daha kolay işlenebilmesi için Markdown formatına çevrilmesi.
|
||||
* Claude Desktop uygulaması ile `fastmcp install` komutu kullanılarak kolay entegrasyon.
|
||||
|
||||
---
|
||||
📋 **Ön Gereksinimler**
|
||||
|
||||
* **Python Sürümü:** Python 3.10 veya daha yeni bir sürümünün sisteminizde kurulu olması gerekmektedir. Python'ı [python.org](https://www.python.org/) adresinden indirebilirsiniz.
|
||||
* **Paket Yöneticisi:** `pip` (Python ile birlikte gelir) veya tercihen `uv` ([Astral](https://astral.sh/uv) tarafından geliştirilen hızlı Python paket yöneticisi).
|
||||
Bu Yargı MCP aracını Claude Desktop ile kullanabilmek için öncelikle aşağıdaki yazılımların sisteminizde kurulu olması gerekmektedir:
|
||||
|
||||
1. **Claude Desktop:** Henüz kurmadıysanız, [Claude Desktop web sitesinden](https://claude.ai/desktop) işletim sisteminize uygun sürümü indirip kurun.
|
||||
2. **Python Sürümü:** **Python 3.11** sürümü tavsiye edilir. Python 3.12 ve üzeri yeni sürümler, bazı bağımlılıklarda (özellikle `playwright` ve ilişkili tarayıcı sürücüleri) belirli ortamlarda uyumluluk sorunlarına yol açabilir. Bu proje için 3.11 sürümü stabilite açısından önerilmektedir.
|
||||
* **Windows Kullanıcıları:** Eğer Python kurulu değilse, [python.org/downloads/windows/](https://www.python.org/downloads/windows/) adresinden Python 3.11'in uygun bir sürümünü indirip kurabilirsiniz. Kurulum sırasında "**Add Python to PATH**" (Python'ı PATH'e ekle) seçeneğini işaretlemeyi unutmayın.
|
||||
* **macOS Kullanıcıları:** macOS genellikle Python ile birlikte gelir. Terminal'de `python3 --version` yazarak kontrol edebilirsiniz. Eğer Python 3.11 değilse veya eski bir sürümse, [python.org](https://www.python.org/downloads/macos/) adresinden veya [Homebrew](https://brew.sh/) (`brew install python@3.11`) ile kurabilirsiniz.
|
||||
* **Linux Kullanıcıları:** Çoğu Linux dağıtımı Python ile gelir. Terminal'de `python3 --version` yazarak kontrol edebilirsiniz. Gerekirse dağıtımınızın paket yöneticisi ile Python 3.11'i kurabilirsiniz (örn: `sudo apt update && sudo apt install python3.11 python3.11-pip python3.11-venv` veya dağıtımınıza uygun komutlar).
|
||||
3. **Paket Yöneticisi:** `pip` (Python ile birlikte gelir) veya tercihen `uv` ([Astral](https://astral.sh/uv) tarafından geliştirilen hızlı Python paket yöneticisi). Kurulum script'lerimiz `uv`'yi sizin için kurmayı deneyecektir.
|
||||
4. **Playwright Tarayıcıları:** KİK modülü Playwright kullandığı için, ilgili tarayıcıların kurulmuş olması gerekir. `KikApiClient` varsayılan olarak Chromium kullanır. Eğer Playwright veya tarayıcıları manuel kuracaksanız:
|
||||
```bash
|
||||
# Önce playwright kütüphanesini kurun (uv veya pip ile)
|
||||
# uv pip install playwright
|
||||
# pip install playwright
|
||||
|
||||
# Sonra tarayıcıları kurun (proje bağımlılıkları kurulduktan sonra da yapılabilir)
|
||||
playwright install --with-deps chromium
|
||||
# '--with-deps' chromium için gerekli işletim sistemi bağımlılıklarını da kurmaya çalışır.
|
||||
```
|
||||
Kurulum scriptleri (`install.bat`, `install.sh`, `install.py`) genellikle `playwright` Python kütüphanesini kurar. Tarayıcıların ayrıca `playwright install` ile kurulması gerekebilir; eğer sunucu başlatılırken KİK modülü hata verirse, bu adımı manuel olarak çalıştırmanız gerekebilir.
|
||||
|
||||
---
|
||||
🚀 **Kolay Kurulum Adımları (Claude Desktop için)**
|
||||
|
||||
Bu bölüm, teknik bilgisi daha az olan kullanıcıların **Yargı MCP** araçlarını Claude Desktop uygulamalarına hızlı ve kolay bir şekilde entegre etmeleri için hazırlanmıştır.
|
||||
|
||||
**Öncelikle Yapılması Gerekenler:**
|
||||
|
||||
1. **Proje Dosyalarını İndirin:**
|
||||
* Bu GitHub deposunun ana sayfasına gidin.
|
||||
* Yeşil renkli "**<> Code**" düğmesine tıklayın.
|
||||
* Açılan menüden "**Download ZIP**" seçeneğini seçin.
|
||||
* İndirdiğiniz ZIP dosyasını bilgisayarınızda kolayca erişebileceğiniz bir klasöre çıkartın (örneğin, `Belgelerim` veya `Masaüstü` altında `yargi-mcp` adında bir klasör oluşturabilirsiniz).
|
||||
|
||||
Proje dosyalarını bilgisayarınıza aldıktan sonra, işletim sisteminize uygun kurulum script'ini çalıştırabilirsiniz.
|
||||
|
||||
### Windows Kullanıcıları İçin (`install.bat`)
|
||||
|
||||
1. Proje dosyalarını çıkarttığınız klasörün içine gidin (örneğin, `C:\Users\KULLANICIADINIZ\Documents\yargi-mcp` klasörü).
|
||||
2. `install.bat` adlı dosyayı bulun.
|
||||
3. Bu dosyaya **çift tıklayarak** çalıştırın.
|
||||
4. Kurulum sırasında bir komut istemi penceresi açılacak ve bazı mesajlar göreceksiniz. Script, gerekli araçları (`uv`, `fastmcp` CLI, `playwright` ve tarayıcıları) sisteminize kurmayı deneyecek ve ardından "**Yargı MCP**" aracını Claude Desktop'a entegre edecektir.
|
||||
* *Not: Bu işlem sırasında script sizden yönetici onayı isteyebilir veya internet bağlantısı gerektirebilir. `uv` kurulumu için PowerShell script çalıştırma politikalarınızda geçici bir değişiklik yapılması gerekebilir; script bunu sizin için halletmeye çalışacaktır.*
|
||||
5. Kurulum tamamlandığında, komut istemi penceresinde bir başarı mesajı göreceksiniz. Pencere, "Devam etmek için bir tuşa basın..." mesajıyla açık kalacaktır. Herhangi bir tuşa basarak pencereyi kapatabilirsiniz.
|
||||
6. **Önemli:** Kurulumun etkili olması için Claude Desktop uygulamasını tamamen kapatıp yeniden başlatmanız gerekebilir.
|
||||
|
||||
### macOS ve Linux Kullanıcıları İçin (`install.sh`)
|
||||
|
||||
1. Proje dosyalarını çıkarttığınız klasörün içine gidin (örneğin, `/Users/KULLANICIADINIZ/Documents/yargi-mcp` klasörü).
|
||||
2. **Terminal** uygulamasını açın:
|
||||
* **macOS'te:**
|
||||
1. **Finder**'ı açın.
|
||||
2. Proje dosyalarını çıkarttığınız klasöre (`yargi-mcp` gibi) gidin.
|
||||
3. Finder penceresinin en altındaki yol çubuğunda (Path Bar), klasör adının üzerine **Control tuşuna basılı tutarak tıklayın** (veya sağ tıklayın).
|
||||
4. Açılan menüden "**Terminal'de Aç**" seçeneğini seçin. (Eğer bu seçenek yoksa, Finder'da `Görünüm > Yol Çubuğunu Göster` seçeneğinin aktif olduğundan emin olun. Alternatif olarak, `Uygulamalar > İzlenceler > Terminal` yolunu izleyip `cd` komutuyla klasörünüze gidin.)
|
||||
* **Linux'ta:** Genellikle dosya yöneticisinde klasöre sağ tıklayıp "Burada Terminal Aç" seçeneğini kullanabilir veya Ctrl+Alt+T kısayoluyla terminal açıp `cd` komutuyla klasörünüze gidebilirsiniz.
|
||||
3. Terminalde, doğru klasörde olduğunuzdan emin olduktan sonra, script'e çalıştırma izni verin (bu işlemi sadece bir kez yapmanız yeterlidir):
|
||||
```bash
|
||||
chmod +x install.sh
|
||||
```
|
||||
4. Script'i çalıştırın:
|
||||
```bash
|
||||
./install.sh
|
||||
```
|
||||
5. Kurulum sırasında terminalde bazı mesajlar göreceksiniz. Script, gerekli araçları (`uv`, `fastmcp` CLI) sisteminize kurmayı deneyecek ve ardından "**Yargı MCP**" aracını Claude Desktop'a entegre edecektir.
|
||||
* *Not: Bu işlem sırasında script sizden şifrenizi (`sudo` yetkileri için, özellikle `uv` kurulumunda) isteyebilir veya internet bağlantısı gerektirebilir.*
|
||||
6. Kurulum tamamlandığında, terminalde bir başarı mesajı göreceksiniz.
|
||||
7. **Önemli:** Kurulumun etkili olması için Claude Desktop uygulamasını tamamen kapatıp yeniden başlatmanız gerekebilir. Ayrıca, eğer `uv` veya `fastmcp` PATH'e yeni eklendiyse, terminalinizi de yeniden başlatmanız veya shell profilinizi (`~/.bashrc`, `~/.zshrc`, `~/.profile` vb.) `source ~/.zshrc` (veya kullandığınız shell'e uygun komutla) yeniden yüklemeniz gerekebilir. Script bu konuda sizi uyaracaktır.
|
||||
|
||||
### Python Script ile Kurulum (`install.py`) (Platform Bağımsız Alternatif)
|
||||
|
||||
Eğer yukarıdaki işletim sistemine özel script'lerde sorun yaşarsanız veya Python tabanlı bir kurulumu tercih ediyorsanız, `install.py` script'ini kullanabilirsiniz. Bu yöntem, sisteminizde Python 3.11'in ve `pip` paket yöneticisinin kurulu ve çalışır durumda olmasını gerektirir.
|
||||
|
||||
1. Proje dosyalarını çıkarttığınız klasörün içine Terminal veya Komut İstemi ile gidin.
|
||||
2. Aşağıdaki komutu çalıştırın (sisteminizdeki Python 3 komutuna göre `python` veya `python3` kullanın):
|
||||
```bash
|
||||
python3 install.py
|
||||
```
|
||||
veya
|
||||
```bash
|
||||
python install.py
|
||||
```
|
||||
3. Script, size gerekli adımlarda rehberlik edecek ve kurulumu tamamlamaya çalışacaktır. Kurulum sırasında ek bağımlılıkların indirilmesi gerekebilir.
|
||||
|
||||
---
|
||||
|
||||
**Kurulum Sonrası**
|
||||
|
||||
Kurulum başarıyla tamamlandıktan sonra, Claude Desktop uygulamasını (gerekirse yeniden başlatarak) açın. Araçlar menüsünde (genellikle ekranın sağ alt köşesindeki çekiç 🛠️ simgesi altında) "**Yargı MCP**" adlı yeni aracı görmelisiniz.
|
||||
|
||||
Herhangi bir sorunla karşılaşırsanız, lütfen [GitHub Issues](https://github.com/saidsurucu/yargi-mcp/issues) bölümünden bize bildirin.
|
||||
|
||||
⚙️ **Kurulum Adımları (Claude Desktop Entegrasyonu Odaklı)**
|
||||
|
||||
@@ -32,11 +122,11 @@ Claude Desktop uygulamasına yükleme yapabilmek için öncelikle `uv` (önerili
|
||||
|
||||
* **macOS ve Linux için:**
|
||||
```bash
|
||||
curl -LsSf [https://astral.sh/uv/install.sh](https://astral.sh/uv/install.sh) | sh
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
* **Windows için (PowerShell kullanarak):**
|
||||
```powershell
|
||||
powershell -c "irm [https://astral.sh/uv/install.ps1](https://astral.sh/uv/install.ps1) | iex"
|
||||
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||
```
|
||||
* Kurulumdan sonra, `uv` komutunun sisteminiz tarafından tanınması için terminalinizi yeniden başlatmanız veya `PATH` ortam değişkeninizi güncellemeniz gerekebilir. `uv --version` komutu ile kurulumu doğrulayabilirsiniz.
|
||||
|
||||
@@ -73,6 +163,7 @@ beautifulsoup4
|
||||
markitdown
|
||||
pydantic
|
||||
aiohttp
|
||||
playwright
|
||||
```
|
||||
(Eğer sunucuyu bağımsız olarak geliştirmek veya test etmek isterseniz, projenizin kök dizininde bir sanal ortam oluşturup – örn: `uv venv` & `source .venv/bin/activate` – bu bağımlılıkları `uv pip install -r requirements.txt` komutuyla kurabilirsiniz.)
|
||||
|
||||
@@ -90,13 +181,14 @@ Yukarıdaki kurulum adımlarını tamamladıktan sonra, bu sunucuyu Claude Deskt
|
||||
--with beautifulsoup4 \
|
||||
--with markitdown \
|
||||
--with pydantic \
|
||||
--with aiohttp
|
||||
--with aiohttp \
|
||||
--with playwright
|
||||
```
|
||||
|
||||
* `--name "Yargı MCP"`: Araç Claude Desktop'ta bu isimle görünecektir.
|
||||
* `--with ...`: Sunucunun çalışması için gereken Python bağımlılıklarını belirtir.
|
||||
|
||||
Bu komut, `uv` kullanarak sunucunuz için izole bir Python ortamı oluşturacak, belirtilen bağımlılıkları kuracak ve aracı Claude Desktop uygulamasına kaydedecektir.
|
||||
Bu komut, `uv` kullanarak (eğer kuruluysa ve bulunabiliyorsa) sunucunuz için izole bir Python ortamı oluşturacak, belirtilen bağımlılıkları kuracak ve aracı Claude Desktop uygulamasına kaydedecektir. Playwright tarayıcılarının (`playwright install chromium` gibi) ayrıca kurulması gerekebilir.
|
||||
|
||||
⚙️ **Claude Desktop Manuel Kurulumu (Yapılandırma Dosyası ile - Alternatif)**
|
||||
|
||||
@@ -119,6 +211,7 @@ Yukarıdaki kurulum adımlarını tamamladıktan sonra, bu sunucuyu Claude Deskt
|
||||
"--with", "markitdown",
|
||||
"--with", "pydantic",
|
||||
"--with", "aiohttp",
|
||||
"--with", "playwright",
|
||||
"--with", "fastmcp",
|
||||
"fastmcp", "run",
|
||||
"/TAM/PROJE/YOLUNUZ/yargi-mcp/mcp_server_main.py"
|
||||
@@ -134,6 +227,7 @@ Yukarıdaki kurulum adımlarını tamamladıktan sonra, bu sunucuyu Claude Deskt
|
||||
|
||||
Bu FastMCP sunucusu aşağıdaki temel araçları sunar:
|
||||
|
||||
|
||||
* **Yargıtay Araçları:**
|
||||
* `search_yargitay_detailed(search_query: YargitayDetailedSearchRequest) -> CompactYargitaySearchResult`: Yargıtay kararlarını detaylı kriterlerle arar.
|
||||
* `get_yargitay_document_markdown(document_id: str) -> YargitayDocumentMarkdown`: Belirli bir Yargıtay kararının metnini Markdown formatında getirir.
|
||||
@@ -159,6 +253,9 @@ Bu FastMCP sunucusu aşağıdaki temel araçları sunar:
|
||||
* `search_anayasa_bireysel_basvuru_report(search_query: AnayasaBireyselReportSearchRequest) -> AnayasaBireyselReportSearchResult`: AYM Bireysel Başvuru "Karar Arama Raporu" oluşturur.
|
||||
* `get_anayasa_bireysel_basvuru_document_markdown(document_url_path: str, page_number: Optional[int] = 1) -> AnayasaBireyselBasvuruDocumentMarkdown`: Belirli bir AYM Bireysel Başvuru kararını URL path'inden alır ve 5.000 karakterlik sayfalanmış Markdown içeriğini getirir.
|
||||
|
||||
* **KİK (Kamu İhale Kurulu) Araçları:**
|
||||
* `search_kik_decisions(search_query: KikSearchRequest) -> KikSearchResult`: KİK (Kamu İhale Kurulu) kararlarını arar.
|
||||
* `get_kik_document_markdown(karar_id: str, page_number: Optional[int] = 1) -> KikDocumentMarkdown`: Belirli bir KİK kararını, Base64 ile encode edilmiş `karar_id`'sini kullanarak alır ve 5.000 karakterlik sayfalanmış Markdown içeriğini getirir.
|
||||
|
||||
📜 **Lisans**
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Entry point for yargi-mcp package."""
|
||||
|
||||
from mcp_server_main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -35,8 +35,6 @@ class DanistayApiClient:
|
||||
headers={
|
||||
"Content-Type": "application/json; charset=UTF-8", # Arama endpoint'leri için
|
||||
"Accept": "application/json, text/plain, */*", # Arama endpoint'leri için
|
||||
# /getDokuman HTML döndürdüğü için Accept header'ı GET isteğinde farklı olabilir
|
||||
# ama httpx genellikle bunu yönetir. Gerekirse özel header eklenebilir.
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
timeout=request_timeout,
|
||||
@@ -44,7 +42,7 @@ class DanistayApiClient:
|
||||
)
|
||||
|
||||
def _prepare_keywords_for_api(self, keywords: List[str]) -> List[str]:
|
||||
return [f'"{k.strip("\"")}"' for k in keywords if k and k.strip()]
|
||||
return ['"' + k.strip('"') + '"' for k in keywords if k and k.strip()]
|
||||
|
||||
async def search_keyword_decisions(
|
||||
self,
|
||||
|
||||
Regular → Executable
@@ -0,0 +1,441 @@
|
||||
# kik_mcp_module/client.py
|
||||
import asyncio
|
||||
from playwright.async_api import (
|
||||
async_playwright,
|
||||
Page,
|
||||
BrowserContext,
|
||||
Browser,
|
||||
Error as PlaywrightError,
|
||||
TimeoutError as PlaywrightTimeoutError
|
||||
)
|
||||
from bs4 import BeautifulSoup
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
import urllib.parse
|
||||
import base64 # Base64 için
|
||||
import re
|
||||
import html as html_parser
|
||||
from markitdown import MarkItDown
|
||||
import os
|
||||
import math
|
||||
import tempfile
|
||||
|
||||
from .models import (
|
||||
KikSearchRequest,
|
||||
KikDecisionEntry,
|
||||
KikSearchResult,
|
||||
KikDocumentMarkdown,
|
||||
KikKararTipi
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class KikApiClient:
|
||||
BASE_URL = "https://ekap.kik.gov.tr"
|
||||
SEARCH_PAGE_PATH = "/EKAP/Vatandas/kurulkararsorgu.aspx"
|
||||
FIELD_LOCATORS = {
|
||||
"karar_tipi_radio_group": "input[name='ctl00$ContentPlaceHolder1$kurulKararTip']",
|
||||
"karar_no": "input[name='ctl00$ContentPlaceHolder1$txtKararNo']",
|
||||
"karar_tarihi_baslangic": "input[name='ctl00$ContentPlaceHolder1$etKararTarihBaslangic$EkapTakvimTextBox_etKararTarihBaslangic']",
|
||||
"karar_tarihi_bitis": "input[name='ctl00$ContentPlaceHolder1$etKararTarihBitis$EkapTakvimTextBox_etKararTarihBitis']",
|
||||
"resmi_gazete_sayisi": "input[name='ctl00$ContentPlaceHolder1$txtResmiGazeteSayisi']",
|
||||
"resmi_gazete_tarihi": "input[name='ctl00$ContentPlaceHolder1$etResmiGazeteTarihi$EkapTakvimTextBox_etResmiGazeteTarihi']",
|
||||
"basvuru_konusu_ihale": "input[name='ctl00$ContentPlaceHolder1$txtBasvuruKonusuIhale']",
|
||||
"basvuru_sahibi": "input[name='ctl00$ContentPlaceHolder1$txtSikayetci']",
|
||||
"ihaleyi_yapan_idare": "input[name='ctl00$ContentPlaceHolder1$txtIhaleyiYapanIdare']",
|
||||
"yil": "select[name='ctl00$ContentPlaceHolder1$ddlYil']",
|
||||
"karar_metni": "input[name='ctl00$ContentPlaceHolder1$txtKararMetni']",
|
||||
"search_button_id": "ctl00_ContentPlaceHolder1_btnAra"
|
||||
}
|
||||
RESULTS_TABLE_ID = "grdKurulKararSorguSonuc"
|
||||
NO_RESULTS_MESSAGE_SELECTOR = "div#ctl00_MessageContent1"
|
||||
VALIDATION_SUMMARY_SELECTOR = "div#ctl00_ValidationSummary1"
|
||||
MODAL_CLOSE_BUTTON_SELECTOR = "div#detayPopUp.in a#btnKapatPencere_0.close"
|
||||
DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000
|
||||
|
||||
def __init__(self, request_timeout: float = 60000):
|
||||
self.playwright_instance: Optional[async_playwright] = None
|
||||
self.browser: Optional[Browser] = None
|
||||
self.context: Optional[BrowserContext] = None
|
||||
self.page: Optional[Page] = None
|
||||
self.request_timeout = request_timeout
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def _ensure_playwright_ready(self, force_new_page: bool = False):
|
||||
async with self._lock:
|
||||
browser_recreated = False
|
||||
context_recreated = False
|
||||
if not self.playwright_instance:
|
||||
self.playwright_instance = await async_playwright().start()
|
||||
if not self.browser or not self.browser.is_connected():
|
||||
if self.browser: await self.browser.close()
|
||||
self.browser = await self.playwright_instance.chromium.launch(headless=True)
|
||||
browser_recreated = True
|
||||
if not self.context or browser_recreated:
|
||||
if self.context: await self.context.close()
|
||||
if not self.browser: raise PlaywrightError("Browser not initialized.")
|
||||
self.context = await self.browser.new_context(
|
||||
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
|
||||
java_script_enabled=True,
|
||||
)
|
||||
context_recreated = True
|
||||
if not self.page or self.page.is_closed() or force_new_page or context_recreated or browser_recreated:
|
||||
if self.page and not self.page.is_closed(): await self.page.close()
|
||||
if not self.context: raise PlaywrightError("Context is None.")
|
||||
self.page = await self.context.new_page()
|
||||
if not self.page: raise PlaywrightError("Failed to create new page.")
|
||||
self.page.set_default_navigation_timeout(self.request_timeout)
|
||||
self.page.set_default_timeout(self.request_timeout)
|
||||
if not self.page or self.page.is_closed():
|
||||
raise PlaywrightError("Playwright page initialization failed.")
|
||||
logger.debug("_ensure_playwright_ready completed.")
|
||||
|
||||
async def close_client_session(self):
|
||||
async with self._lock:
|
||||
# ... (öncekiyle aynı)
|
||||
if self.page and not self.page.is_closed(): await self.page.close(); self.page = None
|
||||
if self.context: await self.context.close(); self.context = None
|
||||
if self.browser: await self.browser.close(); self.browser = None
|
||||
if self.playwright_instance: await self.playwright_instance.stop(); self.playwright_instance = None
|
||||
logger.info("KikApiClient (Playwright): Resources closed.")
|
||||
|
||||
def _parse_decision_entries_from_soup(self, soup: BeautifulSoup, search_karar_tipi: KikKararTipi) -> List[KikDecisionEntry]:
|
||||
entries: List[KikDecisionEntry] = []
|
||||
table = soup.find("table", {"id": self.RESULTS_TABLE_ID})
|
||||
if not table: return entries
|
||||
rows = table.find_all("tr")
|
||||
for row_idx, row in enumerate(rows):
|
||||
if row_idx < 2: continue
|
||||
cells = row.find_all("td")
|
||||
if len(cells) == 6:
|
||||
try:
|
||||
preview_button_tag = cells[0].find("a", id=re.compile(r"btnOnizle$"))
|
||||
event_target = ""
|
||||
if preview_button_tag and preview_button_tag.has_attr('href'):
|
||||
match = re.search(r"__doPostBack\('([^']*)','([^']*)'\)", preview_button_tag['href'])
|
||||
if match: event_target = match.group(1)
|
||||
karar_no_span = cells[1].find("span", id=re.compile(r"lblKno$"))
|
||||
karar_tarihi_span = cells[2].find("span", id=re.compile(r"lblKtar$"))
|
||||
idare_span = cells[3].find("span", id=re.compile(r"lblIdare$"))
|
||||
basvuru_sahibi_span = cells[4].find("span", id=re.compile(r"lblSikayetci$"))
|
||||
ihale_span = cells[5].find("span", id=re.compile(r"lblIhale$"))
|
||||
if not (event_target and karar_no_span and karar_tarihi_span): continue
|
||||
|
||||
# Karar tipini arama parametresinden alıyoruz, çünkü HTML'de direkt olarak bulunmuyor.
|
||||
entry = KikDecisionEntry(
|
||||
preview_event_target=event_target,
|
||||
kararNo=karar_no_span.get_text(strip=True),
|
||||
karar_tipi=search_karar_tipi, # Arama yapılan karar tipini ekle
|
||||
kararTarihi=karar_tarihi_span.get_text(strip=True),
|
||||
idare=idare_span.get_text(strip=True) if idare_span else None,
|
||||
basvuruSahibi=basvuru_sahibi_span.get_text(strip=True) if basvuru_sahibi_span else None,
|
||||
ihaleKonusu=ihale_span.get_text(strip=True) if ihale_span else None,
|
||||
)
|
||||
entries.append(entry)
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing a KIK decision entry row: {e}", exc_info=True)
|
||||
return entries
|
||||
|
||||
def _parse_total_records_from_soup(self, soup: BeautifulSoup) -> int:
|
||||
# ... (öncekiyle aynı) ...
|
||||
try:
|
||||
pager_div = soup.find("div", class_="gridToplamSayi")
|
||||
if pager_div:
|
||||
match = re.search(r"Toplam Kayıt Sayısı:(\d+)", pager_div.get_text(strip=True))
|
||||
if match: return int(match.group(1))
|
||||
except: pass
|
||||
return 0
|
||||
|
||||
def _parse_current_page_from_soup(self, soup: BeautifulSoup) -> int:
|
||||
# ... (öncekiyle aynı) ...
|
||||
try:
|
||||
pager_div = soup.find("div", class_="sayfalama")
|
||||
if pager_div:
|
||||
active_page_span = pager_div.find("span", class_="active")
|
||||
if active_page_span: return int(active_page_span.get_text(strip=True))
|
||||
except: pass
|
||||
return 1
|
||||
|
||||
async def search_decisions(self, search_params: KikSearchRequest) -> KikSearchResult:
|
||||
await self._ensure_playwright_ready()
|
||||
page = self.page
|
||||
search_url = f"{self.BASE_URL}{self.SEARCH_PAGE_PATH}"
|
||||
try:
|
||||
if page.url != search_url:
|
||||
await page.goto(search_url, wait_until="networkidle", timeout=self.request_timeout)
|
||||
search_button_selector = f"a[id='{self.FIELD_LOCATORS['search_button_id']}']"
|
||||
await page.wait_for_selector(search_button_selector, state="visible", timeout=self.request_timeout)
|
||||
|
||||
current_karar_tipi_value = search_params.karar_tipi.value
|
||||
radio_locator_selector = f"{self.FIELD_LOCATORS['karar_tipi_radio_group']}[value='{current_karar_tipi_value}']"
|
||||
if not await page.locator(radio_locator_selector).is_checked():
|
||||
js_target_radio = f"ctl00$ContentPlaceHolder1${current_karar_tipi_value}"
|
||||
async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout):
|
||||
await page.evaluate(f"javascript:__doPostBack('{js_target_radio}','')")
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
async def fill_if_value(selector_key: str, value: Optional[str]):
|
||||
if value is not None: await page.fill(self.FIELD_LOCATORS[selector_key], value)
|
||||
|
||||
# Karar No'yu KİK sitesine göndermeden önce '_' -> '/' dönüşümü yap
|
||||
karar_no_for_kik_form = None
|
||||
if search_params.karar_no: # search_params.karar_no Claude'dan '_' ile gelmiş olabilir
|
||||
karar_no_for_kik_form = search_params.karar_no.replace('_', '/')
|
||||
logger.info(f"Using karar_no '{karar_no_for_kik_form}' (transformed from '{search_params.karar_no}') for KIK form.")
|
||||
|
||||
await fill_if_value('karar_metni', search_params.karar_metni)
|
||||
await fill_if_value('karar_no', karar_no_for_kik_form) # Dönüştürülmüş halini kullan
|
||||
# ... (diğer fill_if_value çağrıları aynı) ...
|
||||
await fill_if_value('karar_tarihi_baslangic', search_params.karar_tarihi_baslangic)
|
||||
await fill_if_value('karar_tarihi_bitis', search_params.karar_tarihi_bitis)
|
||||
await fill_if_value('resmi_gazete_sayisi', search_params.resmi_gazete_sayisi)
|
||||
await fill_if_value('resmi_gazete_tarihi', search_params.resmi_gazete_tarihi)
|
||||
await fill_if_value('basvuru_konusu_ihale', search_params.basvuru_konusu_ihale)
|
||||
await fill_if_value('basvuru_sahibi', search_params.basvuru_sahibi)
|
||||
await fill_if_value('ihaleyi_yapan_idare', search_params.ihaleyi_yapan_idare)
|
||||
|
||||
if search_params.yil:
|
||||
await page.select_option(self.FIELD_LOCATORS['yil'], value=search_params.yil)
|
||||
|
||||
action_is_search_button_click = (search_params.page == 1)
|
||||
event_target_for_submit: str
|
||||
if action_is_search_button_click:
|
||||
event_target_for_submit = self.FIELD_LOCATORS['search_button_id']
|
||||
else: # Pagination
|
||||
page_link_ctl_number = search_params.page + 2
|
||||
event_target_for_submit = f"ctl00$ContentPlaceHolder1$grdKurulKararSorguSonuc$ctl14$ctl{page_link_ctl_number:02d}"
|
||||
|
||||
try:
|
||||
async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout):
|
||||
if action_is_search_button_click:
|
||||
await page.locator(search_button_selector).click()
|
||||
else:
|
||||
await page.evaluate(f"javascript:__doPostBack('{event_target_for_submit}','')")
|
||||
except PlaywrightTimeoutError:
|
||||
await page.wait_for_timeout(2000)
|
||||
|
||||
results_table_dom_selector = f"table#{self.RESULTS_TABLE_ID}"
|
||||
try:
|
||||
await page.wait_for_selector(results_table_dom_selector, timeout=30000, state="attached")
|
||||
await page.wait_for_timeout(2000)
|
||||
except PlaywrightTimeoutError:
|
||||
logger.warning(f"Timeout waiting for results table '{results_table_dom_selector}'.")
|
||||
|
||||
html_content = await page.content()
|
||||
soup = BeautifulSoup(html_content, "html.parser")
|
||||
# ... (hata ve sonuç yok mesajı kontrolü aynı) ...
|
||||
validation_summary_tag = soup.find("div", id=self.VALIDATION_SUMMARY_SELECTOR.split('[')[0].split(':')[0])
|
||||
if validation_summary_tag and validation_summary_tag.get_text(strip=True) and \
|
||||
("display: none" not in validation_summary_tag.get("style", "").lower() if validation_summary_tag.has_attr("style") else True) and \
|
||||
validation_summary_tag.get_text(strip=True) != "":
|
||||
return KikSearchResult(decisions=[], total_records=0, current_page=search_params.page)
|
||||
message_content_div = soup.find("div", id=self.NO_RESULTS_MESSAGE_SELECTOR.split(':')[0])
|
||||
if message_content_div and "kayıt bulunamamıştır" in message_content_div.get_text(strip=True).lower():
|
||||
return KikSearchResult(decisions=[], total_records=0, current_page=1)
|
||||
|
||||
# _parse_decision_entries_from_soup'a arama yapılan karar_tipi'ni gönder
|
||||
decisions = self._parse_decision_entries_from_soup(soup, search_params.karar_tipi)
|
||||
total_records = self._parse_total_records_from_soup(soup)
|
||||
current_page_from_html = self._parse_current_page_from_soup(soup)
|
||||
return KikSearchResult(decisions=decisions, total_records=total_records, current_page=current_page_from_html)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during KIK decision search: {e}", exc_info=True)
|
||||
return KikSearchResult(decisions=[], current_page=search_params.page)
|
||||
|
||||
def _clean_html_for_markdown(self, html_content: str) -> str:
|
||||
# ... (öncekiyle aynı) ...
|
||||
if not html_content: return ""
|
||||
return html_parser.unescape(html_content)
|
||||
|
||||
def _convert_html_to_markdown_internal(self, html_fragment: str) -> Optional[str]:
|
||||
# ... (öncekiyle aynı) ...
|
||||
if not html_fragment: return None
|
||||
cleaned_html = self._clean_html_for_markdown(html_fragment)
|
||||
markdown_output = None; temp_file_path = None
|
||||
try:
|
||||
md_converter = MarkItDown(enable_plugins=True, remove_alt_whitespace=True, keep_underline=True)
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html", encoding="utf-8") as tmp_html_file:
|
||||
tmp_html_file.write(cleaned_html); temp_file_path = tmp_html_file.name
|
||||
markdown_output = md_converter.convert(temp_file_path).text_content
|
||||
if markdown_output: markdown_output = re.sub(r'\n{3,}', '\n\n', markdown_output).strip()
|
||||
except Exception as e: logger.error(f"MarkItDown conversion error: {e}", exc_info=True)
|
||||
finally:
|
||||
if temp_file_path and os.path.exists(temp_file_path): os.remove(temp_file_path)
|
||||
return markdown_output
|
||||
|
||||
|
||||
async def get_decision_document_as_markdown(
|
||||
self,
|
||||
karar_id_b64: str,
|
||||
page_number: int = 1
|
||||
) -> KikDocumentMarkdown:
|
||||
await self._ensure_playwright_ready()
|
||||
# Bu metodun kendi içinde yeni bir 'page' nesnesi ('doc_page_for_content') kullanacağını unutmayın,
|
||||
# ana 'self.page' arama sonuçları sayfasında kalır.
|
||||
current_main_page = self.page # Ana arama sonuçları sayfasını referans alalım
|
||||
|
||||
try:
|
||||
decoded_key = base64.b64decode(karar_id_b64.encode('utf-8')).decode('utf-8')
|
||||
karar_tipi_value, karar_no_for_search = decoded_key.split('|', 1)
|
||||
original_karar_tipi = KikKararTipi(karar_tipi_value)
|
||||
logger.info(f"KIK Get Detail: Decoded karar_id '{karar_id_b64}' to Karar Tipi: {original_karar_tipi.value}, Karar No: {karar_no_for_search}. Requested Markdown Page: {page_number}")
|
||||
except Exception as e_decode:
|
||||
logger.error(f"Invalid karar_id format. Could not decode Base64 or split: {karar_id_b64}. Error: {e_decode}")
|
||||
return KikDocumentMarkdown(retrieved_with_karar_id=karar_id_b64, error_message="Invalid karar_id format.", current_page=page_number)
|
||||
|
||||
default_error_response_data = {
|
||||
"retrieved_with_karar_id": karar_id_b64,
|
||||
"retrieved_karar_no": karar_no_for_search,
|
||||
"retrieved_karar_tipi": original_karar_tipi,
|
||||
"error_message": "An unspecified error occurred.",
|
||||
"current_page": page_number, "total_pages": 1, "is_paginated": False
|
||||
}
|
||||
|
||||
# Ana arama sayfasında olduğumuzdan emin olalım
|
||||
if self.SEARCH_PAGE_PATH not in current_main_page.url:
|
||||
logger.info(f"Not on search page ({current_main_page.url}). Navigating to {self.SEARCH_PAGE_PATH} before targeted search for document.")
|
||||
await current_main_page.goto(f"{self.BASE_URL}{self.SEARCH_PAGE_PATH}", wait_until="networkidle", timeout=self.request_timeout)
|
||||
await current_main_page.wait_for_selector(f"a[id='{self.FIELD_LOCATORS['search_button_id']}']", state="visible", timeout=self.request_timeout)
|
||||
|
||||
targeted_search_params = KikSearchRequest(
|
||||
karar_no=karar_no_for_search,
|
||||
karar_tipi=original_karar_tipi,
|
||||
page=1
|
||||
)
|
||||
logger.info(f"Performing targeted search for Karar No: {karar_no_for_search}")
|
||||
# search_decisions kendi içinde _ensure_playwright_ready çağırır ve self.page'i kullanır.
|
||||
# Bu, current_main_page ile aynı olmalı.
|
||||
search_results = await self.search_decisions(targeted_search_params)
|
||||
|
||||
if not search_results.decisions:
|
||||
default_error_response_data["error_message"] = f"Decision with Karar No '{karar_no_for_search}' (Tipi: {original_karar_tipi.value}) not found by internal search."
|
||||
return KikDocumentMarkdown(**default_error_response_data)
|
||||
|
||||
decision_to_fetch = None
|
||||
for dec_entry in search_results.decisions:
|
||||
if dec_entry.karar_no_str == karar_no_for_search and dec_entry.karar_tipi == original_karar_tipi:
|
||||
decision_to_fetch = dec_entry
|
||||
break
|
||||
|
||||
if not decision_to_fetch:
|
||||
default_error_response_data["error_message"] = f"Karar No '{karar_no_for_search}' (Tipi: {original_karar_tipi.value}) not present with an exact match in first page of targeted search results."
|
||||
return KikDocumentMarkdown(**default_error_response_data)
|
||||
|
||||
decision_preview_event_target = decision_to_fetch.preview_event_target
|
||||
logger.info(f"Found target decision. Using preview_event_target: {decision_preview_event_target} for Karar No: {decision_to_fetch.karar_no_str}")
|
||||
|
||||
iframe_document_url_str = None
|
||||
karar_id_param_from_url_on_doc_page = None
|
||||
document_html_content = ""
|
||||
|
||||
try:
|
||||
logger.info(f"Evaluating __doPostBack on main page to show modal for: {decision_preview_event_target}")
|
||||
# Bu evaluate, self.page (yani current_main_page) üzerinde çalışır
|
||||
await current_main_page.evaluate(f"javascript:__doPostBack('{decision_preview_event_target}','')")
|
||||
await current_main_page.wait_for_timeout(1000)
|
||||
logger.info(f"Executed __doPostBack for {decision_preview_event_target} on main page.")
|
||||
|
||||
iframe_selector = "iframe#iframe_detayPopUp"
|
||||
modal_visible_selector = "div#detayPopUp.in"
|
||||
|
||||
try:
|
||||
logger.info(f"Waiting for modal '{modal_visible_selector}' to be visible and iframe '{iframe_selector}' src to be populated on main page...")
|
||||
await current_main_page.wait_for_function(
|
||||
f"""
|
||||
() => {{
|
||||
const modal = document.querySelector('{modal_visible_selector}');
|
||||
const iframe = document.querySelector('{iframe_selector}');
|
||||
const modalIsTrulyVisible = modal && (window.getComputedStyle(modal).display !== 'none');
|
||||
return modalIsTrulyVisible &&
|
||||
iframe && iframe.getAttribute('src') &&
|
||||
iframe.getAttribute('src').includes('KurulKararGoster.aspx');
|
||||
}}
|
||||
""",
|
||||
timeout=self.request_timeout / 2
|
||||
)
|
||||
iframe_src_value = await current_main_page.locator(iframe_selector).get_attribute("src")
|
||||
logger.info(f"Iframe src populated: {iframe_src_value}")
|
||||
|
||||
except PlaywrightTimeoutError:
|
||||
logger.warning(f"Timeout waiting for KIK iframe src for {decision_preview_event_target}. Trying to parse from static content after presumed update.")
|
||||
html_after_postback = await current_main_page.content()
|
||||
# ... (fallback parsing öncekiyle aynı, default_error_response_data set edilir ve return edilir) ...
|
||||
soup_after_postback = BeautifulSoup(html_after_postback, "html.parser")
|
||||
detay_popup_div = soup_after_postback.find("div", {"id": "detayPopUp", "class": re.compile(r"\bin\b")})
|
||||
if not detay_popup_div: detay_popup_div = soup_after_postback.find("div", {"id": "detayPopUp", "style": re.compile(r"display:\s*block", re.I)})
|
||||
iframe_tag = detay_popup_div.find("iframe", {"id": "iframe_detayPopUp"}) if detay_popup_div else None
|
||||
if iframe_tag and iframe_tag.has_attr("src") and iframe_tag["src"]: iframe_src_value = iframe_tag["src"]
|
||||
else:
|
||||
default_error_response_data["error_message"]="Timeout or failure finding decision content iframe URL after postback."
|
||||
return KikDocumentMarkdown(**default_error_response_data)
|
||||
|
||||
if not iframe_src_value or not iframe_src_value.strip():
|
||||
default_error_response_data["error_message"]="Extracted iframe URL for decision content is empty."
|
||||
return KikDocumentMarkdown(**default_error_response_data)
|
||||
|
||||
# iframe_src_value göreceli bir URL ise, ana sayfanın URL'si ile birleştir
|
||||
iframe_document_url_str = urllib.parse.urljoin(current_main_page.url, iframe_src_value)
|
||||
logger.info(f"Constructed absolute iframe_document_url_str for goto: {iframe_document_url_str}") # Log this absolute URL
|
||||
default_error_response_data["source_url"] = iframe_document_url_str
|
||||
|
||||
parsed_url = urllib.parse.urlparse(iframe_document_url_str)
|
||||
query_params = urllib.parse.parse_qs(parsed_url.query)
|
||||
karar_id_param_from_url_on_doc_page = query_params.get("KararId", [None])[0]
|
||||
default_error_response_data["karar_id_param_from_url"] = karar_id_param_from_url_on_doc_page
|
||||
if not karar_id_param_from_url_on_doc_page:
|
||||
default_error_response_data["error_message"]="KararId (KIK internal ID) not found in extracted iframe URL."
|
||||
return KikDocumentMarkdown(**default_error_response_data)
|
||||
|
||||
logger.info(f"Fetching KIK decision content from iframe URL using a new Playwright page: {iframe_document_url_str}")
|
||||
|
||||
doc_page_for_content = await self.context.new_page()
|
||||
try:
|
||||
# `goto` metoduna MUTLAK URL verilmeli. Loglanan URL'nin mutlak olduğundan emin olalım.
|
||||
await doc_page_for_content.goto(iframe_document_url_str, wait_until="domcontentloaded", timeout=self.request_timeout)
|
||||
document_html_content = await doc_page_for_content.content()
|
||||
except Exception as e_doc_page:
|
||||
logger.error(f"Error navigating or getting content from doc_page ({iframe_document_url_str}): {e_doc_page}")
|
||||
if doc_page_for_content and not doc_page_for_content.is_closed(): await doc_page_for_content.close()
|
||||
default_error_response_data["error_message"]=f"Failed to load decision detail page: {e_doc_page}"
|
||||
return KikDocumentMarkdown(**default_error_response_data)
|
||||
finally:
|
||||
if doc_page_for_content and not doc_page_for_content.is_closed():
|
||||
await doc_page_for_content.close()
|
||||
|
||||
soup_decision_detail = BeautifulSoup(document_html_content, "html.parser")
|
||||
karar_content_span = soup_decision_detail.find("span", {"id": "ctl00_ContentPlaceHolder1_lblKarar"})
|
||||
actual_decision_html = karar_content_span.decode_contents() if karar_content_span else document_html_content
|
||||
full_markdown_content = self._convert_html_to_markdown_internal(actual_decision_html)
|
||||
|
||||
if not full_markdown_content:
|
||||
default_error_response_data["error_message"]="Markdown conversion failed or returned empty content."
|
||||
try:
|
||||
if await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).is_visible(timeout=1000):
|
||||
await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).click()
|
||||
except: pass
|
||||
return KikDocumentMarkdown(**default_error_response_data)
|
||||
|
||||
content_length = len(full_markdown_content); total_pages = math.ceil(content_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE) or 1
|
||||
current_page_clamped = max(1, min(page_number, total_pages))
|
||||
start_index = (current_page_clamped - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE
|
||||
markdown_chunk = full_markdown_content[start_index : start_index + self.DOCUMENT_MARKDOWN_CHUNK_SIZE]
|
||||
|
||||
try:
|
||||
if await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).is_visible(timeout=2000):
|
||||
await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).click()
|
||||
await current_main_page.wait_for_selector(f"div#detayPopUp:not(.in)", timeout=5000)
|
||||
except: pass
|
||||
|
||||
return KikDocumentMarkdown(
|
||||
retrieved_with_karar_id=karar_id_b64,
|
||||
retrieved_karar_no=karar_no_for_search,
|
||||
retrieved_karar_tipi=original_karar_tipi,
|
||||
kararIdParam=karar_id_param_from_url_on_doc_page,
|
||||
markdown_chunk=markdown_chunk, source_url=iframe_document_url_str,
|
||||
current_page=current_page_clamped, total_pages=total_pages,
|
||||
is_paginated=(total_pages > 1), full_content_char_count=content_length
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_decision_document_as_markdown for Karar ID {karar_id_b64}: {e}", exc_info=True)
|
||||
default_error_response_data["error_message"] = f"General error: {str(e)}"
|
||||
return KikDocumentMarkdown(**default_error_response_data)
|
||||
@@ -0,0 +1,77 @@
|
||||
# kik_mcp_module/models.py
|
||||
from pydantic import BaseModel, Field, HttpUrl, computed_field
|
||||
from typing import List, Optional
|
||||
from enum import Enum
|
||||
import base64 # Base64 encoding/decoding için
|
||||
|
||||
class KikKararTipi(str, Enum):
|
||||
"""Enum for KIK (Public Procurement Authority) Decision Types."""
|
||||
UYUSMAZLIK = "rbUyusmazlik"
|
||||
DUZENLEYICI = "rbDuzenleyici"
|
||||
MAHKEME = "rbMahkeme"
|
||||
|
||||
class KikSearchRequest(BaseModel):
|
||||
"""Model for KIK Decision search criteria."""
|
||||
karar_tipi: KikKararTipi = Field(KikKararTipi.UYUSMAZLIK, description="Type of KIK Decision.")
|
||||
karar_no: Optional[str] = Field(None, description="Decision Number (e.g., '2024/UH.II-1766').")
|
||||
karar_tarihi_baslangic: Optional[str] = Field(None, description="Decision Date Start (DD.MM.YYYY).", pattern=r"^\d{2}\.\d{2}\.\d{4}$")
|
||||
karar_tarihi_bitis: Optional[str] = Field(None, description="Decision Date End (DD.MM.YYYY).", pattern=r"^\d{2}\.\d{2}\.\d{4}$")
|
||||
resmi_gazete_sayisi: Optional[str] = Field(None, description="Official Gazette Number.")
|
||||
resmi_gazete_tarihi: Optional[str] = Field(None, description="Official Gazette Date (DD.MM.YYYY).", pattern=r"^\d{2}\.\d{2}\.\d{4}$")
|
||||
basvuru_konusu_ihale: Optional[str] = Field(None, description="Tender subject of the application.")
|
||||
basvuru_sahibi: Optional[str] = Field(None, description="Applicant.")
|
||||
ihaleyi_yapan_idare: Optional[str] = Field(None, description="Procuring Entity.")
|
||||
yil: Optional[str] = Field(None, description="Year of the decision.")
|
||||
karar_metni: Optional[str] = Field(None, description="Keyword/phrase in decision text.")
|
||||
page: int = Field(1, ge=1, description="Results page number.")
|
||||
|
||||
class KikDecisionEntry(BaseModel):
|
||||
"""Represents a single decision entry from KIK search results."""
|
||||
preview_event_target: str = Field(..., description="Internal event target for fetching details.")
|
||||
karar_no_str: str = Field(..., alias="kararNo", description="Raw decision number as extracted from KIK (e.g., '2024/UH.II-1766').")
|
||||
karar_tipi: KikKararTipi = Field(..., description="The type of decision this entry belongs to.")
|
||||
|
||||
karar_tarihi_str: str = Field(..., alias="kararTarihi", description="Decision date.")
|
||||
idare_str: Optional[str] = Field(None, alias="idare", description="Procuring entity.")
|
||||
basvuru_sahibi_str: Optional[str] = Field(None, alias="basvuruSahibi", description="Applicant.")
|
||||
ihale_konusu_str: Optional[str] = Field(None, alias="ihaleKonusu", description="Tender subject.")
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def karar_id(self) -> str:
|
||||
"""
|
||||
A Base64 encoded unique ID for the decision, combining decision type and number.
|
||||
Format before encoding: "{karar_tipi.value}|{karar_no_str}"
|
||||
"""
|
||||
combined_key = f"{self.karar_tipi.value}|{self.karar_no_str}"
|
||||
return base64.b64encode(combined_key.encode('utf-8')).decode('utf-8')
|
||||
|
||||
class Config:
|
||||
populate_by_name = True
|
||||
|
||||
class KikSearchResult(BaseModel):
|
||||
"""Model for KIK search results."""
|
||||
decisions: List[KikDecisionEntry]
|
||||
total_records: int = 0
|
||||
current_page: int = 1
|
||||
|
||||
class KikDocumentMarkdown(BaseModel):
|
||||
"""
|
||||
KIK decision document, with Markdown content potentially paginated.
|
||||
"""
|
||||
retrieved_with_karar_id: Optional[str] = Field(None, description="The Base64 encoded karar_id that was used to request this document.")
|
||||
# Decode edilmiş karar no ve tipini de yanıt olarak ekleyelim, Claude için faydalı olabilir.
|
||||
retrieved_karar_no: Optional[str] = Field(None, description="The raw KIK Decision Number (e.g., '2024/UH.II-1766') this document pertains to.")
|
||||
retrieved_karar_tipi: Optional[KikKararTipi] = Field(None, description="The KIK Decision Type this document pertains to.")
|
||||
|
||||
karar_id_param_from_url: Optional[str] = Field(None, alias="kararIdParam", description="The KIK system's internal KararId parameter from the document's display URL (KurulKararGoster.aspx).")
|
||||
markdown_chunk: Optional[str] = Field(None, description="The requested chunk of the decision content converted to Markdown.")
|
||||
source_url: Optional[str] = Field(None, description="The source URL of the original document (KurulKararGoster.aspx).")
|
||||
error_message: Optional[str] = Field(None, description="Error message if document retrieval or processing failed.")
|
||||
current_page: int = Field(1, description="The current page number of the markdown chunk being returned.")
|
||||
total_pages: int = Field(1, description="The total number of pages the full markdown content is divided into.")
|
||||
is_paginated: bool = Field(False, description="True if the full markdown content is split into multiple pages.")
|
||||
full_content_char_count: Optional[int] = Field(None, description="Total character count of the full markdown content before chunking.")
|
||||
|
||||
class Config:
|
||||
populate_by_name = True
|
||||
+145
-62
@@ -1,11 +1,11 @@
|
||||
# mcp_server_main.py
|
||||
|
||||
import asyncio
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
from pydantic import HttpUrl
|
||||
from typing import Optional
|
||||
from pydantic import HttpUrl, Field
|
||||
from typing import Optional, Dict
|
||||
import urllib.parse # urllib.parse client tarafında kullanılıyor, server'da gerekmeyebilir.
|
||||
|
||||
# --- Logging Configuration Start ---
|
||||
LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
|
||||
@@ -32,7 +32,6 @@ logger = logging.getLogger(__name__)
|
||||
# --- Logging Configuration End ---
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from pydantic import Field
|
||||
|
||||
# --- Module Imports ---
|
||||
from yargitay_mcp_module.client import YargitayOfficialApiClient
|
||||
@@ -52,21 +51,30 @@ from uyusmazlik_mcp_module.client import UyusmazlikApiClient
|
||||
from uyusmazlik_mcp_module.models import (
|
||||
UyusmazlikSearchRequest, UyusmazlikSearchResponse, UyusmazlikDocumentMarkdown
|
||||
)
|
||||
from anayasa_mcp_module.client import AnayasaMahkemesiApiClient # Norm Denetimi Client
|
||||
from anayasa_mcp_module.bireysel_client import AnayasaBireyselBasvuruApiClient # Bireysel Başvuru Client
|
||||
from anayasa_mcp_module.client import AnayasaMahkemesiApiClient
|
||||
from anayasa_mcp_module.bireysel_client import AnayasaBireyselBasvuruApiClient
|
||||
from anayasa_mcp_module.models import (
|
||||
AnayasaNormDenetimiSearchRequest,
|
||||
AnayasaSearchResult,
|
||||
AnayasaDocumentMarkdown, # For Norm Denetimi documents
|
||||
AnayasaBireyselReportSearchRequest, # For Bireysel Başvuru reports
|
||||
AnayasaBireyselReportSearchResult, # For Bireysel Başvuru reports
|
||||
AnayasaBireyselBasvuruDocumentMarkdown, # For Bireysel Başvuru documents
|
||||
AnayasaDocumentMarkdown,
|
||||
AnayasaBireyselReportSearchRequest,
|
||||
AnayasaBireyselReportSearchResult,
|
||||
AnayasaBireyselBasvuruDocumentMarkdown,
|
||||
)
|
||||
# KIK Module Imports
|
||||
from kik_mcp_module.client import KikApiClient
|
||||
from kik_mcp_module.models import (
|
||||
KikKararTipi,
|
||||
KikSearchRequest,
|
||||
KikSearchResult,
|
||||
KikDocumentMarkdown
|
||||
)
|
||||
|
||||
|
||||
app = FastMCP(
|
||||
name="TurkishLawResearchAssistantMCP",
|
||||
instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel).",
|
||||
dependencies=["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp"]
|
||||
instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel, KIK).",
|
||||
dependencies=["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp", "playwright"]
|
||||
)
|
||||
|
||||
# --- API Client Instances ---
|
||||
@@ -76,8 +84,10 @@ emsal_client_instance = EmsalApiClient()
|
||||
uyusmazlik_client_instance = UyusmazlikApiClient()
|
||||
anayasa_norm_client_instance = AnayasaMahkemesiApiClient()
|
||||
anayasa_bireysel_client_instance = AnayasaBireyselBasvuruApiClient()
|
||||
kik_client_instance = KikApiClient()
|
||||
|
||||
# --- MCP Tools for Yargitay ---
|
||||
# ... (Yargıtay araçları öncekiyle aynı, docstringler İngilizce) ...
|
||||
@app.tool()
|
||||
async def search_yargitay_detailed(search_query: YargitayDetailedSearchRequest) -> CompactYargitaySearchResult:
|
||||
"""Searches Yargitay (Court of Cassation) decisions using detailed criteria."""
|
||||
@@ -93,7 +103,7 @@ async def search_yargitay_detailed(search_query: YargitayDetailedSearchRequest)
|
||||
logger.warning("API response for Yargitay search did not contain expected data structure.")
|
||||
return CompactYargitaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'search_yargitay_detailed' with query: {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.exception(f"Error in tool 'search_yargitay_detailed'.") # Query loglaması kaldırıldı, PII içerebilir
|
||||
raise
|
||||
|
||||
@app.tool()
|
||||
@@ -104,14 +114,15 @@ async def get_yargitay_document_markdown(document_id: str) -> YargitayDocumentMa
|
||||
try:
|
||||
return await yargitay_client_instance.get_decision_document_as_markdown(document_id)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'get_yargitay_document_markdown' for ID: {document_id}")
|
||||
logger.exception(f"Error in tool 'get_yargitay_document_markdown'.")
|
||||
raise
|
||||
|
||||
# --- MCP Tools for Danistay ---
|
||||
# ... (Danıştay araçları öncekiyle aynı, docstringler İngilizce) ...
|
||||
@app.tool()
|
||||
async def search_danistay_by_keyword(search_query: DanistayKeywordSearchRequest) -> CompactDanistaySearchResult:
|
||||
"""Searches Danıştay (Council of State) decisions using keywords."""
|
||||
logger.info(f"Tool 'search_danistay_by_keyword' called: {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.info(f"Tool 'search_danistay_by_keyword' called.") # Query loglaması kaldırıldı
|
||||
try:
|
||||
api_response = await danistay_client_instance.search_keyword_decisions(search_query)
|
||||
if api_response.data:
|
||||
@@ -123,13 +134,13 @@ async def search_danistay_by_keyword(search_query: DanistayKeywordSearchRequest)
|
||||
logger.warning("API response for Danistay keyword search did not contain expected data structure.")
|
||||
return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'search_danistay_by_keyword': {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.exception(f"Error in tool 'search_danistay_by_keyword'.")
|
||||
raise
|
||||
|
||||
@app.tool()
|
||||
async def search_danistay_detailed(search_query: DanistayDetailedSearchRequest) -> CompactDanistaySearchResult:
|
||||
"""Performs a detailed search for Danıştay (Council of State) decisions."""
|
||||
logger.info(f"Tool 'search_danistay_detailed' called: {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.info(f"Tool 'search_danistay_detailed' called.") # Query loglaması kaldırıldı
|
||||
try:
|
||||
api_response = await danistay_client_instance.search_detailed_decisions(search_query)
|
||||
if api_response.data:
|
||||
@@ -141,7 +152,7 @@ async def search_danistay_detailed(search_query: DanistayDetailedSearchRequest)
|
||||
logger.warning("API response for Danistay detailed search did not contain expected data structure.")
|
||||
return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'search_danistay_detailed': {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.exception(f"Error in tool 'search_danistay_detailed'.")
|
||||
raise
|
||||
|
||||
@app.tool()
|
||||
@@ -152,14 +163,15 @@ async def get_danistay_document_markdown(document_id: str) -> DanistayDocumentMa
|
||||
try:
|
||||
return await danistay_client_instance.get_decision_document_as_markdown(document_id)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'get_danistay_document_markdown' for ID: {document_id}")
|
||||
logger.exception(f"Error in tool 'get_danistay_document_markdown'.")
|
||||
raise
|
||||
|
||||
# --- MCP Tools for Emsal ---
|
||||
# ... (Emsal araçları öncekiyle aynı, docstringler İngilizce) ...
|
||||
@app.tool()
|
||||
async def search_emsal_detailed_decisions(search_query: EmsalSearchRequest) -> CompactEmsalSearchResult:
|
||||
"""Searches for Emsal (UYAP Precedent) decisions using detailed criteria."""
|
||||
logger.info(f"Tool 'search_emsal_detailed_decisions' called: {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.info(f"Tool 'search_emsal_detailed_decisions' called.") # Query loglaması kaldırıldı
|
||||
try:
|
||||
api_response = await emsal_client_instance.search_detailed_decisions(search_query)
|
||||
if api_response.data:
|
||||
@@ -172,7 +184,7 @@ async def search_emsal_detailed_decisions(search_query: EmsalSearchRequest) -> C
|
||||
logger.warning("API response for Emsal search did not contain expected data structure.")
|
||||
return CompactEmsalSearchResult(decisions=[], total_records=0, requested_page=search_query.page_number, page_size=search_query.page_size)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'search_emsal_detailed_decisions': {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.exception(f"Error in tool 'search_emsal_detailed_decisions'.")
|
||||
raise
|
||||
|
||||
@app.tool()
|
||||
@@ -183,20 +195,21 @@ async def get_emsal_document_markdown(document_id: str) -> EmsalDocumentMarkdown
|
||||
try:
|
||||
return await emsal_client_instance.get_decision_document_as_markdown(document_id)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'get_emsal_document_markdown' for ID: {document_id}")
|
||||
logger.exception(f"Error in tool 'get_emsal_document_markdown'.")
|
||||
raise
|
||||
|
||||
# --- MCP Tools for Uyusmazlik ---
|
||||
# ... (Uyuşmazlık araçları öncekiyle aynı, docstringler İngilizce) ...
|
||||
@app.tool()
|
||||
async def search_uyusmazlik_decisions(
|
||||
search_params: UyusmazlikSearchRequest
|
||||
) -> UyusmazlikSearchResponse:
|
||||
"""Searches for Uyuşmazlık Mahkemesi decisions using various criteria from the form."""
|
||||
logger.info(f"Tool 'search_uyusmazlik_decisions' called with params: {search_params.model_dump_json(exclude_none=True, indent=2)}")
|
||||
"""Searches for Uyuşmazlık Mahkemesi (Court of Jurisdictional Disputes) decisions using various criteria."""
|
||||
logger.info(f"Tool 'search_uyusmazlik_decisions' called.") # Query loglaması kaldırıldı
|
||||
try:
|
||||
return await uyusmazlik_client_instance.search_decisions(search_params)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'search_uyusmazlik_decisions': {search_params.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.exception(f"Error in tool 'search_uyusmazlik_decisions'.")
|
||||
raise
|
||||
|
||||
@app.tool()
|
||||
@@ -212,34 +225,35 @@ async def get_uyusmazlik_document_markdown_from_url(document_url: HttpUrl) -> Uy
|
||||
try:
|
||||
return await uyusmazlik_client_instance.get_decision_document_as_markdown(str(document_url))
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url' for URL: {str(document_url)}")
|
||||
logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url'.")
|
||||
raise
|
||||
|
||||
# --- MCP Tools for Anayasa Mahkemesi (Norm Denetimi) ---
|
||||
# ... (Anayasa Norm araçları öncekiyle aynı, docstringler İngilizce) ...
|
||||
@app.tool()
|
||||
async def search_anayasa_norm_denetimi_decisions(
|
||||
search_query: AnayasaNormDenetimiSearchRequest
|
||||
) -> AnayasaSearchResult:
|
||||
"""
|
||||
Searches Anayasa Mahkemesi (Constitutional Court) Norm Denetimi decisions
|
||||
using various criteria from the official search form. This is for https://normkararlarbilgibankasi.anayasa.gov.tr.
|
||||
Searches Anayasa Mahkemesi (Constitutional Court) Norm Denetimi (Norm Control) decisions
|
||||
using criteria from https://normkararlarbilgibankasi.anayasa.gov.tr.
|
||||
"""
|
||||
logger.info(f"Tool 'search_anayasa_norm_denetimi_decisions' called: {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.info(f"Tool 'search_anayasa_norm_denetimi_decisions' called.") # Query loglaması kaldırıldı
|
||||
try:
|
||||
return await anayasa_norm_client_instance.search_norm_denetimi_decisions(search_query)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'search_anayasa_norm_denetimi_decisions': {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.exception(f"Error in tool 'search_anayasa_norm_denetimi_decisions'.")
|
||||
raise
|
||||
|
||||
@app.tool()
|
||||
async def get_anayasa_norm_denetimi_document_markdown(
|
||||
document_url: str = Field(..., description="The URL path of the AYM Norm Denetimi decision (e.g., /ND/YYYY/NN) or full https URL from normkararlarbilgibankasi.anayasa.gov.tr."),
|
||||
page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content, 1-indexed. Default is 1 for the first 5,000 characters.") # Corrected chunk size in description
|
||||
document_url: str = Field(..., description="The URL path (e.g., /ND/YYYY/NN) or full https URL of the AYM Norm Denetimi decision from normkararlarbilgibankasi.anayasa.gov.tr."),
|
||||
page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1 (first 5,000 characters).")
|
||||
) -> AnayasaDocumentMarkdown:
|
||||
"""
|
||||
Retrieves a specific Anayasa Mahkemesi (Norm Denetimi) decision
|
||||
from its URL and returns its content as paginated Markdown. This is for https://normkararlarbilgibankasi.anayasa.gov.tr.
|
||||
Content is paginated if it exceeds 5,000 characters. Use 'page_number' to get subsequent pages.
|
||||
from its URL and returns its content as paginated Markdown.
|
||||
Content is paginated if it exceeds 5,000 characters. Use 'page_number' for subsequent pages.
|
||||
"""
|
||||
logger.info(f"Tool 'get_anayasa_norm_denetimi_document_markdown' called for URL: {document_url}, Page: {page_number}")
|
||||
if not document_url or not document_url.strip():
|
||||
@@ -248,10 +262,11 @@ async def get_anayasa_norm_denetimi_document_markdown(
|
||||
try:
|
||||
return await anayasa_norm_client_instance.get_decision_document_as_markdown(document_url, page_number=current_page_to_fetch)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'get_anayasa_norm_denetimi_document_markdown' for URL: {document_url}, Page: {current_page_to_fetch}")
|
||||
logger.exception(f"Error in tool 'get_anayasa_norm_denetimi_document_markdown'.")
|
||||
raise
|
||||
|
||||
# --- MCP Tools for Anayasa Mahkemesi (Bireysel Başvuru Karar Raporu & Belgeler) ---
|
||||
# ... (Anayasa Bireysel araçları öncekiyle aynı, docstringler İngilizce) ...
|
||||
@app.tool()
|
||||
async def search_anayasa_bireysel_basvuru_report(
|
||||
search_query: AnayasaBireyselReportSearchRequest
|
||||
@@ -259,79 +274,147 @@ async def search_anayasa_bireysel_basvuru_report(
|
||||
"""
|
||||
Searches Anayasa Mahkemesi (Constitutional Court) Bireysel Başvuru (Individual Application)
|
||||
decisions and generates a 'Karar Arama Raporu' (Decision Search Report).
|
||||
This is for https://kararlarbilgibankasi.anayasa.gov.tr and uses the KararBulteni=1 parameter.
|
||||
The report typically displays 10 decisions per page by default.
|
||||
This is for https://kararlarbilgibankasi.anayasa.gov.tr (uses KararBulteni=1).
|
||||
"""
|
||||
logger.info(f"Tool 'search_anayasa_bireysel_basvuru_report' called: {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.info(f"Tool 'search_anayasa_bireysel_basvuru_report' called.") # Query loglaması kaldırıldı
|
||||
try:
|
||||
return await anayasa_bireysel_client_instance.search_bireysel_basvuru_report(search_query)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'search_anayasa_bireysel_basvuru_report': {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||
logger.exception(f"Error in tool 'search_anayasa_bireysel_basvuru_report'.")
|
||||
raise
|
||||
|
||||
@app.tool()
|
||||
async def get_anayasa_bireysel_basvuru_document_markdown(
|
||||
document_url_path: str = Field(..., description="The URL path of the AYM Bireysel Başvuru decision (e.g., /BB/YYYY/NNNN) from kararlarbilgibankasi.anayasa.gov.tr."),
|
||||
page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content, 1-indexed. Default is 1 for the first 5,000 characters.")
|
||||
document_url_path: str = Field(..., description="The URL path (e.g., /BB/YYYY/NNNN) of the AYM Bireysel Başvuru decision from kararlarbilgibankasi.anayasa.gov.tr."),
|
||||
page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1 (first 5,000 characters).")
|
||||
) -> AnayasaBireyselBasvuruDocumentMarkdown:
|
||||
"""
|
||||
Retrieves a specific Anayasa Mahkemesi Bireysel Başvuru (Individual Application) decision
|
||||
from its URL path (e.g., /BB/YYYY/NNNN found in report results) and returns its content as paginated Markdown.
|
||||
This is for https://kararlarbilgibankasi.anayasa.gov.tr.
|
||||
Content is paginated if it exceeds 5,000 characters. Use 'page_number' to get subsequent pages.
|
||||
from its URL path (e.g., /BB/YYYY/NNNN) and returns content as paginated Markdown.
|
||||
Content is paginated if it exceeds 5,000 characters. Use 'page_number' for subsequent pages.
|
||||
"""
|
||||
logger.info(f"Tool 'get_anayasa_bireysel_basvuru_document_markdown' called for URL path: {document_url_path}, Page: {page_number}")
|
||||
if not document_url_path or not document_url_path.strip() or not document_url_path.startswith("/BB/"):
|
||||
raise ValueError("Document URL path (e.g., /BB/YYYY/NNNN) is required for Anayasa Bireysel Başvuru document retrieval.")
|
||||
current_page_to_fetch = page_number if page_number is not None and page_number >= 1 else 1
|
||||
try:
|
||||
return await anayasa_bireysel_client_instance.get_decision_document_as_markdown(document_url_path, page_number=current_page_to_fetch)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'get_anayasa_bireysel_basvuru_document_markdown'.")
|
||||
raise
|
||||
|
||||
# --- MCP Tools for KIK (Kamu İhale Kurulu) ---
|
||||
@app.tool()
|
||||
async def search_kik_decisions(search_query: KikSearchRequest) -> KikSearchResult:
|
||||
"""
|
||||
Searches KIK (Public Procurement Authority) decisions.
|
||||
"""
|
||||
logger.info(f"Tool 'search_kik_decisions' called.") # Query loglaması kaldırıldı
|
||||
try:
|
||||
api_response = await kik_client_instance.search_decisions(search_query)
|
||||
page_param_for_log = search_query.page if hasattr(search_query, 'page') else 1
|
||||
if not api_response.decisions and api_response.total_records == 0 and page_param_for_log == 1:
|
||||
logger.warning(f"KIK search returned no decisions for query.") # Query detayı kaldırıldı
|
||||
return api_response
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in KIK search tool 'search_kik_decisions'.")
|
||||
current_page_val = search_query.page if hasattr(search_query, 'page') else 1
|
||||
return KikSearchResult(decisions=[], total_records=0, current_page=current_page_val)
|
||||
|
||||
@app.tool()
|
||||
async def get_kik_document_markdown(
|
||||
karar_id: str = Field(..., description="The Base64 encoded KIK decision identifier."),
|
||||
page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1.")
|
||||
) -> KikDocumentMarkdown:
|
||||
"""
|
||||
Retrieves a specific KIK (Public Procurement Authority) decision using its Base64 encoded 'karar_id'.
|
||||
Content is returned as paginated Markdown.
|
||||
"""
|
||||
logger.info(f"Tool 'get_kik_document_markdown' called for KIK karar_id: {karar_id}, Markdown Page: {page_number}")
|
||||
|
||||
if not karar_id or not karar_id.strip():
|
||||
logger.error("KIK Document retrieval: karar_id cannot be empty.")
|
||||
return KikDocumentMarkdown(
|
||||
retrieved_with_karar_id=karar_id,
|
||||
error_message="karar_id is required and must be a non-empty string.",
|
||||
current_page=page_number or 1,
|
||||
total_pages=1,
|
||||
is_paginated=False
|
||||
)
|
||||
|
||||
current_page_to_fetch = page_number if page_number is not None and page_number >= 1 else 1
|
||||
|
||||
try:
|
||||
return await anayasa_bireysel_client_instance.get_decision_document_as_markdown(document_url_path, page_number=current_page_to_fetch)
|
||||
return await kik_client_instance.get_decision_document_as_markdown(
|
||||
karar_id_b64=karar_id,
|
||||
page_number=current_page_to_fetch
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in tool 'get_anayasa_bireysel_basvuru_document_markdown' for URL path: {document_url_path}, Page: {current_page_to_fetch}")
|
||||
raise
|
||||
logger.exception(f"Error in KIK document retrieval tool 'get_kik_document_markdown' for karar_id: {karar_id}")
|
||||
return KikDocumentMarkdown(
|
||||
retrieved_with_karar_id=karar_id,
|
||||
error_message=f"Tool-level error during KIK document retrieval: {str(e)}",
|
||||
current_page=current_page_to_fetch,
|
||||
total_pages=1,
|
||||
is_paginated=False
|
||||
)
|
||||
|
||||
# --- Application Shutdown Handling ---
|
||||
# ... (perform_cleanup ve main fonksiyonları öncekiyle aynı, değişiklik yok) ...
|
||||
def perform_cleanup():
|
||||
logger.info("MCP Server performing cleanup...")
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_event_loop_policy().get_event_loop()
|
||||
if loop.is_closed():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
except RuntimeError: # pragma: no cover
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
clients_to_close = [
|
||||
globals().get('yargitay_client_instance'),
|
||||
globals().get('danistay_client_instance'),
|
||||
globals().get('emsal_client_instance'),
|
||||
globals().get('uyusmazlik_client_instance'),
|
||||
globals().get('anayasa_norm_client_instance'),
|
||||
globals().get('anayasa_bireysel_client_instance')
|
||||
globals().get('anayasa_bireysel_client_instance'),
|
||||
globals().get('kik_client_instance')
|
||||
]
|
||||
async def close_all_clients_async():
|
||||
tasks = []
|
||||
for client_instance in clients_to_close:
|
||||
if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session):
|
||||
logger.info(f"Closing client session for {client_instance.__class__.__name__} via atexit.")
|
||||
loop.run_until_complete(client_instance.close_client_session())
|
||||
logger.info(f"Scheduling close for client session: {client_instance.__class__.__name__}")
|
||||
tasks.append(client_instance.close_client_session())
|
||||
if tasks:
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
client_name = "Unknown Client"
|
||||
if i < len(clients_to_close) and clients_to_close[i] is not None:
|
||||
client_name = clients_to_close[i].__class__.__name__
|
||||
logger.error(f"Error closing client {client_name}: {result}")
|
||||
try:
|
||||
if loop.is_running():
|
||||
asyncio.ensure_future(close_all_clients_async(), loop=loop)
|
||||
logger.info("Client cleanup tasks scheduled on running event loop.")
|
||||
else:
|
||||
loop.run_until_complete(close_all_clients_async())
|
||||
logger.info("Client cleanup tasks completed via run_until_complete.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during atexit cleanup: {e}")
|
||||
logger.info("MCP Server atexit cleanup attempt finished.")
|
||||
|
||||
logger.error(f"Error during atexit cleanup execution: {e}", exc_info=True)
|
||||
logger.info("MCP Server atexit cleanup process finished.")
|
||||
atexit.register(perform_cleanup)
|
||||
|
||||
# --- Main Entry Point ---
|
||||
if __name__ == "__main__":
|
||||
logger.info(f"Starting {app.name} server (main execution block)...")
|
||||
def main():
|
||||
logger.info(f"Starting {app.name} server via main() function...")
|
||||
logger.info(f"Logs will be written to: {LOG_FILE_PATH}")
|
||||
try:
|
||||
asyncio.run(app.run_sse_async(host="127.0.0.1", port=8890, log_level="debug"))
|
||||
app.run()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Server shut down by user (KeyboardInterrupt).")
|
||||
except Exception as e:
|
||||
logger.exception("Server failed to start or crashed.")
|
||||
finally:
|
||||
logger.info(f"{app.name} server has shut down.")
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,24 @@
|
||||
[project]
|
||||
name = "yargi-mcp"
|
||||
version = "0.1.0"
|
||||
description = "MCP Server For Turkish Legal Databases"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"beautifulsoup4>=4.13.4",
|
||||
"fastmcp>=2.2.10",
|
||||
"httpx>=0.28.1",
|
||||
"markitdown>=0.1.1",
|
||||
"pydantic>=2.11.4",
|
||||
"aiohttp>=3.11.18",
|
||||
"playwright>=1.52.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
yargi-mcp = "mcp_server_main:main"
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = ["mcp_server_main"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["*_mcp_module"]
|
||||
Reference in New Issue
Block a user